Fossil

Check-in [b699040d]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Improvements to line-numbered text output.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: b699040d701464ced678af7a7c353871d23440dd505fb9c8ef68043eed388271
User & Date: drh 2020-08-16 13:39:35
References
2020-08-16
16:06
Fix segfault in /artifact introduced by check-in [b699040d701464ce] and reported on the forum. ... (check-in: 5a9ac6ca user: drh tags: trunk)
Context
2020-08-16
14:08
When compiling with FOSSIL_DEBUG (from the --fossil-debug configure option) do not attempt to "compress" built-in javascript by removing comments and surplus whitespace. This makes the javascript easier to read in a debugger. ... (check-in: 41b9873b user: drh tags: trunk)
13:39
Improvements to line-numbered text output. ... (check-in: b699040d user: drh tags: trunk)
13:33
Fix scrolling bug in FF and Chrome. ... (check-in: 22f2d083 user: drh tags: trunk)
08:13
Removed a now-unused CSS class. ... (Closed-Leaf check-in: 1cb792fd user: stephan tags: line-number-selection)
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to skins/ardoise/css.txt.

570
571
572
573
574
575
576



577
578
579
580
581
582
583
  margin: 0 .2rem;
  font-size: 90%;
  white-space: nowrap;
  background: #000;
  border: 2px solid #bbb;
  border-radius: 5px
}



pre > code {
  padding: 1rem 1.5rem;
  white-space: pre
}
td,
th {
  padding: 1px 5px;







>
>
>







570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
  margin: 0 .2rem;
  font-size: 90%;
  white-space: nowrap;
  background: #000;
  border: 2px solid #bbb;
  border-radius: 5px
}
table.numbered-lines td.file-content > pre {
  margin-top: -2px/*offset CODE tag border*/;
}
pre > code {
  padding: 1rem 1.5rem;
  white-space: pre
}
td,
th {
  padding: 1px 5px;

Changes to skins/eagle/css.txt.

397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
  font-family: "courier new";
}

div.filetreeline:hover {
  background-color: #7EA2D9;
}

div.selectedText {
  background-color: #7EA2D9;
}

.statistics-report-graph-line {
  background-color: #7EA2D9;
}








|







397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
  font-family: "courier new";
}

div.filetreeline:hover {
  background-color: #7EA2D9;
}

table.numbered-lines td.line-numbers span.selected-line {
  background-color: #7EA2D9;
}

.statistics-report-graph-line {
  background-color: #7EA2D9;
}

Changes to skins/xekri/css.txt.

998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016


/**************************************
 * Did not encounter these
 */

/* selected lines of text within a linenumbered artifact display */
div.selectedText {
  font-weight: bold;
  color: #00f;
  background-color: #d5d5ff;
  border: 1px #00f solid;
}

/* format for missing privileges note on user setup page */
p.missingPriv {
  color: #00f;
}








|



|







998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016


/**************************************
 * Did not encounter these
 */

/* selected lines of text within a linenumbered artifact display */
table.numbered-lines td.line-numbers span.selected-line {
  font-weight: bold;
  color: #00f;
  background-color: #d5d5ff;
  border-color: #00f;
}

/* format for missing privileges note on user setup page */
p.missingPriv {
  color: #00f;
}

Changes to src/ajax.c.

