Fossil

Changes On Branch markdown-tagrefs
Login

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
285
286

287
288
289
290
291
292
293

294
295
296
297
298
299
300
        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.

   */
  F.page.setPageTitle = function(title){
    const t = document.querySelector('title');
    if(t) t.innerText = title;
    return this;
  };


  /**
     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.








|
|
>

|

|


|
>







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
91
92
93
94
95
96
97
98
99
100
101
102
       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 bcl = document.body.classList;
    const resized = function f(){
      if(f.$disabled) return;
      const wh = window.innerHeight,
            com = bcl.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;







<



|







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
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
        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')

      },
      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) */
      },
      filterState:{

        activeUser: undefined,
        match: function(uname){












          return this.activeUser===uname || !this.activeUser;

        }


      },
      /**
         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







|
>


















|
>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
|
>
|
>
>







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
275

276
277
278
279
280
281
282
      /* 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.filterState.match(e.dataset.xfrom)){

          e.classList.add('hidden');
        }
        if(atEnd){
          const fe = mip.nextElementSibling;
          if(fe) mip.parentNode.insertBefore(e, fe);
          else D.append(mip.parentNode, e);
        }else{







|
>







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
497
498
499
500
501
502
503
504
        if(e===this.e.currentView){
          return e;
        }
        this.e.views.forEach(function(E){
          if(e!==E) D.addClass(E,'hidden');
        });
        this.e.currentView = e;
        if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow();
        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







<







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
526
527
528
529
530
531
532
533
            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.filterState.activeUser===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*/)







|







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






































































548

549
550
551
552
553
554
555
        //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;
      },






































































      /** 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')){







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>







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
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
        return this;
      },
      /**
         Applies user name filter to all current messages, or clears
         the filter if uname is falsy.
      */
      setUserFilter: function(uname){
        this.filterState.activeUser = uname;

        const mw = this.e.viewMessages.querySelectorAll('.message-widget');




        const self = this;
        let eLast;
        if(!uname){
          D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'),
                        'hidden');
        }else{
          mw.forEach(function(w){
            if(self.filterState.match(w.dataset.xfrom)){
              w.classList.remove('hidden');

              eLast = w;

            }else{
              w.classList.add('hidden');
            }







          });
        }
        if(eLast) eLast.scrollIntoView(false);

        else this.scrollMessagesTo(1);
        cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
          e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
        });

        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







|
>
|
>
>
>
>

<
<
<
<
<
|
<
|
>
|
>
|
|
|
>
>
>
>
>
>
>
|

<
>
|
<
<
<
>







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
880
881
882
883
884
885
886
887
888





889
890
891


















































































892
893
894
895
896
897
898
      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);
        delete f.$eSelected;
      }else{
        if(f.$eSelected) f.$eSelected.classList.remove('selected');
        f.$eSelected = eUser;
        eUser.classList.add('selected');
        cs.setUserFilter(uname);
      }
      return false;
    }, false);





    return cs;
  })()/*Chat initialization*/;




















































































  /** 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;
    }







<

<
<





>
>
>
>
>



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
                  D.a(F.repoUrl('timeline',{
                    u: eMsg.dataset.xfrom,
                    y: 'a'
                  }), "User's Timeline"),
                  'target', '_blank'
                );
                D.append(toolbar2, timelineLink);
                if(Chat.filterState.activeUser &&
                   Chat.filterState.match(eMsg.dataset.xfrom)){
                  /* Add a button to clear user 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.setUserFilter(false);
                          eMsg.scrollIntoView(false);
                          Chat.animate(
                            eMsg.firstElementChild, 'anim-flip-h'
                            //eMsg.firstElementChild, 'anim-flip-v'
                            //eMsg.childNodes, 'anim-rotate-360'
                            //eMsg.childNodes, 'anim-flip-v'
                            //eMsg, 'anim-flip-v'
                          );
                        })
                    )
                  );
                }/*jump-to button*/
              }
              const tab = eMsg.querySelector('.message-widget-tab');
              D.append(tab, this.e);







|
<
|









|
<
|
<
<
<
<
<
<







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
1820
1821
1822
1823

1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835

1836
1837
1838
1839
1840
1841
1842
      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 menu entries... the most frequently-needed ones "should"
       (arguably) be closer to the start of this list. */
    /**
       Settings ops structure:


       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.

       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.







<
<

|
>












>







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
2393
2394
2395
2396

2397
2398
2399
2400
2401
2402
2403
2404
      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 the main input field's current
     text. It is assumed that Chat.e.viewSearch===Chat.e.currentView.
  */
  Chat.submitSearch = function(){

    const 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", {







|
|

|
>
|







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
665
666
667
668
669
670
671
672
673
674
675
676
677
678







  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 350ms linear;
}
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
body.chat .anim-fade-out-fast {
  animation: fade-out 250ms linear;
}
@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}















|






|






>
>
>
>
>
>
>
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
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
  wiki_convert(&in, &out, flags);
  blob_write_to_file(&out, "-");
}

/*
** COMMAND: test-markdown-render
**
** Usage: %fossil test-markdown-render FILE ...
**
** Render markdown in FILE as HTML on stdout.


** 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);

    blob_read_from_file(&in, g.argv[i], ExtFILE);
    if( g.argc>3 ){
      fossil_print("<!------ %h ------->\n", g.argv[i]);






    }
    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);







|

|
>
>



















>
|
|
|
>
>
>
>
>
>







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);