Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Changes In Branch markdown-tagrefs Excluding Merge-Ins
This is equivalent to a diff from fc853823b2 to 347084af90
2024-07-06
| ||
09:33 | /chat: when tapping on a #NNNN reference, if the referred-to message is not loaded in the local history then search the chat history for message #NNNN. ... (Leaf check-in: 347084af90 user: stephan tags: markdown-tagrefs) | |
2024-07-03
| ||
15:01 | Add the application/sql mime type to doc.c. ... (check-in: 7c76c6aa73 user: stephan tags: trunk) | |
12:55 | Merge trunk into the markdown-tagrefs branch to begin experimentation with tying chat #NNN references into the new search capabilities. ... (check-in: 5e26fd4c10 user: stephan tags: markdown-tagrefs) | |
12:38 | Add /chat history search. ... (check-in: fc853823b2 user: stephan tags: trunk) | |
10:26 | Apply the logic in/around [ec68aaf42536b4fb] to the chat search so that it does not abort, and generate an error log entry, when given characters which fts5 does not like. ... (Closed-Leaf check-in: b698ba9942 user: stephan tags: fts5-chat-search) | |
2024-07-02
| ||
08:19 | For the previous check-in, disable the submit button rather than use alert(). ... (check-in: fe24713a27 user: danield tags: trunk) | |
Changes to src/backlink.c.
︙ | ︙ | |||
283 284 285 286 287 288 289 290 291 292 293 294 295 296 | void *v){ return 1; } static int mkdn_noop_linebreak(Blob *b1, void *v){ return 1; } static int mkdn_noop_r_html_tag(Blob *b1, Blob *b2, void *v){ return 1; } static int (*mkdn_noop_tri_emphas)(Blob*, Blob*, char, void*) = mkdn_noop_emphasis; static int mkdn_noop_footnoteref(Blob *b1, const Blob *b2, const Blob *b3, int i1, int i2, void *v){ return 1; } /* ** Scan markdown text and add self-hyperlinks to the BACKLINK table. */ void markdown_extract_links( char *zInputText, Backlink *p | > > | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | void *v){ return 1; } static int mkdn_noop_linebreak(Blob *b1, void *v){ return 1; } static int mkdn_noop_r_html_tag(Blob *b1, Blob *b2, void *v){ return 1; } static int (*mkdn_noop_tri_emphas)(Blob*, Blob*, char, void*) = mkdn_noop_emphasis; static int mkdn_noop_footnoteref(Blob *b1, const Blob *b2, const Blob *b3, int i1, int i2, void *v){ return 1; } static int mkdn_noop_tagref(Blob *b1,Blob *b2, enum mkd_tagspan t, void *p){ return 1; } /* ** Scan markdown text and add self-hyperlinks to the BACKLINK table. */ void markdown_extract_links( char *zInputText, Backlink *p |
︙ | ︙ | |||
317 318 319 320 321 322 323 324 325 326 327 328 329 330 | /* codespan */ mkdn_noop_codespan, /* dbl_emphas */ mkdn_noop_dbl_emphas, /* emphasis */ mkdn_noop_emphasis, /* image */ mkdn_noop_image, /* linebreak */ mkdn_noop_linebreak, /* link */ backlink_md_link, /* r_html_tag */ mkdn_noop_r_html_tag, /* tri_emphas */ mkdn_noop_tri_emphas, /* footnoteref*/ mkdn_noop_footnoteref, 0, /* entity */ 0, /* normal_text */ "*_", /* emphasis characters */ 0 /* client data */ | > | 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 | /* codespan */ mkdn_noop_codespan, /* dbl_emphas */ mkdn_noop_dbl_emphas, /* emphasis */ mkdn_noop_emphasis, /* image */ mkdn_noop_image, /* linebreak */ mkdn_noop_linebreak, /* link */ backlink_md_link, /* r_html_tag */ mkdn_noop_r_html_tag, /* #tags */ mkdn_noop_tagref, /* tri_emphas */ mkdn_noop_tri_emphas, /* footnoteref*/ mkdn_noop_footnoteref, 0, /* entity */ 0, /* normal_text */ "*_", /* emphasis characters */ 0 /* client data */ |
︙ | ︙ |
Changes to src/chat.c.
︙ | ︙ | |||
214 215 216 217 218 219 220 221 222 223 224 225 226 227 | @ <strong>Tap the title</strong> of this widget to toggle @ the list on and off. @ </span> @ <span>Active users (sorted by last message time)</span> @ </div> @ <div id='chat-user-list'></div> @ </div> @ <div id='chat-preview' class='hidden chat-view'> @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> @ <div id='chat-preview-content'></div> @ <div class='button-bar'><button class='action-close'>Close Preview</button></div> @ </div> @ <div id='chat-config' class='hidden chat-view'> @ <div id='chat-config-options'></div> | > | 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | @ <strong>Tap the title</strong> of this widget to toggle @ the list on and off. @ </span> @ <span>Active users (sorted by last message time)</span> @ </div> @ <div id='chat-user-list'></div> @ </div> @ <button id='chat-clear-filter' class='hidden'>Clear filter</button> @ <div id='chat-preview' class='hidden chat-view'> @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> @ <div id='chat-preview-content'></div> @ <div class='button-bar'><button class='action-close'>Close Preview</button></div> @ </div> @ <div id='chat-config' class='hidden chat-view'> @ <div id='chat-config-options'></div> |
︙ | ︙ |
Changes to src/fossil.bootstrap.js.
︙ | ︙ | |||
278 279 280 281 282 283 284 | console.error(eventName,"event listener threw:",e); } } return this; }; /** | | | > | | | > | 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | console.error(eventName,"event listener threw:",e); } } return this; }; /** Sets the innerText of the page's TITLE tag to the given text and returns this object. If passed a falsy value then the title is reverted to its page-load-time value. */ F.page.setPageTitle = function f(title){ const t = document.querySelector('title'); if(t) t.innerText = title || f.$orig; return this; }; F.onPageLoad(()=>F.page.setPageTitle.$orig = document.querySelector('title').innerText); /** Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for N milliseconds. If `immediate` is passed, call the callback immediately and hinder future invocations until at least the given time has passed. |
︙ | ︙ |
Changes to src/fossil.page.chat.js.
︙ | ︙ | |||
84 85 86 87 88 89 90 | causing the input area to move off-screen. While we're here, we also use this to cap the max-height of the input field so that pasting huge text does not scroll the upper area of the input widget off-screen. */ const elemsToCount = GetFramingElements(); const contentArea = E1('div.content'); | < | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | causing the input area to move off-screen. While we're here, we also use this to cap the max-height of the input field so that pasting huge text does not scroll the upper area of the input widget off-screen. */ const elemsToCount = GetFramingElements(); const contentArea = E1('div.content'); const resized = function f(){ if(f.$disabled) return; const wh = window.innerHeight, com = document.body.classList.contains('chat-only-mode'); var ht; var extra = 0; if(com){ ht = wh; }else{ elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false); ht = wh - extra; |
︙ | ︙ | |||
149 150 151 152 153 154 155 | viewPreview: E1('#chat-preview'), previewContent: E1('#chat-preview-content'), viewSearch: E1('#chat-search'), searchContent: E1('#chat-search-content'), btnPreview: E1('#chat-button-preview'), views: document.querySelectorAll('.chat-view'), activeUserListWrapper: E1('#chat-user-list-wrapper'), | | > | > | | > > > > > > > > > > > > | > | > > | 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | viewPreview: E1('#chat-preview'), previewContent: E1('#chat-preview-content'), viewSearch: E1('#chat-search'), searchContent: E1('#chat-search-content'), btnPreview: E1('#chat-button-preview'), views: document.querySelectorAll('.chat-view'), activeUserListWrapper: E1('#chat-user-list-wrapper'), activeUserList: E1('#chat-user-list'), btnClearFilter: E1('#chat-clear-filter') }, me: F.user.name, mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, pageIsActive: 'visible'===document.visibilityState, changesSincePageHidden: 0, notificationBubbleColor: 'white', totalMessageCount: 0, // total # of inbound messages //! Number of messages to load for the history buttons loadMessageCount: Math.abs(F.config.chat.initSize || 20), ajaxInflight: 0, usersLastSeen:{ /* Map of user names to their most recent message time (JS Date object). Only messages received by the chat client are considered. */ /* Reminder: to convert a Julian time J to JS: new Date((J - 2440587.5) * 86400000) */ }, filter: { user:{ activeTag: undefined, match: function(uname){ return !this.activeTag || this.activeTag===uname; }, matchElem: function(e){ return !this.activeTag || this.activeTag===e.dataset.xfrom; } }, hashtag:{ activeTag: undefined, match: function(tag){ return !this.activeTag || tag===this.activeTag; }, matchElem: function(e){ return !this.activeTag || !!e.querySelector('[data-hashtag="'+this.activeTag+'"]'); } }, current: undefined/*gets set to current active filter*/ }, /** Gets (no args) or sets (1 arg) the current input text field value, taking into account single- vs multi-line input. The getter returns a trim()'d string and the setter returns this object. As a special case, if arguments[0] is a boolean value, it behaves like a getter and, if arguments[0]===true |
︙ | ︙ | |||
268 269 270 271 272 273 274 | /* Injects DOM element e as a new row in the chat, at the oldest end of the list if atEnd is truthy, else at the newest end of the list. */ injectMessageElem: function f(e, atEnd){ const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, holder = this.e.viewMessages, prevMessage = this.e.newestMessage; | | > | 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | /* Injects DOM element e as a new row in the chat, at the oldest end of the list if atEnd is truthy, else at the newest end of the list. */ injectMessageElem: function f(e, atEnd){ const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, holder = this.e.viewMessages, prevMessage = this.e.newestMessage; if(this.filter.current && !this.filter.current.matchElem(e)){ e.classList.add('hidden'); } if(atEnd){ const fe = mip.nextElementSibling; if(fe) mip.parentNode.insertBefore(e, fe); else D.append(mip.parentNode, e); }else{ |
︙ | ︙ | |||
490 491 492 493 494 495 496 | if(e===this.e.currentView){ return e; } this.e.views.forEach(function(E){ if(e!==E) D.addClass(E,'hidden'); }); this.e.currentView = e; | < | 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | if(e===this.e.currentView){ return e; } this.e.views.forEach(function(E){ if(e!==E) D.addClass(E,'hidden'); }); this.e.currentView = e; D.removeClass(e,'hidden'); this.animate(this.e.currentView, 'anim-fade-in-fast'); return this.e.currentView; }, /** Updates the "active user list" view if we are not currently batch-loading messages and if the active user list UI element |
︙ | ︙ | |||
519 520 521 522 523 524 525 | else if(l) return -1; else if(r) return 1; else return 0; }; callee.addUserElem = function(u){ const uSpan = D.addClass(D.span(), 'chat-user'); const uDate = self.usersLastSeen[u]; | | | 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 | else if(l) return -1; else if(r) return 1; else return 0; }; callee.addUserElem = function(u){ const uSpan = D.addClass(D.span(), 'chat-user'); const uDate = self.usersLastSeen[u]; if(self.filter.user.activeTag===u){ uSpan.classList.add('selected'); } uSpan.dataset.uname = u; D.append(uSpan, u, "\n", D.append( D.addClass(D.span(),'timestamp'), localTimeString(uDate)//.substr(5/*chop off year*/) |
︙ | ︙ | |||
541 542 543 544 545 546 547 | //D.clearElement(this.e.activeUserList); D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); Object.keys(this.usersLastSeen).sort( callee.sortUsersSeen ).forEach(callee.addUserElem); return this; }, | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 | //D.clearElement(this.e.activeUserList); D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); Object.keys(this.usersLastSeen).sort( callee.sortUsersSeen ).forEach(callee.addUserElem); return this; }, /** For each Chat.MessageWidget element (X.message-widget) for which predicate(elem) returns true, the 'hidden' class is removed from that message. For all others, 'hidden' is added. If predicate is falsy, 'hidden' is removed from all elements. After filtering, it will try to scroll the last not-filtered-out message into view, but exactly where it scrolls into view (top, middle, button) is unpredictable. Returns this object. The argument may optionally be an object from this.filter, in which case its matchElem() method becomes the predicate. Note that this does not encapsulate certain filter-specific logic which applies changes to elements other than the main message list or this.e.btnClearFilter. */ applyMessageFilter: function(predicate){ const self = this; let eLast; console.debug("applyMessageFilter(",predicate,")"); if(!predicate){ D.removeClass(this.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden'); D.addClass(this.e.btnClearFilter, 'hidden'); }else if('function'!==typeof predicate && predicate.matchElem){ /* assume Chat.filter object */ const p = predicate; predicate = (e)=>p.matchElem(e); } if(predicate){ this.e.viewMessages.querySelectorAll('.message-widget').forEach(function(e){ if(predicate(e)){ e.classList.remove('hidden'); eLast = e; }else{ e.classList.add('hidden'); } }); D.removeClass(this.e.btnClearFilter, 'hidden'); } this.setCurrentView(this.e.viewMessages); if(eLast) eLast.scrollIntoView(false); else this.scrollMessagesTo(1); return this; }, /** Clears the current message filter, if any, and clears the activeTag property of all members of this.filter. Returns this object. This also unfortunately performs some filter-type-specific logic which we have not yet managed to encapsulate more cleanly. */ clearFilters: function(){ if(!this.filter.current) return this; this.filter.current = undefined; this.applyMessageFilter(false); const self = this; Object.keys(this.filter).forEach(function(k){ const f = self.filter[k]; if(f) f.activeTag = undefined; }); this.e.activeUserList.querySelectorAll('.chat-user').forEach( /*Unfortante filter-specific logic*/ (e)=>e.classList.remove('selected') ); return this; }, /** Show or hide the active user list. Returns this object. */ showActiveUserList: function(yes){ if(0===arguments.length) yes = true; this.e.activeUserListWrapper.classList[ yes ? 'remove' : 'add' ]('hidden'); D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ |
︙ | ︙ | |||
571 572 573 574 575 576 577 | return this; }, /** Applies user name filter to all current messages, or clears the filter if uname is falsy. */ setUserFilter: function(uname){ | | > | > > > > < < < < < | < | > | > | | | > > > > > > > | < > | < < < > | 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 | return this; }, /** Applies user name filter to all current messages, or clears the filter if uname is falsy. */ setUserFilter: function(uname){ if(!uname || (this.filter.current && this.filter.current!==this.filter.user)){ this.clearFilters(); } this.filter.user.activeTag = uname; if(uname) this.applyMessageFilter(this.filter.user); this.filter.current = uname ? this.filter.user : undefined; const self = this; this.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){ e.classList[ self.filter.user.activeTag===e.dataset.uname ? 'add' : 'remove' ]('selected'); }); return this; }, /** Applies a hashtag filter to all current messages, or clears the filter if tag is falsy. */ setHashtagFilter: function(tag){ if(!tag || (this.filter.current && this.filter.current!==this.filter.hashtag)){ this.clearFilters(); } this.filter.hashtag.activeTag = tag; if(tag) this.applyMessageFilter(this.filter.hashtag); this.filter.current = tag ? this.filter.hashtag : undefined; return this; }, /** If animations are enabled, passes its arguments to D.addClassBriefly(), else this is a no-op. If cb is a function, it is called after the |
︙ | ︙ | |||
873 874 875 876 877 878 879 | const uname = eUser.dataset.uname; let eLast; cs.setCurrentView(cs.e.viewMessages); if(eUser.classList.contains('selected')){ /* If curently selected, toggle filter off */ eUser.classList.remove('selected'); cs.setUserFilter(false); | < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 | const uname = eUser.dataset.uname; let eLast; cs.setCurrentView(cs.e.viewMessages); if(eUser.classList.contains('selected')){ /* If curently selected, toggle filter off */ eUser.classList.remove('selected'); cs.setUserFilter(false); }else{ eUser.classList.add('selected'); cs.setUserFilter(uname); } return false; }, false); cs.e.btnClearFilter.addEventListener('click',function(){ D.addClass(this,'hidden'); cs.clearFilters(); }, false); return cs; })()/*Chat initialization*/; /** An experiment in history navigation: when a message numtag is clicked, we push the origin message onto the history and set up the back button to return to that message. */ window.onpopstate = function(event){ const msgid = Chat.numtagHistoryStack.pop(); if(msgid){ const e = Chat.setCurrentView(Chat.e.viewMessages). querySelector('.message-widget[data-msgid="'+msgid+'"]'); //console.debug("Popping history back to",msgid, e); if(e){ Chat.MessageWidget.scrollToMessageElem(e); return; } } Chat.scrollMessagesTo(1); }; Chat.numtagHistoryStack = [ /* Relying on the pushHistory() state object for holding the message ID is completely misbehaving, not giving us the expected state object when window.onpopstate is triggered (plus, the browser persists it, which introduces its own problems). Thus we use our own stack of message IDs for history navigation purposes. */]; /** If e or one of its parents has the given CSS class, that element is returned, else falsy is returned. */ const findParentWithClass = function(e, className){ while(e && !e.classList.contains(className)){ e = e.parentNode; } return e; }; /** To be passed each MessageWidget's top-level DOM element after initial processing of the message, to set up hashtag and numtag references. */ const setupHashtags = function f(elem){ if(!f.$clickTag){ f.$clickTag = function(ev){ /* Click handler for hashtags */ const tag = ev.target.dataset.hashtag; if(tag){ Chat.setHashtagFilter( tag===Chat.filter.hashtag.activeTag ? false : tag ); } }; f.$clickNum = function(ev){ /* Click handler for #NNN references */ const tag = ev.target.dataset.numtag; if(tag){ const e = Chat.e.viewMessages.querySelector( '.message-widget[data-msgid="'+tag+'"]' ); if(e){ Chat.MessageWidget.scrollToMessageElem(e); //Set up window.history() state... const p = 0 ? false : findParentWithClass(ev.target, 'message-widget'); if(p){ const state = {msgId: p.dataset.msgid}; Chat.numtagHistoryStack.push(p.dataset.msgid); const rc = window.history.pushState(state, ""); //console.debug("Pushing history for msgid", state); //console.debug("Chat.numtagHistoryStack =",Chat.numtagHistoryStack); } }else{ Chat.submitSearch('#'+tag); } } }; } elem.querySelectorAll('[data-hashtag]').forEach(function(e){ e.dataset.hashtag = e.dataset.hashtag.toLowerCase(); e.addEventListener('click', f.$clickTag, false); }) elem.querySelectorAll('[data-numtag]').forEach( (e)=>e.addEventListener('click', f.$clickNum, false) ) }/*setupHashtags()*/; /** Returns the first .message-widget element in DOM element e's lineage. */ const findMessageWidgetParent = function(e){ while( e && !e.classList.contains('message-widget')){ e = e.parentNode; } |
︙ | ︙ | |||
1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 | // perfectly safe to use in this context. if(m.xmsg && 'string' !== typeof m.xmsg){ // Used by Chat.reportErrorAsMessage() D.append(contentTarget, m.xmsg); }else{ contentTarget.innerHTML = m.xmsg; contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank); if(F.pikchr){ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr')); } } } //console.debug("tab",this.e.tab); //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild); | > | 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 | // perfectly safe to use in this context. if(m.xmsg && 'string' !== typeof m.xmsg){ // Used by Chat.reportErrorAsMessage() D.append(contentTarget, m.xmsg); }else{ contentTarget.innerHTML = m.xmsg; contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank); setupHashtags(contentTarget); if(F.pikchr){ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr')); } } } //console.debug("tab",this.e.tab); //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild); |
︙ | ︙ | |||
1258 1259 1260 1261 1262 1263 1264 | D.a(F.repoUrl('timeline',{ u: eMsg.dataset.xfrom, y: 'a' }), "User's Timeline"), 'target', '_blank' ); D.append(toolbar2, timelineLink); | | < | | < | < < < < < < | 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 | D.a(F.repoUrl('timeline',{ u: eMsg.dataset.xfrom, y: 'a' }), "User's Timeline"), 'target', '_blank' ); D.append(toolbar2, timelineLink); if(Chat.filter.current){ /* Add a button to clear filter and jump to this message in its original context. */ D.append( this.e, D.append( D.addClass(D.div(), 'toolbar'), D.button( "Message in context", function(){ self.hide(); Chat.clearFilters(); Chat.MessageWidget.scrollToMessageElem(eMsg); }) ) ); }/*jump-to button*/ } const tab = eMsg.querySelector('.message-widget-tab'); D.append(tab, this.e); |
︙ | ︙ | |||
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 | this.refresh(); } }/*f.popup*/; }/*end static init*/ const theMsg = findMessageWidgetParent(ev.target); if(theMsg) f.popup.show(theMsg); }/*_handleLegendClicked()*/ }; return ctor; })()/*MessageWidget*/; /** A widget for loading more messages (context) around a /chat-query result message. | > > > > > > > > > > | 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 | this.refresh(); } }/*f.popup*/; }/*end static init*/ const theMsg = findMessageWidgetParent(ev.target); if(theMsg) f.popup.show(theMsg); }/*_handleLegendClicked()*/ }/*MessageWidget.prototype*/; /** Assumes that e is a MessageWidget element, ensures that Chat.e.viewMessages is visible, scrolls the message, and animates it a bit to make it more visible. */ ctor.scrollToMessageElem = function(e){ if(e.firstElementChild){ Chat.setCurrentView(Chat.e.viewMessages); e.scrollIntoView(false); Chat.animate(e, 'anim-fade-out-in'); } }; return ctor; })()/*MessageWidget*/; /** A widget for loading more messages (context) around a /chat-query result message. |
︙ | ︙ | |||
1813 1814 1815 1816 1817 1818 1819 | optAu.theLegend.addEventListener('click',function(){ D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed'); if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ Chat.animate(optAu.theList,'anim-flip-v'); } }, false); }/*namedOptions.activeUsers additional setup*/ | < < | > > | 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 | optAu.theLegend.addEventListener('click',function(){ D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed'); if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ Chat.animate(optAu.theList,'anim-flip-v'); } }, false); }/*namedOptions.activeUsers additional setup*/ /** Settings options structure: an array of Objects with the following properties: label: string for the UI boolValue: string (name of Chat.settings setting) or a function which returns true or false. If it is a string, it gets replaced by a function which returns Chat.settings.getBool(thatString) and the string gets assigned to the persistentSetting property of this object. select: SELECT element (instead of boolValue) callback: optional handler to call after setting is modified. It gets passed the setting object: {key:string, value:something}. Its "this" is the options object. If this object has a boolValue string or a persistentSetting property, the argument passed to the callback is a settings object in the form {key:K, value:V}. If this object does not have boolValue string or persistentSetting then the callback is passed an event object in response to the config option's UI widget being activated, normally a 'change' event. |
︙ | ︙ | |||
2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 | (function(){/*set up message preview*/ const btnPreview = Chat.e.btnPreview; Chat.setPreviewText = function(t){ this.setCurrentView(this.e.viewPreview); this.e.previewContent.innerHTML = t; this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); this.inputFocus(); }; Chat.e.viewPreview.querySelector('button.action-close'). addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); let previewPending = false; const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields]; const submit = function(ev){ | > | 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 | (function(){/*set up message preview*/ const btnPreview = Chat.e.btnPreview; Chat.setPreviewText = function(t){ this.setCurrentView(this.e.viewPreview); this.e.previewContent.innerHTML = t; this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); setupHashtags(this.e.previewContent)/*arguable, for usability reasons*/; this.inputFocus(); }; Chat.e.viewPreview.querySelector('button.action-close'). addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); let previewPending = false; const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields]; const submit = function(ev){ |
︙ | ︙ | |||
2386 2387 2388 2389 2390 2391 2392 | D.append(e, "Enter search terms in the message field. "+ "Use #NNNNN to search for the message with ID NNNNN."); } return e; }; Chat.clearSearch(true); /** | | | | > | | 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 | D.append(e, "Enter search terms in the message field. "+ "Use #NNNNN to search for the message with ID NNNNN."); } return e; }; Chat.clearSearch(true); /** Submits a history search using either its argument or the the main input field's current text. */ Chat.submitSearch = function(term){ Chat.setCurrentView(Chat.e.viewSearch); if(!arguments.length) term = this.inputValue(true); const eMsgTgt = this.clearSearch(true); if( !term ) return; D.append( eMsgTgt, "Searching for ",term," ..."); const fd = new FormData(); fd.set('q', term); F.fetch( "chat-query", { |
︙ | ︙ |
Changes to src/markdown.c.
︙ | ︙ | |||
37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /* mkd_autolink -- type of autolink */ enum mkd_autolink { MKDA_NOT_AUTOLINK, /* used internally when it is not an autolink*/ MKDA_NORMAL, /* normal http/http/ftp link */ MKDA_EXPLICIT_EMAIL, /* e-mail link with explicit mailto: */ MKDA_IMPLICIT_EMAIL /* e-mail link without mailto: */ }; /* mkd_renderer -- functions for rendering parsed data */ struct mkd_renderer { /* document level callbacks */ void (*prolog)(struct Blob *ob, void *opaque); void (*epilog)(struct Blob *ob, void *opaque); void (*footnotes)(struct Blob *ob, const struct Blob *items, void *opaque); | > > > > > > | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | /* mkd_autolink -- type of autolink */ enum mkd_autolink { MKDA_NOT_AUTOLINK, /* used internally when it is not an autolink*/ MKDA_NORMAL, /* normal http/http/ftp link */ MKDA_EXPLICIT_EMAIL, /* e-mail link with explicit mailto: */ MKDA_IMPLICIT_EMAIL /* e-mail link without mailto: */ }; /* mkd_tagspan -- type of tagged <span> */ enum mkd_tagspan { MKDT_HASHTAG, /* #hashtags */ MKDT_NUMTAG /* #123[.456] /chat or /forum message IDs. */ }; /* mkd_renderer -- functions for rendering parsed data */ struct mkd_renderer { /* document level callbacks */ void (*prolog)(struct Blob *ob, void *opaque); void (*epilog)(struct Blob *ob, void *opaque); void (*footnotes)(struct Blob *ob, const struct Blob *items, void *opaque); |
︙ | ︙ | |||
78 79 80 81 82 83 84 85 86 87 88 89 90 91 | int (*emphasis)(struct Blob *ob, struct Blob *text, char c,void*opaque); int (*image)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *alt, void *opaque); int (*linebreak)(struct Blob *ob, void *opaque); int (*link)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *content, void *opaque); int (*raw_html_tag)(struct Blob *ob, struct Blob *tag, void *opaque); int (*triple_emphasis)(struct Blob *ob, struct Blob *text, char c, void *opaque); int (*footnote_ref)(struct Blob *ob, const struct Blob *span, const struct Blob *upc, int index, int locus, void *opaque); /* low level callbacks - NULL copies input directly into the output */ void (*entity)(struct Blob *ob, struct Blob *entity, void *opaque); | > > | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | int (*emphasis)(struct Blob *ob, struct Blob *text, char c,void*opaque); int (*image)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *alt, void *opaque); int (*linebreak)(struct Blob *ob, void *opaque); int (*link)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *content, void *opaque); int (*raw_html_tag)(struct Blob *ob, struct Blob *tag, void *opaque); int (*tagspan)(struct Blob *ob, struct Blob *ref, enum mkd_tagspan type, void *opaque); int (*triple_emphasis)(struct Blob *ob, struct Blob *text, char c, void *opaque); int (*footnote_ref)(struct Blob *ob, const struct Blob *span, const struct Blob *upc, int index, int locus, void *opaque); /* low level callbacks - NULL copies input directly into the output */ void (*entity)(struct Blob *ob, struct Blob *entity, void *opaque); |
︙ | ︙ | |||
948 949 950 951 952 953 954 955 956 957 958 959 960 961 | blob_init(&work, data, end); rndr->make.entity(ob, &work, rndr->make.opaque); }else{ blob_append(ob, data, end); } return end; } /* ** char_langle_tag -- '<' when tags or autolinks are allowed. */ static size_t char_langle_tag( struct Blob *ob, struct render *rndr, | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 | blob_init(&work, data, end); rndr->make.entity(ob, &work, rndr->make.opaque); }else{ blob_append(ob, data, end); } return end; } /* char_hashref_tag -- '#' followed by "word" characters to tag ** post numbers, hashtags, etc. ** ** Basic syntax: ** ** ^[a-zA-Z]X* ** ** Where X is: ** ** - Any number of alphanumeric characters. ** ** - Single underscores. Adjacent underscores are not recognized ** as valid hashtags. That decision is somewhat arbitrary ** and up for debate. ** ** Hashtags must end at the end of input or be followed by whitespace ** or what appears to be the end or separator of a logical ** natural-language construct, e.g. period, colon, etc. ** ** Current limitations of this implementation: ** ** - Currently requires starting alpha and trailing ** alphanumeric or underscores. "Should" be extended to ** handle #X[.Y], where X and optional Y are integer ** values, for forum post references. */ static size_t char_hashref_tag( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ size_t end; struct Blob work = BLOB_INITIALIZER; int nUscore = 0; /* Consecutive underscore counter */ int numberMode = 0 /* 0 for normal, 1 for #NNN numeric, and 2 for #NNN.NNN. */; if(offset>0 && !fossil_isspace(data[-1])){ /* Only ever match if the *previous* character is whitespace or ** we're at the start of the input. Note that we rely on fossil ** processing emphasis markup before reaching this function, so ** *#Hash* will Do The Right Thing. Not that this means that ** "#Hash." will match while ".#Hash" won't. That's okay. */ return 0; } assert( '#' == data[0] ); if(size < 2) return 0; end = 2; if(fossil_isdigit(data[1])){ numberMode = 1; }else if(!fossil_isalpha(data[1])){ switch(data[1] & 0xF0){ /* Reminder: UTF8 char lengths can be determined by ** masking against 0xF0: 0xf0==4, 0xe0==3, 0xc0==2, ** else 1. */ case 0xF0: end+=3; break; case 0xE0: end+=2; break; case 0xC0: end+=1; break; default: return 0; } } #if 0 fprintf(stderr,"HASHREF offset=%d size=%d: %.*s\n", (int)offset, (int)size, (int)size, data); #endif #define HASHTAG_LEGAL_END \ case ' ': case '\t': case '\r': case '\n': \ case ':': case ';': case '!': case '?': case ',' /* ^^^^ '.' is handled separately */ for(; end < size; ++end){ char ch = data[end]; switch(ch & 0xF0){ case 0xF0: end+=3; continue; case 0xE0: end+=2; continue; case 0xC0: end+=1; continue; case 0x80: goto hashref_bailout /*invalid UTF8*/; default: break; } #if 0 fprintf(stderr,"hashtag? checking... length=%d: %.*s\n", (int)end, (int)end, data); #endif switch(ch){ case '_': /* Multiple adjacent underscores not permitted. */ if(++nUscore>1) goto hashref_bailout; numberMode = 0; break; case '.': if(1==numberMode) ++numberMode; ch = 0; break; HASHTAG_LEGAL_END: ch = 0; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': nUscore = 0; break; default: if(numberMode || !fossil_isalpha(ch)){ goto hashref_bailout; } nUscore = 0; break; } if(ch) continue; break; } if((end<3/* #. or some such */ && !numberMode) || end>size/*from truncated multi-byte char*/){ return 0; } if(numberMode>1){ /* Check for trailing part of #NNN.nnn... */ assert('.'==data[end]); if(end<size-1 && fossil_isdigit(data[end+1])){ for(++end; end<size; ++end){ if(!fossil_isdigit(data[end])) break; } } } #if 0 fprintf(stderr,"???HASHREF length=%d: %.*s\n", (int)end, (int)end, data); #endif if(end<size){ /* Only match if we end at end of input or what "might" be the end of a natural language grammar construct, e.g. period or [semi]colon. */ switch(data[end]){ case '.': HASHTAG_LEGAL_END: break; default: goto hashref_bailout; } } blob_init(&work, data + 1, end - 1); rndr->make.tagspan(ob, &work, numberMode ? MKDT_NUMTAG : MKDT_HASHTAG, rndr->make.opaque); return end; hashref_bailout: #if 0 fprintf(stderr,"BAILING HASHREF examined=%d:\n[%.*s] of\n[%.*s]\n", (int)end, (int)end, data, (int)size, data); #endif #undef HASHTAG_LEGAL_END return 0; } /* ** char_langle_tag -- '<' when tags or autolinks are allowed. */ static size_t char_langle_tag( struct Blob *ob, struct render *rndr, |
︙ | ︙ | |||
2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 | for(i=0; rndr.make.emph_chars[i]; i++){ rndr.active_char[(unsigned char)rndr.make.emph_chars[i]] = char_emphasis; } } if( rndr.make.codespan ) rndr.active_char['`'] = char_codespan; if( rndr.make.linebreak ) rndr.active_char['\n'] = char_linebreak; if( rndr.make.image || rndr.make.link ) rndr.active_char['['] = char_link; if( rndr.make.footnote_ref ) rndr.active_char['('] = char_footnote; rndr.active_char['<'] = char_langle_tag; rndr.active_char['\\'] = char_escape; rndr.active_char['&'] = char_entity; /* first pass: iterate over lines looking for references, * copying everything else into "text" */ | > | 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 | for(i=0; rndr.make.emph_chars[i]; i++){ rndr.active_char[(unsigned char)rndr.make.emph_chars[i]] = char_emphasis; } } if( rndr.make.codespan ) rndr.active_char['`'] = char_codespan; if( rndr.make.linebreak ) rndr.active_char['\n'] = char_linebreak; if( rndr.make.image || rndr.make.link ) rndr.active_char['['] = char_link; rndr.active_char['#'] = char_hashref_tag; if( rndr.make.footnote_ref ) rndr.active_char['('] = char_footnote; rndr.active_char['<'] = char_langle_tag; rndr.active_char['\\'] = char_escape; rndr.active_char['&'] = char_entity; /* first pass: iterate over lines looking for references, * copying everything else into "text" */ |
︙ | ︙ |
Changes to src/markdown_html.c.
︙ | ︙ | |||
825 826 827 828 829 830 831 832 833 834 835 836 837 838 | if( link ) blob_appendb(ob, link); }else{ blob_appendb(ob, content); } blob_append(ob, zClose, -1); return 1; } static int html_triple_emphasis( struct Blob *ob, struct Blob *text, char c, void *opaque ){ | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 | if( link ) blob_appendb(ob, link); }else{ blob_appendb(ob, content); } blob_append(ob, zClose, -1); return 1; } /* Invoked for @name and #tag tagged words, marked up in the ** output text in a way that JS and CSS can do something ** interesting with them. This isn't standard Markdown, so ** it's implementation-specific what occurs here. More, each ** Fossil feature using Markdown is free to apply styling and ** behavior to these in feature-specific ways. */ static int html_tagspan( struct Blob *ob, /* Write the output here */ struct Blob *text, /* The word after the tag character */ enum mkd_tagspan type, /* Which type of tagspan we're creating */ void *opaque ){ if( text==0 ){ /* no-op */ }else{ char cPrefix = '!'; blob_append_literal(ob, "<span data-"); switch (type) { case MKDT_HASHTAG: cPrefix = '#'; blob_append_literal(ob, "hashtag"); break; case MKDT_NUMTAG: cPrefix = '#'; blob_append_literal(ob, "numtag"); break; } blob_append_literal(ob, "=\""); html_quote(ob, blob_buffer(text), blob_size(text)); blob_append_literal(ob, "\""); blob_appendf(ob, ">%c%b</span>", cPrefix,text); } return 1; } static int html_triple_emphasis( struct Blob *ob, struct Blob *text, char c, void *opaque ){ |
︙ | ︙ | |||
883 884 885 886 887 888 889 890 891 892 893 894 895 896 | html_codespan, html_double_emphasis, html_emphasis, html_image, html_linebreak, html_link, html_raw_html_tag, html_triple_emphasis, html_footnote_ref, /* low level elements */ 0, /* entity */ html_normal_text, | > | 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 | html_codespan, html_double_emphasis, html_emphasis, html_image, html_linebreak, html_link, html_raw_html_tag, html_tagspan, html_triple_emphasis, html_footnote_ref, /* low level elements */ 0, /* entity */ html_normal_text, |
︙ | ︙ |
Changes to src/style.chat.css.
︙ | ︙ | |||
621 622 623 624 625 626 627 628 629 630 631 632 633 634 | body.chat #chat-user-list:not(.timestamps) .timestamp { display: none; } body.chat #chat-user-list .chat-user.selected { font-weight: bold; text-decoration: underline; } body.chat .searchForm { margin-top: 1em; } body.chat .spacer-widget button { margin-left: 1ex; margin-right: 1ex; | > > > > > > > > > > > | 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 | body.chat #chat-user-list:not(.timestamps) .timestamp { display: none; } body.chat #chat-user-list .chat-user.selected { font-weight: bold; text-decoration: underline; } body.chat span[data-hashtag], body.chat span[data-numtag]{ font-family: monospace; text-decoration: underline; cursor: pointer; } body.chat #chat-clear-filter { margin: 0.25em 0.5em; } body.chat .searchForm { margin-top: 1em; } body.chat .spacer-widget button { margin-left: 1ex; margin-right: 1ex; |
︙ | ︙ | |||
658 659 660 661 662 663 664 | from { transform: rotateX(0deg); } to { transform: rotateX(360deg); } } body.chat .anim-fade-in { animation: fade-in 750ms linear; } body.chat .anim-fade-in-fast { | | | > > > > > > > | 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 | from { transform: rotateX(0deg); } to { transform: rotateX(360deg); } } body.chat .anim-fade-in { animation: fade-in 750ms linear; } body.chat .anim-fade-in-fast { animation: fade-in 300ms linear; } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } body.chat .anim-fade-out-fast { animation: fade-out 300ms linear; } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } body.chat .anim-fade-out-in { animation: fade-out-in 1000ms linear; } @keyframes fade-out-in { 0%,100% { opacity: 0 } 50% { opacity: 1 } } |
Changes to src/wikiformat.c.
︙ | ︙ | |||
1899 1900 1901 1902 1903 1904 1905 | wiki_convert(&in, &out, flags); blob_write_to_file(&out, "-"); } /* ** COMMAND: test-markdown-render ** | | | > > > | | | > > > > > > | 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 | wiki_convert(&in, &out, flags); blob_write_to_file(&out, "-"); } /* ** COMMAND: test-markdown-render ** ** Usage: %fossil test-markdown-render TEXT ... ** ** Render markdown in TEXT as HTML on stdout, where TEXT ** may be a file name or a markdown-formatted string. ** ** Options: ** ** --safe Restrict the output to use only "safe" HTML ** --lint-footnotes Print stats for footnotes-related issues ** --dark-pikchr Render pikchrs in dark mode */ void test_markdown_render(void){ Blob in, out; int i; int bSafe = 0, bFnLint = 0; db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); bSafe = find_option("safe",0,0)!=0; bFnLint = find_option("lint-footnotes",0,0)!=0; if( find_option("dark-pikchr",0,0)!=0 ){ pikchr_to_html_add_flags( PIKCHR_PROCESS_DARK_MODE ); } verify_all_options(); for(i=2; i<g.argc; i++){ blob_zero(&out); if(file_isfile(g.argv[i], ExtFILE)){ blob_read_from_file(&in, g.argv[i], ExtFILE); if( g.argc>3 ){ fossil_print("<!------ %h ------->\n", g.argv[i]); } }else{ blob_init(&in, g.argv[i], -1); if( g.argc>3 ){ fossil_print("<!------ script #%d ------->\n", i-1); } } markdown_to_html(&in, 0, &out); safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); safe_html(&out); blob_write_to_file(&out, "-"); blob_reset(&in); blob_reset(&out); |
︙ | ︙ |