128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
    case AJAX_RENDER_WIKI:
      safe_html_context(DOCSRC_FILE);
      wiki_render_by_mimetype(pContent, zMime);
      break;
    default:{
      const char *zContent = blob_str(pContent);
      if(AJAX_PREVIEW_LINE_NUMBERS & flags){
        output_text_with_line_numbers(zContent, "on");

      }else{
        const char *zExt = strrchr(zName,'.');
        if(zExt && zExt[1]){
          CX("<pre><code class='language-%s'>%h</code></pre>",
             zExt+1, zContent);
        }else{
          CX("<pre>%h</pre>", zContent);







|
>







128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    case AJAX_RENDER_WIKI:
      safe_html_context(DOCSRC_FILE);
      wiki_render_by_mimetype(pContent, zMime);
      break;
    default:{
      const char *zContent = blob_str(pContent);
      if(AJAX_PREVIEW_LINE_NUMBERS & flags){
        output_text_with_line_numbers(zContent, blob_size(pContent),
                                      zName, "on");
      }else{
        const char *zExt = strrchr(zName,'.');
        if(zExt && zExt[1]){
          CX("<pre><code class='language-%s'>%h</code></pre>",
             zExt+1, zContent);
        }else{
          CX("<pre>%h</pre>", zContent);

Changes to src/attach.c.

615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
  blob_zero(&attach);
  if( fShowContent ){
    const char *z;
    content_get(ridSrc, &attach);
    blob_to_utf8_no_bom(&attach, 0);
    z = blob_str(&attach);
    if( zLn ){
      output_text_with_line_numbers(z, zLn);
    }else{
      @ <pre>
      @ %h(z)
      @ </pre>
    }
  }else if( strncmp(zMime, "image/", 6)==0 ){
    int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);







|







615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
  blob_zero(&attach);
  if( fShowContent ){
    const char *z;
    content_get(ridSrc, &attach);
    blob_to_utf8_no_bom(&attach, 0);
    z = blob_str(&attach);
    if( zLn ){
      output_text_with_line_numbers(z, blob_size(&attach), zName, zLn);
    }else{
      @ <pre>
      @ %h(z)
      @ </pre>
    }
  }else if( strncmp(zMime, "image/", 6)==0 ){
    int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);

Changes to src/default.css.

438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
}
span.usertype:before {
  content:"'";
}
span.usertype:after {
  content:"'";
}
div.selectedText {
  font-weight: bold;
  color: blue;
  background-color: #d5d5ff;
  border: 1px blue solid;
}
p.missingPriv {
 color: blue;
}
span.wikiruleHead {
  font-weight: bold;
}
td.tktDspLabel {







<
<
<
<
<
<







438
439
440
441
442
443
444






445
446
447
448
449
450
451
}
span.usertype:before {
  content:"'";
}
span.usertype:after {
  content:"'";
}






p.missingPriv {
 color: blue;
}
span.wikiruleHead {
  font-weight: bold;
}
td.tktDspLabel {
951
952
953
954
955
956
957





958
959
960
961
962
963
964
965
966
967
968
}
.warning {
  color: darkred;
  background: yellow;
  opacity: 0.7;
}
.hidden {





  position: absolute;
  opacity: 0;
  pointer-events: none;
  display: none;
}
input {
  max-width: 95%;
}
textarea {
  max-width: 95%;
}







>
>
>
>
>
|
|
|
|







945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
}
.warning {
  color: darkred;
  background: yellow;
  opacity: 0.7;
}
.hidden {
  /* The framework-wide way of hiding elements is to assign them this
     CSS class. To make them visible again, remove it. The !important
     qualifiers are unfortunate but sometimes necessary when hidden
     element has other classes which specify visibility-related
     options. */
  position: absolute !important;
  opacity: 0 !important;
  pointer-events: none !important;
  display: none !important;
}
input {
  max-width: 95%;
}
textarea {
  max-width: 95%;
}
1146
1147
1148
1149
1150
1151
1152






























































































































  vertical-align: sub;
}
.input-with-label > label {
  font-weight: initial;
  margin: 0 0.25em 0 0.25em;
  vertical-align: middle;
}





































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
  vertical-align: sub;
}
.input-with-label > label {
  font-weight: initial;
  margin: 0 0.25em 0 0.25em;
  vertical-align: middle;
}

table.numbered-lines {
  width: 100%;
  table-layout: fixed /* required to keep ultra-wide code from exceeding
                         window width, and instead force a scrollbar
                         on them. */;
}
table.numbered-lines > tbody > tr {
  font-family: monospace;
  font-size: 1.2em;
  line-height: 1.35;
  white-space: pre;
}
table.numbered-lines > tbody > tr > td {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  white-space: inherit;
  margin: 0;
  vertical-align: top;
  padding: 0.25em 0 0 0 /*prevents slight overlap at top */;
}
table.numbered-lines td.line-numbers {
  width: 4.5em;
}
table.numbered-lines td.line-numbers > span:first-of-type {
  margin-top: 0.25em/*must match top PADDING of
                      td.file-content > pre > code*/;
}
table.numbered-lines td.line-numbers > span {
  display: block;
  margin: 0;
  padding: 0;
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
  cursor: pointer;
  white-space: pre;
  margin-right: 2px/*keep selection from nudging the right column */;
  text-align: right;
}
table.numbered-lines td.line-numbers > span:hover {
  background-color: rgba(112, 112, 112, 0.25);
}
table.numbered-lines td.file-content {
  padding-left: 0.25em;
}
table.numbered-lines td.file-content > pre,
table.numbered-lines td.file-content > pre > code {
  margin: 0;
  padding: 0;
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
  white-space: pre;
  display: block/*necessary for certain skins!*/;
}
table.numbered-lines td.file-content > pre {
}
table.numbered-lines td.file-content > pre > code {
  overflow: auto;
  padding-left: 0.5em;
  padding-right: 0.5em;
  padding-top: 0.25em/*any top padding here must match the top MARGIN of
                       td.line-numbers's first span child or the
                       lines/code will get misaligned. */;
  padding-bottom: 0.25em/*prevents a slight overlap at bottom from
                          triggering a scroller*/;
}
table.numbered-lines td.file-content > pre > code > * {
  /* Defense against syntax highlighters indirectly messing up these
     properties... */
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
}
table.numbered-lines td.line-numbers span.selected-line/*replacement*/ {
  font-weight: bold;
  color: blue;
  background-color: #d5d5ff;
  border: 1px blue solid;
  border-top-width: 0;
  border-bottom-width: 0;
  padding: 0;
  margin: 0;
}
table.numbered-lines td.line-numbers span.selected-line.start {
  border-top-width: 1px;
  margin-top: -1px/*restore alignment*/;
}
table.numbered-lines td.line-numbers span.selected-line.end {
  border-bottom-width: 1px;
  margin-top: -1px/*restore alignment*/;
}
table.numbered-lines td.line-numbers span.selected-line.start.end {
  margin-top: -2px/*restore alignment*/;
}

.fossil-tooltip {
  text-align: center;
  padding: 0.2em 1em;
  border: 1px solid black;
  border-radius: 0.25em;
  position: absolute;
  display: inline-block;
  z-index: 100;
  box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
  background-color: inherit;
  font-size: 80%;
}

.fossil-toast {/* "toast"-style popup message */
  padding: 0.25em 0.5em;
  margin: 0;
  border-radius: 0.25em;
  font-size: 1em;
  opacity: 0.8;
  border-size: 1px;
  border-style: dotted;
  border-color: rgb( 127, 127, 127, 0.5 );
}

blockquote.file-content {
  /* file content block in the /file page */
  margin: 0 1em;
}

Changes to src/diff.c.

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/*
** Count the number of lines in the input string.  Include the last line
** in the count even if it lacks the \n terminator.  If an empty string
** is specified, the number of lines is zero.  For the purposes of this
** function, a string is considered empty if it contains no characters
** -OR- it contains only NUL characters.
*/
static int count_lines(
  const char *z,
  int n,
  int *pnLine
){
  int nLine;
  const char *zNL, *z2;
  for(nLine=0, z2=z; (zNL = strchr(z2,'\n'))!=0; z2=zNL+1, nLine++){}







|







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/*
** Count the number of lines in the input string.  Include the last line
** in the count even if it lacks the \n terminator.  If an empty string
** is specified, the number of lines is zero.  For the purposes of this
** function, a string is considered empty if it contains no characters
** -OR- it contains only NUL characters.
*/
int count_lines(
  const char *z,
  int n,
  int *pnLine
){
  int nLine;
  const char *zNL, *z2;
  for(nLine=0, z2=z; (zNL = strchr(z2,'\n'))!=0; z2=zNL+1, nLine++){}

Changes to src/file.c.

2395
2396
2397
2398
2399
2400
2401









  if( dryRunFlag!=0 ){
    fossil_print("dry-run: would have touched %d file(s)\n",
                 changeCount);
  }else{
    fossil_print("Touched %d file(s)\n", changeCount);
  }
}
















>
>
>
>
>
>
>
>
>
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
  if( dryRunFlag!=0 ){
    fossil_print("dry-run: would have touched %d file(s)\n",
                 changeCount);
  }else{
    fossil_print("Touched %d file(s)\n", changeCount);
  }
}

/*
** If zFileName is not NULL and contains a '.', this returns a pointer
** to the position after the final '.', else it returns NULL.
*/
const char * file_extension(const char *zFileName){
  const char * zExt = strrchr(zFileName, '.');
  return zExt ? &zExt[1] : 0;
}

Changes to src/fossil.bootstrap.js.

1












2
3
4
5
6
7
8
"use strict";












(function(global){
  /* Bootstrapping bits for the global.fossil object. Must be
     loaded after style.c:style_emit_script_tag() has initialized
     that object.
  */

  const F = global.fossil;

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







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"use strict";
(function () {
  /* CustomEvent polyfill, courtesy of Mozilla:
     https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
  */
  if(typeof window.CustomEvent === "function") return false;
  window.CustomEvent = function(event, params) {
    if(!params) params = {bubbles: false, cancelable: false, detail: null};
    const evt = document.createEvent('CustomEvent');
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
  };
})();
(function(global){
  /* Bootstrapping bits for the global.fossil object. Must be
     loaded after style.c:style_emit_script_tag() has initialized
     that object.
  */

  const F = global.fossil;

Added src/fossil.copybutton.js.









































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
(function(F/*fossil object*/){
  /**
     A basic API for creating and managing a copy-to-clipboard button.

     Requires: fossil.bootstrap, fossil.dom
  */
  const D = F.dom;

  /**
     Initializes element e as a copy button using the given options
     object.

     The first argument may be a DOM element or a string (CSS selector
     suitable for use with document.querySelector()).

     Options:

     .copyFromElement: DOM element

     .copyFromId: DOM element ID

     One of copyFromElement or copyFromId must be provided, but copyFromId
     may optionally be provided via e.dataset.copyFromId.

     .extractText: optional callback which is triggered when the copy
     button is clicked. I tmust return the text to copy to the
     clipboard. The default is to extract it from the copy-from
     element, using its [value] member, if it has one, else its
     [innerText]. A client-provided callback may use any data source
     it likes, so long as it's synchronous. If this function returns a
     falsy value then the clipboard is not modified. This function is
     called with the fully expanded/resolved options object as its
     "this" (that's a different instance than the one passed to this
     function!).

     .cssClass: optional CSS class, or list of classes, to apply to e.

     .style: optional object of properties to copy directly into
     e.style.

     .oncopy: an optional callback function which is added as an event
     listener for the 'text-copied' event (see below). There is
     functionally no difference from setting this option or adding a
     'text-copied' event listener to the element, and this option is
     considered to be a convenience form of that.

     Note that this function's own defaultOptions object holds default
     values for some options. Any changes made to that object affect
     any future calls to this function.

     Be aware that clipboard functionality might or might not be
     available in any given environment. If this button appears to
     have no effect, that may be because it is not enabled/available
     in the current platform.

     The copy button emits custom event 'text-copied' after it has
     successfully copied text to the clipboard. The event's "detail"
     member is an object with a "text" property holding the copied
     text. Other properties may be added in the future. The event is
     not fired if copying to the clipboard fails (e.g. is not
     available in the current environment).

     Returns the copy-initialized element.

     Example:

     const button = fossil.copyButton('#my-copy-button', {
       copyFromId: 'some-other-element-id'
     });
     button.addEventListener('text-copied',function(ev){
       fossil.dom.flashOnce(ev.target);
       console.debug("Copied text:",ev.detail.text);
     });
  */
  F.copyButton = function f(e, opt){
    if('string'===typeof e){
      e = document.querySelector(e);
    }    
    opt = F.mergeLastWins(f.defaultOptions, opt);
    if(opt.cssClass){
      D.addClass(e, opt.cssClass);
    }
    var srcId, srcElem;
    if(opt.copyFromElement){
      srcElem = opt.copyFromElement;
    }else if((srcId = opt.copyFromId || e.dataset.copyFromId)){
      srcElem = document.querySelector('#'+srcId);
    }
    const extract = opt.extractText || (
      undefined===srcElem.value ? ()=>srcElem.innerText : ()=>srcElem.value
    );
    D.copyStyle(e, opt.style);
    e.addEventListener(
      'click',
      function(){
        const txt = extract.call(opt);
        if(txt && D.copyTextToClipboard(txt)){
          e.dispatchEvent(new CustomEvent('text-copied',{
            detail: {text: txt}
          }));
        }
      },
      false
    );
    if('function' === typeof opt.oncopy){
      e.addEventListener('text-copied', opt.oncopy, false);
    }
    return e;
  };

  F.copyButton.defaultOptions = {
    cssClass: 'copy-button',
    style: {/*properties copied as-is into element.style*/}
  };
  
})(window.fossil);

Changes to src/fossil.dom.js.

463
464
465
466
467
468
469
470
471
472
473





























































































474
475
  dom.selectOne = function(x,origin){
    var src = origin || document,
        e = src.querySelector(x);
    if(!e){
      e = new Error("Cannot find DOM element: "+x);
      console.error(e, src);
      throw e;
    }
    return e;
  };






























































































  return F.dom = dom;
})(window.fossil);











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


463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
  dom.selectOne = function(x,origin){
    var src = origin || document,
        e = src.querySelector(x);
    if(!e){
      e = new Error("Cannot find DOM element: "+x);
      console.error(e, src);
      throw e;
    }
    return e;
  };

  /**
     "Blinks" the given element a single time for the given number of
     milliseconds, defaulting (if the 2nd argument is falsy or not a
     number) to flashOnce.defaultTimeMs. If a 3rd argument is passed
     in, it must be a function, and it gets callback back at the end
     of the asynchronous flashing processes.

     This will only activate once per element during that timeframe -
     further calls will become no-ops until the blink is
     completed. This routine adds a dataset member to the element for
     the duration of the blink, to allow it to block multiple blinks.

     If passed 2 arguments and the 2nd is a function, it behaves as if
     it were called as (arg1, undefined, arg2).

     Returns e, noting that the flash itself is asynchronous and may
     still be running, or not yet started, when this function returns.
  */
  dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
    if(e.dataset.isBlinking){
      return;
    }
    if(2===arguments.length && 'function' ===typeof howLongMs){
      afterFlashCallback = howLongMs;
      howLongMs = f.defaultTimeMs;
    }
    if(!howLongMs || 'number'!==typeof howLongMs){
      howLongMs = f.defaultTimeMs;
    }
    e.dataset.isBlinking = true;
    const transition = e.style.transition;
    e.style.transition = "opacity "+howLongMs+"ms ease-in-out";
    const opacity = e.style.opacity;
    e.style.opacity = 0;
    setTimeout(function(){
      e.style.transition = transition;
      e.style.opacity = opacity;
      delete e.dataset.isBlinking;
      if(afterFlashCallback) afterFlashCallback();
    }, howLongMs);
    return e;
  };
  dom.flashOnce.defaultTimeMs = 400;

  /**
     Attempts to copy the given text to the system clipboard. Returns
     true if it succeeds, else false.
  */
  dom.copyTextToClipboard = function(text){
    if( window.clipboardData && window.clipboardData.setData ){
      clipboardData.setData('Text',text);
      return true;
    }else{
      const x = document.createElement("textarea");
      x.style.position = 'fixed';
      x.value = text;
      document.body.appendChild(x);
      x.select();
      var rc;
      try{
        document.execCommand('copy');
        rc = true;
      }catch(err){
        rc = false;
      }finally{
        document.body.removeChild(x);
      }
      return rc;
    }
  };

  /**
     Copies all properties from the 2nd argument (a plain object) into
     the style member of the first argument (DOM element or a
     forEach-capable list of elements). If the 2nd argument is falsy
     or empty, this is a no-op.

     Returns its first argument.
  */
  dom.copyStyle = function f(e, style){
    if(e.forEach){
      e.forEach((x)=>f(x, style));
      return e;
    }
    if(style){
      let k;
      for(k in style){
        if(style.hasOwnProperty(k)) e.style[k] = style[k];
      }
    }
    return e;
  };

  return F.dom = dom;
})(window.fossil);

Added src/fossil.numbered-lines.js.























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
(function callee(arg){
  /*
    JS counterpart of info.c:output_text_with_line_numbers()
    which ties an event handler to the line numbers to allow
    selection of individual lines or ranges.

    Requires: fossil.bootstrap, fossil.dom, fossil.popupwidget,
    fossil.copybutton
  */
  var tbl = arg || document.querySelectorAll('table.numbered-lines');
  if(!tbl) return /* no matching elements */;
  else if(!arg){
    if(tbl.length>1){ /* multiple query results: recurse */
      tbl.forEach( (t)=>callee(t) );
      return;
    }else{/* single query result */
      tbl = tbl[0];
    }
  }
  const F = window.fossil, D = F.dom;
  const tdLn = tbl.querySelector('td.line-numbers');
  const lineState = {
    urlArgs: (window.location.search||'?').replace(/&?\bln=[^&]*/,''),
    start: 0, end: 0
  };

  const lineTip = new fossil.PopupWidget({
    style: {
      cursor: 'pointer'
    },
    refresh: function(){
      const link = this.state.link;
      D.clearElement(link);
      if(lineState.start){
        const ls = [lineState.start];
        if(lineState.end) ls.push(lineState.end);
        link.dataset.url = (
          window.location.toString().split('?')[0]
            + lineState.urlArgs + '&ln='+ls.join('-')
        );
        D.append(
          D.clearElement(link),
          ' ',
          (ls.length===1 ? 'line ' : 'lines ')+ls.join('-')
        );
      }else{
        D.append(link, "No lines selected.");
      }
    },
    init: function(){
      const e = this.e;
      const btnCopy = D.span(),
            link = D.span();
      this.state = {link};
      F.copyButton(btnCopy,{
        copyFromElement: link,
        extractText: ()=>link.dataset.url,
        oncopy: (ev)=>{
          D.flashOnce(ev.target, undefined, ()=>lineTip.hide());
          F.toast("Copied link to clipboard.");
        }
      });
      this.e.addEventListener('click', ()=>btnCopy.click(), false);
      D.append(this.e, btnCopy, link)
    }
  });

  tbl.addEventListener('click', ()=>lineTip.hide(), true);
  
  tdLn.addEventListener('click', function f(ev){
    if('SPAN'!==ev.target.tagName) return;
    else if('number' !== typeof f.mode){
      f.mode = 0 /*0=none selected, 1=1 selected, 2=2 selected*/;
      f.spans = tdLn.querySelectorAll('span');
      f.selected = tdLn.querySelectorAll('span.selected-line');
      f.unselect = (e)=>D.removeClass(e, 'selected-line','start','end');
    }
    ev.stopPropagation();
    const ln = +ev.target.innerText;
    if(2===f.mode){/*Reset selection*/
      f.mode = 0;
    }
    if(0===f.mode){/*Select single line*/
      lineState.end = 0;
      lineState.start = ln;
      f.mode = 1;
    }else if(1===f.mode){
      if(ln === lineState.start){/*Unselect line*/
        lineState.start = 0;
        f.mode = 0;
      }else{/*Select range*/
        if(ln<lineState.start){
          lineState.end = lineState.start;
          lineState.start = ln;
        }else{
          lineState.end = ln;
        }
        f.mode = 2;
      }
    }
    if(f.selected){/*Unmark previously-selected lines.*/
      f.selected.forEach(f.unselect);
      f.selected = undefined;
    }
    if(0===f.mode){
      lineTip.hide();
    }else{/*Mark selected lines*/
      const rect = ev.target.getBoundingClientRect();
      f.selected = [];
      if(f.spans.length>=lineState.start){
        let i = lineState.start, end = lineState.end || lineState.start, span = f.spans[i-1];
        for( ; i<=end && span; span = f.spans[i++] ){
          span.classList.add('selected-line');
          f.selected.push(span);
          if(i===lineState.start) span.classList.add('start');
          if(i===end) span.classList.add('end');
        }
      }
      lineTip.refresh().show(rect.right+3, rect.top-4);
    }
  }, false);
  
})();

Added src/fossil.popupwidget.js.











































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
(function(F/*fossil object*/){
  /**
     A very basic tooltip-like widget. It's intended to be popped up
     to display basic information or basic user interaction
     components, e.g. a copy-to-clipboard button.

     Requires: fossil.bootstrap, fossil.dom
  */
  const D = F.dom;

  /**
     Creates a new tooltip-like widget using the given options object.

     Options:

     .refresh: callback which is called just before the tooltip is
     revealed or moved. It must refresh the contents of the tooltip,
     if needed, by applying the content to/within this.e, which is the
     base DOM element for the tooltip (and is a child of
     document.body). If the contents are static and set up via the
     .init option then this callback is not needed.

     .adjustX: an optional callback which is called when the tooltip
     is to be displayed at a given position and passed the X
     viewport-relative coordinate. This routine must either return its
     argument as-is or return an adjusted value. The intent is to
     allow a given tooltip may be positioned more appropriately for a
     given context, if needed (noting that the desired position can,
     and probably should, be passed to the show() method
     instead). This class's API assumes that clients give it
     viewport-relative coordinates, and it will take care to translate
     those to page-relative, so this callback should not do so.

     .adjustY: the Y counterpart of adjustX.

     .init: optional callback called one time to initialize the state
     of the tooltip. This is called after the this.e has been created
     and added (initially hidden) to the DOM. If this is called, it is
     removed from the object immediately after it is called.

     All callback options are called with the PopupWidget object as
     their "this".


     .cssClass: optional CSS class, or list of classes, to apply to
     the new element.

     .style: optional object of properties to copy directly into
     the element's style object.     

     The options passed to this constructor get normalized into a
     separate object which includes any default values for options not
     provided by the caller. That object is available this the
     resulting PopupWidget's options property. Default values for any
     options not provided by the caller are pulled from
     PopupWidget.defaultOptions, and modifying those affects all
     future calls to this method but has no effect on existing
     instances.


     Example:

     const tip = new fossil.PopupWidget({
       init: function(){
         // optionally populate DOM element this.e with the widget's
         // content.
       },
       refresh: function(){
         // (re)populate/refresh the contents of the main
         // wrapper element, this.e.
       }
     });

     tip.show(50, 100);
     // ^^^ viewport-relative coordinates. See show() for other options.

  */
  F.PopupWidget = function f(opt){
    opt = F.mergeLastWins(f.defaultOptions,opt);
    this.options = opt;
    const e = this.e = D.addClass(D.div(), opt.cssClass);
    this.show(false);
    if(opt.style){
      let k;
      for(k in opt.style){
        if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
      }
    }
    D.append(document.body, e/*must be in the DOM for size calc. to work*/);
    D.copyStyle(e, opt.style);
    if(opt.init){
      opt.init.call(this);
      delete opt.init;
    }
  };

  /**
     Default options for the PopupWidget constructor. These values are
     used for any options not provided by the caller. Any changes made
     to this instace affect future calls to PopupWidget() but have no
     effect on existing instances.
  */
  F.PopupWidget.defaultOptions = {
    cssClass: 'fossil-tooltip',
    style: undefined /*{optional properties copied as-is into element.style}*/,
    adjustX: (x)=>x,
    adjustY: (y)=>y,
    refresh: function(){},
    init: undefined /* optional initialization function */
  };

  F.PopupWidget.prototype = {

    /** Returns true if the widget is currently being shown, else false. */
    isShown: function(){return !this.e.classList.contains('hidden')},

    /** Calls the refresh() method of the options object and returns
        this object. */
    refresh: function(){
      if(this.options.refresh){
        this.options.refresh.call(this);
      }
      return this;
    },

    /**
       Shows or hides the tooltip.

       Usages:

       (bool showIt) => hide it or reveal it at its last position.

       (x, y) => reveal/move it at/to the given
       relative-to-the-viewport position, which will be adjusted to make
       it page-relative.

       (DOM element) => reveal/move it at/to a position based on the
       the given element (adjusted slightly).

       For the latter two, this.options.adjustX() and adjustY() will
       be called to adjust it further.

       Returns this object.

       Sidebar: showing/hiding the widget is, as is conventional for
       this framework, done by removing/adding the 'hidden' CSS class
       to it, so that class must be defined appropriately.
    */
    show: function(){
      var x = undefined, y = undefined, showIt;
      if(2===arguments.length){
        x = arguments[0];
        y = arguments[1];
        showIt = true;
      }else if(1===arguments.length){
        if(arguments[0] instanceof HTMLElement){
          const p = arguments[0];
          const r = p.getBoundingClientRect();
          x = r.x + r.x/5;
          y = r.y - r.height/2;
          showIt = true;
        }else{
          showIt = !!arguments[0];
        }
      }
      if(showIt){
        this.refresh();
        x = this.options.adjustX.call(this,x);
        y = this.options.adjustY.call(this,y);
        x += window.pageXOffset;
        y += window.pageYOffset;
      }
      if(showIt){
        if('number'===typeof x && 'number'===typeof y){
          this.e.style.left = x+"px";
          this.e.style.top = y+"px";
        }
        D.removeClass(this.e, 'hidden');
      }else{
        D.addClass(this.e, 'hidden');
        delete this.e.style.removeProperty('left');
        delete this.e.style.removeProperty('top');
      }
      return this;
    },

    hide: function(){return this.show(false)}
  }/*F.PopupWidget.prototype*/;

  /**
     Convenience wrapper around a PopupWidget which pops up a shared
     PopupWidget instance to show toast-style messages (commonly seen
     on Android). Its arguments may be anything suitable for passing
     to fossil.dom.append(), and each argument is first append()ed to
     the toast widget, then the widget is shown for
     F.toast.config.displayTimeMs milliseconds. This is called while
     a toast is currently being displayed, the first will be overwritten
     and the time until the message is hidden will be reset.

     The toast is always shown at the viewport-relative coordinates
     defined by the F.toast.config.position.

     The toaster's DOM element has the CSS classes fossil-tooltip
     and fossil-toast, so can be style via those.
  */
  F.toast = function f(/*...*/){
    if(!f.toast){
      f.toast = function ff(argsObject){
        if(!ff.toaster) ff.toaster = new F.PopupWidget({
          cssClass: ['fossil-tooltip', 'fossil-toast']
        });
        if(f._timer) clearTimeout(f._timer);
        D.clearElement(ff.toaster.e);
        var i = 0;
        for( ; i < argsObject.length; ++i ){
          D.append(ff.toaster.e, argsObject[i]);
        };
        ff.toaster.show(f.config.position.x, f.config.position.y);
        f._timer = setTimeout(()=>ff.toaster.hide(), f.config.displayTimeMs);
      };
    }
    f.toast(arguments);
  };
  F.toast.config = {
    position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
    displayTimeMs: 2500
  };

})(window.fossil);

Changes to src/info.c.

2009
2010
2011
2012
2013
2014
2015
2016

2017




2018
2019
2020
2021
2022
2023
2024
2025
2026


2027
2028
2029
2030
2031
2032



2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051

2052
2053
2054









2055

2056
2057
2058

2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069


2070
2071
2072
2073
2074
2075













2076




2077
2078
2079
2080
2081
2082
2083
2084
2085
2086



2087
2088
2089
2090
2091
2092
2093
2094
2095


2096
2097


























2098
2099
2100
2101
2102
2103
2104
    }
  }
  manifest_destroy(pManifest);
  return rid;
}

