Fossil SCM

All major features except saving are implemented.

stephan 2020-07-30 00:05 ajax-wiki-editor
Commit 5d61cec568febc3058909d25c7d2bf19b284a9b97c6ae005a91dfbf7eb67bf48
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -7,19 +7,21 @@
77
fossil.tabs, fossil.storage, fossil.confirmer.
88
99
Custom events which can be listened for via
1010
fossil.page.addEventListener():
1111
12
- - Event 'wiki-loaded': passes on information when it
12
+ - Event 'wiki-page-loaded': passes on information when it
1313
loads a wiki (whether from the network or its internal local-edit
1414
cache), in the form of an "winfo" object:
1515
1616
{
1717
name: string,
1818
mimetype: mimetype string,
19
+ type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20
+ version: UUID string or null for a sandbox page,
21
+ parent: parent UUID string or null if no parent,
1922
content: string
20
- version: UUID string // POTENTIAL TODO. Currently edit only the latest version.
2123
}
2224
2325
The internal docs and code frequently use the term "winfo", and such
2426
references refer to an object with that form.
2527
@@ -136,11 +138,11 @@
136138
}
137139
return this.index;
138140
},
139141
_fireStashEvent: function(){
140142
if(this._disableNextEvent) delete this._disableNextEvent;
141
- else F.page.dispatchEvent('wikiedit-stash-updated', this);
143
+ else F.page.dispatchEvent('wiki-stash-updated', this);
142144
},
143145
/**
144146
Returns the stashed version, if any, for the given winfo object.
145147
*/
146148
getWinfo: function(winfo){
@@ -160,14 +162,16 @@
160162
updateWinfo: function(winfo,content){
161163
const ndx = this.getIndex(),
162164
key = this.indexKey(winfo),
163165
old = ndx[key];
164166
const record = old || (ndx[key]={
165
- name: winfo.name,
166
- mimetype: winfo.mimetype,
167
- isSandbox: !!winfo.isSandbox
167
+ name: winfo.name
168168
});
169
+ record.mimetype = winfo.mimetype;
170
+ record.type = winfo.type;
171
+ record.parent = winfo.parent;
172
+ record.version = winfo.version;
169173
record.stashTime = new Date().getTime();
170174
this.storeIndex();
171175
if(arguments.length>1){
172176
F.storage.set(this.contentKey(key), content);
173177
}
@@ -180,12 +184,13 @@
180184
*/
181185
stashedContent: function(winfo){
182186
return F.storage.get(this.contentKey(this.indexKey(winfo)));
183187
},
184188
/** Returns true if we have stashed content for the given winfo
185
- record. */
189
+ record or page name. */
186190
hasStashedContent: function(winfo){
191
+ if('string'===typeof winfo) winfo = {name: winfo};
187192
return F.storage.contains(this.contentKey(this.indexKey(winfo)));
188193
},
189194
/** Unstashes the given winfo record and its content.
190195
Returns this. */
191196
unstash: function(winfo){
@@ -255,10 +260,64 @@
255260
if(forceEvent){
256261
// Force UI update
257262
s.dispatchEvent(new Event('change',{target:s}));
258263
}
259264
};
265
+
266
+ const WikiList = {
267
+ e: {},
268
+ refreshStashMarks: function(){
269
+ this.e.select.querySelectorAll(
270
+ 'option'
271
+ ).forEach(function(o){
272
+ if($stash.hasStashedContent(o.value)) D.addClass(o, 'stashed');
273
+ else D.removeClass(o, 'stashed');
274
+ });
275
+ },
276
+ init: function(parentElem){
277
+ const sel = D.select(), btn = D.button("Reload page list");
278
+ this.e.select = sel;
279
+ D.addClass(parentElem, 'wikiedit-page-list-wrapper');
280
+ D.clearElement(parentElem);
281
+ D.append(
282
+ parentElem, btn,
283
+ D.append(D.span(), "Select a page to edit:"),
284
+ sel,
285
+ D.append(D.span(), "* = local edits exist."),
286
+ );
287
+ D.attr(sel, 'size', 10);
288
+ D.option(D.disable(D.clearElement(sel)), "Loading...");
289
+ const self = this;
290
+ btn.addEventListener(
291
+ 'click',
292
+ function(){
293
+ F.fetch('wikiajax/list',{
294
+ responseType: 'json',
295
+ onload: function(list){
296
+ D.clearElement(sel);
297
+ list.forEach((e)=>D.option(sel, e));
298
+ //D.option(sel, "sandbox");
299
+ D.enable(sel);
300
+ self.refreshStashMarks();
301
+ }
302
+ });
303
+ },
304
+ false
305
+ );
306
+ btn.click();
307
+ sel.addEventListener(
308
+ 'change',
309
+ (e)=>P.loadPage(e.target.value),
310
+ false
311
+ );
312
+ F.page.addEventListener(
313
+ 'wiki-stash-updated',
314
+ ()=>this.refreshStashMarks(),
315
+ false
316
+ );
317
+ }
318
+ };
260319
261320
/**
262321
Keep track of how many in-flight AJAX requests there are so we
263322
can disable input elements while any are pending. For
264323
simplicity's sake we simply disable ALL OF IT while any AJAX is
@@ -303,17 +362,18 @@
303362
P.tabs = new fossil.TabManager('#wikiedit-tabs');
304363
P.e = { /* various DOM elements we work with... */
305364
taEditor: E('#wikiedit-content-editor'),
306365
// btnCommit: E("#wikiedit-btn-commit"),
307366
btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
308
- selectMimetype: E('#select-mimetype select'),
367
+ selectMimetype: E('select[name=mimetype]'),
309368
selectFontSizeWrap: E('#select-font-size'),
310369
// selectDiffWS: E('select[name=diff_ws]'),
311370
cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
312371
previewTarget: E('#wikiedit-tab-preview-wrapper'),
313372
diffTarget: E('#wikiedit-tab-diff-wrapper'),
314373
tabs:{
374
+ pageList: E('#wikiedit-tab-pages'),
315375
content: E('#wikiedit-tab-content'),
316376
preview: E('#wikiedit-tab-preview'),
317377
diff: E('#wikiedit-tab-diff')
318378
//commit: E('#wikiedit-tab-commit')
319379
}
@@ -329,11 +389,11 @@
329389
P.tabs.addEventListener(
330390
/* Set up auto-refresh of the preview tab... */
331391
'before-switch-to', function(ev){
332392
if(ev.detail===P.e.tabs.preview){
333393
P.baseHrefForWiki();
334
- if(P.e.cbAutoPreview.checked) P.preview();
394
+ if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
335395
}else if(ev.detail===P.e.tabs.diff){
336396
/* Work around a weird bug where the page gets wider than
337397
the window when the diff tab is NOT in view and the
338398
current SBS diff widget is wider than the window. When
339399
the diff IS in view then CSS overflow magically reduces
@@ -381,10 +441,22 @@
381441
P.e.taEditor.addEventListener(
382442
'change', ()=>P.stashContentChange(), false
383443
);
384444
385445
P.selectMimetype(false, true);
446
+ P.e.selectMimetype.addEventListener(
447
+ 'change',
448
+ function(e){
449
+ if(P.winfo){
450
+ P.winfo.mimetype = e.target.value;
451
+ P.stashContentChange(true);
452
+ }
453
+ },
454
+ false
455
+ );
456
+
457
+
386458
const selectFontSize = E('select[name=editor_font_size]');
387459
if(selectFontSize){
388460
selectFontSize.addEventListener(
389461
"change",function(e){
390462
const ed = P.e.taEditor;
@@ -399,22 +471,33 @@
399471
);
400472
}
401473
402474
P.addEventListener(
403475
// Clear certain views when new content is loaded/set
404
- 'wikiedit-content-replaced',
476
+ 'wiki-content-replaced',
405477
()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
406478
);
407479
P.addEventListener(
408
- // Clear certain views after a non-dry-run commit
409
- 'wikiedit-saved',
480
+ // Clear certain views after a save
481
+ 'wiki-saved',
410482
(e)=>{
411483
if(!e.detail.dryRun){
412484
D.clearElement(P.e.diffTarget, P.e.previewTarget);
413485
}
414486
}
415487
);
488
+ P.addEventListener(
489
+ // Update title on wiki page load
490
+ 'wiki-page-loaded',
491
+ function(ev){
492
+ const title = 'Wiki Editor: '+ev.detail.name;
493
+ document.head.querySelector('title').innerText = title;
494
+ document.querySelector('div.header .title').innerText = title;
495
+ },
496
+ false
497
+ );
498
+ WikiList.init( P.e.tabs.pageList.firstElementChild );
416499
}/*F.onPageLoad()*/);
417500
418501
/**
419502
Returns true if fossil.page.winfo is set, indicating that a page
420503
has been loaded, else it reports an error and returns false.
@@ -430,18 +513,18 @@
430513
/**
431514
Getter (if called with no args) or setter (if passed an arg) for
432515
the current file content.
433516
434517
The setter form sets the content, dispatches a
435
- 'wikiedit-content-replaced' event, and returns this object.
518
+ 'wiki-content-replaced' event, and returns this object.
436519
*/
437520
P.wikiContent = function f(){
438521
if(0===arguments.length){
439522
return f.get();
440523
}else{
441524
f.set(arguments[0] || '');
442
- this.dispatchEvent('wikiedit-content-replaced', this);
525
+ this.dispatchEvent('wiki-content-replaced', this);
443526
return this;
444527
}
445528
};
446529
/* Default get/set impls for file content */
447530
P.wikiContent.get = function(){return P.e.taEditor.value};
@@ -494,12 +577,11 @@
494577
UI elements to reflect the loaded state. If passed no arguments
495578
then it re-uses the values from the currently-loaded page, reloading
496579
it (emitting an error message if no file is loaded).
497580
498581
Returns this object, noting that the load is async. After loading
499
- it triggers a 'wikiedit-file-loaded' event, passing it
500
- this.winfo.
582
+ it triggers a 'wiki-page-loaded' event, passing it this.winfo.
501583
502584
If a locally-edited copy of the given file/rev is found, that
503585
copy is used instead of one fetched from the server, but it is
504586
still treated as a load event.
505587
@@ -525,25 +607,30 @@
525607
const onload = (r)=>{
526608
delete self.winfo;
527609
self.winfo = {
528610
name: r.name,
529611
mimetype: r.mimetype,
530
- type: !!r.type
612
+ type: r.type,
613
+ version: r.version,
614
+ parent: r.parent
531615
};
616
+ self.previewNeedsUpdate = true;
532617
self.e.selectMimetype.value = r.mimetype;
533618
self.tabs.switchToTab(self.e.tabs.content);
534619
self.wikiContent(r.content);
535
- self.dispatchEvent('wiki-loaded', r);
620
+ self.dispatchEvent('wiki-page-loaded', r);
536621
};
537622
const semiWinfo = {name: name};
538623
const stashWinfo = this.getStashedWinfo(semiWinfo);
539624
if(stashWinfo){ // fake a response from the stash...
540625
this.winfo = stashWinfo;
541626
onload({
542627
name: stashWinfo.name,
543628
mimetype: stashWinfo.mimetype,
544629
type: stashWinfo.type,
630
+ version: stashWinfo.version,
631
+ parent: stashWinfo.parent,
545632
content: this.contentFromStash()
546633
});
547634
F.message("Fetched from the local-edit storage:",
548635
stashWinfo.name);
549636
return this;
@@ -600,11 +687,12 @@
600687
).fetch('wikiajax/preview',{
601688
payload: fd,
602689
onload: (r,header)=>{
603690
callback(r);
604691
F.message('Updated preview.');
605
- P.dispatchEvent('wikiedit-preview-updated',{
692
+ P.previewNeedsUpdate = false;
693
+ P.dispatchEvent('wiki-preview-updated',{
606694
mimetype: mimetype,
607695
element: P.e.previewTarget
608696
});
609697
},
610698
onerror: (e)=>{
@@ -644,18 +732,17 @@
644732
if(!affirmPageLoaded()) return this;
645733
const content = this.wikiContent(),
646734
self = this,
647735
target = this.e.diffTarget;
648736
const fd = new FormData();
649
- fd.append('filename',this.winfo.filename);
650
- fd.append('checkin', this.winfo.checkin);
737
+ fd.append('page',this.winfo.name);
651738
fd.append('sbs', sbs ? 1 : 0);
652739
fd.append('content',content);
653740
if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
654741
F.message(
655742
"Fetching diff..."
656
- ).fetch('ajax/wiki-diff',{
743
+ ).fetch('wikiajax/diff',{
657744
payload: fd,
658745
onload: function(c){
659746
target.innerHTML = [
660747
"<div>Diff <code>[",
661748
self.winfo.checkin,
@@ -679,17 +766,19 @@
679766
both the winfo and content are updated.
680767
*/
681768
P.stashContentChange = function(onlyWinfo){
682769
if(affirmPageLoaded(true)){
683770
const wi = this.winfo;
771
+ wi.mimetype = P.e.selectMimetype.value;
684772
if(onlyWinfo && $stash.hasStashedContent(wi)){
685773
$stash.updateWinfo(wi);
686774
}else{
687775
$stash.updateWinfo(wi, P.wikiContent());
688776
}
689
- F.message("Stashed page ["+wi.name+"].");
777
+ F.message("Stashed change(s) to page ["+wi.name+"].");
690778
$stash.prune();
779
+ this.previewNeedsUpdate = true;
691780
}
692781
return this;
693782
};
694783
695784
/**
@@ -697,10 +786,11 @@
697786
F.storage. Returns this.
698787
*/
699788
P.unstashContent = function(){
700789
const winfo = arguments[0] || this.winfo;
701790
if(winfo){
791
+ this.previewNeedsUpdate = true;
702792
$stash.unstash(winfo);
703793
//console.debug("Unstashed",winfo);
704794
F.message("Unstashed page ["+winfo.name+"].");
705795
}
706796
return this;
707797
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -7,19 +7,21 @@
7 fossil.tabs, fossil.storage, fossil.confirmer.
8
9 Custom events which can be listened for via
10 fossil.page.addEventListener():
11
12 - Event 'wiki-loaded': passes on information when it
13 loads a wiki (whether from the network or its internal local-edit
14 cache), in the form of an "winfo" object:
15
16 {
17 name: string,
18 mimetype: mimetype string,
 
 
 
19 content: string
20 version: UUID string // POTENTIAL TODO. Currently edit only the latest version.
21 }
22
23 The internal docs and code frequently use the term "winfo", and such
24 references refer to an object with that form.
25
@@ -136,11 +138,11 @@
136 }
137 return this.index;
138 },
139 _fireStashEvent: function(){
140 if(this._disableNextEvent) delete this._disableNextEvent;
141 else F.page.dispatchEvent('wikiedit-stash-updated', this);
142 },
143 /**
144 Returns the stashed version, if any, for the given winfo object.
145 */
146 getWinfo: function(winfo){
@@ -160,14 +162,16 @@
160 updateWinfo: function(winfo,content){
161 const ndx = this.getIndex(),
162 key = this.indexKey(winfo),
163 old = ndx[key];
164 const record = old || (ndx[key]={
165 name: winfo.name,
166 mimetype: winfo.mimetype,
167 isSandbox: !!winfo.isSandbox
168 });
 
 
 
 
169 record.stashTime = new Date().getTime();
170 this.storeIndex();
171 if(arguments.length>1){
172 F.storage.set(this.contentKey(key), content);
173 }
@@ -180,12 +184,13 @@
180 */
181 stashedContent: function(winfo){
182 return F.storage.get(this.contentKey(this.indexKey(winfo)));
183 },
184 /** Returns true if we have stashed content for the given winfo
185 record. */
186 hasStashedContent: function(winfo){
 
187 return F.storage.contains(this.contentKey(this.indexKey(winfo)));
188 },
189 /** Unstashes the given winfo record and its content.
190 Returns this. */
191 unstash: function(winfo){
@@ -255,10 +260,64 @@
255 if(forceEvent){
256 // Force UI update
257 s.dispatchEvent(new Event('change',{target:s}));
258 }
259 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
261 /**
262 Keep track of how many in-flight AJAX requests there are so we
263 can disable input elements while any are pending. For
264 simplicity's sake we simply disable ALL OF IT while any AJAX is
@@ -303,17 +362,18 @@
303 P.tabs = new fossil.TabManager('#wikiedit-tabs');
304 P.e = { /* various DOM elements we work with... */
305 taEditor: E('#wikiedit-content-editor'),
306 // btnCommit: E("#wikiedit-btn-commit"),
307 btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
308 selectMimetype: E('#select-mimetype select'),
309 selectFontSizeWrap: E('#select-font-size'),
310 // selectDiffWS: E('select[name=diff_ws]'),
311 cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
312 previewTarget: E('#wikiedit-tab-preview-wrapper'),
313 diffTarget: E('#wikiedit-tab-diff-wrapper'),
314 tabs:{
 
315 content: E('#wikiedit-tab-content'),
316 preview: E('#wikiedit-tab-preview'),
317 diff: E('#wikiedit-tab-diff')
318 //commit: E('#wikiedit-tab-commit')
319 }
@@ -329,11 +389,11 @@
329 P.tabs.addEventListener(
330 /* Set up auto-refresh of the preview tab... */
331 'before-switch-to', function(ev){
332 if(ev.detail===P.e.tabs.preview){
333 P.baseHrefForWiki();
334 if(P.e.cbAutoPreview.checked) P.preview();
335 }else if(ev.detail===P.e.tabs.diff){
336 /* Work around a weird bug where the page gets wider than
337 the window when the diff tab is NOT in view and the
338 current SBS diff widget is wider than the window. When
339 the diff IS in view then CSS overflow magically reduces
@@ -381,10 +441,22 @@
381 P.e.taEditor.addEventListener(
382 'change', ()=>P.stashContentChange(), false
383 );
384
385 P.selectMimetype(false, true);
 
 
 
 
 
 
 
 
 
 
 
 
386 const selectFontSize = E('select[name=editor_font_size]');
387 if(selectFontSize){
388 selectFontSize.addEventListener(
389 "change",function(e){
390 const ed = P.e.taEditor;
@@ -399,22 +471,33 @@
399 );
400 }
401
402 P.addEventListener(
403 // Clear certain views when new content is loaded/set
404 'wikiedit-content-replaced',
405 ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
406 );
407 P.addEventListener(
408 // Clear certain views after a non-dry-run commit
409 'wikiedit-saved',
410 (e)=>{
411 if(!e.detail.dryRun){
412 D.clearElement(P.e.diffTarget, P.e.previewTarget);
413 }
414 }
415 );
 
 
 
 
 
 
 
 
 
 
 
416 }/*F.onPageLoad()*/);
417
418 /**
419 Returns true if fossil.page.winfo is set, indicating that a page
420 has been loaded, else it reports an error and returns false.
@@ -430,18 +513,18 @@
430 /**
431 Getter (if called with no args) or setter (if passed an arg) for
432 the current file content.
433
434 The setter form sets the content, dispatches a
435 'wikiedit-content-replaced' event, and returns this object.
436 */
437 P.wikiContent = function f(){
438 if(0===arguments.length){
439 return f.get();
440 }else{
441 f.set(arguments[0] || '');
442 this.dispatchEvent('wikiedit-content-replaced', this);
443 return this;
444 }
445 };
446 /* Default get/set impls for file content */
447 P.wikiContent.get = function(){return P.e.taEditor.value};
@@ -494,12 +577,11 @@
494 UI elements to reflect the loaded state. If passed no arguments
495 then it re-uses the values from the currently-loaded page, reloading
496 it (emitting an error message if no file is loaded).
497
498 Returns this object, noting that the load is async. After loading
499 it triggers a 'wikiedit-file-loaded' event, passing it
500 this.winfo.
501
502 If a locally-edited copy of the given file/rev is found, that
503 copy is used instead of one fetched from the server, but it is
504 still treated as a load event.
505
@@ -525,25 +607,30 @@
525 const onload = (r)=>{
526 delete self.winfo;
527 self.winfo = {
528 name: r.name,
529 mimetype: r.mimetype,
530 type: !!r.type
 
 
531 };
 
532 self.e.selectMimetype.value = r.mimetype;
533 self.tabs.switchToTab(self.e.tabs.content);
534 self.wikiContent(r.content);
535 self.dispatchEvent('wiki-loaded', r);
536 };
537 const semiWinfo = {name: name};
538 const stashWinfo = this.getStashedWinfo(semiWinfo);
539 if(stashWinfo){ // fake a response from the stash...
540 this.winfo = stashWinfo;
541 onload({
542 name: stashWinfo.name,
543 mimetype: stashWinfo.mimetype,
544 type: stashWinfo.type,
 
 
545 content: this.contentFromStash()
546 });
547 F.message("Fetched from the local-edit storage:",
548 stashWinfo.name);
549 return this;
@@ -600,11 +687,12 @@
600 ).fetch('wikiajax/preview',{
601 payload: fd,
602 onload: (r,header)=>{
603 callback(r);
604 F.message('Updated preview.');
605 P.dispatchEvent('wikiedit-preview-updated',{
 
606 mimetype: mimetype,
607 element: P.e.previewTarget
608 });
609 },
610 onerror: (e)=>{
@@ -644,18 +732,17 @@
644 if(!affirmPageLoaded()) return this;
645 const content = this.wikiContent(),
646 self = this,
647 target = this.e.diffTarget;
648 const fd = new FormData();
649 fd.append('filename',this.winfo.filename);
650 fd.append('checkin', this.winfo.checkin);
651 fd.append('sbs', sbs ? 1 : 0);
652 fd.append('content',content);
653 if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
654 F.message(
655 "Fetching diff..."
656 ).fetch('ajax/wiki-diff',{
657 payload: fd,
658 onload: function(c){
659 target.innerHTML = [
660 "<div>Diff <code>[",
661 self.winfo.checkin,
@@ -679,17 +766,19 @@
679 both the winfo and content are updated.
680 */
681 P.stashContentChange = function(onlyWinfo){
682 if(affirmPageLoaded(true)){
683 const wi = this.winfo;
 
684 if(onlyWinfo && $stash.hasStashedContent(wi)){
685 $stash.updateWinfo(wi);
686 }else{
687 $stash.updateWinfo(wi, P.wikiContent());
688 }
689 F.message("Stashed page ["+wi.name+"].");
690 $stash.prune();
 
691 }
692 return this;
693 };
694
695 /**
@@ -697,10 +786,11 @@
697 F.storage. Returns this.
698 */
699 P.unstashContent = function(){
700 const winfo = arguments[0] || this.winfo;
701 if(winfo){
 
702 $stash.unstash(winfo);
703 //console.debug("Unstashed",winfo);
704 F.message("Unstashed page ["+winfo.name+"].");
705 }
706 return this;
707
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -7,19 +7,21 @@
7 fossil.tabs, fossil.storage, fossil.confirmer.
8
9 Custom events which can be listened for via
10 fossil.page.addEventListener():
11
12 - Event 'wiki-page-loaded': passes on information when it
13 loads a wiki (whether from the network or its internal local-edit
14 cache), in the form of an "winfo" object:
15
16 {
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page,
21 parent: parent UUID string or null if no parent,
22 content: string
 
23 }
24
25 The internal docs and code frequently use the term "winfo", and such
26 references refer to an object with that form.
27
@@ -136,11 +138,11 @@
138 }
139 return this.index;
140 },
141 _fireStashEvent: function(){
142 if(this._disableNextEvent) delete this._disableNextEvent;
143 else F.page.dispatchEvent('wiki-stash-updated', this);
144 },
145 /**
146 Returns the stashed version, if any, for the given winfo object.
147 */
148 getWinfo: function(winfo){
@@ -160,14 +162,16 @@
162 updateWinfo: function(winfo,content){
163 const ndx = this.getIndex(),
164 key = this.indexKey(winfo),
165 old = ndx[key];
166 const record = old || (ndx[key]={
167 name: winfo.name
 
 
168 });
169 record.mimetype = winfo.mimetype;
170 record.type = winfo.type;
171 record.parent = winfo.parent;
172 record.version = winfo.version;
173 record.stashTime = new Date().getTime();
174 this.storeIndex();
175 if(arguments.length>1){
176 F.storage.set(this.contentKey(key), content);
177 }
@@ -180,12 +184,13 @@
184 */
185 stashedContent: function(winfo){
186 return F.storage.get(this.contentKey(this.indexKey(winfo)));
187 },
188 /** Returns true if we have stashed content for the given winfo
189 record or page name. */
190 hasStashedContent: function(winfo){
191 if('string'===typeof winfo) winfo = {name: winfo};
192 return F.storage.contains(this.contentKey(this.indexKey(winfo)));
193 },
194 /** Unstashes the given winfo record and its content.
195 Returns this. */
196 unstash: function(winfo){
@@ -255,10 +260,64 @@
260 if(forceEvent){
261 // Force UI update
262 s.dispatchEvent(new Event('change',{target:s}));
263 }
264 };
265
266 const WikiList = {
267 e: {},
268 refreshStashMarks: function(){
269 this.e.select.querySelectorAll(
270 'option'
271 ).forEach(function(o){
272 if($stash.hasStashedContent(o.value)) D.addClass(o, 'stashed');
273 else D.removeClass(o, 'stashed');
274 });
275 },
276 init: function(parentElem){
277 const sel = D.select(), btn = D.button("Reload page list");
278 this.e.select = sel;
279 D.addClass(parentElem, 'wikiedit-page-list-wrapper');
280 D.clearElement(parentElem);
281 D.append(
282 parentElem, btn,
283 D.append(D.span(), "Select a page to edit:"),
284 sel,
285 D.append(D.span(), "* = local edits exist."),
286 );
287 D.attr(sel, 'size', 10);
288 D.option(D.disable(D.clearElement(sel)), "Loading...");
289 const self = this;
290 btn.addEventListener(
291 'click',
292 function(){
293 F.fetch('wikiajax/list',{
294 responseType: 'json',
295 onload: function(list){
296 D.clearElement(sel);
297 list.forEach((e)=>D.option(sel, e));
298 //D.option(sel, "sandbox");
299 D.enable(sel);
300 self.refreshStashMarks();
301 }
302 });
303 },
304 false
305 );
306 btn.click();
307 sel.addEventListener(
308 'change',
309 (e)=>P.loadPage(e.target.value),
310 false
311 );
312 F.page.addEventListener(
313 'wiki-stash-updated',
314 ()=>this.refreshStashMarks(),
315 false
316 );
317 }
318 };
319
320 /**
321 Keep track of how many in-flight AJAX requests there are so we
322 can disable input elements while any are pending. For
323 simplicity's sake we simply disable ALL OF IT while any AJAX is
@@ -303,17 +362,18 @@
362 P.tabs = new fossil.TabManager('#wikiedit-tabs');
363 P.e = { /* various DOM elements we work with... */
364 taEditor: E('#wikiedit-content-editor'),
365 // btnCommit: E("#wikiedit-btn-commit"),
366 btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
367 selectMimetype: E('select[name=mimetype]'),
368 selectFontSizeWrap: E('#select-font-size'),
369 // selectDiffWS: E('select[name=diff_ws]'),
370 cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
371 previewTarget: E('#wikiedit-tab-preview-wrapper'),
372 diffTarget: E('#wikiedit-tab-diff-wrapper'),
373 tabs:{
374 pageList: E('#wikiedit-tab-pages'),
375 content: E('#wikiedit-tab-content'),
376 preview: E('#wikiedit-tab-preview'),
377 diff: E('#wikiedit-tab-diff')
378 //commit: E('#wikiedit-tab-commit')
379 }
@@ -329,11 +389,11 @@
389 P.tabs.addEventListener(
390 /* Set up auto-refresh of the preview tab... */
391 'before-switch-to', function(ev){
392 if(ev.detail===P.e.tabs.preview){
393 P.baseHrefForWiki();
394 if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
395 }else if(ev.detail===P.e.tabs.diff){
396 /* Work around a weird bug where the page gets wider than
397 the window when the diff tab is NOT in view and the
398 current SBS diff widget is wider than the window. When
399 the diff IS in view then CSS overflow magically reduces
@@ -381,10 +441,22 @@
441 P.e.taEditor.addEventListener(
442 'change', ()=>P.stashContentChange(), false
443 );
444
445 P.selectMimetype(false, true);
446 P.e.selectMimetype.addEventListener(
447 'change',
448 function(e){
449 if(P.winfo){
450 P.winfo.mimetype = e.target.value;
451 P.stashContentChange(true);
452 }
453 },
454 false
455 );
456
457
458 const selectFontSize = E('select[name=editor_font_size]');
459 if(selectFontSize){
460 selectFontSize.addEventListener(
461 "change",function(e){
462 const ed = P.e.taEditor;
@@ -399,22 +471,33 @@
471 );
472 }
473
474 P.addEventListener(
475 // Clear certain views when new content is loaded/set
476 'wiki-content-replaced',
477 ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
478 );
479 P.addEventListener(
480 // Clear certain views after a save
481 'wiki-saved',
482 (e)=>{
483 if(!e.detail.dryRun){
484 D.clearElement(P.e.diffTarget, P.e.previewTarget);
485 }
486 }
487 );
488 P.addEventListener(
489 // Update title on wiki page load
490 'wiki-page-loaded',
491 function(ev){
492 const title = 'Wiki Editor: '+ev.detail.name;
493 document.head.querySelector('title').innerText = title;
494 document.querySelector('div.header .title').innerText = title;
495 },
496 false
497 );
498 WikiList.init( P.e.tabs.pageList.firstElementChild );
499 }/*F.onPageLoad()*/);
500
501 /**
502 Returns true if fossil.page.winfo is set, indicating that a page
503 has been loaded, else it reports an error and returns false.
@@ -430,18 +513,18 @@
513 /**
514 Getter (if called with no args) or setter (if passed an arg) for
515 the current file content.
516
517 The setter form sets the content, dispatches a
518 'wiki-content-replaced' event, and returns this object.
519 */
520 P.wikiContent = function f(){
521 if(0===arguments.length){
522 return f.get();
523 }else{
524 f.set(arguments[0] || '');
525 this.dispatchEvent('wiki-content-replaced', this);
526 return this;
527 }
528 };
529 /* Default get/set impls for file content */
530 P.wikiContent.get = function(){return P.e.taEditor.value};
@@ -494,12 +577,11 @@
577 UI elements to reflect the loaded state. If passed no arguments
578 then it re-uses the values from the currently-loaded page, reloading
579 it (emitting an error message if no file is loaded).
580
581 Returns this object, noting that the load is async. After loading
582 it triggers a 'wiki-page-loaded' event, passing it this.winfo.
 
583
584 If a locally-edited copy of the given file/rev is found, that
585 copy is used instead of one fetched from the server, but it is
586 still treated as a load event.
587
@@ -525,25 +607,30 @@
607 const onload = (r)=>{
608 delete self.winfo;
609 self.winfo = {
610 name: r.name,
611 mimetype: r.mimetype,
612 type: r.type,
613 version: r.version,
614 parent: r.parent
615 };
616 self.previewNeedsUpdate = true;
617 self.e.selectMimetype.value = r.mimetype;
618 self.tabs.switchToTab(self.e.tabs.content);
619 self.wikiContent(r.content);
620 self.dispatchEvent('wiki-page-loaded', r);
621 };
622 const semiWinfo = {name: name};
623 const stashWinfo = this.getStashedWinfo(semiWinfo);
624 if(stashWinfo){ // fake a response from the stash...
625 this.winfo = stashWinfo;
626 onload({
627 name: stashWinfo.name,
628 mimetype: stashWinfo.mimetype,
629 type: stashWinfo.type,
630 version: stashWinfo.version,
631 parent: stashWinfo.parent,
632 content: this.contentFromStash()
633 });
634 F.message("Fetched from the local-edit storage:",
635 stashWinfo.name);
636 return this;
@@ -600,11 +687,12 @@
687 ).fetch('wikiajax/preview',{
688 payload: fd,
689 onload: (r,header)=>{
690 callback(r);
691 F.message('Updated preview.');
692 P.previewNeedsUpdate = false;
693 P.dispatchEvent('wiki-preview-updated',{
694 mimetype: mimetype,
695 element: P.e.previewTarget
696 });
697 },
698 onerror: (e)=>{
@@ -644,18 +732,17 @@
732 if(!affirmPageLoaded()) return this;
733 const content = this.wikiContent(),
734 self = this,
735 target = this.e.diffTarget;
736 const fd = new FormData();
737 fd.append('page',this.winfo.name);
 
738 fd.append('sbs', sbs ? 1 : 0);
739 fd.append('content',content);
740 if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
741 F.message(
742 "Fetching diff..."
743 ).fetch('wikiajax/diff',{
744 payload: fd,
745 onload: function(c){
746 target.innerHTML = [
747 "<div>Diff <code>[",
748 self.winfo.checkin,
@@ -679,17 +766,19 @@
766 both the winfo and content are updated.
767 */
768 P.stashContentChange = function(onlyWinfo){
769 if(affirmPageLoaded(true)){
770 const wi = this.winfo;
771 wi.mimetype = P.e.selectMimetype.value;
772 if(onlyWinfo && $stash.hasStashedContent(wi)){
773 $stash.updateWinfo(wi);
774 }else{
775 $stash.updateWinfo(wi, P.wikiContent());
776 }
777 F.message("Stashed change(s) to page ["+wi.name+"].");
778 $stash.prune();
779 this.previewNeedsUpdate = true;
780 }
781 return this;
782 };
783
784 /**
@@ -697,10 +786,11 @@
786 F.storage. Returns this.
787 */
788 P.unstashContent = function(){
789 const winfo = arguments[0] || this.winfo;
790 if(winfo){
791 this.previewNeedsUpdate = true;
792 $stash.unstash(winfo);
793 //console.debug("Unstashed",winfo);
794 F.message("Unstashed page ["+winfo.name+"].");
795 }
796 return this;
797
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -40,6 +40,26 @@
4040
body.wikiedit .wikiedit-options > div > * {
4141
margin: 0.25em;
4242
}
4343
body.wikiedit .wikiedit-options.flex-container.flex-row {
4444
align-items: first baseline;
45
+}
46
+body.wikiedit .wikiedit-page-list-wrapper {
47
+ display: flex;
48
+ flex-direction: column;
49
+ flex-wrap: wrap;
50
+ justify-content: center;
51
+ align-items: start;
52
+}
53
+body.wikiedit .wikiedit-page-list-wrapper select option {
54
+ margin: 0.5em 0;
55
+}
56
+body.wikiedit .wikiedit-page-list-wrapper select option.stashed::before {
57
+ content: "* ";
58
+}
59
+body.wikiedit textarea {
60
+ max-width: calc(100% - 1em);
61
+}
62
+
63
+body.wikiedit .tabs .tab-panel {
64
+ overflow: auto;
4565
}
4666
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -40,6 +40,26 @@
40 body.wikiedit .wikiedit-options > div > * {
41 margin: 0.25em;
42 }
43 body.wikiedit .wikiedit-options.flex-container.flex-row {
44 align-items: first baseline;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45 }
46
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -40,6 +40,26 @@
40 body.wikiedit .wikiedit-options > div > * {
41 margin: 0.25em;
42 }
43 body.wikiedit .wikiedit-options.flex-container.flex-row {
44 align-items: first baseline;
45 }
46 body.wikiedit .wikiedit-page-list-wrapper {
47 display: flex;
48 flex-direction: column;
49 flex-wrap: wrap;
50 justify-content: center;
51 align-items: start;
52 }
53 body.wikiedit .wikiedit-page-list-wrapper select option {
54 margin: 0.5em 0;
55 }
56 body.wikiedit .wikiedit-page-list-wrapper select option.stashed::before {
57 content: "* ";
58 }
59 body.wikiedit textarea {
60 max-width: calc(100% - 1em);
61 }
62
63 body.wikiedit .tabs .tab-panel {
64 overflow: auto;
65 }
66
+126 -36
--- src/wiki.c
+++ src/wiki.c
@@ -102,11 +102,10 @@
102102
" WHERE tagid=%d AND mtime<%.16g"
103103
" ORDER BY mtime DESC LIMIT 1",
104104
tagid, mtime);
105105
}
106106
107
-
108107
/*
109108
** WEBPAGE: home
110109
** WEBPAGE: index
111110
** WEBPAGE: not_found
112111
**
@@ -637,26 +636,38 @@
637636
return azStyles[1];
638637
}
639638
640639
/*
641640
** Tries to fetch a wiki page for the given name. If found, it
642
- ** returns true, else false. If pRid is not NULL then if found *pRid is
643
- ** set to its RID. If ppWiki is not NULL then if found *ppWiki is set
644
- ** to the loaded wiki object, which the caller is responsible for
645
- ** passing to manifest_destroy().
641
+ ** returns true, else false.
642
+ **
643
+ ** versionsBack specifies how many versions back in the history to
644
+ ** fetch. Use 0 for the latest version, 1 for its parent, etc.
645
+ **
646
+ ** If pRid is not NULL then if a result is found *pRid is set to its
647
+ ** RID. If ppWiki is not NULL then if found *ppWiki is set to the
648
+ ** loaded wiki object, which the caller is responsible for passing to
649
+ ** manifest_destroy().
646650
*/
647
-int wiki_fetch_by_name( const char *zPageName, int * pRid,
648
- Manifest **ppWiki ){
651
+int wiki_fetch_by_name( const char *zPageName,
652
+ unsigned int versionsBack,
653
+ int * pRid, Manifest **ppWiki ){
649654
Manifest *pWiki = 0;
650655
char *zTag = mprintf("wiki-%s", zPageName);
651
- const int rid = db_int(0,
652
- "SELECT rid FROM tagxref"
653
- " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
654
- " ORDER BY mtime DESC", zTag
655
- );
656
+ Stmt q = empty_Stmt;
657
+ int rid = 0;
656658
659
+ db_prepare(&q, "SELECT rid FROM tagxref"
660
+ " WHERE tagid=(SELECT tagid FROM tag WHERE"
661
+ " tagname=%Q) "
662
+ " ORDER BY mtime DESC LIMIT -1 OFFSET %u", zTag,
663
+ versionsBack);
657664
fossil_free(zTag);
665
+ if(SQLITE_ROW == db_step(&q)){
666
+ rid = db_column_int(&q, 0);
667
+ }
668
+ db_finalize(&q);
658669
if( rid == 0 ){
659670
return 0;
660671
}
661672
else if(pRid){
662673
*pRid = rid;
@@ -683,14 +694,16 @@
683694
** ajax_route_error(). On success, an object in this form:
684695
**
685696
** { name: "page name",
686697
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
687698
** mimetype: "mime type",
699
+** version: UUID string or null for a sandbox page,
700
+** parent: "parent uuid" or null,
688701
** content: "page content"
689702
** }
690703
*/
691
-static void wiki_ajax_route_fetch(){
704
+static void wiki_ajax_route_fetch(void){
692705
const char * zPageName = P("page");
693706
int isSandbox;
694707
695708
if( zPageName==0 || zPageName[0]==0 ){
696709
ajax_route_error(400,"Missing page name.");
@@ -700,38 +713,93 @@
700713
isSandbox = is_sandbox(zPageName);
701714
if( isSandbox ){
702715
char * zMimetype =
703716
db_get("sandbox-mimetype","text/x-fossil-wiki");
704717
CX("{\"name\": %!j, \"type\": \"sandbox\", "
705
- "\"mimetype\": %!j, \"content\": ""}",
718
+ "\"mimetype\": %!j, \"version\": null, \"parent\": null, "
719
+ "\"content\": \"\"}",
706720
zPageName, zMimetype);
707721
fossil_free(zMimetype);
708722
}else{
709723
Manifest * pWiki = 0;
710
- if( !wiki_fetch_by_name(zPageName, 0, &pWiki) ){
724
+ char * zUuid;
725
+ if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
711726
ajax_route_error(404, "Wiki page not found.");
712727
return;
713728
}
729
+ zUuid = rid_to_uuid(pWiki->rid);
714730
CX("{\"name\": %!j, \"type\": %!j, "
715
- "\"mimetype\": %!j, \"content\": %!j}",
731
+ "\"version\": %!j, "
732
+ "\"mimetype\": %!j, ",
716733
pWiki->zWikiTitle,
717
- wiki_page_type_name(pWiki->zWikiTitle),
718
- pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki",
719
- pWiki->zWiki);
734
+ wiki_page_type_name(pWiki->zWikiTitle),
735
+ zUuid,
736
+ pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki");
737
+ CX("\"parent\": ");
738
+ if(pWiki->nParent){
739
+ CX("%!j, ", pWiki->azParent[0]);
740
+ }else{
741
+ CX("null, ");
742
+ }
743
+ CX("\"content\": %!j}", pWiki->zWiki);
744
+ fossil_free(zUuid);
720745
manifest_destroy(pWiki);
721746
}
722747
}
748
+
749
+/*
750
+** Ajax route handler for /wikiajax/diff.
751
+**
752
+** URL params:
753
+**
754
+** page = the wiki page name
755
+** content = the new/edited wiki page content
756
+*/
757
+static void wiki_ajax_route_diff(void){
758
+ const char * zPageName = P("page");
759
+ Blob contentNew = empty_blob, contentOrig = empty_blob;
760
+ Manifest * pParent = 0;
761
+ const char * zContent = P("content");
762
+ u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR;
763
+
764
+ if( zPageName==0 || zPageName[0]==0 ){
765
+ ajax_route_error(400,"Missing page name.");
766
+ return;
767
+ }
768
+ switch(atoi(PD("sbs","0"))){
769
+ case 0: diffFlags |= DIFF_LINENO; break;
770
+ default: diffFlags |= DIFF_SIDEBYSIDE;
771
+ }
772
+ switch(atoi(PD("ws","2"))){
773
+ case 1: diffFlags |= DIFF_IGNORE_EOLWS; break;
774
+ case 2: diffFlags |= DIFF_IGNORE_ALLWS; break;
775
+ default: break;
776
+ }
777
+ wiki_fetch_by_name( zPageName, 0, 0, &pParent );
778
+ if( pParent && pParent->zWiki && *pParent->zWiki ){
779
+ blob_init(&contentOrig, pParent->zWiki, -1);
780
+ }else{
781
+ blob_init(&contentOrig, "", 0);
782
+ }
783
+ blob_init(&contentNew, zContent ? zContent : "", -1);
784
+ cgi_set_content_type("text/html");
785
+ ajax_render_diff(&contentNew, &contentOrig, diffFlags);
786
+ blob_reset(&contentNew);
787
+ blob_reset(&contentOrig);
788
+ manifest_destroy(pParent);
789
+}
790
+
723791
724792
/*
725793
** Ajax route handler for /wikiajax/preview.
726794
**
727795
** URL params:
728796
**
729797
** mimetype = the wiki page mimetype (determines rendering style)
730798
** content = the wiki page content
731799
*/
732
-static void wiki_ajax_route_preview(){
800
+static void wiki_ajax_route_preview(void){
733801
Blob content = empty_blob;
734802
const char * zMimetype = PD("mimetype","text/x-fossil-wiki");
735803
const char * zContent = P("content");
736804
737805
if( zContent==0 ){
@@ -741,24 +809,56 @@
741809
blob_init(&content, zContent, -1);
742810
cgi_set_content_type("text/html");
743811
wiki_render_by_mimetype(&content, zMimetype);
744812
blob_reset(&content);
745813
}
814
+
815
+/*
816
+** Ajax route handler for /wikiajax/list.
817
+**
818
+** Responds with JSON. On error, an object in the form documented by
819
+** ajax_route_error(). On success, an array of strings (page names)
820
+** sorted case-insensitively.
821
+*/
822
+static void wiki_ajax_route_list(void){
823
+ Stmt q = empty_Stmt;
824
+ int n = 0;
825
+
826
+ cgi_set_content_type("application/json");
827
+ db_prepare(&q, "SELECT"
828
+ " substr(tagname,6) AS name"
829
+ " FROM tag WHERE tagname GLOB 'wiki-*'"
830
+ " UNION SELECT 'sandbox' AS name"
831
+ " ORDER BY name COLLATE NOCASE");
832
+ CX("[");
833
+ while( SQLITE_ROW==db_step(&q) ){
834
+ if(n++){
835
+ CX(",");
836
+ }
837
+ CX("%!j", db_column_text(&q,0));
838
+ }
839
+ db_finalize(&q);
840
+ CX("]");
841
+}
842
+
746843
747844
/*
748845
** WEBPAGE: wikiajax
749846
**
750847
** An internal dispatcher for wiki AJAX operations. Not for direct
751
-** client use.
848
+** client use. All routes defined by this interface are app-internal,
849
+** subject to change
752850
*/
753851
void wiki_ajax_page(void){
754852
const char * zName = P("name");
755853
AjaxRoute routeName = {0,0,0,0};
756854
const AjaxRoute * pRoute = 0;
757855
const AjaxRoute routes[] = {
758856
/* Keep these sorted by zName (for bsearch()) */
857
+ {"diff", wiki_ajax_route_diff, 1, 1},
759858
{"fetch", wiki_ajax_route_fetch, 0, 0},
859
+ {"list", wiki_ajax_route_list, 0, 0},
760860
{"preview", wiki_ajax_route_preview, 1, 1}
761861
/* /preview access mode: whether or not wiki-write mode is
762862
needed really depends on multiple factors. e.g. the sandbox
763863
page does not normally require more than anonymous access.
764864
TODO: set its write-mode to false and do the check manually
@@ -817,11 +917,11 @@
817917
int found = 0;
818918
if( !wiki_special_permission(zPageName) ){
819919
login_needed(0);
820920
return;
821921
}
822
- found = wiki_fetch_by_name(zPageName, &rid, 0);
922
+ found = wiki_fetch_by_name(zPageName, 0, &rid, 0);
823923
if( !found ){
824924
/* TODO: set up for a new page */
825925
}
826926
if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
827927
login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
@@ -843,32 +943,22 @@
843943
{
844944
CX("<div id='wikiedit-tab-pages' "
845945
"data-tab-parent='wikiedit-tabs' "
846946
"data-tab-label='Page List'"
847947
">");
848
- CX("<div class='flex-container flex-row child-gap-small'>");
849
- CX("TODO: page selection list.");
850
- CX("</div>");
851
- CX("</div>"/*#tab-file-content*/);
948
+ CX("<div>Loading wiki pages list...</div>");
949
+ CX("</div>"/*#wikiedit-tab-pages*/);
852950
}
853951
854952
/******* Content tab *******/
855953
{
856954
CX("<div id='wikiedit-tab-content' "
857955
"data-tab-parent='wikiedit-tabs' "
858
- "data-tab-label='Page Editor' "
859
- "data-tab-select='1'"
956
+ "data-tab-label='Page Editor'"
860957
">");
861958
CX("<div class='flex-container flex-row child-gap-small'>");
862
- style_select_list_str("select-mimetype",
863
- "mimetype",
864
- "Mimetype", 0,
865
- "text/x-fossil-wiki",
866
- "Fossil wiki", "text/x-fossil-wiki",
867
- "Markdown", "text/x-markdown",
868
- "Plain text", "text/plain",
869
- NULL);
959
+ mimetype_option_menu(0);
870960
CX("<button class='wikiedit-content-reload confirmer' "
871961
"title='Reload the file from the server, discarding "
872962
"any local edits. To help avoid accidental loss of "
873963
"edits, it requires confirmation (a second click) within "
874964
"a few seconds or it will not reload.'"
875965
--- src/wiki.c
+++ src/wiki.c
@@ -102,11 +102,10 @@
102 " WHERE tagid=%d AND mtime<%.16g"
103 " ORDER BY mtime DESC LIMIT 1",
104 tagid, mtime);
105 }
106
107
108 /*
109 ** WEBPAGE: home
110 ** WEBPAGE: index
111 ** WEBPAGE: not_found
112 **
@@ -637,26 +636,38 @@
637 return azStyles[1];
638 }
639
640 /*
641 ** Tries to fetch a wiki page for the given name. If found, it
642 ** returns true, else false. If pRid is not NULL then if found *pRid is
643 ** set to its RID. If ppWiki is not NULL then if found *ppWiki is set
644 ** to the loaded wiki object, which the caller is responsible for
645 ** passing to manifest_destroy().
 
 
 
 
 
646 */
647 int wiki_fetch_by_name( const char *zPageName, int * pRid,
648 Manifest **ppWiki ){
 
649 Manifest *pWiki = 0;
650 char *zTag = mprintf("wiki-%s", zPageName);
651 const int rid = db_int(0,
652 "SELECT rid FROM tagxref"
653 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
654 " ORDER BY mtime DESC", zTag
655 );
656
 
 
 
 
 
657 fossil_free(zTag);
 
 
 
 
658 if( rid == 0 ){
659 return 0;
660 }
661 else if(pRid){
662 *pRid = rid;
@@ -683,14 +694,16 @@
683 ** ajax_route_error(). On success, an object in this form:
684 **
685 ** { name: "page name",
686 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
687 ** mimetype: "mime type",
 
 
688 ** content: "page content"
689 ** }
690 */
691 static void wiki_ajax_route_fetch(){
692 const char * zPageName = P("page");
693 int isSandbox;
694
695 if( zPageName==0 || zPageName[0]==0 ){
696 ajax_route_error(400,"Missing page name.");
@@ -700,38 +713,93 @@
700 isSandbox = is_sandbox(zPageName);
701 if( isSandbox ){
702 char * zMimetype =
703 db_get("sandbox-mimetype","text/x-fossil-wiki");
704 CX("{\"name\": %!j, \"type\": \"sandbox\", "
705 "\"mimetype\": %!j, \"content\": ""}",
 
706 zPageName, zMimetype);
707 fossil_free(zMimetype);
708 }else{
709 Manifest * pWiki = 0;
710 if( !wiki_fetch_by_name(zPageName, 0, &pWiki) ){
 
711 ajax_route_error(404, "Wiki page not found.");
712 return;
713 }
 
714 CX("{\"name\": %!j, \"type\": %!j, "
715 "\"mimetype\": %!j, \"content\": %!j}",
 
716 pWiki->zWikiTitle,
717 wiki_page_type_name(pWiki->zWikiTitle),
718 pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki",
719 pWiki->zWiki);
 
 
 
 
 
 
 
 
720 manifest_destroy(pWiki);
721 }
722 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
724 /*
725 ** Ajax route handler for /wikiajax/preview.
726 **
727 ** URL params:
728 **
729 ** mimetype = the wiki page mimetype (determines rendering style)
730 ** content = the wiki page content
731 */
732 static void wiki_ajax_route_preview(){
733 Blob content = empty_blob;
734 const char * zMimetype = PD("mimetype","text/x-fossil-wiki");
735 const char * zContent = P("content");
736
737 if( zContent==0 ){
@@ -741,24 +809,56 @@
741 blob_init(&content, zContent, -1);
742 cgi_set_content_type("text/html");
743 wiki_render_by_mimetype(&content, zMimetype);
744 blob_reset(&content);
745 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
747 /*
748 ** WEBPAGE: wikiajax
749 **
750 ** An internal dispatcher for wiki AJAX operations. Not for direct
751 ** client use.
 
752 */
753 void wiki_ajax_page(void){
754 const char * zName = P("name");
755 AjaxRoute routeName = {0,0,0,0};
756 const AjaxRoute * pRoute = 0;
757 const AjaxRoute routes[] = {
758 /* Keep these sorted by zName (for bsearch()) */
 
759 {"fetch", wiki_ajax_route_fetch, 0, 0},
 
760 {"preview", wiki_ajax_route_preview, 1, 1}
761 /* /preview access mode: whether or not wiki-write mode is
762 needed really depends on multiple factors. e.g. the sandbox
763 page does not normally require more than anonymous access.
764 TODO: set its write-mode to false and do the check manually
@@ -817,11 +917,11 @@
817 int found = 0;
818 if( !wiki_special_permission(zPageName) ){
819 login_needed(0);
820 return;
821 }
822 found = wiki_fetch_by_name(zPageName, &rid, 0);
823 if( !found ){
824 /* TODO: set up for a new page */
825 }
826 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
827 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
@@ -843,32 +943,22 @@
843 {
844 CX("<div id='wikiedit-tab-pages' "
845 "data-tab-parent='wikiedit-tabs' "
846 "data-tab-label='Page List'"
847 ">");
848 CX("<div class='flex-container flex-row child-gap-small'>");
849 CX("TODO: page selection list.");
850 CX("</div>");
851 CX("</div>"/*#tab-file-content*/);
852 }
853
854 /******* Content tab *******/
855 {
856 CX("<div id='wikiedit-tab-content' "
857 "data-tab-parent='wikiedit-tabs' "
858 "data-tab-label='Page Editor' "
859 "data-tab-select='1'"
860 ">");
861 CX("<div class='flex-container flex-row child-gap-small'>");
862 style_select_list_str("select-mimetype",
863 "mimetype",
864 "Mimetype", 0,
865 "text/x-fossil-wiki",
866 "Fossil wiki", "text/x-fossil-wiki",
867 "Markdown", "text/x-markdown",
868 "Plain text", "text/plain",
869 NULL);
870 CX("<button class='wikiedit-content-reload confirmer' "
871 "title='Reload the file from the server, discarding "
872 "any local edits. To help avoid accidental loss of "
873 "edits, it requires confirmation (a second click) within "
874 "a few seconds or it will not reload.'"
875
--- src/wiki.c
+++ src/wiki.c
@@ -102,11 +102,10 @@
102 " WHERE tagid=%d AND mtime<%.16g"
103 " ORDER BY mtime DESC LIMIT 1",
104 tagid, mtime);
105 }
106
 
107 /*
108 ** WEBPAGE: home
109 ** WEBPAGE: index
110 ** WEBPAGE: not_found
111 **
@@ -637,26 +636,38 @@
636 return azStyles[1];
637 }
638
639 /*
640 ** Tries to fetch a wiki page for the given name. If found, it
641 ** returns true, else false.
642 **
643 ** versionsBack specifies how many versions back in the history to
644 ** fetch. Use 0 for the latest version, 1 for its parent, etc.
645 **
646 ** If pRid is not NULL then if a result is found *pRid is set to its
647 ** RID. If ppWiki is not NULL then if found *ppWiki is set to the
648 ** loaded wiki object, which the caller is responsible for passing to
649 ** manifest_destroy().
650 */
651 int wiki_fetch_by_name( const char *zPageName,
652 unsigned int versionsBack,
653 int * pRid, Manifest **ppWiki ){
654 Manifest *pWiki = 0;
655 char *zTag = mprintf("wiki-%s", zPageName);
656 Stmt q = empty_Stmt;
657 int rid = 0;
 
 
 
658
659 db_prepare(&q, "SELECT rid FROM tagxref"
660 " WHERE tagid=(SELECT tagid FROM tag WHERE"
661 " tagname=%Q) "
662 " ORDER BY mtime DESC LIMIT -1 OFFSET %u", zTag,
663 versionsBack);
664 fossil_free(zTag);
665 if(SQLITE_ROW == db_step(&q)){
666 rid = db_column_int(&q, 0);
667 }
668 db_finalize(&q);
669 if( rid == 0 ){
670 return 0;
671 }
672 else if(pRid){
673 *pRid = rid;
@@ -683,14 +694,16 @@
694 ** ajax_route_error(). On success, an object in this form:
695 **
696 ** { name: "page name",
697 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
698 ** mimetype: "mime type",
699 ** version: UUID string or null for a sandbox page,
700 ** parent: "parent uuid" or null,
701 ** content: "page content"
702 ** }
703 */
704 static void wiki_ajax_route_fetch(void){
705 const char * zPageName = P("page");
706 int isSandbox;
707
708 if( zPageName==0 || zPageName[0]==0 ){
709 ajax_route_error(400,"Missing page name.");
@@ -700,38 +713,93 @@
713 isSandbox = is_sandbox(zPageName);
714 if( isSandbox ){
715 char * zMimetype =
716 db_get("sandbox-mimetype","text/x-fossil-wiki");
717 CX("{\"name\": %!j, \"type\": \"sandbox\", "
718 "\"mimetype\": %!j, \"version\": null, \"parent\": null, "
719 "\"content\": \"\"}",
720 zPageName, zMimetype);
721 fossil_free(zMimetype);
722 }else{
723 Manifest * pWiki = 0;
724 char * zUuid;
725 if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
726 ajax_route_error(404, "Wiki page not found.");
727 return;
728 }
729 zUuid = rid_to_uuid(pWiki->rid);
730 CX("{\"name\": %!j, \"type\": %!j, "
731 "\"version\": %!j, "
732 "\"mimetype\": %!j, ",
733 pWiki->zWikiTitle,
734 wiki_page_type_name(pWiki->zWikiTitle),
735 zUuid,
736 pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki");
737 CX("\"parent\": ");
738 if(pWiki->nParent){
739 CX("%!j, ", pWiki->azParent[0]);
740 }else{
741 CX("null, ");
742 }
743 CX("\"content\": %!j}", pWiki->zWiki);
744 fossil_free(zUuid);
745 manifest_destroy(pWiki);
746 }
747 }
748
749 /*
750 ** Ajax route handler for /wikiajax/diff.
751 **
752 ** URL params:
753 **
754 ** page = the wiki page name
755 ** content = the new/edited wiki page content
756 */
757 static void wiki_ajax_route_diff(void){
758 const char * zPageName = P("page");
759 Blob contentNew = empty_blob, contentOrig = empty_blob;
760 Manifest * pParent = 0;
761 const char * zContent = P("content");
762 u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR;
763
764 if( zPageName==0 || zPageName[0]==0 ){
765 ajax_route_error(400,"Missing page name.");
766 return;
767 }
768 switch(atoi(PD("sbs","0"))){
769 case 0: diffFlags |= DIFF_LINENO; break;
770 default: diffFlags |= DIFF_SIDEBYSIDE;
771 }
772 switch(atoi(PD("ws","2"))){
773 case 1: diffFlags |= DIFF_IGNORE_EOLWS; break;
774 case 2: diffFlags |= DIFF_IGNORE_ALLWS; break;
775 default: break;
776 }
777 wiki_fetch_by_name( zPageName, 0, 0, &pParent );
778 if( pParent && pParent->zWiki && *pParent->zWiki ){
779 blob_init(&contentOrig, pParent->zWiki, -1);
780 }else{
781 blob_init(&contentOrig, "", 0);
782 }
783 blob_init(&contentNew, zContent ? zContent : "", -1);
784 cgi_set_content_type("text/html");
785 ajax_render_diff(&contentNew, &contentOrig, diffFlags);
786 blob_reset(&contentNew);
787 blob_reset(&contentOrig);
788 manifest_destroy(pParent);
789 }
790
791
792 /*
793 ** Ajax route handler for /wikiajax/preview.
794 **
795 ** URL params:
796 **
797 ** mimetype = the wiki page mimetype (determines rendering style)
798 ** content = the wiki page content
799 */
800 static void wiki_ajax_route_preview(void){
801 Blob content = empty_blob;
802 const char * zMimetype = PD("mimetype","text/x-fossil-wiki");
803 const char * zContent = P("content");
804
805 if( zContent==0 ){
@@ -741,24 +809,56 @@
809 blob_init(&content, zContent, -1);
810 cgi_set_content_type("text/html");
811 wiki_render_by_mimetype(&content, zMimetype);
812 blob_reset(&content);
813 }
814
815 /*
816 ** Ajax route handler for /wikiajax/list.
817 **
818 ** Responds with JSON. On error, an object in the form documented by
819 ** ajax_route_error(). On success, an array of strings (page names)
820 ** sorted case-insensitively.
821 */
822 static void wiki_ajax_route_list(void){
823 Stmt q = empty_Stmt;
824 int n = 0;
825
826 cgi_set_content_type("application/json");
827 db_prepare(&q, "SELECT"
828 " substr(tagname,6) AS name"
829 " FROM tag WHERE tagname GLOB 'wiki-*'"
830 " UNION SELECT 'sandbox' AS name"
831 " ORDER BY name COLLATE NOCASE");
832 CX("[");
833 while( SQLITE_ROW==db_step(&q) ){
834 if(n++){
835 CX(",");
836 }
837 CX("%!j", db_column_text(&q,0));
838 }
839 db_finalize(&q);
840 CX("]");
841 }
842
843
844 /*
845 ** WEBPAGE: wikiajax
846 **
847 ** An internal dispatcher for wiki AJAX operations. Not for direct
848 ** client use. All routes defined by this interface are app-internal,
849 ** subject to change
850 */
851 void wiki_ajax_page(void){
852 const char * zName = P("name");
853 AjaxRoute routeName = {0,0,0,0};
854 const AjaxRoute * pRoute = 0;
855 const AjaxRoute routes[] = {
856 /* Keep these sorted by zName (for bsearch()) */
857 {"diff", wiki_ajax_route_diff, 1, 1},
858 {"fetch", wiki_ajax_route_fetch, 0, 0},
859 {"list", wiki_ajax_route_list, 0, 0},
860 {"preview", wiki_ajax_route_preview, 1, 1}
861 /* /preview access mode: whether or not wiki-write mode is
862 needed really depends on multiple factors. e.g. the sandbox
863 page does not normally require more than anonymous access.
864 TODO: set its write-mode to false and do the check manually
@@ -817,11 +917,11 @@
917 int found = 0;
918 if( !wiki_special_permission(zPageName) ){
919 login_needed(0);
920 return;
921 }
922 found = wiki_fetch_by_name(zPageName, 0, &rid, 0);
923 if( !found ){
924 /* TODO: set up for a new page */
925 }
926 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
927 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
@@ -843,32 +943,22 @@
943 {
944 CX("<div id='wikiedit-tab-pages' "
945 "data-tab-parent='wikiedit-tabs' "
946 "data-tab-label='Page List'"
947 ">");
948 CX("<div>Loading wiki pages list...</div>");
949 CX("</div>"/*#wikiedit-tab-pages*/);
 
 
950 }
951
952 /******* Content tab *******/
953 {
954 CX("<div id='wikiedit-tab-content' "
955 "data-tab-parent='wikiedit-tabs' "
956 "data-tab-label='Page Editor'"
 
957 ">");
958 CX("<div class='flex-container flex-row child-gap-small'>");
959 mimetype_option_menu(0);
 
 
 
 
 
 
 
960 CX("<button class='wikiedit-content-reload confirmer' "
961 "title='Reload the file from the server, discarding "
962 "any local edits. To help avoid accidental loss of "
963 "edits, it requires confirmation (a second click) within "
964 "a few seconds or it will not reload.'"
965

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button