Index: src/ajax.c ================================================================== --- src/ajax.c +++ src/ajax.c @@ -163,10 +163,20 @@ }else{ CX("
%b
",&out); } blob_reset(&out); } + +/* +** Uses P(zKey) to fetch a CGI environment variable. If that var is +** NULL or starts with '0' or 'f' then this function returns false, +** else it returns true. +*/ +int ajax_p_bool(char const *zKey){ + const char * zVal = P(zKey); + return (!zVal || '0'==*zVal || 'f'==*zVal) ? 0 : 1; +} /* ** Helper for /ajax routes. Clears the CGI content buffer, sets an ** HTTP error status code, and queues up a JSON response in the form ** of an object: @@ -323,10 +333,11 @@ if(zRenderMode!=0){ cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); } } +#if INTERFACE /* ** Internal mapping of ajax sub-route names to various metadata. */ struct AjaxRoute { const char *zName; /* Name part of the route after "ajax/" */ @@ -334,16 +345,17 @@ int bWriteMode; /* True if requires write mode */ int bPost; /* True if requires POST (i.e. CSRF ** verification) */ }; typedef struct AjaxRoute AjaxRoute; +#endif /*INTERFACE*/ /* ** Comparison function for bsearch() for searching an AjaxRoute ** list for a matching name. */ -static int cmp_ajax_route_name(const void *a, const void *b){ +int cmp_ajax_route_name(const void *a, const void *b){ const AjaxRoute * rA = (const AjaxRoute*)a; const AjaxRoute * rB = (const AjaxRoute*)b; return fossil_strcmp(rA->zName, rB->zName); } Index: src/default.css ================================================================== --- src/default.css +++ src/default.css @@ -919,5 +919,178 @@ } img { max-width: 100%; height: auto; } + +/** + .tab-xxx: styles for fossil.tabs.js. +*/ +.tab-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; +} +.tab-container > #fossil-status-bar { + margin-top: 0; +} +.tab-container > .tabs { + padding: 0.25em; + margin: 0; + display: flex; + flex-direction: column; + border-width: 1px; + border-style: outset; + border-color: inherit; +} +.tab-container > .tabs > .tab-panel { + align-self: stretch; + flex: 10 1 auto; + display: block; +} +.tab-container > .tab-bar { + display: flex; + flex-direction: row; + flex: 1 10 auto; + align-self: stretch; + flex-wrap: wrap; +} +.tab-container > .tab-bar > .tab-button { + display: inline-block; + border-radius: 0.25em 0.25em 0 0; + margin: 0 0.1em; + padding: 0.25em 0.75em; + align-self: baseline; + border-color: inherit; + border-width: 1px; + border-bottom: none; + border-top-style: inset; + border-left-style: inset; + border-right-style: inset; + cursor: pointer; + opacity: 0.6; +} +.tab-container > .tab-bar > .tab-button.selected { + text-decoration: underline; + opacity: 1.0; + border-top-style: outset; + border-left-style: outset; + border-right-style: outset; +} + +/** + The flex-xxx classes can be used to create basic flexbox layouts + through the application of classes to the containing/contained + objects. +*/ +.flex-container { + display: flex; +} +.flex-container.flex-row { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} +.flex-container .flex-grow { + flex-grow: 10; + flex-shrink: 0; +} +.flex-container .flex-shrink { + flex-grow: 0; + flex-shrink: 10; +} +.flex-container.flex-row.stretch { + flex-wrap: wrap; + align-items: baseline; + justify-content: stretch; + margin: 0; +} +.flex-container.flex-column { + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} +.flex-container.flex-column.stretch { + align-items: stretch; + margin: 0; +} +.flex-container.child-gap-small > * { + margin: 0.25em; +} +#fossil-status-bar { + display: block; + font-family: monospace; + border-width: 1px; + border-style: inset; + border-color: inherit; + min-height: 1.5em; + font-size: 1.2em; + padding: 0.2em; + margin: 0.25em 0; + flex: 0 0 auto; +} +.font-size-100 { + font-size: 100%; +} +.font-size-125 { + font-size: 125%; +} +.font-size-150 { + font-size: 150%; +} +.font-size-175 { + font-size: 175%; +} +.font-size-200 { + font-size: 200%; +} + +/** + .input-with-label is intended to be a wrapper element which + contain both a LABEL tag and an INPUT or SELECT control. + The wrapper is "necessary", as opposed to placing the INPUT + in the LABEL, so that we can include multiple INPUT + elements (e.g. a set of radio buttons). +*/ +.input-with-label { + border: 1px inset #808080; + border-radius: 0.25em; + padding: 0.25em 0.4em; + margin: 0 0.5em; + display: inline-block; + cursor: default; +} +.input-with-label > * { + vertical-align: middle; +} +.input-with-label > label { + display: inline; /* some skins set label display to block! */ +} +.input-with-label > input { + margin: 0; +} +.input-with-label > button { + margin: 0; +} +.input-with-label > select { + margin: 0; +} +.input-with-label > input[type=text] { + margin: 0; +} +.input-with-label > textarea { + margin: 0; +} +.input-with-label > input[type=checkbox] { + vertical-align: sub; +} +.input-with-label > input[type=radio] { + vertical-align: sub; +} +.input-with-label > label { + font-weight: initial; + margin: 0 0.25em 0 0.25em; + vertical-align: middle; +} Index: src/fileedit.c ================================================================== --- src/fileedit.c +++ src/fileedit.c @@ -1630,15 +1630,19 @@ /* will be moved into the tab container via JS */); /* Main tab container... */ CX("
"); + /* The .hidden class on the following tab elements is to help lessen + the FOUC effect of the tabs before JS re-assembles them. */ + /***** File/version info tab *****/ { CX("
"); CX("
" "File/Version" "
No file loaded.
" "
"); @@ -1649,11 +1653,12 @@ /******* Content tab *******/ { CX("
"); CX("
"); CX("" @@ -1773,11 +1780,12 @@ } /****** Commit ******/ CX("
"); { /******* Commit flags/options *******/ CX("
"); style_labeled_checkbox("cb-dry-run", @@ -1894,11 +1902,12 @@ CX("
"/*#fileedit-tab-commit*/); /****** Help/Tips ******/ CX("
"); { CX("

Help & Tips

"); CX("
    "); CX("
  • Only files matching the fileedit-glob " @@ -1949,28 +1958,26 @@ blob_appendf(&endScript,");\n"); } blob_reset(&err); CheckinMiniInfo_cleanup(&cimi); - style_emit_script_fossil_bootstrap(0); - append_diff_javascript(1); - builtin_request_js("fossil.fetch.js"); - builtin_request_js("fossil.dom.js"); - builtin_request_js("fossil.tabs.js"); - builtin_request_js("fossil.confirmer.js"); - builtin_request_js("fossil.storage.js"); - + + builtin_request_js("sbsdiff.js"); + style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer", + "storage", 0); + builtin_fulfill_js_requests(); /* ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is ** used for dynamically toggling certain UI components on and off. - ** Must come before fossil.page.fileedit.js and after the fetches - ** above. + ** Must come after window.fossil has been intialized and before + ** fossil.page.fileedit.js. Potential TODO: move this into the + ** window.fossil bootstrapping so that we don't have to "fulfill" + ** the JS multiple times. */ - builtin_fulfill_js_requests(); ajax_emit_js_preview_modes(1); - builtin_request_js("fossil.page.fileedit.js"); + builtin_fulfill_js_requests(); if(blob_size(&endScript)>0){ style_emit_script_tag(0,0); CX("\n(function(){\n"); CX("try{\n%b}\n" "catch(e){" Index: src/forum.c ================================================================== --- src/forum.c +++ src/forum.c @@ -749,22 +749,18 @@ forumthread_delete(pThread); return target; } /* -** The first time this is called, it emits SCRIPT tags to load various -** forum-related JavaScript. Ideally it should be called near the end -** of the page, immediately before the call to style_footer() (which -** closes the document's and tags). Calls after the first -** are a no-op. +** Emits all JS code required by /forumpost. */ -static void forum_emit_page_js(){ +static void forumpost_emit_page_js(){ static int once = 0; if(0==once){ once = 1; + style_emit_script_fossil_bootstrap(1); builtin_request_js("forum.js"); - style_emit_script_fossil_bootstrap(0); builtin_request_js("fossil.dom.js"); builtin_request_js("fossil.page.forumpost.js"); } } @@ -894,11 +890,11 @@ }else{ style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); forum_display_hierarchical(froot, fpid); } - forum_emit_page_js(); + forumpost_emit_page_js(); style_footer(); } /* ** Return true if a forum post should be moderated. Index: src/forum.js ================================================================== --- src/forum.js +++ src/forum.js @@ -14,6 +14,6 @@ var h = x[0].scrollHeight; var y = absoluteY(x[0]); if( w>h ) y = y + (h-w)/2; if( y>0 ) window.scrollTo(0, y); } -})() +})(); Index: src/fossil.page.fileedit.js ================================================================== --- src/fossil.page.fileedit.js +++ src/fossil.page.fileedit.js @@ -121,11 +121,11 @@ and that would be horribly inefficient (meaning "battery-consuming" on mobile devices). */ const $stash = { keys: { - index: F.page.name+'/index' + index: F.page.name+'.index' }, /** index: { "CHECKIN_HASH:FILENAME": {file info w/o content} ... @@ -149,27 +149,12 @@ /** Returns the index object, fetching it from the stash or creating it anew on the first call. */ getIndex: function(){ if(!this.index){ this.index = F.storage.getJSON( - this.keys.index, undefined + this.keys.index, {} ); - if(!this.index){ - /*check for and remove/replace older name. This whole block - can be removed once the test phase is done (don't want to - invalidate the testers' edits on the test server). When - doing so, be sure to replace undefined in the above - getJSON() call with {}. */ - const oldName = F.page.name+':index'; - this.index = F.storage.getJSON(oldName,undefined); - if(this.index){ - F.storage.remove(oldName); - this.storeIndex(); - }else{ - this.index = {}; - } - } } return this.index; }, _fireStashEvent: function(){ if(this._disableNextEvent) delete this._disableNextEvent; ADDED src/fossil.page.wikiedit.js Index: src/fossil.page.wikiedit.js ================================================================== --- /dev/null +++ src/fossil.page.wikiedit.js @@ -0,0 +1,1194 @@ +(function(F/*the fossil object*/){ + "use strict"; + /** + Client-side implementation of the /wikiedit app. Requires that + the fossil JS bootstrapping is complete and that several fossil + JS APIs have been installed: fossil.fetch, fossil.dom, + fossil.tabs, fossil.storage, fossil.confirmer. + + Custom events which can be listened for via + fossil.page.addEventListener(): + + - Event 'wiki-page-loaded': passes on information when it + loads a wiki (whether from the network or its internal local-edit + cache), in the form of an "winfo" object: + + { + name: string, + mimetype: mimetype string, + type: "normal" | "tag" | "checkin" | "branch" | "sandbox", + version: UUID string or null for a sandbox page or new page, + parent: parent UUID string or null if no parent, + content: string + } + + The internal docs and code frequently use the term "winfo", and such + references refer to an object with that form. + + The fossil.page.wikiContent() method gets or sets the current + file content for the page. + + - Event 'wiki-saved': is fired when a commit completes, + passing on the same info as fileedit-file-loaded. + + - Event 'wiki-content-replaced': when the editor's content is + replaced, as opposed to it being edited via user + interaction. This normally happens via selecting a file to + load. The event detail is the fossil.page object, not the current + file content. + + - Event 'wiki-preview-updated': when the preview is refreshed + from the server, this event passes on information about the preview + change in the form of an object: + + { + element: the DOM element which contains the content preview. + mimetype: the page's mimetype. + } + + Here's an example which can be used with the highlightjs code + highlighter to update the highlighting when the preview is + refreshed in "wiki" mode (which includes fossil-native wiki and + markdown): + + fossil.page.addEventListener( + 'wiki-preview-updated', + (ev)=>{ + if(ev.detail.mimetype!=='text/plain'){ + ev.detail.element.querySelectorAll( + 'code[class^=language-]' + ).forEach((e)=>hljs.highlightBlock(e)); + } + } + ); + */ + const E = (s)=>document.querySelector(s), + D = F.dom, + P = F.page; + + P.config = { + /* Symbolic markers to denote certain edit state. Note that + the symbols themselves are *actually* defined in CSS, so if + they're changed there they also need to be changed here.*/ + editStateMarkers: { + isNew: '[+]', + isModified: '[*]' + } + }; + + /** + $stash is an internal-use-only object for managing "stashed" + local edits, to help avoid that users accidentally lose content + by switching tabs or following links or some such. The basic + theory of operation is... + + All "stashed" state is stored using fossil.storage. + + - When the current wiki content is modified by the user, the + current state of the page is stashed. + + - When saving, the stashed entry for the previous version is + removed from the stash. + + - When "loading", we use any stashed state for the given + checkin/file combination. When forcing a re-load of content, + any stashed entry for that combination is removed from the + stash. + + - Every time P.stashContentChange() updates the stash, it is + pruned to $stash.prune.defaultMaxCount most-recently-updated + entries. + + - This API often refers to "winfo objects." Those are objects + with a minimum of {page,mimetype} properties (which must be + valid), and the page name is used as basis for the stash keys + for any given page. + + The structure of the stash is a bit convoluted for efficiency's + sake: we store a map of file info (winfo) objects separately from + those files' contents because otherwise we would be required to + JSONize/de-JSONize the file content when stashing/restoring it, + and that would be horribly inefficient (meaning "battery-consuming" + on mobile devices). + */ + const $stash = { + keys: { + index: F.page.name+'.index' + }, + /** + index: { + "PAGE_NAME": {wiki page info w/o content} + ... + } + + In F.storage we... + + - Store this.index under the key this.keys.index. + + - Store each page's content under the key + (P.name+'/PAGE_NAME'). These are stored separately from the + index entries to avoid having to JSONize/de-JSONize the + content. The assumption/hope is that the browser can store + those records "directly," without any intermediary + encoding/decoding going on. + */ + indexKey: function(winfo){return winfo.name}, + /** Returns the key for storing content for the given key suffix, + by prepending P.name to suffix. */ + contentKey: function(suffix){return P.name+'/'+suffix}, + /** Returns the index object, fetching it from the stash or creating + it anew on the first call. */ + getIndex: function(){ + if(!this.index){ + this.index = F.storage.getJSON( + this.keys.index, {} + ); + } + return this.index; + }, + _fireStashEvent: function(){ + if(this._disableNextEvent) delete this._disableNextEvent; + else F.page.dispatchEvent('wiki-stash-updated', this); + }, + /** + Returns the stashed version, if any, for the given winfo object. + */ + getWinfo: function(winfo){ + const ndx = this.getIndex(); + return ndx[this.indexKey(winfo)]; + }, + /** Serializes this object's index to F.storage. Returns this. */ + storeIndex: function(){ + if(this.index) F.storage.setJSON(this.keys.index,this.index); + return this; + }, + /** Updates the stash record for the given winfo + and (optionally) content. If passed 1 arg, only + the winfo stash is updated, else both the winfo + and its contents are (re-)stashed. Returns this. + */ + updateWinfo: function(winfo,content){ + const ndx = this.getIndex(), + key = this.indexKey(winfo), + old = ndx[key]; + const record = old || (ndx[key]={ + name: winfo.name + }); + record.mimetype = winfo.mimetype; + record.type = winfo.type; + record.parent = winfo.parent; + record.version = winfo.version; + record.stashTime = new Date().getTime(); + this.storeIndex(); + if(arguments.length>1){ + F.storage.set(this.contentKey(key), content); + } + this._fireStashEvent(); + return this; + }, + /** + Returns the stashed content, if any, for the given winfo + object. + */ + stashedContent: function(winfo){ + return F.storage.get(this.contentKey(this.indexKey(winfo))); + }, + /** Returns true if we have stashed content for the given winfo + record or page name. */ + hasStashedContent: function(winfo){ + if('string'===typeof winfo) winfo = {name: winfo}; + return F.storage.contains(this.contentKey(this.indexKey(winfo))); + }, + /** Unstashes the given winfo record and its content. + Returns this. */ + unstash: function(winfo){ + const ndx = this.getIndex(), + key = this.indexKey(winfo); + delete winfo.stashTime; + delete ndx[key]; + F.storage.remove(this.contentKey(key)); + this.storeIndex(); + this._fireStashEvent(); + return this; + }, + /** + Clears all $stash entries from F.storage. Returns this. + */ + clear: function(){ + const ndx = this.getIndex(), + self = this; + let count = 0; + Object.keys(ndx).forEach(function(k){ + ++count; + const e = ndx[k]; + delete ndx[k]; + F.storage.remove(self.contentKey(k)); + }); + F.storage.remove(this.keys.index); + delete this.index; + if(count) this._fireStashEvent(); + return this; + }, + /** + Removes all but the maxCount most-recently-updated stash + entries, where maxCount defaults to this.prune.defaultMaxCount. + */ + prune: function f(maxCount){ + const ndx = this.getIndex(); + const li = []; + if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount; + Object.keys(ndx).forEach((k)=>li.push(ndx[k])); + li.sort((l,r)=>l.stashTime - r.stashTime); + let n = 0; + while(li.length>maxCount){ + ++n; + const e = li.shift(); + this._disableNextEvent = true; + this.unstash(e); + console.warn("Pruned oldest local file edit entry:",e); + } + if(n) this._fireStashEvent(); + } + }; + $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize || 10; + P.$stash = $stash /* we have to expose this for the new-page case :/ */; + + /** + Internal workaround to select the current preview mode + and fire a change event if the value actually changes + or if forceEvent is truthy. + */ + P.selectMimetype = function(modeValue, forceEvent){ + const s = this.e.selectMimetype; + if(!modeValue) modeValue = s.value; + else if(s.value != modeValue){ + s.value = modeValue; + forceEvent = true; + } + if(forceEvent){ + // Force UI update + s.dispatchEvent(new Event('change',{target:s})); + } + }; + + /** + Sets up and maintains the widgets for the list of wiki pages. + */ + const WikiList = { + e: { + filterCheckboxes: { + /*map of wiki page type to checkbox for list filtering purposes, + except for "sandbox" type, which is assumed to be covered by + the "normal" type filter. */}, + }, + cache: { + names: { + /* Map of page names to "something." We don't map to their + winfo bits because those regularly get swapped out via + de/serialization. We need this map to support the add-new-page + feature, to give us a way to check for dupes without asking + the server or walking through the whole selection list. + */} + }, + /** Updates OPTION elements to reflect whether the page has + local changes or is new/unsaved. */ + refreshStashMarks: function(){ + this.cache.names = {/*must reset it to acount for local page removals*/}; + const select = this.e.select, self = this; + Object.keys(select.options).forEach(function(key){ + const opt = select.options[key]; + const stashed = $stash.getWinfo({name:opt.value}); + if(stashed){ + const isNew = 'sandbox'===stashed.type ? false : !stashed.version; + D.addClass(opt, isNew ? 'stashed-new' :'stashed'); + }else{ + D.removeClass(opt, 'stashed', 'stashed-new'); + } + self.cache.names[opt.value] = true; + }); + }, + /** Removes the given wiki page entry from the page selection + list, if it's in the list. */ + removeEntry: function(name){ + const sel = this.e.select; + var ndx = sel.selectedIndex; + sel.value = name; + if(sel.selectedIndex>-1){ + if(ndx === sel.selectedIndex) ndx = -1; + sel.options.remove(sel.selectedIndex); + } + sel.selectedIndex = ndx; + }, + + /** + Rebuilds the selection list. Necessary when it's loaded from + the server or we locally create a new page. */ + _rebuildList: function callee(){ + /* Jump through some hoops to integrate new/unsaved + pages into the list of existing pages... We use a map + as an intermediary in order to filter out any local-stash + dupes from server-side copies. */ + const list = this.cache.pageList; + if(!list) return; + if(!callee.sorticase){ + callee.sorticase = function(l,r){ + if(l===r) return 0; + l = l.toLowerCase(); + r = r.toLowerCase(); + return l<=r ? -1 : 1; + }; + } + const map = {}, ndx = $stash.getIndex(), sel = this.e.select; + D.clearElement(sel); + list.forEach((winfo)=>map[winfo.name] = winfo); + Object.keys(ndx).forEach(function(key){ + const winfo = ndx[key]; + if(!winfo.version/*new page*/) map[winfo.name] = winfo; + }); + const self = this; + Object.keys(map) + .sort(callee.sorticase) + .forEach(function(name){ + const winfo = map[name]; + const opt = D.option(sel, winfo.name); + const wtype = opt.dataset.wtype = + winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal'); + const cb = self.e.filterCheckboxes[wtype]; + if(cb && !cb.checked) D.addClass(opt, 'hidden'); + }); + D.enable(sel); + if(P.winfo) sel.value = P.winfo.name; + this.refreshStashMarks(); + }, + + /** Loads the page list and populates the selection list. */ + loadList: function callee(){ + delete this.pageMap; + if(!callee.onload){ + const self = this; + callee.onload = function(list){ + self.cache.pageList = list; + self._rebuildList(); + F.message("Loaded page list."); + }; + } + F.fetch('wikiajax/list',{ + urlParams:{verbose:true}, + responseType: 'json', + onload: callee.onload + }); + return this; + }, + + /** + Returns true if the given name appears to be a valid + wiki page name, noting that the final arbitrator is the + server. On validation error it emits a message via fossil.error() + and returns false. + */ + validatePageName: function(name){ + var err; + if(!name){ + err = "may not be empty"; + }else if(this.cache.names.hasOwnProperty(name)){ + err = "page already exists: "+name; + }else if(name.length>100){ + err = "too long (limit is 100)"; + }else if(/\s{2,}/.test(name)){ + err = "multiple consecutive spaces"; + }else if(/[\t\r\n]/.test(name)){ + err = "contains control character(s)"; + }else{ + let i = 0, n = name.length, c; + for( ; i < n; ++i ){ + if(name.charCodeAt(i)<0x20){ + err = "contains control character(s)"; + break; + } + } + } + if(err){ + F.error("Invalid name:",err); + } + return !err; + }, + + /** + If the given name is valid, a new page with that (trimmed) name + is added to the local stash. + */ + addNewPage: function(name){ + name = name.trim(); + if(!this.validatePageName(name)) return false; + var wtype = 'normal'; + if(0===name.indexOf('checkin/')) wtype = 'checkin'; + else if(0===name.indexOf('branch/')) wtype = 'branch'; + else if(0===name.indexOf('tag/')) wtype = 'tag'; + /* ^^^ note that we're not validating that, e.g., checkin/XYZ + has a full artifact ID after "checkin/". */ + const winfo = { + name: name, type: wtype, mimetype: 'text/x-fossil-wiki', + version: null, parent: null + }; + $stash.updateWinfo(winfo, ''); + this._rebuildList(); + P.loadPage(winfo.name); + return true; + }, + + /** + Installs a wiki page selection list into the given parent DOM + element and loads the page list from the server. + */ + init: function(parentElem){ + const sel = D.select(), btn = D.addClass(D.button("Reload page list"), 'save'); + this.e.select = sel; + D.addClass(parentElem, 'WikiList'); + D.clearElement(parentElem); + D.append( + parentElem, + D.append(D.fieldset("Select a page to edit"), + sel) + ); + D.attr(sel, 'size', 15); + D.option(D.disable(D.clearElement(sel)), "Loading..."); + + /** Set up filter checkboxes for the various types + of wiki pages... */ + const fsFilter = D.fieldset("Wiki page types"), + fsFilterBody = D.div(), + filters = ['normal', 'branch', 'checkin', 'tag'] + ; + D.append(fsFilter, fsFilterBody); + D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch'); + const filterSelection = function(wtype, show){ + sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){ + if(show) opt.classList.remove('hidden'); + else opt.classList.add('hidden'); + }); + }; + const self = this; + filters.forEach(function(wtype){ + const cbId = 'wtype-filter-'+wtype, + lbl = D.attr(D.append(D.label(),wtype), + 'for', cbId), + cb = D.attr(D.input('checkbox'), 'id', cbId), + span = D.append(D.span(), cb, lbl); + self.e.filterCheckboxes[wtype] = cb; + cb.checked = true; + filterSelection(wtype, cb.checked); + cb.addEventListener( + 'change', + function(ev){filterSelection(wtype, ev.target.checked)}, + false + ); + D.append(fsFilterBody, span); + }); + + /* A legend of the meanings of the symbols we use in + the OPTION elements to denote certain state. */ + const fsLegend = D.fieldset("Edit status"), + fsLegendBody = D.div(); + D.append(fsLegend, fsLegendBody); + D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch'); + D.append( + fsLegendBody, + D.append(D.span(), P.config.editStateMarkers.isModified, + " = page has local edits"), + D.append(D.span(), P.config.editStateMarkers.isNew, + " = page is new/unsaved") + ); + + const fsNewPage = D.fieldset("Create new page"), + fsNewPageBody = D.div(), + newPageName = D.input('text'), + newPageBtn = D.button("Add page locally") + ; + D.append(parentElem, fsNewPage); + D.append(fsNewPage, fsNewPageBody); + D.addClass(fsNewPageBody, 'flex-container', 'flex-column', 'new-page'); + D.append( + fsNewPageBody, newPageName, newPageBtn, + D.append(D.addClass(D.span(), 'mini-tip'), + "New pages exist only in this browser until they are saved.") + ); + newPageBtn.addEventListener('click', function(){ + if(self.addNewPage(newPageName.value)){ + newPageName.value = ''; + } + }, false); + + D.append( + parentElem, + D.append(D.addClass(D.div(), 'fieldset-wrapper'), + fsFilter, fsNewPage, fsLegend) + ); + + D.append(parentElem, btn); + btn.addEventListener('click', ()=>this.loadList(), false); + this.loadList(); + sel.addEventListener('change', (e)=>P.loadPage(e.target.value), false); + F.page.addEventListener('wiki-stash-updated', ()=>this.refreshStashMarks()); + delete this.init; + } + }; + + /** + Keep track of how many in-flight AJAX requests there are so we + can disable input elements while any are pending. For + simplicity's sake we simply disable ALL OF IT while any AJAX is + pending, rather than disabling operation-specific UI elements, + which would be a huge maintenance hassle. + + Noting, however, that this global on/off is not *quite* + pedantically correct. Pedantically speaking. If an element is + disabled before an XHR starts, this code "should" notice that and + not include it in the to-re-enable list. That would be annoying + to do, and becomes impossible to do properly once multiple XHRs + are in transit and an element is disabled seprately between two + of those in-transit requests (that would be an unlikely, but + possible, corner case). + */ + const ajaxState = { + count: 0 /* in-flight F.fetch() requests */, + toDisable: undefined /* elements to disable during ajax activity */ + }; + F.fetch.beforesend = function f(){ + if(!ajaxState.toDisable){ + ajaxState.toDisable = document.querySelectorAll( + ['button:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])' + ].join(',') + ); + } + if(1===++ajaxState.count){ + D.addClass(document.body, 'waiting'); + D.disable(ajaxState.toDisable); + } + }; + F.fetch.aftersend = function(){ + if(0===--ajaxState.count){ + D.removeClass(document.body, 'waiting'); + D.enable(ajaxState.toDisable); + delete ajaxState.toDisable /* required to avoid enable/disable + race condition with the save button */; + } + }; + + F.onPageLoad(function() { + document.body.classList.add('wikiedit'); + P.base = {tag: E('base'), wikiUrl: F.repoUrl('wiki')}; + P.base.originalHref = P.base.tag.href; + P.e = { /* various DOM elements we work with... */ + taEditor: E('#wikiedit-content-editor'), + btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"), + btnSave: E("button.wikiedit-save"), + selectMimetype: E('select[name=mimetype]'), + selectFontSizeWrap: E('#select-font-size'), +// selectDiffWS: E('select[name=diff_ws]'), + cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'), + previewTarget: E('#wikiedit-tab-preview-wrapper'), + diffTarget: E('#wikiedit-tab-diff-wrapper'), + attachmentWrapper: E("#wikiedit-attachments"), + tabContainer: E('#wikiedit-tabs'), + tabs:{ + pageList: E('#wikiedit-tab-pages'), + content: E('#wikiedit-tab-content'), + preview: E('#wikiedit-tab-preview'), + diff: E('#wikiedit-tab-diff'), + misc: E('#wikiedit-tab-misc') + //commit: E('#wikiedit-tab-commit') + } + }; + P.tabs = new fossil.TabManager(D.clearElement(P.e.tabContainer)); + P.tabs.e.container.insertBefore( + /* Move the status bar between the tab buttons and + tab panels. Seems to be the best fit in terms of + functionality and visibility. */ + E('#fossil-status-bar'), P.tabs.e.tabs + ); + P.tabs.addEventListener( + /* Set up some before-switch-to tab event tasks... */ + 'before-switch-to', function(ev){ + const theTab = ev.detail, btnSlot = theTab.querySelector('.save-button-slot'); + if(btnSlot){ + /* Several places make sense for a save button, so we'll + move that button around to those tabs where it makes sense. */ + btnSlot.parentNode.insertBefore( P.e.btnSave, btnSlot ); + P.updateSaveButton(); + } + if(theTab===P.e.tabs.preview){ + P.baseHrefForWiki(); + if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview(); + }else if(theTab===P.e.tabs.diff){ + /* Work around a weird bug where the page gets wider than + the window when the diff tab is NOT in view and the + current SBS diff widget is wider than the window. When + the diff IS in view then CSS overflow magically reduces + the page size again. Weird. Maybe FF-specific. Note that + this weirdness happens even though P.e.diffTarget's parent + is hidden (and therefore P.e.diffTarget is also hidden). + */ + D.removeClass(P.e.diffTarget, 'hidden'); + }else if(theTab===P.e.tabs.misc){ + P.updateAttachmentView(); + } + } + ); + P.tabs.addEventListener( + /* Set up auto-refresh of the preview tab... */ + 'before-switch-from', function(ev){ + const theTab = ev.detail; + if(theTab===P.e.tabs.preview){ + P.baseHrefRestore(); + }else if(theTab===P.e.tabs.diff){ + /* See notes in the before-switch-to handler. */ + D.addClass(P.e.diffTarget, 'hidden'); + } + } + ); + + F.connectPagePreviewers( + P.e.tabs.preview.querySelector( + '#btn-preview-refresh' + ) + ); + + const diffButtons = E('#wikiedit-tab-diff-buttons'); + diffButtons.querySelector('button.sbs').addEventListener( + "click",(e)=>P.diff(true), false + ); + diffButtons.querySelector('button.unified').addEventListener( + "click",(e)=>P.diff(false), false + ); + if(0) P.e.btnCommit.addEventListener( + "click",(e)=>P.commit(), false + ); + F.confirmer(P.e.btnReload, { + confirmText: "Really reload, losing edits?", + onconfirm: function(e){ + const w = P.winfo; + if(!w){ + F.error("No page loaded."); + return; + } + if(!w.version/* new/unsaved page */ + && w.type!=='sandbox' + && P.wikiContent()){ + F.error("This new/unsaved page has content.", + "To really discard this page,", + "first clear its content", + "then use the Discard button."); + return; + } + P.unstashContent() + if(w.version || w.type==='sandbox'){ + P.loadPage(); + }else{ + WikiList.removeEntry(w.name); + P.updatePageTitle(); + delete P.winfo; + F.message("Discarded new page ["+w.name+"]."); + } + }, + ticks: 3 + }); + F.confirmer(P.e.btnSave, { + confirmText: "Really save changes?", + onconfirm: function(e){ + const w = P.winfo; + if(!w){ + F.error("No page loaded."); + return; + } + P.save(); + }, + ticks: 3 + }); + + P.e.taEditor.addEventListener( + 'change', ()=>P.stashContentChange(), false + ); + + P.selectMimetype(false, true); + P.e.selectMimetype.addEventListener( + 'change', + function(e){ + if(P.winfo){ + P.winfo.mimetype = e.target.value; + P.stashContentChange(true); + } + }, + false + ); + + const selectFontSize = E('select[name=editor_font_size]'); + if(selectFontSize){ + selectFontSize.addEventListener( + "change",function(e){ + const ed = P.e.taEditor; + ed.className = ed.className.replace( + /\bfont-size-\d+/g, '' ); + ed.classList.add('font-size-'+e.target.value); + }, false + ); + selectFontSize.dispatchEvent( + // Force UI update + new Event('change',{target:selectFontSize}) + ); + } + + P.addEventListener( + // Clear certain views when new content is loaded/set + 'wiki-content-replaced', + ()=>{ + P.previewNeedsUpdate = true; + D.clearElement(P.e.diffTarget, P.e.previewTarget); + } + ); + P.addEventListener( + // Clear certain views after a save + 'wiki-saved', + (e)=>{ + D.clearElement(P.e.diffTarget, P.e.previewTarget); + // TODO: replace preview with new content + } + ); + WikiList.init( P.e.tabs.pageList.firstElementChild ); + P.addEventListener( + // Update various state on wiki page load + 'wiki-page-loaded', + function(ev){ + delete P.winfo; + const winfo = ev.detail; + P.winfo = winfo; + P.previewNeedsUpdate = true; + P.e.selectMimetype.value = winfo.mimetype; + P.tabs.switchToTab(P.e.tabs.content); + P.wikiContent(winfo.content || ''); + WikiList.e.select.value = winfo.name; + if(!winfo.version && winfo.type!=='sandbox'){ + F.error('You are editing a new, unsaved page:',winfo.name); + } + P.updatePageTitle(); + }, + false + ); + P.addEventListener('wiki-stash-updated', ()=>P.updateSaveButton()) + .updatePageTitle().updateAttachmentView().updateSaveButton(); + }/*F.onPageLoad()*/); + + /** + Returns true if fossil.page.winfo is set, indicating that a page + has been loaded, else it reports an error and returns false. + + If passed a truthy value any error message about not having + a wiki page loaded is suppressed. + */ + const affirmPageLoaded = function(quiet){ + if(!P.winfo && !quiet) F.error("No wiki page is loaded."); + return !!P.winfo; + }; + + /** + Update the page title and header based on the state of + this.winfo. A no-op if this.winfo is not set. Returns this. + */ + P.updatePageTitle = function f(){ + if(!f.titleElement){ + f.titleElement = document.head.querySelector('title'); + f.pageTitleHeader = document.querySelector('#wikiedit-page-name > span'); + } + const title = ['Wiki Editor:']; + const wi = P.winfo; + if(wi){ + if(!wi.version && 'sandbox'!==wi.type) title.push(P.config.editStateMarkers.isNew); + else if($stash.getWinfo(wi)) title.push(P.config.editStateMarkers.isModified) + title.push(wi.name); + }else{ + title.push('(no page loaded)'); + } + f.pageTitleHeader.innerText = title[1]; + f.titleElement.innerText = title.join(' '); + return this; + }; + + /** + Change the save button depending on whether we have stuff to save + or not. + */ + P.updateSaveButton = function(){ + if(!this.winfo || !this.getStashedWinfo(this.winfo)){ + D.disable(this.e.btnSave).innerText = + "There are no changes to save"; + }else{ + D.enable(this.e.btnSave).innerText = "Save changes"; + } + return this; + }; + + /** Updates attachment-related links and returns this. */ + P.updateAttachmentView = function(){ + const wrapper = P.e.attachmentWrapper; + D.clearElement(wrapper); + const ul = D.ul(); + D.append(wrapper, ul); + if(!P.winfo){ + D.append(D.li(ul), + "Load a page to get access to its attachment-related pages."); + return this; + }else if(!P.winfo.version){ + D.append(D.li(ul), + "A new/unsaved page cannot have attachments. Save it first."); + return this; + } + const wi = P.winfo; + D.append( + D.li(ul), + D.a(F.repoUrl('attachadd',{ + page:wi.name, + from: F.repoUrl('wikiedit',{ + name: wi.name + }) + }), "Add attachments.") + ); + D.append( + D.li(ul), + D.a(F.repoUrl('attachlist',{page:wi.name}), + "List attachments"), + " (if any)." + ); + return this; + }; + + /** + Getter (if called with no args) or setter (if passed an arg) for + the current file content. + + The setter form sets the content, dispatches a + 'wiki-content-replaced' event, and returns this object. + */ + P.wikiContent = function f(){ + if(0===arguments.length){ + return f.get(); + }else{ + f.set(arguments[0] || ''); + this.dispatchEvent('wiki-content-replaced', this); + return this; + } + }; + /* Default get/set impls for file content */ + P.wikiContent.get = function(){return P.e.taEditor.value}; + P.wikiContent.set = function(content){P.e.taEditor.value = content}; + + /** + For use when installing a custom editor widget. Pass it the + getter and setter callbacks to fetch resp. set the content of the + custom widget. They will be triggered via + P.wikiContent(). Returns this object. + */ + P.setContentMethods = function(getter, setter){ + this.wikiContent.get = getter; + this.wikiContent.set = setter; + return this; + }; + + /** + Removes the default editor widget (and any dependent elements) + from the DOM, adds the given element in its place, removes this + method from this object, and returns this object. + */ + P.replaceEditorElement = function(newEditor){ + P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor); + P.e.taEditor.remove(); + P.e.selectFontSizeWrap.remove(); + delete this.replaceEditorElement; + return P; + }; + + /** + Sets the current page's base.href to {g.zTop}/wiki. + */ + P.baseHrefForWiki = function f(){ + this.base.tag.href = this.base.wikiUrl; + return this; + }; + + /** + Sets the document's base.href value to its page-load-time + setting. + */ + P.baseHrefRestore = function(){ + this.base.tag.href = this.base.originalHref; + }; + + + /** + loadPage() loads the given wiki page and updates the relevant + UI elements to reflect the loaded state. If passed no arguments + then it re-uses the values from the currently-loaded page, reloading + it (emitting an error message if no file is loaded). + + Returns this object, noting that the load is async. After loading + it triggers a 'wiki-page-loaded' event, passing it this.winfo. + + If a locally-edited copy of the given file/rev is found, that + copy is used instead of one fetched from the server, but it is + still treated as a load event. + + Alternate call forms: + + - no arguments: re-loads from this.winfo. + + - 1 non-string argument: assumed to be an winfo-style + object. Must have at least the {name} property, but need not have + other winfo state. + */ + P.loadPage = function(name){ + if(0===arguments.length){ + /* Reload from this.winfo */ + if(!affirmPageLoaded()) return this; + name = this.winfo.name; + }else if(1===arguments.length && 'string' !== typeof name){ + /* Assume winfo-like object */ + const arg = arguments[0]; + name = arg.name; + } + const onload = (r)=>this.dispatchEvent('wiki-page-loaded', r); + const stashWinfo = this.getStashedWinfo({name: name}); + if(stashWinfo){ // fake a response from the stash... + F.message("Fetched from the local-edit storage:", + stashWinfo.name); + onload({ + name: stashWinfo.name, + mimetype: stashWinfo.mimetype, + type: stashWinfo.type, + version: stashWinfo.version, + parent: stashWinfo.parent, + content: $stash.stashedContent(stashWinfo) + }); + return this; + } + F.message( + "Loading content..." + ).fetch('wikiajax/fetch',{ + urlParams: { + page: name + }, + responseType: 'json', + onload:(r)=>{ + F.message('Loaded page ['+r.name+'].'); + onload(r); + } + }); + return this; + }; + + /** + Fetches the page preview based on the contents and settings of + this page's input fields, and updates the UI with with the + preview. + + Returns this object, noting that the operation is async. + */ + P.preview = function f(switchToTab){ + if(!affirmPageLoaded()) return this; + const target = this.e.previewTarget, + self = this; + const updateView = function(c){ + D.clearElement(target); + if('string'===typeof c) target.innerHTML = c; + if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview); + }; + return this._postPreview(this.wikiContent(), updateView); + }; + + /** + Callback for use with F.connectPagePreviewers() + */ + P._postPreview = function(content,callback){ + if(!affirmPageLoaded()) return this; + if(!content){ + callback(content); + return this; + } + const fd = new FormData(); + const mimetype = this.e.selectMimetype.value; + fd.append('page', this.winfo.name); + fd.append('mimetype',mimetype); + fd.append('content',content || ''); + F.message( + "Fetching preview..." + ).fetch('wikiajax/preview',{ + payload: fd, + onload: (r,header)=>{ + callback(r); + F.message('Updated preview.'); + P.previewNeedsUpdate = false; + P.dispatchEvent('wiki-preview-updated',{ + mimetype: mimetype, + element: P.e.previewTarget + }); + }, + onerror: (e)=>{ + fossil.fetch.onerror(e); + callback("Error fetching preview: "+e); + } + }); + return this; + }; + + /** + Undo some of the SBS diff-rendering bits which hurt us more than + they help... + */ + P.tweakSbsDiffs2 = function(){ + if(1){ + const dt = this.e.diffTarget; + dt.querySelectorAll('.sbsdiffcols .difftxtcol').forEach( + (dtc)=>{ + const pre = dtc.querySelector('pre'); + pre.style.width = 'initial'; + //pre.removeAttribute('style'); + //console.debug("pre width =",pre.style.width); + } + ); + } + this.tweakSbsDiffs(); + }; + + /** + Fetches the content diff based on the contents and settings of + this page's input fields, and updates the UI with the diff view. + + Returns this object, noting that the operation is async. + */ + P.diff = function f(sbs){ + if(!affirmPageLoaded()) return this; + const content = this.wikiContent(), + self = this, + target = this.e.diffTarget; + const fd = new FormData(); + fd.append('page',this.winfo.name); + fd.append('sbs', sbs ? 1 : 0); + fd.append('content',content); + if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value); + F.message( + "Fetching diff..." + ).fetch('wikiajax/diff',{ + payload: fd, + onload: function(c){ + target.innerHTML = [ + "
    Diff [", + self.winfo.name, + "] → Local Edits
    ", + c||'No changes.' + ].join(''); + if(sbs) P.tweakSbsDiffs2(); + F.message('Updated diff.'); + self.tabs.switchToTab(self.e.tabs.diff); + } + }); + return this; + }; + + /** + Saves the current wiki page and re-populates the editor + with the saved state. + */ + P.save = function callee(){ + if(!affirmPageLoaded()) return this; + const self = this; + const content = this.wikiContent(); + if(!callee.onload){ + callee.onload = function(w){ + const oldWinfo = self.winfo; + self.unstashContent(oldWinfo); + self.winfo = w; + self.updatePageTitle(); + self.dispatchEvent('wiki-page-loaded', w); + F.message("Saved page: ["+w.name+"]."); + } + } + const fd = new FormData(), w = P.winfo; + fd.append('page',w.name); + fd.append('mimetype', w.mimetype); + fd.append('isnew', w.version ? 0 : 1); + fd.append('content', P.wikiContent()); + F.message( + "Saving page..." + ).fetch('wikiajax/save',{ + payload: fd, + responseType: 'json', + onload: callee.onload + }); + return this; + }; + + /** + Updates P.winfo for certain state and stashes P.winfo, with the + current content fetched via P.wikiContent(). + + If passed truthy AND the stash already has stashed content for + the current page, only the stashed winfo record is updated, else + both the winfo and content are updated. + */ + P.stashContentChange = function(onlyWinfo){ + if(affirmPageLoaded(true)){ + const wi = this.winfo; + wi.mimetype = P.e.selectMimetype.value; + if(onlyWinfo && $stash.hasStashedContent(wi)){ + $stash.updateWinfo(wi); + }else{ + $stash.updateWinfo(wi, P.wikiContent()); + } + F.message("Stashed change(s) to page ["+wi.name+"]."); + P.updatePageTitle(); + $stash.prune(); + this.previewNeedsUpdate = true; + } + return this; + }; + + /** + Removes any stashed state for the current P.winfo (if set) from + F.storage. Returns this. + */ + P.unstashContent = function(){ + const winfo = arguments[0] || this.winfo; + if(winfo){ + this.previewNeedsUpdate = true; + $stash.unstash(winfo); + //console.debug("Unstashed",winfo); + F.message("Unstashed page ["+winfo.name+"]."); + } + return this; + }; + + /** + Clears all stashed file state from F.storage. Returns this. + */ + P.clearStash = function(){ + $stash.clear(); + return this; + }; + + /** + If stashed content for P.winfo exists, it is returned, else + undefined is returned. + */ + P.contentFromStash = function(){ + return affirmPageLoaded(true) ? $stash.stashedContent(this.winfo) : undefined; + }; + + /** + If a stashed version of the given winfo object exists (same + filename/checkin values), return it, else return undefined. + */ + P.getStashedWinfo = function(winfo){ + return $stash.getWinfo(winfo); + }; + +})(window.fossil); Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -154,11 +154,10 @@ $(SRCDIR)/webmail.c \ $(SRCDIR)/wiki.c \ $(SRCDIR)/wikiformat.c \ $(SRCDIR)/winfile.c \ $(SRCDIR)/winhttp.c \ - $(SRCDIR)/wysiwyg.c \ $(SRCDIR)/xfer.c \ $(SRCDIR)/xfersetup.c \ $(SRCDIR)/zip.c EXTRA_FILES = \ @@ -228,10 +227,11 @@ $(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 \ @@ -257,10 +257,11 @@ $(SRCDIR)/sounds/d.wav \ $(SRCDIR)/sounds/e.wav \ $(SRCDIR)/sounds/f.wav \ $(SRCDIR)/style.admin_log.css \ $(SRCDIR)/style.fileedit.css \ + $(SRCDIR)/style.wikiedit.css \ $(SRCDIR)/tree.js \ $(SRCDIR)/useredit.js \ $(SRCDIR)/wiki.wiki TRANS_SRC = \ @@ -402,11 +403,10 @@ $(OBJDIR)/webmail_.c \ $(OBJDIR)/wiki_.c \ $(OBJDIR)/wikiformat_.c \ $(OBJDIR)/winfile_.c \ $(OBJDIR)/winhttp_.c \ - $(OBJDIR)/wysiwyg_.c \ $(OBJDIR)/xfer_.c \ $(OBJDIR)/xfersetup_.c \ $(OBJDIR)/zip_.c OBJ = \ @@ -548,11 +548,10 @@ $(OBJDIR)/webmail.o \ $(OBJDIR)/wiki.o \ $(OBJDIR)/wikiformat.o \ $(OBJDIR)/winfile.o \ $(OBJDIR)/winhttp.o \ - $(OBJDIR)/wysiwyg.o \ $(OBJDIR)/xfer.o \ $(OBJDIR)/xfersetup.o \ $(OBJDIR)/zip.o all: $(OBJDIR) $(APPNAME) @@ -884,11 +883,10 @@ $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \ $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \ $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \ $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \ $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \ - $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \ $(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \ $(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \ $(OBJDIR)/zip_.c:$(OBJDIR)/zip.h \ $(SRCDIR)/sqlite3.h \ $(SRCDIR)/th.h \ @@ -2015,18 +2013,10 @@ $(OBJDIR)/winhttp.o: $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/winhttp.o -c $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h: $(OBJDIR)/headers -$(OBJDIR)/wysiwyg_.c: $(SRCDIR)/wysiwyg.c $(OBJDIR)/translate - $(OBJDIR)/translate $(SRCDIR)/wysiwyg.c >$@ - -$(OBJDIR)/wysiwyg.o: $(OBJDIR)/wysiwyg_.c $(OBJDIR)/wysiwyg.h $(SRCDIR)/config.h - $(XTCC) -o $(OBJDIR)/wysiwyg.o -c $(OBJDIR)/wysiwyg_.c - -$(OBJDIR)/wysiwyg.h: $(OBJDIR)/headers - $(OBJDIR)/xfer_.c: $(SRCDIR)/xfer.c $(OBJDIR)/translate $(OBJDIR)/translate $(SRCDIR)/xfer.c >$@ $(OBJDIR)/xfer.o: $(OBJDIR)/xfer_.c $(OBJDIR)/xfer.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/xfer.o -c $(OBJDIR)/xfer_.c Index: src/makemake.tcl ================================================================== --- src/makemake.tcl +++ src/makemake.tcl @@ -164,11 +164,10 @@ webmail wiki wikiformat winfile winhttp - wysiwyg xfer xfersetup zip http_ssl } Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -1077,24 +1077,14 @@ @ in forum posts, make this setting be "btw". The default is an @ empty string which means that Fossil never allows Markdown documents @ to generate unsafe HTML. @ (Property: "safe-html")

    @
    - @
    - onoff_attribute("Enable WYSIWYG Wiki Editing", - "wysiwyg-wiki", "wysiwyg-wiki", 0, 0); - @

    Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages. - @ The WYSIWYG editor generates HTML instead of markup, which makes - @ subsequent manual editing more difficult. - @ (Property: "wysiwyg-wiki")

    - @
    onoff_attribute("Use HTML as wiki markup language", "wiki-use-html", "wiki-use-html", 0, 0); @

    Use HTML as the wiki markup language. Wiki links will still be parsed - @ but all other wiki formatting will be ignored. This option is helpful - @ if you have chosen to use a rich HTML editor for wiki markup such as - @ TinyMCE.

    + @ but all other wiki formatting will be ignored.

    @

    CAUTION: when @ enabling, all HTML tags and attributes are accepted in the wiki. @ No sanitization is done. This means that it is very possible for malicious @ users to inject dangerous HTML, CSS and JavaScript code into your wiki.

    @

    This should only be enabled when wiki editing is limited Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -1423,24 +1423,27 @@ ** bootstrap the window.fossil object, using the built-in file ** fossil.bootstrap.js (not to be confused with bootstrap.js). ** ** Subsequent calls are no-ops. ** -** If passed a true value, it emits the contents directly to the page -** output, else it emits a script tag with a src=builtin/... to load -** the script. It always outputs a small pre-bootstrap element in its -** own script tag to initialize parts which need C-runtime-level -** information, before loading the main fossil.bootstrap.js either -** inline or via a \n"); } } + +/* +** Convenience wrapper which calls builtin_request_js() for a series +** of builtin scripts named fossil.NAME.js. The first time it is +** called, it also calls style_emit_script_fossil_bootstrap() to +** initialize the window.fossil JS API. The first argument is a +** no-meaning dummy required by the va_start() interface. All +** subsequent arguments must be strings of the NAME part of +** fossil.NAME.js, followed by a NULL argument to terminate the list. +** +** e.g. pass it (0, "fetch", "dom", "tabs", 0) to load those 3 +** APIs. Do not forget the trailing 0! +*/ +void style_emit_fossil_js_apis( int dummy, ... ) { + static int once = 0; + const char *zArg; + char * zName; + va_list vargs; + + if(0==once++){ + style_emit_script_fossil_bootstrap(1); + } + va_start(vargs,dummy); + while( (zArg = va_arg (vargs, const char *))!=0 ){ + zName = mprintf("fossil.%s.js", zArg); + builtin_request_js(zName); + fossil_free(zName); + } + va_end(vargs); +} Index: src/style.fileedit.css ================================================================== --- src/style.fileedit.css +++ src/style.fileedit.css @@ -183,186 +183,5 @@ width: initial; } body.fileedit .sbsdiffcols div.difftxtcol pre { max-width: 44em; } - -/** - Styles for fossil.tabs.js. As of this writing, currently - only used by /fileedit, but it is anticipated that these - will eventually need to migrate to default_css.txt for use - in the wiki and/or forum pages when implementing tabbed - ajax-based previews. -*/ -.tab-container { - width: 100%; - display: flex; - flex-direction: column; - align-items: stretch; -} -.tab-container > #fossil-status-bar { - margin-top: 0; -} -.tab-container > .tabs { - padding: 0.25em; - margin: 0; - display: flex; - flex-direction: column; - border-width: 1px; - border-style: outset; - border-color: inherit; -} -.tab-container > .tabs > .tab-panel { - align-self: stretch; - flex: 10 1 auto; - display: block; -} -.tab-container > .tab-bar { - display: flex; - flex-direction: row; - flex: 1 10 auto; - align-self: stretch; - flex-wrap: wrap; -} -.tab-container > .tab-bar > .tab-button { - display: inline-block; - border-radius: 0.5em 0.5em 0 0; - margin: 0 0.1em; - padding: 0.25em 0.75em; - align-self: baseline; - border-color: inherit; - border-width: 1px; - border-bottom: none; - border-top-style: inset; - border-left-style: inset; - border-right-style: inset; - cursor: pointer; - opacity: 0.6; -} -.tab-container > .tab-bar > .tab-button.selected { - text-decoration: underline; - opacity: 1.0; - border-top-style: outset; - border-left-style: outset; - border-right-style: outset; -} - -/** - Styles developed for /fileedit but which have wider - applicability... - - As of this writing, these are only used by /fileedit, but it is - anticipated that they will eventually need to be migrated over to - default_css.txt for use in other pages (specifically wiki and forum - page/post editors). -*/ -.flex-container { - display: flex; -} -.flex-container.flex-row { - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - align-items: center; -} -.flex-container .flex-grow { - flex-grow: 10; - flex-shrink: 0; -} -.flex-container .flex-shrink { - flex-grow: 0; - flex-shrink: 10; -} -.flex-container.flex-row.stretch { - flex-wrap: wrap; - align-items: baseline; - justify-content: stretch; - margin: 0; -} -.flex-container.flex-column { - flex-direction: column; - flex-wrap: wrap; - justify-content: center; - align-items: center; -} -.flex-container.flex-column.stretch { - align-items: stretch; - margin: 0; -} -.flex-container.child-gap-small > * { - margin: 0.25em; -} -#fossil-status-bar { - display: block; - font-family: monospace; - border-width: 1px; - border-style: inset; - border-color: inherit; - min-height: 1.5em; - font-size: 1.2em; - padding: 0.2em; - margin: 0.25em 0; - flex: 0 0 auto; -} -.font-size-100 { - font-size: 100%; -} -.font-size-125 { - font-size: 125%; -} -.font-size-150 { - font-size: 150%; -} -.font-size-175 { - font-size: 175%; -} -.font-size-200 { - font-size: 200%; -} - -/** - .input-with-label is intended to be a wrapper element which - contain both a LABEL tag and an INPUT or SELECT control. - The wrapper is "necessary", as opposed to placing the INPUT - in the LABEL, so that we can include multiple INPUT - elements (e.g. a set of radio buttons). -*/ -.input-with-label { - border: 1px inset #808080; - border-radius: 0.5em; - padding: 0.25em 0.4em; - margin: 0 0.5em; - display: inline-block; - cursor: default; -} -.input-with-label > * { - vertical-align: middle; -} -.input-with-label > label { - display: inline; /* some skins set label display to block! */ -} -.input-with-label > input { - margin: 0; -} -.input-with-label > button { - margin: 0; -} -.input-with-label > select { - margin: 0; -} -.input-with-label > input[type=text] { - margin: 0; -} -.input-with-label > textarea { - margin: 0; -} -.input-with-label > input[type=checkbox] { - vertical-align: sub; -} -.input-with-label > input[type=radio] { - vertical-align: sub; -} -.input-with-label > label { - font-weight: initial; - margin: 0 0.25em 0 0.25em; - vertical-align: middle; -} ADDED src/style.wikiedit.css Index: src/style.wikiedit.css ================================================================== --- /dev/null +++ src/style.wikiedit.css @@ -0,0 +1,124 @@ +body.wikieedit.waiting * { + /* Triggered during AJAX requests. */ + cursor: wait; +} +body.wikiedit textarea, +body.wikiedit textarea:focus, +body.wikiedit input, +body.wikiedit input:focus, +body.wikiedit select, +body.wikiedit select:focus{ + /* The sudden appearance of a border (as in the Ardoise skin) + shifts the layout in unsightly ways */ + border: initial; +} +body.wikiedit div.wikiedit-preview { + margin: 0; + padding: 0; +} +body.wikiedit #wikiedit-tabs { + margin: 1em 0 0 0; +} +body.wikiedit #wikiedit-tab-preview-wrapper { + overflow: auto; +} +body.wikiedit .tab-container > .tabs > .tab-panel > .wikiedit-options { + margin-top: 0; + border: none; + border-radius: 0; + border-bottom-width: 1px; + border-bottom-style: dotted; +} +body.wikiedit .tab-container > .tabs > .tab-panel > .wikiedit-options > button { + vertical-align: middle; + margin: 0.5em; +} +body.wikiedit .tab-container > .tabs > .tab-panel > .wikiedit-options > input { + vertical-align: middle; + margin: 0.5em; +} +body.wikiedit .tab-container > .tabs > .tab-panel > .wikiedit-options > .input-with-label { + vertical-align: middle; + margin: 0.5em; +} +body.wikiedit .wikiedit-options > div > * { + margin: 0.25em; +} +body.wikiedit .wikiedit-options.flex-container.flex-row { + align-items: first baseline; +} +body.wikiedit .WikiList { + display: flex; + flex-direction: column; + align-items: start; +} +body.wikiedit .WikiList select { + font-size: 110%; + margin: initial; + height: initial /* some skins set these to a fix height */; +} +body.wikiedit .WikiList select option { + margin: 0.5em 0; +} +body.wikiedit .WikiList select option.stashed::before { +/* Maintenance reminder: the option.stashed/stashed-new "content" values + are duplicated in fossil.page.wikiedit.js and need to be changed there + if they are changed here: see fossil.page.config.editStateMarkers */ + content: "[*] "; +} +body.wikiedit .WikiList select option.stashed-new::before { + content: "[+] "; +} +body.wikiedit textarea { + max-width: initial; +} +body.wikiedit .tabs .tab-panel { + /* Needed for wide diffs */ + overflow: auto; +} +body.wikiedit .WikiList fieldset { + padding: 0.25em; + border-width: 1px /* Ardoise skin sets this to 0 */; +} +body.wikiedit .WikiList legend { + font-size: 90%; +} +body.wikiedit .WikiList fieldset > :not(legend) { + /* Stretch page selection list when it's empty or only has short page names */ + width: 100%; +} +body.wikiedit .WikiList .fieldset-wrapper { + /* Container for the filter and edit status fieldsets */ + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: stretch; + margin: 0; +} +body.wikiedit .WikiList button.save { + margin: 1em 0 0 0; +} +body.wikiedit .WikiList .new-page { + align-items: flex-start; + max-width: 15em; +} +body.wikiedit .WikiList .new-page input { +} +body.wikiedit #wikiedit-tab-misc h3 { + margin: 0; +} +body.wikiedit span.mini-tip { + font-size: 80%; +} + +body.wikiedit span.save-button-slot { + /* These invisible placeholders mark spots in the UI + (max. 1 per tab) to where the save button gets + relocated as we switch between tabs. */ + display: none; +} + +body.wikiedit #wikiedit-page-name > span { + font-family: monospace; +} Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -102,11 +102,10 @@ " WHERE tagid=%d AND mtime<%.16g" " ORDER BY mtime DESC LIMIT 1", tagid, mtime); } - /* ** WEBPAGE: home ** WEBPAGE: index ** WEBPAGE: not_found ** @@ -398,10 +397,24 @@ if( sqlite3_strglob("tag/*", zPageName)==0 ){ return WIKITYPE_TAG; } return WIKITYPE_NORMAL; } + +/* +** Returns a JSON-friendly string form of the integer value returned +** by wiki_page_type(zPageName). +*/ +const char * wiki_page_type_name(const char *zPageName){ + switch(wiki_page_type(zPageName)){ + case WIKITYPE_CHECKIN: return "checkin"; + case WIKITYPE_BRANCH: return "branch"; + case WIKITYPE_TAG: return "tag"; + case WIKITYPE_NORMAL: + default: return "normal"; + } +} /* ** Add an appropriate style_header() for either the /wiki or /wikiedit page ** for zPageName. zExtra is an empty string for /wiki but has the text ** "Edit: " for /wikiedit. @@ -541,16 +554,11 @@ zMimetype = wiki_filter_mimetypes(zMimetype); if( !g.isHome && !noSubmenu ){ if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki)) && wiki_special_permission(zPageName) ){ - if( db_get_boolean("wysiwyg-wiki", 0) ){ - style_submenu_element("Edit", "%R/wikiedit?name=%T&wysiwyg=1", - zPageName); - }else{ - style_submenu_element("Edit", "%R/wikiedit?name=%T", zPageName); - } + style_submenu_element("Edit", "%R/wikiedit?name=%T", zPageName); }else if( rid && g.perm.ApndWiki ){ style_submenu_element("Edit", "%R/wikiappend?name=%T", zPageName); } if( g.perm.Hyperlink ){ style_submenu_element("History", "%R/whistory?name=%T", zPageName); @@ -620,220 +628,656 @@ return azStyles[i+1]; } } return azStyles[1]; } + +/* + ** Tries to fetch a wiki page for the given name. If found, it + ** returns true, else false. + ** + ** versionsBack specifies how many versions back in the history to + ** fetch. Use 0 for the latest version, 1 for its parent, etc. + ** + ** If pRid is not NULL then if a result is found *pRid is set to its + ** RID. If ppWiki is not NULL then if found *ppWiki is set to the + ** loaded wiki object, which the caller is responsible for passing to + ** manifest_destroy(). + */ +static int wiki_fetch_by_name( const char *zPageName, + unsigned int versionsBack, + int * pRid, Manifest **ppWiki ){ + Manifest *pWiki = 0; + char *zTag = mprintf("wiki-%s", zPageName); + Stmt q = empty_Stmt; + int rid = 0; + + db_prepare(&q, "SELECT rid FROM tagxref" + " WHERE tagid=(SELECT tagid FROM tag WHERE" + " tagname=%Q) " + " ORDER BY mtime DESC LIMIT -1 OFFSET %u", zTag, + versionsBack); + fossil_free(zTag); + if(SQLITE_ROW == db_step(&q)){ + rid = db_column_int(&q, 0); + } + db_finalize(&q); + if( rid == 0 ){ + return 0; + } + else if(pRid){ + *pRid = rid; + } + if(ppWiki){ + pWiki = manifest_get(rid, CFTYPE_WIKI, 0); + if( pWiki==0 ){ + /* "Cannot happen." */ + return 0; + } + *ppWiki = pWiki; + } + return 1; +} + +/* +** Determines whether the wiki page with the given name can be edited +** or created by the current user. If not, an AJAX error is queued and +** false is returned, else true is returned. A NULL, empty, or +** malformed name is considered non-writable, regardless of the user. +** +** If pRid is not NULL then this function writes the page's rid to +** *pRid (whether or not access is granted). On error or if the page +** does not yet exist, *pRid will be set to 0. +** +** Note that the sandbox is a special case: it is a pseudo-page with +** no rid and the /wikiajax API does not allow anyone to actually save +** a sandbox page, but it is reported as writable here (with rid 0). +*/ +static int wiki_ajax_can_write(const char *zPageName, int * pRid){ + int rid = 0; + const char * zErr = 0; + + if(pRid) *pRid = 0; + if(!zPageName || !*zPageName + || !wiki_name_is_wellformed((unsigned const char *)zPageName)){ + zErr = "Invalid page name."; + }else if(is_sandbox(zPageName)){ + return 1; + }else{ + wiki_fetch_by_name(zPageName, 0, &rid, 0); + if(pRid) *pRid = rid; + if(!wiki_special_permission(zPageName)){ + zErr = "Editing this page requires non-wiki write permissions."; + }else if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){ + return 3; + }else if(rid && !g.perm.WrWiki){ + zErr = "Requires wiki-write permissions."; + }else if(!rid && !g.perm.NewWiki){ + zErr = "Requires new-wiki permissions."; + }else{ + zErr = "Cannot happen! Please report this as a bug."; + } + } + ajax_route_error(403, "%s", zErr); + return 0; +} + +/* +** Loads the given wiki page, sets the response type to +** application/json, and emits it as a JSON object. If zPageName is a +** sandbox page then a "fake" object is emitted, as the wikiajax API +** does not permit saving the sandbox. +** +** Returns true on success, false on error, and on error it +** queues up a JSON-format error response. +** +** Output JSON format: +** +** { name: "page name", +** type: "normal" | "tag" | "checkin" | "branch" | "sandbox", +** mimetype: "mime type", +** version: UUID string or null for a sandbox page, +** parent: "parent uuid" or null if no parent, +** content: "page content" +** } +** +** If includeContent is false then the content member is elided. +*/ +static int wiki_ajax_emit_page_object(const char *zPageName, + int includeContent){ + Manifest * pWiki = 0; + char * zUuid; + + cgi_set_content_type("application/json"); + if( is_sandbox(zPageName) ){ + char * zMimetype = + db_get("sandbox-mimetype","text/x-fossil-wiki"); + char * zBody = db_get("sandbox",""); + CX("{\"name\": %!j, \"type\": \"sandbox\", " + "\"mimetype\": %!j, \"version\": null, \"parent\": null", + zPageName, zMimetype); + if(includeContent){ + CX(", \"content\": %!j", + zBody); + } + CX("}"); + fossil_free(zMimetype); + fossil_free(zBody); + return 1; + }else if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){ + ajax_route_error(404, "Wiki page could not be loaded: %s", + zPageName); + return 0; + }else{ + zUuid = rid_to_uuid(pWiki->rid); + CX("{\"name\": %!j, \"type\": %!j, " + "\"version\": %!j, " + "\"mimetype\": %!j, ", + pWiki->zWikiTitle, + wiki_page_type_name(pWiki->zWikiTitle), + zUuid, + pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki"); + CX("\"parent\": "); + if(pWiki->nParent){ + CX("%!j", pWiki->azParent[0]); + }else{ + CX("null"); + } + if(includeContent){ + CX(", \"content\": %!j", pWiki->zWiki); + } + CX("}"); + fossil_free(zUuid); + manifest_destroy(pWiki); + return 2; + } +} + +/* +** Ajax route handler for /wikiajax/save. +** +** URL params: +** +** page = the wiki page name. +** mimetype = content mime type. +** content = page content. Fossil considers an empty page to +** be "deleted". +** isnew = 1 if the page is to be newly-created, else 0 or +** not send. +** +** Responds with JSON. On error, an object in the form documented by +** ajax_route_error(). On success, an object in the form documented +** for wiki_ajax_emit_page_object(). +** +** The wikiajax API disallows saving of a sandbox pseudo-page, and +** will respond with an error if asked to save one. +** +** Reminder: the original implementation implements sandbox-page +** saving using: +** +** db_set("sandbox",zBody,0); +** db_set("sandbox-mimetype",zMimetype,0); +** +*/ +static void wiki_ajax_route_save(void){ + const char *zPageName = P("page"); + const char *zMimetype = P("mimetype"); + const char *zContent = P("content"); + const int isNew = atoi(PD("isnew","0"))==1; + Blob content = empty_blob; + int parentRid = 0; + int rollback = 0; + + if(!wiki_ajax_can_write(zPageName, &parentRid)){ + return; + }else if(is_sandbox(zPageName)){ + ajax_route_error(403,"Saving a sandbox page is prohibited."); + return; + } + + /* These isNew checks are just me being pedantic. The hope is + to avoid accidental addition of new pages which differ only + by the case of their name. We could just as easily derive + isNew based on whether or not the page already exists. */ + if(isNew){ + if(parentRid>0){ + ajax_route_error(403,"Requested a new page, " + "but it already exists with RID %d: %s", + parentRid, zPageName); + return; + } + }else if(parentRid==0){ + ajax_route_error(403,"Creating new page [%s] requires passing " + "isnew=1.", zPageName); + return; + } + + blob_init(&content, zContent ? zContent : "", -1); + db_begin_transaction(); + wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0); + rollback = wiki_ajax_emit_page_object(zPageName, 1) ? 0 : 1; + db_end_transaction(rollback); +} + +/* +** Ajax route handler for /wikiajax/fetch. +** +** URL params: +** +** page = the wiki page name +** +** Responds with JSON. On error, an object in the form documented by +** ajax_route_error(). On success, an object in the form documented +** for wiki_ajax_emit_page_object(). +*/ +static void wiki_ajax_route_fetch(void){ + const char * zPageName = P("page"); + + if( zPageName==0 || zPageName[0]==0 ){ + ajax_route_error(400,"Missing page name."); + return; + } + wiki_ajax_emit_page_object(zPageName, 1); +} + +/* +** Ajax route handler for /wikiajax/diff. +** +** URL params: +** +** page = the wiki page name +** content = the new/edited wiki page content +** +** Requires that the user have write access solely to avoid some +** potential abuse cases. It does not actually write anything. +*/ +static void wiki_ajax_route_diff(void){ + const char * zPageName = P("page"); + Blob contentNew = empty_blob, contentOrig = empty_blob; + Manifest * pParent = 0; + const char * zContent = P("content"); + u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR; + + if( zPageName==0 || zPageName[0]==0 ){ + ajax_route_error(400,"Missing page name."); + return; + }else if(!wiki_ajax_can_write(zPageName, 0)){ + return; + } + switch(atoi(PD("sbs","0"))){ + case 0: diffFlags |= DIFF_LINENO; break; + default: diffFlags |= DIFF_SIDEBYSIDE; + } + switch(atoi(PD("ws","2"))){ + case 1: diffFlags |= DIFF_IGNORE_EOLWS; break; + case 2: diffFlags |= DIFF_IGNORE_ALLWS; break; + default: break; + } + wiki_fetch_by_name( zPageName, 0, 0, &pParent ); + if( pParent && pParent->zWiki && *pParent->zWiki ){ + blob_init(&contentOrig, pParent->zWiki, -1); + }else{ + blob_init(&contentOrig, "", 0); + } + blob_init(&contentNew, zContent ? zContent : "", -1); + cgi_set_content_type("text/html"); + ajax_render_diff(&contentOrig, &contentNew, diffFlags); + blob_reset(&contentNew); + blob_reset(&contentOrig); + manifest_destroy(pParent); +} + +/* +** Ajax route handler for /wikiajax/preview. +** +** URL params: +** +** page = wiki page name. This is only needed for authorization +** checking. +** mimetype = the wiki page mimetype (determines rendering style) +** content = the wiki page content +*/ +static void wiki_ajax_route_preview(void){ + const char * zPageName = P("page"); + const char * zContent = P("content"); + + if(!wiki_ajax_can_write(zPageName, 0)){ + return; + }else if( zContent==0 ){ + ajax_route_error(400,"Missing content to preview."); + return; + }else{ + Blob content = empty_blob; + const char * zMimetype = PD("mimetype","text/x-fossil-wiki"); + + blob_init(&content, zContent, -1); + cgi_set_content_type("text/html"); + wiki_render_by_mimetype(&content, zMimetype); + blob_reset(&content); + } +} + +/* +** Ajax route handler for /wikiajax/list. +** +** Optional parameters: verbose, includeContent (see below). +** +** Responds with JSON. On error, an object in the form documented by +** ajax_route_error(). +** +** On success, it emits an array of strings (page names) sorted +** case-insensitively. If the "verbose" parameter is passed in then +** the result list contains objects in the format documented for +** wiki_ajax_emit_page_object(). The content of each object is elided +** unless the "includeContent" parameter is passed on. +** +** The result list always contains an entry +** named "sandbox" which represents the sandbox pseudo-page. +*/ +static void wiki_ajax_route_list(void){ + Stmt q = empty_Stmt; + int n = 0; + const int verbose = ajax_p_bool("verbose"); + const int includeContent = ajax_p_bool("includeContent"); + + cgi_set_content_type("application/json"); + db_begin_transaction(); + db_prepare(&q, "SELECT" + " substr(tagname,6) AS name" + " FROM tag WHERE tagname GLOB 'wiki-*'" + " UNION SELECT 'Sandbox' AS name" + " ORDER BY name COLLATE NOCASE"); + CX("["); + while( SQLITE_ROW==db_step(&q) ){ + char const * zName = db_column_text(&q,0); + if(n++){ + CX(","); + } + if(verbose==0){ + CX("%!j", zName); + }else{ + wiki_ajax_emit_page_object(zName, includeContent); + } + } + db_finalize(&q); + db_end_transaction(0); + CX("]"); +} + + +/* +** WEBPAGE: wikiajax +** +** An internal dispatcher for wiki AJAX operations. Not for direct +** client use. All routes defined by this interface are app-internal, +** subject to change +*/ +void wiki_ajax_page(void){ + const char * zName = P("name"); + AjaxRoute routeName = {0,0,0,0}; + const AjaxRoute * pRoute = 0; + const AjaxRoute routes[] = { + /* Keep these sorted by zName (for bsearch()) */ + {"diff", wiki_ajax_route_diff, 1, 1}, + {"fetch", wiki_ajax_route_fetch, 0, 0}, + {"list", wiki_ajax_route_list, 0, 0}, + {"preview", wiki_ajax_route_preview, 0, 1} + /* preview access mode: whether or not wiki-write mode is needed + really depends on multiple factors. e.g. the sandbox page does + not normally require more than anonymous access. We set its + write-mode to false and do those checks manually in that route's + handler. + */, + {"save", wiki_ajax_route_save, 1, 1} + }; + + if(zName==0 || zName[0]==0){ + ajax_route_error(400,"Missing required [route] 'name' parameter."); + return; + } + routeName.zName = zName; + pRoute = (const AjaxRoute *)bsearch(&routeName, routes, + count(routes), sizeof routes[0], + cmp_ajax_route_name); + if(pRoute==0){ + ajax_route_error(404,"Ajax route not found."); + return; + } + login_check_credentials(); + if( pRoute->bWriteMode!=0 && g.perm.WrWiki==0 ){ + ajax_route_error(403,"Write permissions required."); + return; + }else if(0==cgi_csrf_safe(pRoute->bPost)){ + ajax_route_error(403, + "CSRF violation (make sure sending of HTTP " + "Referer headers is enabled for XHR " + "connections)."); + return; + } + pRoute->xCallback(); +} /* ** WEBPAGE: wikiedit -** URL: /wikiedit?name=PAGENAME +** URL: /wikedit?name=PAGENAME +** +** The main front-end for the Ajax-based wiki editor app. Passing +** in the name of an unknown page will trigger the creation +** of a new page (which is not actually created in the database +** until the user explicitly saves it). If passed no page name, +** the user may select a page from the list on the first UI tab. ** -** Edit a wiki page. +** When creating a new page, the mimetype URL parameter may optionally +** be used to set its mimetype to one of text/x-fossil-wiki, +** text/x-markdown, or text/plain, defauling to the former. */ void wikiedit_page(void){ - char *zTag; - int rid = 0; + const char *zPageName; + const char * zMimetype = P("mimetype"); int isSandbox; - Blob wiki; - Manifest *pWiki = 0; - const char *zPageName; - int n; - const char *z; - char *zBody = (char*)P("w"); - const char *zMimetype = wiki_filter_mimetypes(P("mimetype")); - int isWysiwyg = P("wysiwyg")!=0; - int goodCaptcha = 1; - int eType = WIKITYPE_UNKNOWN; - int havePreview = 0; - - if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; } - if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; } - if( zBody ){ - if( isWysiwyg ){ - Blob body; - blob_zero(&body); - htmlTidy(zBody, &body); - zBody = blob_str(&body); - }else{ - zBody = mprintf("%s", zBody); - } - } + int found = 0; + login_check_credentials(); zPageName = PD("name",""); - if( check_name(zPageName) ) return; + if(zPageName && *zPageName){ + if( check_name(zPageName) ) return; + } isSandbox = is_sandbox(zPageName); if( isSandbox ){ if( !g.perm.WrWiki ){ login_needed(g.anon.WrWiki); return; } - if( zBody==0 ){ - zBody = db_get("sandbox",""); - zMimetype = db_get("sandbox-mimetype","text/x-fossil-wiki"); - } - }else{ - zTag = mprintf("wiki-%s", zPageName); - rid = db_int(0, - "SELECT rid FROM tagxref" - " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" - " ORDER BY mtime DESC", zTag - ); - free(zTag); + found = 1; + }else if( zPageName!=0 ){ + int rid = 0; if( !wiki_special_permission(zPageName) ){ login_needed(0); return; } + found = wiki_fetch_by_name(zPageName, 0, &rid, 0); if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki); return; } - if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ - zBody = pWiki->zWiki; - zMimetype = pWiki->zMimetype; - } - } - if( P("submit")!=0 && zBody!=0 - && (goodCaptcha = captcha_is_correct(0)) - ){ - char *zDate; - Blob cksum; - blob_zero(&wiki); - db_begin_transaction(); - if( isSandbox ){ - db_set("sandbox",zBody,0); - db_set("sandbox-mimetype",zMimetype,0); - }else{ - login_verify_csrf_secret(); - zDate = date_in_standard_format("now"); - blob_appendf(&wiki, "D %s\n", zDate); - free(zDate); - blob_appendf(&wiki, "L %F\n", zPageName); - if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ - blob_appendf(&wiki, "N %s\n", zMimetype); - } - if( rid ){ - char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); - blob_appendf(&wiki, "P %s\n", zUuid); - free(zUuid); - } - if( !login_is_nobody() ){ - blob_appendf(&wiki, "U %F\n", login_name()); - } - blob_appendf(&wiki, "W %d\n%s\n", strlen(zBody), zBody); - md5sum_blob(&wiki, &cksum); - blob_appendf(&wiki, "Z %b\n", &cksum); - blob_reset(&cksum); - wiki_put(&wiki, 0, wiki_need_moderation(0)); - } - db_end_transaction(0); - cgi_redirectf("wiki?name=%T", zPageName); - } - if( P("cancel")!=0 ){ - cgi_redirectf("wiki?name=%T", zPageName); - return; - } - if( zBody==0 ){ - zBody = mprintf(""); - } - style_set_current_page("%T?name=%T", g.zPath, zPageName); - eType = wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "Edit: "); - if( rid && !isSandbox && g.perm.ApndWiki ){ - if( g.perm.Attach ){ - style_submenu_element("Attach", - "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T", - g.zTop, zPageName, g.zTop, zPageName); - } - } - if( !goodCaptcha ){ - @

    Error: Incorrect security code.

    - } - blob_zero(&wiki); - while( fossil_isspace(zBody[0]) ) zBody++; - blob_append(&wiki, zBody, -1); - if( P("preview")!=0 ){ - havePreview = 1; - if( zBody[0] ){ - @ Preview:
    - safe_html_context(DOCSRC_WIKI); - wiki_render_by_mimetype(&wiki, zMimetype); - @
    - blob_reset(&wiki); - } - } - for(n=2, z=zBody; z[0]; z++){ - if( z[0]=='\n' ) n++; - } - if( n<20 ) n = 20; - if( n>30 ) n = 30; - if( !isWysiwyg ){ - /* Traditional markup-only editing */ - char *zPlaceholder = 0; - switch( eType ){ - case WIKITYPE_NORMAL: { - zPlaceholder = mprintf("Enter text for wiki page %s", zPageName); - break; - } - case WIKITYPE_BRANCH: { - zPlaceholder = mprintf("Enter notes about branch %s", zPageName+7); - break; - } - case WIKITYPE_CHECKIN: { - zPlaceholder = mprintf("Enter notes about check-in %.20s", zPageName+8); - break; - } - case WIKITYPE_TAG: { - zPlaceholder = mprintf("Enter notes about tag %s", zPageName+4); - break; - } - } - form_begin(0, "%R/wikiedit"); - @
    %z(href("%R/markup_help"))Markup style: - mimetype_option_menu(zMimetype); - @
    - @
    - fossil_free(zPlaceholder); - if( db_get_boolean("wysiwyg-wiki", 0) ){ - @ - } - @ - }else{ - /* Wysiwyg editing */ - Blob html, temp; - havePreview = 1; - form_begin("", "%R/wikiedit"); - @
    - @ - blob_zero(&temp); - wiki_convert(&wiki, &temp, 0); - blob_zero(&html); - htmlTidy(blob_str(&temp), &html); - blob_reset(&temp); - wysiwygEditor("w", blob_str(&html), 60, n); - blob_reset(&html); - @
    - @ - } - login_insert_csrf_secret(); - if( havePreview ){ - if( isWysiwyg || zBody[0] ){ - @ - }else{ - @ - } - } - @ - @ - @
    - captcha_generate(0); - @ - manifest_destroy(pWiki); - blob_reset(&wiki); + } + style_header("Wiki Editor"); + + /* Status bar */ + CX("
    " + "Status messages will go here.
    \n" + /* will be moved into the tab container via JS */); + + CX("
    Editing: (no file loaded)
    "); + + /* Main tab container... */ + CX("
    Loading...
    "); + /* The .hidden class on the following tab elements is to help lessen + the FOUC effect of the tabs before JS re-assembles them. */ + + /******* Page list *******/ + { + CX("
    "); + CX("
    Loading wiki pages list...
    "); + CX("
    "/*#wikiedit-tab-pages*/); + } + + /******* Content tab *******/ + { + CX("
    "); + CX("
    "); + CX("" + ""); + mimetype_option_menu(0); + CX(""); + style_select_list_int("select-font-size", + "editor_font_size", "Editor font size", + NULL/*tooltip*/, + 100, + "100%", 100, "125%", 125, + "150%", 150, "175%", 175, + "200%", 200, NULL); + CX(""); + CX(""/*will get moved around dynamically*/); + CX(""); + CX("
    "); + CX("
    "); + CX(""); + CX("
    "/*textarea wrapper*/); + CX("
    "/*#tab-file-content*/); + } + /****** Preview tab ******/ + { + CX("
    "); + CX("
    "); + CX(""); + /* Toggle auto-update of preview when the Preview tab is selected. */ + style_labeled_checkbox("cb-preview-autoupdate", + NULL, + "Auto-refresh?", + "1", 1, + "If on, the preview will automatically " + "refresh when this tab is selected."); + CX(""); + CX("
    "/*.wikiedit-options*/); + CX("
    "); + CX("
    "/*#wikiedit-tab-preview*/); + } + + /****** Diff tab ******/ + { + CX("
    "); + + CX("
    "); + CX("" + ""); + CX(""); + CX("
    "); + CX("
    " + "Diffs will be shown here." + "
    "); + CX("
    "/*#wikiedit-tab-diff*/); + } + + /****** The obligatory "Misc" tab ******/ + { + CX("
    "); + CX("

    Wiki formatting rules

    "); + CX(""); + CX("

    Attachments

    "); + CX("
    " + /* Filled out by JS */); + CX("

    The \"Sandbox\" Page

    "); + CX("

    The page named \"Sandbox\" is not a real wiki page. " + "It provides a place where users may test out wiki syntax " + "without having to actually save anything, nor pollute " + "the repo with endless test runs. Any attempt to save the " + "sandbox page will fail.

    "); + CX("

    Wiki Name Rules

    "); + well_formed_wiki_name_rules(); + CX("
    "/*#wikiedit-tab-save*/); + } + + builtin_request_js("sbsdiff.js"); + style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer", + "storage", "page.wikiedit", 0); + builtin_fulfill_js_requests(); + /* Dynamically populate the editor... */ + style_emit_script_tag(0,0); + CX("\nfossil.onPageLoad(function(){\n"); + CX("const P = fossil.page;\n" + "try{\n"); + if(!found && zPageName && *zPageName){ + /* For a new page, stick a dummy entry in the JS-side stash + and "load" it from there. */ + CX("const winfo = {" + "\"name\": %!j, \"mimetype\": %!j, " + "\"type\": %!j, " + "\"parent\": null, \"version\": null" + "};\n", + zPageName, + zMimetype ? zMimetype : "text/x-fossil-wiki", + wiki_page_type_name(zPageName)); + /* If the JS-side stash already has this page, load that + copy from the stash, otherwise inject a new stash entry + for it and load *that* one... */ + CX("if(!P.$stash.getWinfo(winfo)){" + "P.$stash.updateWinfo(winfo,'');" + "}\n"); + } + if(zPageName && *zPageName){ + CX("P.loadPage(%!j);\n", zPageName); + } + CX("}catch(e){" + "fossil.error(e); console.error('Exception:',e);" + "}\n"); + CX("});\n"/*fossil.onPageLoad()*/); + style_emit_script_tag(1,0); style_footer(); } /* ** WEBPAGE: wikinew @@ -851,17 +1295,11 @@ return; } zName = PD("name",""); zMimetype = wiki_filter_mimetypes(P("mimetype")); if( zName[0] && wiki_name_is_wellformed((const unsigned char *)zName) ){ - if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")==0 - && db_get_boolean("wysiwyg-wiki", 0) - ){ - cgi_redirectf("wikiedit?name=%T&wysiwyg=1", zName); - }else{ - cgi_redirectf("wikiedit?name=%T&mimetype=%s", zName, zMimetype); - } + cgi_redirectf("wikiedit?name=%T&mimetype=%s", zName, zMimetype); } style_header("Create A New Wiki Page"); wiki_standard_submenu(W_ALL_BUT(W_NEW)); @

    Rules for wiki page names:

    well_formed_wiki_name_rules(); @@ -1441,11 +1879,11 @@ ** -M|--mimetype TEXT-FORMAT The mime type of the update. ** Defaults to the type used by ** the previous version of the ** page, or text/x-fossil-wiki. ** Valid values are: text/x-fossil-wiki, -** text/markdown and text/plain. fossil, +** text/x-markdown and text/plain. fossil, ** markdown or plain can be specified as ** synonyms of these values. ** -t|--technote DATETIME Specifies the timestamp of ** the technote to be created or ** updated. When updating a tech note DELETED src/wysiwyg.c Index: src/wysiwyg.c ================================================================== --- src/wysiwyg.c +++ /dev/null @@ -1,328 +0,0 @@ -/* -** Copyright (c) 2012 D. Richard Hipp -** -** This program is free software; you can redistribute it and/or -** modify it under the terms of the Simplified BSD License (also -** known as the "2-Clause License" or "FreeBSD License".) -** -** This program is distributed in the hope that it will be useful, -** but without any warranty; without even the implied warranty of -** merchantability or fitness for a particular purpose. -** -** Author contact information: -** drh@hwaci.com -** http://www.hwaci.com/drh/ -** -******************************************************************************* -** -** This file contains code that generates WYSIWYG text editors on -** web pages. -*/ -#include "config.h" -#include -#include -#include "wysiwyg.h" - - -/* -** Output code for a WYSIWYG editor. The caller must have already generated -** the
    that will contain the editor, and the call must generate the -** corresponding
    after this routine returns. The caller must include -** an onsubmit= attribute on the
    element that invokes the -** wysiwygSubmit() function. -** -** There can only be a single WYSIWYG editor per frame. -*/ -void wysiwygEditor( - const char *zId, /* ID for this editor */ - const char *zContent, /* Initial content (HTML) */ - int w, int h /* Initial width and height */ -){ - - @ - - @ - @
    Edit mode: - @
    - @
    - @ - @ - @ - @ - @
    - @
    - @ - - @ - - @ - - @ - - @ - - @ - - @ - - @ - - @ - @ - - @ - - @ - - @ - - @ - - @ - -#if 0 /* Cut/Copy/Paste requires special browser permissions for security - ** reasons. So omit these buttons */ - @ - - @ - - @ -#endif - - @
    - @
    %s(zContent)
    - @ - -} Index: win/Makefile.dmc ================================================================== --- win/Makefile.dmc +++ win/Makefile.dmc @@ -28,13 +28,13 @@ SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_TRUSTED_SCHEMA=0 SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_TRUSTED_SCHEMA=0 -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen -SRC = add_.c ajax_.c alerts_.c allrepo_.c attach_.c backlink_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c capabilities_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c deltafunc_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c encode_.c etag_.c event_.c export_.c extcgi_.c file_.c fileedit_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c fuzz_.c glob_.c graph_.c gzip_.c hname_.c hook_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c repolist_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c setupuser_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c terminal_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c +SRC = add_.c ajax_.c alerts_.c allrepo_.c attach_.c backlink_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c capabilities_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c deltafunc_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c encode_.c etag_.c event_.c export_.c extcgi_.c file_.c fileedit_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c fuzz_.c glob_.c graph_.c gzip_.c hname_.c hook_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c repolist_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c setupuser_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c terminal_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c xfer_.c xfersetup_.c zip_.c -OBJ = $(OBJDIR)\add$O $(OBJDIR)\ajax$O $(OBJDIR)\alerts$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backlink$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\capabilities$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\deltafunc$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\extcgi$O $(OBJDIR)\file$O $(OBJDIR)\fileedit$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\fuzz$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\hook$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\repolist$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\setupuser$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\terminal$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O +OBJ = $(OBJDIR)\add$O $(OBJDIR)\ajax$O $(OBJDIR)\alerts$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backlink$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\capabilities$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\deltafunc$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\extcgi$O $(OBJDIR)\file$O $(OBJDIR)\fileedit$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\fuzz$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\hook$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\repolist$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\setupuser$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\terminal$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O RC=$(DMDIR)\bin\rcc RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ @@ -49,11 +49,11 @@ $(OBJDIR)\fossil.res: $B\win\fossil.rc $(RC) $(RCFLAGS) -o$@ $** $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res - +echo add ajax alerts allrepo attach backlink backoffice bag bisect blob branch browse builtin bundle cache capabilities captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd deltafunc descendants diff diffcmd dispatch doc encode etag event export extcgi file fileedit finfo foci forum fshell fusefs fuzz glob graph gzip hname hook http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp repolist report rss schema search security_audit setup setupuser sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar terminal th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ + +echo add ajax alerts allrepo attach backlink backoffice bag bisect blob branch browse builtin bundle cache capabilities captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd deltafunc descendants diff diffcmd dispatch doc encode etag event export extcgi file fileedit finfo foci forum fshell fusefs fuzz glob graph gzip hname hook http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp repolist report rss schema search security_audit setup setupuser sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar terminal th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp xfer xfersetup zip shell sqlite3 th th_lang > $@ +echo fossil >> $@ +echo fossil >> $@ +echo $(LIBS) >> $@ +echo. >> $@ +echo fossil >> $@ @@ -962,16 +962,10 @@ $(TCC) -o$@ -c winhttp_.c winhttp_.c : $(SRCDIR)\winhttp.c +translate$E $** > $@ -$(OBJDIR)\wysiwyg$O : wysiwyg_.c wysiwyg.h - $(TCC) -o$@ -c wysiwyg_.c - -wysiwyg_.c : $(SRCDIR)\wysiwyg.c - +translate$E $** > $@ - $(OBJDIR)\xfer$O : xfer_.c xfer.h $(TCC) -o$@ -c xfer_.c xfer_.c : $(SRCDIR)\xfer.c +translate$E $** > $@ @@ -987,7 +981,7 @@ zip_.c : $(SRCDIR)\zip.c +translate$E $** > $@ headers: makeheaders$E page_index.h builtin_data.h VERSION.h - +makeheaders$E add_.c:add.h ajax_.c:ajax.h alerts_.c:alerts.h allrepo_.c:allrepo.h attach_.c:attach.h backlink_.c:backlink.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h capabilities_.c:capabilities.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h deltafunc_.c:deltafunc.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h extcgi_.c:extcgi.h file_.c:file.h fileedit_.c:fileedit.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h fuzz_.c:fuzz.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h hook_.c:hook.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h repolist_.c:repolist.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h setupuser_.c:setupuser.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h terminal_.c:terminal.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h + +makeheaders$E add_.c:add.h ajax_.c:ajax.h alerts_.c:alerts.h allrepo_.c:allrepo.h attach_.c:attach.h backlink_.c:backlink.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h capabilities_.c:capabilities.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h deltafunc_.c:deltafunc.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h extcgi_.c:extcgi.h file_.c:file.h fileedit_.c:fileedit.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h fuzz_.c:fuzz.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h hook_.c:hook.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h repolist_.c:repolist.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h setupuser_.c:setupuser.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h terminal_.c:terminal.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h @copy /Y nul: headers Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -566,11 +566,10 @@ $(SRCDIR)/webmail.c \ $(SRCDIR)/wiki.c \ $(SRCDIR)/wikiformat.c \ $(SRCDIR)/winfile.c \ $(SRCDIR)/winhttp.c \ - $(SRCDIR)/wysiwyg.c \ $(SRCDIR)/xfer.c \ $(SRCDIR)/xfersetup.c \ $(SRCDIR)/zip.c EXTRA_FILES = \ @@ -640,10 +639,11 @@ $(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 \ @@ -669,10 +669,11 @@ $(SRCDIR)/sounds/d.wav \ $(SRCDIR)/sounds/e.wav \ $(SRCDIR)/sounds/f.wav \ $(SRCDIR)/style.admin_log.css \ $(SRCDIR)/style.fileedit.css \ + $(SRCDIR)/style.wikiedit.css \ $(SRCDIR)/tree.js \ $(SRCDIR)/useredit.js \ $(SRCDIR)/wiki.wiki TRANS_SRC = \ @@ -814,11 +815,10 @@ $(OBJDIR)/webmail_.c \ $(OBJDIR)/wiki_.c \ $(OBJDIR)/wikiformat_.c \ $(OBJDIR)/winfile_.c \ $(OBJDIR)/winhttp_.c \ - $(OBJDIR)/wysiwyg_.c \ $(OBJDIR)/xfer_.c \ $(OBJDIR)/xfersetup_.c \ $(OBJDIR)/zip_.c OBJ = \ @@ -960,11 +960,10 @@ $(OBJDIR)/webmail.o \ $(OBJDIR)/wiki.o \ $(OBJDIR)/wikiformat.o \ $(OBJDIR)/winfile.o \ $(OBJDIR)/winhttp.o \ - $(OBJDIR)/wysiwyg.o \ $(OBJDIR)/xfer.o \ $(OBJDIR)/xfersetup.o \ $(OBJDIR)/zip.o APPNAME = fossil.exe @@ -1321,11 +1320,10 @@ $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \ $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \ $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \ $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \ $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \ - $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \ $(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \ $(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \ $(OBJDIR)/zip_.c:$(OBJDIR)/zip.h \ $(SRCDIR)/sqlite3.h \ $(SRCDIR)/th.h \ @@ -2454,18 +2452,10 @@ $(OBJDIR)/winhttp.o: $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/winhttp.o -c $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h: $(OBJDIR)/headers -$(OBJDIR)/wysiwyg_.c: $(SRCDIR)/wysiwyg.c $(TRANSLATE) - $(TRANSLATE) $(SRCDIR)/wysiwyg.c >$@ - -$(OBJDIR)/wysiwyg.o: $(OBJDIR)/wysiwyg_.c $(OBJDIR)/wysiwyg.h $(SRCDIR)/config.h - $(XTCC) -o $(OBJDIR)/wysiwyg.o -c $(OBJDIR)/wysiwyg_.c - -$(OBJDIR)/wysiwyg.h: $(OBJDIR)/headers - $(OBJDIR)/xfer_.c: $(SRCDIR)/xfer.c $(TRANSLATE) $(TRANSLATE) $(SRCDIR)/xfer.c >$@ $(OBJDIR)/xfer.o: $(OBJDIR)/xfer_.c $(OBJDIR)/xfer.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/xfer.o -c $(OBJDIR)/xfer_.c Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -488,11 +488,10 @@ "$(OX)\webmail_.c" \ "$(OX)\wiki_.c" \ "$(OX)\wikiformat_.c" \ "$(OX)\winfile_.c" \ "$(OX)\winhttp_.c" \ - "$(OX)\wysiwyg_.c" \ "$(OX)\xfer_.c" \ "$(OX)\xfersetup_.c" \ "$(OX)\zip_.c" EXTRA_FILES = "$(SRCDIR)\..\skins\aht\details.txt" \ @@ -561,10 +560,11 @@ "$(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" \ @@ -590,10 +590,11 @@ "$(SRCDIR)\sounds\d.wav" \ "$(SRCDIR)\sounds\e.wav" \ "$(SRCDIR)\sounds\f.wav" \ "$(SRCDIR)\style.admin_log.css" \ "$(SRCDIR)\style.fileedit.css" \ + "$(SRCDIR)\style.wikiedit.css" \ "$(SRCDIR)\tree.js" \ "$(SRCDIR)\useredit.js" \ "$(SRCDIR)\wiki.wiki" OBJ = "$(OX)\add$O" \ @@ -740,11 +741,10 @@ "$(OX)\webmail$O" \ "$(OX)\wiki$O" \ "$(OX)\wikiformat$O" \ "$(OX)\winfile$O" \ "$(OX)\winhttp$O" \ - "$(OX)\wysiwyg$O" \ "$(OX)\xfer$O" \ "$(OX)\xfersetup$O" \ "$(OX)\zip$O" \ !if $(FOSSIL_ENABLE_MINIZ)!=0 "$(OX)\miniz$O" \ @@ -967,11 +967,10 @@ echo "$(OX)\webmail.obj" >> $@ echo "$(OX)\wiki.obj" >> $@ echo "$(OX)\wikiformat.obj" >> $@ echo "$(OX)\winfile.obj" >> $@ echo "$(OX)\winhttp.obj" >> $@ - echo "$(OX)\wysiwyg.obj" >> $@ echo "$(OX)\xfer.obj" >> $@ echo "$(OX)\xfersetup.obj" >> $@ echo "$(OX)\zip.obj" >> $@ !if $(FOSSIL_ENABLE_MINIZ)!=0 echo "$(OX)\miniz.obj" >> $@ @@ -1155,10 +1154,11 @@ 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" >> $@ @@ -1184,10 +1184,11 @@ echo "$(SRCDIR)\sounds/d.wav" >> $@ echo "$(SRCDIR)\sounds/e.wav" >> $@ echo "$(SRCDIR)\sounds/f.wav" >> $@ echo "$(SRCDIR)\style.admin_log.css" >> $@ echo "$(SRCDIR)\style.fileedit.css" >> $@ + echo "$(SRCDIR)\style.wikiedit.css" >> $@ echo "$(SRCDIR)\tree.js" >> $@ echo "$(SRCDIR)\useredit.js" >> $@ echo "$(SRCDIR)\wiki.wiki" >> $@ "$(OX)\add$O" : "$(OX)\add_.c" "$(OX)\add.h" @@ -2028,16 +2029,10 @@ $(TCC) /Fo$@ /Fd$(@D)\ -c "$(OX)\winhttp_.c" "$(OX)\winhttp_.c" : "$(SRCDIR)\winhttp.c" "$(OBJDIR)\translate$E" $** > $@ -"$(OX)\wysiwyg$O" : "$(OX)\wysiwyg_.c" "$(OX)\wysiwyg.h" - $(TCC) /Fo$@ /Fd$(@D)\ -c "$(OX)\wysiwyg_.c" - -"$(OX)\wysiwyg_.c" : "$(SRCDIR)\wysiwyg.c" - "$(OBJDIR)\translate$E" $** > $@ - "$(OX)\xfer$O" : "$(OX)\xfer_.c" "$(OX)\xfer.h" $(TCC) /Fo$@ /Fd$(@D)\ -c "$(OX)\xfer_.c" "$(OX)\xfer_.c" : "$(SRCDIR)\xfer.c" "$(OBJDIR)\translate$E" $** > $@ @@ -2196,14 +2191,13 @@ "$(OX)\webmail_.c":"$(OX)\webmail.h" \ "$(OX)\wiki_.c":"$(OX)\wiki.h" \ "$(OX)\wikiformat_.c":"$(OX)\wikiformat.h" \ "$(OX)\winfile_.c":"$(OX)\winfile.h" \ "$(OX)\winhttp_.c":"$(OX)\winhttp.h" \ - "$(OX)\wysiwyg_.c":"$(OX)\wysiwyg.h" \ "$(OX)\xfer_.c":"$(OX)\xfer.h" \ "$(OX)\xfersetup_.c":"$(OX)\xfersetup.h" \ "$(OX)\zip_.c":"$(OX)\zip.h" \ "$(SRCDIR)\sqlite3.h" \ "$(SRCDIR)\th.h" \ "$(OX)\VERSION.h" \ "$(SRCDIR)\cson_amalgamation.h" @copy /Y nul: $@ Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -58,10 +58,14 @@ "[/help?cmd=sql|fossil sql]" command supports new output modes ".mode box" and ".mode json". * Add the "obscure()" SQL function to the "[/help?cmd=sql|fossil sql]" command. * [./delta_format.wiki|Delta compression] is now applied to forum edits. + * The [/help?cmd=/wikiedit|wiki editor] has been modernized and is + now Ajax-based. The WYSIWYG editing option for Fossil-format wiki + pages was removed. (Please let us know, via the site's Support menu, + if that removal unduly impacts you.) * Countless documentation enhancements.

    Changes for Version 2.11 (2020-05-25)