/*
** The "z" argument is a string that contains the text of a source code

** file.  This routine appends that text to the HTTP reply with line numbering.




**
** zLn is the ?ln= parameter for the HTTP query.  If there is an argument,
** then highlight that line number and scroll to it once the page loads.
** If there are two line numbers, highlight the range of lines.
** Multiple ranges can be highlighed by adding additional line numbers
** separated by a non-digit character (also not one of [-,.]).
*/
void output_text_with_line_numbers(
  const char *z,


  const char *zLn
){
  int iStart, iEnd;    /* Start and end of region to highlight */
  int n = 0;           /* Current line number */
  int i = 0;           /* Loop index */
  int iTop = 0;        /* Scroll so that this line is on top of screen. */



  Stmt q;

  iStart = iEnd = atoi(zLn);
  db_multi_exec(
    "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)");
  if( iStart>0 ){
    do{
      while( fossil_isdigit(zLn[i]) ) i++;
      if( zLn[i]==',' || zLn[i]=='-' || zLn[i]=='.' ){
        i++;
        while( zLn[i]=='.' ){ i++; }
        iEnd = atoi(&zLn[i]);
        while( fossil_isdigit(zLn[i]) ) i++;
      }
      while( fossil_isdigit(zLn[i]) ) i++;
      if( iEnd<iStart ) iEnd = iStart;
      db_multi_exec(
        "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
      );

      iStart = iEnd = atoi(&zLn[i++]);
    }while( zLn[i] && iStart && iEnd );
  }









  db_prepare(&q, "SELECT min(iStart), max(iEnd) FROM lnos");

  if( db_step(&q)==SQLITE_ROW ){
    iStart = db_column_int(&q, 0);
    iEnd = db_column_int(&q, 1);

    iTop = iStart - 15 + (iEnd-iStart)/4;
    if( iTop>iStart - 2 ) iTop = iStart-2;
  }
  db_finalize(&q);
  @ <pre>
  while( z[0] ){
    n++;
    db_prepare(&q,
      "SELECT min(iStart), max(iEnd) FROM lnos"
      " WHERE iStart <= %d AND iEnd >= %d", n, n);
    if( db_step(&q)==SQLITE_ROW ){


      iStart = db_column_int(&q, 0);
      iEnd = db_column_int(&q, 1);
    }
    db_finalize(&q);
    for(i=0; z[i] && z[i]!='\n'; i++){}
    if( n==iTop ) cgi_append_content("<span id=\"scrollToMe\">", -1);













    if( n==iStart ){




      cgi_append_content("<div class=\"selectedText\">",-1);
    }
    cgi_printf("%6d  ", n);
    if( i>0 ){
      char *zHtml = htmlize(z, i);
      cgi_append_content(zHtml, -1);
      fossil_free(zHtml);
    }
    if( n==iTop ) cgi_append_content("</span>", -1);
    if( n==iEnd ) cgi_append_content("</div>", -1);



    else cgi_append_content("\n", 1);
    z += i;
    if( z[0]=='\n' ) z++;
  }
  if( n<iEnd ) cgi_printf("</div>");
  @ </pre>
  if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){
    builtin_request_js("scroll.js");
  }


}




























