Index: skins/ardoise/css.txt ================================================================== --- skins/ardoise/css.txt +++ skins/ardoise/css.txt @@ -572,10 +572,13 @@ 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, Index: skins/eagle/css.txt ================================================================== --- skins/eagle/css.txt +++ skins/eagle/css.txt @@ -399,11 +399,11 @@ div.filetreeline:hover { background-color: #7EA2D9; } -div.selectedText { +table.numbered-lines td.line-numbers span.selected-line { background-color: #7EA2D9; } .statistics-report-graph-line { background-color: #7EA2D9; Index: skins/xekri/css.txt ================================================================== --- skins/xekri/css.txt +++ skins/xekri/css.txt @@ -1000,15 +1000,15 @@ /************************************** * Did not encounter these */ /* selected lines of text within a linenumbered artifact display */ -div.selectedText { +table.numbered-lines td.line-numbers span.selected-line { font-weight: bold; color: #00f; background-color: #d5d5ff; - border: 1px #00f solid; + border-color: #00f; } /* format for missing privileges note on user setup page */ p.missingPriv { color: #00f; Index: src/ajax.c ================================================================== --- src/ajax.c +++ src/ajax.c @@ -130,11 +130,12 @@ 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"); + output_text_with_line_numbers(zContent, blob_size(pContent), + zName, "on"); }else{ const char *zExt = strrchr(zName,'.'); if(zExt && zExt[1]){ CX("
%h
",
zExt+1, zContent);
Index: src/attach.c
==================================================================
--- src/attach.c
+++ src/attach.c
@@ -617,11 +617,11 @@
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);
+ output_text_with_line_numbers(z, blob_size(&attach), zName, zLn);
}else{
@ @ %h(z) @} Index: src/default.css ================================================================== --- src/default.css +++ src/default.css @@ -440,16 +440,10 @@ 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; @@ -953,14 +947,19 @@ color: darkred; background: yellow; opacity: 0.7; } .hidden { - position: absolute; - opacity: 0; - pointer-events: none; - display: none; + /* 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 { @@ -1158,5 +1157,131 @@ .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; +} Index: src/diff.c ================================================================== --- src/diff.c +++ src/diff.c @@ -125,11 +125,11 @@ ** 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( +int count_lines( const char *z, int n, int *pnLine ){ int nLine; Index: src/file.c ================================================================== --- src/file.c +++ src/file.c @@ -2397,5 +2397,14 @@ 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; +} Index: src/fossil.bootstrap.js ================================================================== --- src/fossil.bootstrap.js +++ src/fossil.bootstrap.js @@ -1,6 +1,18 @@ "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. */ ADDED src/fossil.copybutton.js Index: src/fossil.copybutton.js ================================================================== --- /dev/null +++ src/fossil.copybutton.js @@ -0,0 +1,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); Index: src/fossil.dom.js ================================================================== --- src/fossil.dom.js +++ src/fossil.dom.js @@ -465,10 +465,103 @@ 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; ADDED src/fossil.numbered-lines.js Index: src/fossil.numbered-lines.js ================================================================== --- /dev/null +++ src/fossil.numbered-lines.js @@ -0,0 +1,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
- 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("", -1);
- if( n==iStart ){
- cgi_append_content("",-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("", -1);
- if( n==iEnd ) cgi_append_content("", -1);
- else cgi_append_content("\n", 1);
- z += i;
- if( z[0]=='\n' ) z++;
- }
- if( n");
- @
+ /*cgi_printf("", nSpans);*/
+ cgi_append_content("", -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, 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 | ",-1);
+ if(zExt && *zExt){
+ cgi_printf(" |
+ @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; + zExt = file_extension(zFileName); if( zLn ){ - output_text_with_line_numbers(z, zLn); + output_text_with_line_numbers(z, blob_size(&content), + zFileName, zLn); }else if( zExt && zExt[1] ){ @- @}else{ @%h(z)
+ @%h(z)
@@ %h(z) @Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -223,15 +223,18 @@ $(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 \ Index: src/scroll.js ================================================================== --- src/scroll.js +++ src/scroll.js @@ -1,2 +1,2 @@ /* Cause the page to scroll so that the #scrollToMe is visible */ -document.getElementById('scrollToMe').scrollIntoView(true); +(document.getElementById('scrollToMe')||document.body).scrollIntoView(true); Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -635,15 +635,18 @@ $(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 \ Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -556,15 +556,18 @@ "$(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" \ @@ -1150,15 +1153,18 @@ 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" >> $@