/*
** WEBPAGE: artifact
** WEBPAGE: file
** WEBPAGE: whatis
**
** Typical usage:







|
>
|
>
>
>
>









>
>






>
>
>



















>



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

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

|
|



>
>


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







2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083

2084






2085
2086
2087

2088
2089


2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110




2111
2112

2113
2114
2115
2116


2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
    }
  }
  manifest_destroy(pManifest);
  return rid;
}

/*
** The "z" argument is a string that contains the text of a source
** code file and nZ is its length in bytes. This routine appends that
** text to the HTTP reply with line numbering.
**
** zName is the content's file name, if any (it may be NULL). If that
** name contains a '.' then the part after the final '.' is used as
** the X part of a "language-X" CSS class on the generated CODE block.
**
** zLn is the ?ln= parameter for the HTTP query.  If there is an argument,
** then highlight that line number and scroll to it once the page loads.
** If there are two line numbers, highlight the range of lines.
** Multiple ranges can be highlighed by adding additional line numbers
** separated by a non-digit character (also not one of [-,.]).
*/
void output_text_with_line_numbers(
  const char *z,
  int nZ,
  const char *zName,
  const char *zLn
){
  int iStart, iEnd;    /* Start and end of region to highlight */
  int n = 0;           /* Current line number */
  int i = 0;           /* Loop index */
  int iTop = 0;        /* Scroll so that this line is on top of screen. */
  int nLine = 0;       /* content line count */
  int nSpans = 0;      /* number of distinct zLn spans */
  const char *zExt = file_extension(zName);
  Stmt q;

  iStart = iEnd = atoi(zLn);
  db_multi_exec(
    "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)");
  if( iStart>0 ){
    do{
      while( fossil_isdigit(zLn[i]) ) i++;
      if( zLn[i]==',' || zLn[i]=='-' || zLn[i]=='.' ){
        i++;
        while( zLn[i]=='.' ){ i++; }
        iEnd = atoi(&zLn[i]);
        while( fossil_isdigit(zLn[i]) ) i++;
      }
      while( fossil_isdigit(zLn[i]) ) i++;
      if( iEnd<iStart ) iEnd = iStart;
      db_multi_exec(
        "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
      );
      ++nSpans;
      iStart = iEnd = atoi(&zLn[i++]);
    }while( zLn[i] && iStart && iEnd );
  }
  /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/
  cgi_append_content("<table class='numbered-lines'><tbody>"
                     "<tr><td class='line-numbers'>", -1);
  iStart = iEnd = 0;
  count_lines(z, nZ, &nLine);
  for( n=1 ; n<=nLine; ++n ){
    const char * zAttr = "";
    const char * zId = "";
    if(nSpans>0 && iEnd==0){/*Grab the next range of zLn marking*/
      db_prepare(&q, "SELECT iStart, iEnd FROM lnos "
                 "WHERE iStart >= %d ORDER BY iStart", n);
      if( db_step(&q)==SQLITE_ROW ){
        iStart = db_column_int(&q, 0);
        iEnd = db_column_int(&q, 1);
        if(!iTop){
          iTop = iStart - 15 + (iEnd-iStart)/4;
          if( iTop>iStart - 2 ) iTop = iStart-2;
        }

      }else{






        /* Note that overlapping multi-spans, e.g. 10-15+12-20,
           can cause us to miss a row. */
        iStart = iEnd = 0;

      }
      db_finalize(&q);


      --nSpans;
      /*cgi_printf("<!-- iStart=%d, iEnd=%d -->", iStart, iEnd);*/
    }
    if(n==iTop) {
      zId = " id='scrollToMe'";
    }
    if(n==iStart){/*Figure out which CSS class(es) this line needs...*/
      if(n==iEnd){
        zAttr = " class='selected-line start end'";
        iEnd = 0;
      }else{
        zAttr = " class='selected-line start'";
      }
      iStart = 0;
    }else if(n==iEnd){
      zAttr = " class='selected-line end'";
      iEnd = 0;
    }else if( n>iStart && n<iEnd ){
      zAttr = " class='selected-line'";
    }
    cgi_printf("<span%s%s>%6d</span>", zId, zAttr, n);




  }
  cgi_append_content("</td><td class='file-content'><pre>",-1);

  if(zExt && *zExt){
    cgi_printf("<code class='language-%h'>",zExt);
  }else{
    cgi_append_content("<code>", -1);


  }
  cgi_printf("%z", htmlize(z, nZ));
  CX("</code></pre></td></tr></tbody></table>\n");
  if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){
    builtin_request_js("scroll.js");
  }
  style_emit_fossil_js_apis(0, "dom", "copybutton", "popupwidget",
                            "numbered-lines", 0);
}

/*
** COMMAND: test-line-numbers
**
** Usage: %fossil test-line-numbers FILE ?LN-SPEC?
**
*/
void cmd_test_line_numbers(void){
  Blob content = empty_blob;
  const char * zLn = "";
  const char * zFilename = 0;

  if(g.argc < 3){
    usage("FILE");
  }else if(g.argc>3){
    zLn = g.argv[3];
  }
  db_find_and_open_repository(0,0);
  zFilename = g.argv[2];
  fossil_print("%s %s\n", zFilename, zLn);

  blob_read_from_file(&content, zFilename, ExtFILE);
  output_text_with_line_numbers(blob_str(&content), blob_size(&content),
                                zFilename, zLn);
  blob_reset(&content);
  fossil_print("%b\n", cgi_output_blob());
}

/*
** WEBPAGE: artifact
** WEBPAGE: file
** WEBPAGE: whatis
**
** Typical usage:
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403

2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
    }else{
      style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid);
      if( zLn==0 || atoi(zLn)==0 ){
        style_submenu_checkbox("ln", "Line Numbers", 0, 0);
      }
      blob_to_utf8_no_bom(&content, 0);
      zMime = mimetype_from_content(&content);
      @ <blockquote>
      if( zMime==0 ){
        const char *z, *zFileName, *zExt;
        z = blob_str(&content);
        zFileName = db_text(0,
         "SELECT name FROM mlink, filename"
         " WHERE filename.fnid=mlink.fnid"
         "   AND mlink.fid=%d",
         rid);
        zExt = zFileName ? strrchr(zFileName, '.') : 0;
        if( zLn ){
          output_text_with_line_numbers(z, zLn);

        }else if( zExt && zExt[1] ){
          @ <pre>
          @ <code class="language-%s(zExt+1)">%h(z)</code>
          @ </pre>
        }else{
          @ <pre>
          @ %h(z)
          @ </pre>
        }
      }else if( strncmp(zMime, "image/", 6)==0 ){







|








|

|
>


|







2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
    }else{
      style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid);
      if( zLn==0 || atoi(zLn)==0 ){
        style_submenu_checkbox("ln", "Line Numbers", 0, 0);
      }
      blob_to_utf8_no_bom(&content, 0);
      zMime = mimetype_from_content(&content);
      @ <blockquote class="file-content">
      if( zMime==0 ){
        const char *z, *zFileName, *zExt;
        z = blob_str(&content);
        zFileName = db_text(0,
         "SELECT name FROM mlink, filename"
         " WHERE filename.fnid=mlink.fnid"
         "   AND mlink.fid=%d",
         rid);
        zExt = file_extension(zFileName);
        if( zLn ){
          output_text_with_line_numbers(z, blob_size(&content),
                                        zFileName, zLn);
        }else if( zExt && zExt[1] ){
          @ <pre>
          @ <code class="language-%s(zExt)">%h(z)</code>
          @ </pre>
        }else{
          @ <pre>
          @ %h(z)
          @ </pre>
        }
      }else if( strncmp(zMime, "image/", 6)==0 ){

Changes to src/main.mk.

221
222
223
224
225
226
227

228
229

230
231
232

233
234
235
236
237
238
239
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/default.css \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.confirmer.js \

  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \

  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.page.forumpost.js \
  $(SRCDIR)/fossil.page.wikiedit.js \

  $(SRCDIR)/fossil.storage.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \







>


>



>







221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/default.css \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.confirmer.js \
  $(SRCDIR)/fossil.copybutton.js \
  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.numbered-lines.js \
  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.page.forumpost.js \
  $(SRCDIR)/fossil.page.wikiedit.js \
  $(SRCDIR)/fossil.popupwidget.js \
  $(SRCDIR)/fossil.storage.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \

Changes to src/scroll.js.

1
2
/* Cause the page to scroll so that the #scrollToMe is visible */
document.getElementById('scrollToMe').scrollIntoView(true);

|
1
2
/* Cause the page to scroll so that the #scrollToMe is visible */
(document.getElementById('scrollToMe')||document.body).scrollIntoView(true);

Changes to win/Makefile.mingw.

633
634
635
636
637
638
639

640
641

642
643
644

645
646
647
648
649
650
651
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/default.css \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.confirmer.js \

  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \

  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.page.forumpost.js \
  $(SRCDIR)/fossil.page.wikiedit.js \

  $(SRCDIR)/fossil.storage.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \







>


>



>







633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/default.css \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.confirmer.js \
  $(SRCDIR)/fossil.copybutton.js \
  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.numbered-lines.js \
  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.page.forumpost.js \
  $(SRCDIR)/fossil.page.wikiedit.js \
  $(SRCDIR)/fossil.popupwidget.js \
  $(SRCDIR)/fossil.storage.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \

Changes to win/Makefile.msc.

554
555
556
557
558
559
560

561
562

563
564
565

566
567
568
569
570
571
572
        "$(SRCDIR)\ci_edit.js" \
        "$(SRCDIR)\copybtn.js" \
        "$(SRCDIR)\default.css" \
        "$(SRCDIR)\diff.tcl" \
        "$(SRCDIR)\forum.js" \
        "$(SRCDIR)\fossil.bootstrap.js" \
        "$(SRCDIR)\fossil.confirmer.js" \

        "$(SRCDIR)\fossil.dom.js" \
        "$(SRCDIR)\fossil.fetch.js" \

        "$(SRCDIR)\fossil.page.fileedit.js" \
        "$(SRCDIR)\fossil.page.forumpost.js" \
        "$(SRCDIR)\fossil.page.wikiedit.js" \

        "$(SRCDIR)\fossil.storage.js" \
        "$(SRCDIR)\fossil.tabs.js" \
        "$(SRCDIR)\graph.js" \
        "$(SRCDIR)\href.js" \
        "$(SRCDIR)\login.js" \
        "$(SRCDIR)\markdown.md" \
        "$(SRCDIR)\menu.js" \







>


>



>







554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
        "$(SRCDIR)\ci_edit.js" \
        "$(SRCDIR)\copybtn.js" \
        "$(SRCDIR)\default.css" \
        "$(SRCDIR)\diff.tcl" \
        "$(SRCDIR)\forum.js" \
        "$(SRCDIR)\fossil.bootstrap.js" \
        "$(SRCDIR)\fossil.confirmer.js" \
        "$(SRCDIR)\fossil.copybutton.js" \
        "$(SRCDIR)\fossil.dom.js" \
        "$(SRCDIR)\fossil.fetch.js" \
        "$(SRCDIR)\fossil.numbered-lines.js" \
        "$(SRCDIR)\fossil.page.fileedit.js" \
        "$(SRCDIR)\fossil.page.forumpost.js" \
        "$(SRCDIR)\fossil.page.wikiedit.js" \
        "$(SRCDIR)\fossil.popupwidget.js" \
        "$(SRCDIR)\fossil.storage.js" \
        "$(SRCDIR)\fossil.tabs.js" \
        "$(SRCDIR)\graph.js" \
        "$(SRCDIR)\href.js" \
        "$(SRCDIR)\login.js" \
        "$(SRCDIR)\markdown.md" \
        "$(SRCDIR)\menu.js" \
1148
1149
1150
1151
1152
1153
1154

1155
1156

1157
1158
1159

1160
1161
1162
1163
1164
1165
1166
	echo "$(SRCDIR)\ci_edit.js" >> $@
	echo "$(SRCDIR)\copybtn.js" >> $@
	echo "$(SRCDIR)\default.css" >> $@
	echo "$(SRCDIR)\diff.tcl" >> $@
	echo "$(SRCDIR)\forum.js" >> $@
	echo "$(SRCDIR)\fossil.bootstrap.js" >> $@
	echo "$(SRCDIR)\fossil.confirmer.js" >> $@

	echo "$(SRCDIR)\fossil.dom.js" >> $@
	echo "$(SRCDIR)\fossil.fetch.js" >> $@

	echo "$(SRCDIR)\fossil.page.fileedit.js" >> $@
	echo "$(SRCDIR)\fossil.page.forumpost.js" >> $@
	echo "$(SRCDIR)\fossil.page.wikiedit.js" >> $@

	echo "$(SRCDIR)\fossil.storage.js" >> $@
	echo "$(SRCDIR)\fossil.tabs.js" >> $@
	echo "$(SRCDIR)\graph.js" >> $@
	echo "$(SRCDIR)\href.js" >> $@
	echo "$(SRCDIR)\login.js" >> $@
	echo "$(SRCDIR)\markdown.md" >> $@
	echo "$(SRCDIR)\menu.js" >> $@







>


>



>







1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
	echo "$(SRCDIR)\ci_edit.js" >> $@
	echo "$(SRCDIR)\copybtn.js" >> $@
	echo "$(SRCDIR)\default.css" >> $@
	echo "$(SRCDIR)\diff.tcl" >> $@
	echo "$(SRCDIR)\forum.js" >> $@
	echo "$(SRCDIR)\fossil.bootstrap.js" >> $@
	echo "$(SRCDIR)\fossil.confirmer.js" >> $@
	echo "$(SRCDIR)\fossil.copybutton.js" >> $@
	echo "$(SRCDIR)\fossil.dom.js" >> $@
	echo "$(SRCDIR)\fossil.fetch.js" >> $@
	echo "$(SRCDIR)\fossil.numbered-lines.js" >> $@
	echo "$(SRCDIR)\fossil.page.fileedit.js" >> $@
	echo "$(SRCDIR)\fossil.page.forumpost.js" >> $@
	echo "$(SRCDIR)\fossil.page.wikiedit.js" >> $@
	echo "$(SRCDIR)\fossil.popupwidget.js" >> $@
	echo "$(SRCDIR)\fossil.storage.js" >> $@
	echo "$(SRCDIR)\fossil.tabs.js" >> $@
	echo "$(SRCDIR)\graph.js" >> $@
	echo "$(SRCDIR)\href.js" >> $@
	echo "$(SRCDIR)\login.js" >> $@
	echo "$(SRCDIR)\markdown.md" >> $@
	echo "$(SRCDIR)\menu.js" >> $@