From 509356c6964b1c04dbc920e5cb756dceb7f5c0fd Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Sun, 4 Apr 2021 13:18:41 +0100 Subject: [PATCH] Add Node.js support, refactor modelling of jobs and sitemaps, and use modals framework --- core/language/en-GB/Publishing/Modal.tid | 5 + core/modules/commands/publish.js | 46 +++ core/modules/publisher-handler.js | 354 +++++++++--------- core/modules/publishers/filesystem.js | 52 +++ core/modules/startup/rootwidget.js | 9 +- core/modules/startup/startup.js | 4 + core/modules/utils/dom/modal.js | 42 ++- core/modules/widgets/image.js | 20 +- core/ui/ControlPanel/Publishing.tid | 22 +- core/ui/PageControls/publish.tid | 16 +- core/wiki/publishing-jobs/Default.tid | 9 + core/wiki/routes/StaticSite/HTML.tid | 7 + core/wiki/routes/StaticSite/Images.tid | 10 +- core/wiki/routes/StaticSite/Index.tid | 10 +- core/wiki/routes/StaticSite/Job.tid | 6 - core/wiki/routes/StaticSite/StaticHTML.tid | 9 - core/wiki/routes/StaticSite/Styles.tid | 10 +- core/wiki/sitemaps/StaticSite.tid | 7 + .../tiddlers/publishing/Publishing.tid | 45 ++- plugins/tiddlywiki/jszip/jszip-publisher.js | 9 +- themes/tiddlywiki/vanilla/base.tid | 41 +- 21 files changed, 451 insertions(+), 282 deletions(-) create mode 100644 core/language/en-GB/Publishing/Modal.tid create mode 100644 core/modules/commands/publish.js create mode 100644 core/modules/publishers/filesystem.js create mode 100644 core/wiki/publishing-jobs/Default.tid create mode 100644 core/wiki/routes/StaticSite/HTML.tid delete mode 100644 core/wiki/routes/StaticSite/Job.tid delete mode 100644 core/wiki/routes/StaticSite/StaticHTML.tid create mode 100644 core/wiki/sitemaps/StaticSite.tid diff --git a/core/language/en-GB/Publishing/Modal.tid b/core/language/en-GB/Publishing/Modal.tid new file mode 100644 index 000000000..ca843d174 --- /dev/null +++ b/core/language/en-GB/Publishing/Modal.tid @@ -0,0 +1,5 @@ +title: $:/language/Publishing/Modal +subtitle: Publishing: ''<$transclude field="caption"><$view field="title"/>'' +footer: <$button message="tm-close-tiddler">Cancel + +Publishing <$text text=<>/> files via the "{{!!publisher}}" publisher. diff --git a/core/modules/commands/publish.js b/core/modules/commands/publish.js new file mode 100644 index 000000000..a6e52301e --- /dev/null +++ b/core/modules/commands/publish.js @@ -0,0 +1,46 @@ +/*\ +title: $:/core/modules/commands/publish.js +type: application/javascript +module-type: command + +Publish static files + +\*/ +(function(){ + + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; + + exports.info = { + name: "publish", + synchronous: false + }; + + var Command = function(params,commander,callback) { + this.params = params; + this.commander = commander; + this.callback = callback; + }; + + Command.prototype.execute = function() { + if(this.params.length < 1) { + return "Missing filename filter"; + } + var self = this, + wiki = this.commander.wiki, + jobTiddler = this.params[0], + variableList = this.params.slice(1), + variables = Object.create(null); + while(variableList.length >= 2) { + variables[variableList[0]] = variableList[1]; + variableList = variableList.slice(2); + } + $tw.publisherHandler.publish(jobTiddler,this.callback,{commander: this.commander,variables: variables}); + return null; + }; + + exports.Command = Command; + + })(); + \ No newline at end of file diff --git a/core/modules/publisher-handler.js b/core/modules/publisher-handler.js index 047541685..43319a26a 100644 --- a/core/modules/publisher-handler.js +++ b/core/modules/publisher-handler.js @@ -12,223 +12,266 @@ The publisher manages publishing extracts of wikis as external files /*global $tw: false */ "use strict"; +var PUBLISHING_MODAL_TITLE = "$:/language/Publishing/Modal"; + /* Instantiate the publisher manager with the following options -widget: optional widget for attaching event handlers +wiki: wiki object to be used +commander: commander object to be used for output */ function PublisherHandler(options) { - this.widget = options.widget; this.wiki = options.wiki; - this.reset(); - if(this.widget) { - this.widget.addEventListener("tm-publish-start",this.onPublishStart.bind(this)); - this.widget.addEventListener("tm-publish-route",this.onPublishRoute.bind(this)); - this.widget.addEventListener("tm-publish-end",this.onPublishEnd.bind(this)); - } + this.commander = options.commander; } -PublisherHandler.prototype.onPublishStart = function(event) { - var publisherName = event.paramObject["publisher-name"], - publisherParamsTitle = event.paramObject["publish-params-title"], - publisherParamsTiddler = publisherParamsTitle && this.wiki.getTiddler(publisherParamsTitle); - if(publisherName && publisherParamsTiddler) { - this.publisherName = publisherName; - this.publisherParamsTitle = publisherParamsTitle; - this.publisherParams = publisherParamsTiddler.fields; - this.routes = []; +/* +Publish a job + +jobTitle: title of tiddler containing details of the job +callback: completion callback invoked callback(err) +options: Include: + +commander: commander object associated with publishing under Node.js +variables: hashmap of variables to be passed to renderings +*/ +PublisherHandler.prototype.publish = function(jobTitle,callback,options) { + if(jobTitle) { + var job = new PublishingJob(jobTitle,this,options); + job.publish(callback); } }; -PublisherHandler.prototype.onPublishEnd = function(event) { - if(this.publisherName && this.publisherParams && this.routes) { - this.publish(); - } -}; +function PublishingJob(jobTitle,publisherHandler,options) { + options = options || {}; + // Save params + this.jobTitle = jobTitle; + this.publisherHandler = publisherHandler; + this.commander = options.commander; + this.publishVariables = options.variables || Object.create(null); +} -PublisherHandler.prototype.onPublishRoute = function(event) { - if(this.publisherName && this.publisherParams && this.routes) { - this.routes.push(event.paramObject); +/* +Start publishing +*/ +PublishingJob.prototype.publish = function(callback) { + var self = this; + // Get the job tiddler and check it is enabled + this.jobTiddler = this.publisherHandler.wiki.getTiddler(this.jobTitle); + if(this.jobTiddler && this.jobTiddler.fields.enabled === "yes") { + // Get the list of tiddlers to be exported, defaulting to all non-system tiddlers + this.exportList = this.publisherHandler.wiki.filterTiddlers(this.jobTiddler.fields["export-filter"] || "[!is[system]]"); + // Get the job variables + this.jobVariables = this.extractVariables(this.jobTiddler); + // Get publisher + this.publisher = this.getPublisher(this.jobTiddler.fields.publisher); + if(this.publisher) { + // Get the sitemap + this.sitemap = this.publisherHandler.wiki.getTiddler(this.jobTiddler.fields.sitemap); + if(this.sitemap) { + // Get the sitemap variables + this.sitemapVariables = this.extractVariables(this.sitemap); + // Collect the operations from each route + this.operations = []; + $tw.utils.each(this.sitemap.fields.list,function(routeTitle) { + var routeTiddler = self.publisherHandler.wiki.getTiddler(routeTitle); + if(routeTiddler) { + Array.prototype.push.apply(self.operations,self.getOperationsForRoute(routeTiddler)); + } + }); + // Display the progress modal + if($tw.modal) { + self.progressModal = $tw.modal.display(PUBLISHING_MODAL_TITLE,{ + progress: true, + variables: { + currentTiddler: this.jobTitle, + totalFiles: this.operations.length + "" + }, + onclose: function(event) { + if(event !== self) { + // The modal was closed other than by us programmatically + self.isCancelled = true; + } + } + }); + } + // Send the operations to the publisher + this.executeOperations(function(err) { + if(self.progressModal) { + self.progressModal.closeHandler(self); + } + callback(err); + }); + } else { + return callback("Missing sitemap"); + } + } else { + return callback("Unrecognised publisher"); + } + } else { + return callback("Missing or disabled job tiddler"); } }; /* Instantiate the required publisher object */ -PublisherHandler.prototype.getPublisher = function() { - var self = this, - publisher; +PublishingJob.prototype.getPublisher = function(publisherName) { + var publisher; $tw.modules.forEachModuleOfType("publisher",function(title,module) { - if(module.name === self.publisherName) { + if(module.name === publisherName) { publisher = module; } }); - return publisher && publisher.create(this.publisherParams); + return publisher && publisher.create(this.jobTiddler.fields,this.publisherHandler,this); }; /* -Expand publish routes to separate commands +Extract the variables from tiddler fields prefixed "var-" */ -PublisherHandler.prototype.expandRoutes = function(routes) { +PublishingJob.prototype.extractVariables = function(tiddler) { + var variables = {}; + $tw.utils.each(tiddler.getFieldStrings(),function(value,name) { + if(name.substring(0,4) === "var-") { + variables[name.substring(4)] = value; + } + }); + return variables; +}; + +/* +Expand publish routes to separate operations +*/ +PublishingJob.prototype.getOperationsForRoute = function(routeTiddler) { var self = this, - commands = []; - $tw.utils.each(routes,function(route) { - var filter = route.filter || "DUMMY_RESULT"; // If no filter is provided, use a dummy filter that returns a single result - switch(route["route-type"]) { + operations = [], + routeFilter = routeTiddler.fields["route-tiddler-filter"] || "DUMMY_RESULT", // If no filter is provided, use a dummy filter that returns a single result + tiddlers = self.publisherHandler.wiki.filterTiddlers(routeFilter,null,self.publisherHandler.wiki.makeTiddlerIterator(this.exportList)); + if(routeFilter) { + switch(routeTiddler.fields["route-type"]) { case "save": - if(filter && route.path) { - $tw.utils.each(self.wiki.filterTiddlers(filter),function(title) { - commands.push({ + if(routeTiddler.fields["route-path-filter"]) { + $tw.utils.each(tiddlers,function(title) { + operations.push({ "route-type": "save", - path: self.resolveParameterisedPath(route.path,title), + path: self.resolvePathFilter(routeTiddler.fields["route-path-filter"],title), title: title }); }); } break; case "render": - if(filter && route.path && route.template) { - $tw.utils.each(self.wiki.filterTiddlers(filter),function(title) { - commands.push({ + if(routeTiddler.fields["route-path-filter"] && routeTiddler.fields["route-template"]) { + var routeVariables = $tw.utils.extend({},this.publishVariables,this.jobVariables,this.sitemapVariables,this.extractVariables(routeTiddler)); + $tw.utils.each(tiddlers,function(title) { + operations.push({ "route-type": "render", - path: self.resolveParameterisedPath(route.path,title), + path: self.resolvePathFilter(routeTiddler.fields["route-path-filter"],title), title: title, - template: route.template + template: routeTiddler.fields["route-template"], + variables: routeVariables }); }); } break; } - }); - return commands; + } + return operations; }; /* -Apply a tiddler to a parameterised path to create a usable path +Apply a tiddler to a filter to create a usable path */ -PublisherHandler.prototype.resolveParameterisedPath = function(route,title) { - var self = this; - // Split the route on $$ markers - var tiddler = this.wiki.getTiddler(title), - output = []; - $tw.utils.each(route.split(/(\$[a-z_]+\$)/),function(part) { - var match = part.match(/\$([a-z]+)_([a-z]+)\$/); - if(match) { - var value; - // Get the base value - switch(match[1]) { - case "uri": - case "title": - value = title; - break; - case "type": - value = tiddler.fields.type || "text/vnd.tiddlywiki"; - break; - } - // Apply the encoding function - switch(match[2]) { - case "encoded": - value = encodeURIComponent(value); - break; - case "doubleencoded": - value = encodeURIComponent(encodeURIComponent(value)); - break; - case "slugify": - value = self.wiki.slugify(value); - break; +PublishingJob.prototype.resolvePathFilter = function(pathFilter,title) { + var tiddler = this.publisherHandler.wiki.getTiddler(title); + return this.publisherHandler.wiki.filterTiddlers(pathFilter,{ + getVariable: function(name) { + switch(name) { + case "currentTiddler": + return "" + this.imageSource; case "extension": - value = ($tw.config.contentTypeInfo[value] || {extension: "."}).extension.slice(1); - break; + return "" + ($tw.config.contentTypeInfo[tiddler.fields.type || "text/vnd.tiddlywiki"] || {extension: ""}).extension; + default: + return $tw.rootWidget.getVariable(name); } - output.push(value); - } else { - output.push(part); } - }); - return output.join(""); + },this.publisherHandler.wiki.makeTiddlerIterator([title]))[0]; }; /* -Publish the routes in this.routes[] +Execute the operations for this job */ -PublisherHandler.prototype.publish = function(callback) { +PublishingJob.prototype.executeOperations = function(callback) { var self = this, report = {overwrites: []}, - commands = this.expandRoutes(this.routes), - nextCommand = 0, - publisher = this.getPublisher(), - performNextCommand = function() { - // Set progress - self.setProgress(nextCommand,commands.length); + nextOperation = 0, + performNextOperation = function() { + // Check for having been cancelled + if(self.isCancelled) { + if(self.publisher.publishCancel) { + self.publisher.publishCancel(); + } + return callback("CANCELLED"); + } + // Update progress + if(self.progressModal) { + self.progressModal.setProgress(nextOperation,self.operations.length); + } // Check for having finished - if(nextCommand >= commands.length) { - publisher.publishEnd(function() { - self.saveReport(report); - self.reset(); - self.hideProgress(); - if(callback) { - $tw.utils.nextTick(callback); - } + if(nextOperation >= self.operations.length) { + $tw.utils.nextTick(function() { + self.publisher.publishEnd(callback); }); } else { - // Execute this command - var fileDetails = self.prepareCommand(commands[nextCommand]); - nextCommand += 1; - publisher.publishFile(fileDetails,function() { - $tw.utils.nextTick(performNextCommand); + // Execute this operation + var fileDetails = self.prepareOperation(self.operations[nextOperation]); + nextOperation += 1; + self.publisher.publishFile(fileDetails,function() { + $tw.utils.nextTick(performNextOperation); }); } }; - // Fail if we didn't get a publisher - if(!publisher) { - alert("Publisher " + this.publisherName + " not found"); - return; - } - this.displayProgress("Publishing"); // Tell the publisher to start, and get back an array of the existing paths - publisher.publishStart(function(existingPaths) { + self.publisher.publishStart(function(existingPaths) { var paths = {}; - $tw.utils.each(commands,function(command) { - if(command.path in paths) { - report.overwrites.push(command.path); + $tw.utils.each(self.operations,function(operation) { + if(operation.path in paths) { + report.overwrites.push(operation.path); } - paths[command.path] = true; + paths[operation.path] = true; }); - // Run the commands - $tw.utils.nextTick(performNextCommand); + // Run the operations + performNextOperation(); }); }; /* -Construct a file details object from a command object +Construct a file details object from an operation object */ -PublisherHandler.prototype.prepareCommand = function(command) { - var tiddler = this.wiki.getTiddler(command.title), +PublishingJob.prototype.prepareOperation = function(operation) { + var tiddler = this.publisherHandler.wiki.getTiddler(operation.title), fileDetails = { - path: command.path - }; - switch(command["route-type"]) { + path: operation.path + }; + switch(operation["route-type"]) { case "save": fileDetails.text = tiddler.fields.text || ""; fileDetails.type = tiddler.fields.type || ""; + fileDetails.isBase64 = ($tw.config.contentTypeInfo[tiddler.fields.type] || {}).encoding === "base64"; break; case "render": - fileDetails.text = this.wiki.renderTiddler("text/plain",command.template,{variables: {currentTiddler: command.title}}); + fileDetails.text = this.publisherHandler.wiki.renderTiddler("text/plain",operation.template,{ + variables: $tw.utils.extend( + {currentTiddler: operation.title}, + operation.variables + ) + }); fileDetails.type = "text/html"; break; } return fileDetails; }; -/* -*/ -PublisherHandler.prototype.reset = function() { - this.publisherName = null; - this.publisherParams = null; - this.routes = null; -}; - - -PublisherHandler.prototype.saveReport = function(report) { +PublishingJob.prototype.saveReport = function(report) { // Create the report tiddler var reportTitle = this.wiki.generateNewTitle("$:/temp/publish-report"); $tw.wiki.addTiddler({ @@ -242,45 +285,6 @@ PublisherHandler.prototype.saveReport = function(report) { $tw.wiki.addTiddler(new $tw.Tiddler(paramsTiddler,{list: list})); }; -PublisherHandler.prototype.displayProgress = function(message) { - if($tw.browser) { - this.progressWrapper = document.createElement("div"); - this.progressWrapper.className = "tc-progress-bar-wrapper"; - this.progressText = document.createElement("div"); - this.progressText.className = "tc-progress-bar-text"; - this.progressText.appendChild(document.createTextNode(message)); - this.progressWrapper.appendChild(this.progressText); - this.progressBar = document.createElement("div"); - this.progressBar.className = "tc-progress-bar"; - this.progressWrapper.appendChild(this.progressBar); - this.progressPercent = document.createElement("div"); - this.progressPercent.className = "tc-progress-bar-percent"; - this.progressWrapper.appendChild(this.progressPercent); - document.body.appendChild(this.progressWrapper); - } -}; - -PublisherHandler.prototype.hideProgress = function() { - if($tw.browser && this.progressWrapper) { - this.progressWrapper.parentNode.removeChild(this.progressWrapper); - this.progressWrapper = null; - } -}; - -PublisherHandler.prototype.setProgress = function(numerator,denominator) { - if($tw.browser && this.progressWrapper) { - // Remove old progress - while(this.progressPercent.hasChildNodes()) { - this.progressPercent.removeChild(this.progressPercent.firstChild); - } - // Set new text - var percent = (numerator * 100 /denominator).toFixed(2) + "%"; - this.progressPercent.appendChild(document.createTextNode(percent)); - // Set bar width - this.progressBar.style.width = percent; - } -}; - exports.PublisherHandler = PublisherHandler; })(); diff --git a/core/modules/publishers/filesystem.js b/core/modules/publishers/filesystem.js new file mode 100644 index 000000000..b452c7577 --- /dev/null +++ b/core/modules/publishers/filesystem.js @@ -0,0 +1,52 @@ +/*\ +title: $:/core/modules/publishers/filesystem.js +type: application/javascript +module-type: publisher + +Handles publishing to the Node.js filesystem + +\*/ +(function(){ + + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; + + exports.name = "filesystem"; + + exports.create = function(params,publisherHandler,publishingJob) { + return new FileSystemPublisher(params,publisherHandler,publishingJob); + }; + + function FileSystemPublisher(params,publisherHandler,publishingJob) { + this.params = params; + this.publisherHandler = publisherHandler; + this.publishingJob = publishingJob; + }; + + FileSystemPublisher.prototype.publishStart = function(callback) { + console.log("publishStart"); + // Returns a list of the previously published files + callback([]); + }; + + FileSystemPublisher.prototype.publishFile = function(item,callback) { + var fs = require("fs"), + path = require("path"), + filepath = path.resolve(this.publishingJob.commander.outputPath,item.path); + $tw.utils.createFileDirectories(filepath); + fs.writeFile(filepath,item.text,item.isBase64 ? "base64" : "utf8",function(err) { + if(err) { + console.log("File writing error",err) + } + callback(err); + }); + }; + + FileSystemPublisher.prototype.publishEnd = function(callback) { + console.log("publishEnd"); + callback(null); + }; + + })(); + \ No newline at end of file diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index 38f80ec4e..ca450b1f4 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -72,10 +72,11 @@ exports.startup = function() { } }); } - // Install the publisher handler - $tw.publisherHandler = new $tw.PublisherHandler({ - wiki: $tw.wiki, - widget: $tw.rootWidget + // Hook up events for the publisher handler + $tw.rootWidget.addEventListener("tm-publish",function(event) { + $tw.publisherHandler.publish(event.paramObject.job,function(err) { + console.log("Finished publishing with result:",err); + }); }); // If we're being viewed on a data: URI then give instructions for how to save if(document.location.protocol === "data:") { diff --git a/core/modules/startup/startup.js b/core/modules/startup/startup.js index f681b71d1..e80115fb1 100755 --- a/core/modules/startup/startup.js +++ b/core/modules/startup/startup.js @@ -129,6 +129,10 @@ exports.startup = function() { dirtyTracking: !$tw.syncadaptor, preloadDirty: $tw.boot.preloadDirty || [] }); + // Install the publisher handler + $tw.publisherHandler = new $tw.PublisherHandler({ + wiki: $tw.wiki + }); // Host-specific startup if($tw.browser) { // Install the popup manager diff --git a/core/modules/utils/dom/modal.js b/core/modules/utils/dom/modal.js index db66b1abc..3d30f4cf8 100644 --- a/core/modules/utils/dom/modal.js +++ b/core/modules/utils/dom/modal.js @@ -12,8 +12,9 @@ Modal message mechanism /*global $tw: false */ "use strict"; -var widget = require("$:/core/modules/widgets/widget.js"); -var navigator = require("$:/core/modules/widgets/navigator.js"); +var widget = require("$:/core/modules/widgets/widget.js"), + navigator = require("$:/core/modules/widgets/navigator.js"), + dm = $tw.utils.domMaker; var Modal = function(wiki) { this.wiki = wiki; @@ -26,6 +27,10 @@ Display a modal dialogue options: see below Options include: downloadLink: Text of a big download link to include + variables: variables to be passed to the modal + event: optional DOM event that initiated the modal + progress: set to true to add a progress bar + onclose: callback for when the modal is closed */ Modal.prototype.display = function(title,options) { options = options || {}; @@ -47,7 +52,6 @@ Modal.prototype.display = function(title,options) { "tv-story-list": (options.event && options.event.widget ? options.event.widget.getVariable("tv-story-list") : ""), "tv-history-list": (options.event && options.event.widget ? options.event.widget.getVariable("tv-history-list") : "") },options.variables); - // Create the wrapper divs var wrapper = this.srcDocument.createElement("div"), modalBackdrop = this.srcDocument.createElement("div"), @@ -55,6 +59,7 @@ Modal.prototype.display = function(title,options) { modalHeader = this.srcDocument.createElement("div"), headerTitle = this.srcDocument.createElement("h3"), modalBody = this.srcDocument.createElement("div"), + modalProgress = this.srcDocument.createElement("div"), modalLink = this.srcDocument.createElement("a"), modalFooter = this.srcDocument.createElement("div"), modalFooterHelp = this.srcDocument.createElement("span"), @@ -71,6 +76,7 @@ Modal.prototype.display = function(title,options) { $tw.utils.addClass(modalWrapper,"tc-modal"); $tw.utils.addClass(modalHeader,"tc-modal-header"); $tw.utils.addClass(modalBody,"tc-modal-body"); + $tw.utils.addClass(modalProgress,"tc-modal-progress"); $tw.utils.addClass(modalFooter,"tc-modal-footer"); // Join them together wrapper.appendChild(modalBackdrop); @@ -78,6 +84,15 @@ Modal.prototype.display = function(title,options) { modalHeader.appendChild(headerTitle); modalWrapper.appendChild(modalHeader); modalWrapper.appendChild(modalBody); + if(options.progress) { + var modalProgressBar = this.srcDocument.createElement("div"); + modalProgressBar.className = "tc-modal-progress-bar"; + modalProgress.appendChild(modalProgressBar); + var modalProgressPercent = this.srcDocument.createElement("div"); + modalProgressPercent.className = "tc-modal-progress-percent"; + modalProgress.appendChild(modalProgressPercent); + modalWrapper.appendChild(modalProgress); + } modalFooter.appendChild(modalFooterHelp); modalFooter.appendChild(modalFooterButtons); modalWrapper.appendChild(modalFooter); @@ -105,7 +120,6 @@ Modal.prototype.display = function(title,options) { parentWidget: $tw.rootWidget }); navigatorWidgetNode.render(modalBody,null); - // Render the title of the message var headerWidgetNode = this.wiki.makeTranscludeWidget(title,{ field: "subtitle", @@ -182,6 +196,10 @@ Modal.prototype.display = function(title,options) { this.wiki.addEventListener("change",refreshHandler); // Add the close event handler var closeHandler = function(event) { + // Call the onclose handler + if(options.onclose) { + options.onclose(event); + } // Remove our refresh handler self.wiki.removeEventListener("change",refreshHandler); // Decrease the modal count and adjust the body class @@ -236,6 +254,22 @@ Modal.prototype.display = function(title,options) { $tw.utils.setStyle(modalWrapper,[ {transform: "translateY(0px)"} ]); + // Return the wrapper node + return { + domNode: wrapper, + closeHandler: closeHandler, + setProgress: function(numerator,denominator) { + // Remove old progress + while(modalProgressPercent.hasChildNodes()) { + modalProgressPercent.removeChild(modalProgressPercent.firstChild); + } + // Set new text + var percent = (numerator * 100 /denominator).toFixed(2) + "%"; + modalProgressPercent.appendChild(self.srcDocument.createTextNode(percent)); + // Set bar width + modalProgressBar.style.width = percent; + } + }; }; Modal.prototype.adjustPageClass = function() { diff --git a/core/modules/widgets/image.js b/core/modules/widgets/image.js index 4ecafbd2b..7b1137dca 100644 --- a/core/modules/widgets/image.js +++ b/core/modules/widgets/image.js @@ -43,6 +43,7 @@ ImageWidget.prototype = new Widget(); Render this widget into the DOM */ ImageWidget.prototype.render = function(parent,nextSibling) { + var self = this; this.parentDomNode = parent; this.computeAttributes(); this.execute(); @@ -58,9 +59,24 @@ ImageWidget.prototype.render = function(parent,nextSibling) { if(this.wiki.isImageTiddler(this.imageSource)) { var type = tiddler.fields.type, text = tiddler.fields.text, - _canonical_uri = tiddler.fields._canonical_uri; + _canonical_uri = tiddler.fields._canonical_uri, + imageTemplateFilter = this.getVariable("tv-image-template-filter"); + // If present, use var-tv-image-template-filter to generate a URL; + if(imageTemplateFilter) { + src = this.wiki.filterTiddlers(imageTemplateFilter,{ + getVariable: function(name) { + switch(name) { + case "currentTiddler": + return "" + this.imageSource; + case "extension": + return "" + ($tw.config.contentTypeInfo[type] || {extension: ""}).extension; + default: + return self.getVariable(name); + } + } + },this.wiki.makeTiddlerIterator([this.imageSource]))[0]; // If the tiddler has body text then it doesn't need to be lazily loaded - if(text) { + } else if(text) { // Render the appropriate element for the image type switch(type) { case "application/pdf": diff --git a/core/ui/ControlPanel/Publishing.tid b/core/ui/ControlPanel/Publishing.tid index f5f9ba76d..68492dfc5 100644 --- a/core/ui/ControlPanel/Publishing.tid +++ b/core/ui/ControlPanel/Publishing.tid @@ -6,7 +6,7 @@ caption: {{$:/language/ControlPanel/Publishing/Caption}}
-<$list filter="[all[shadows+tiddlers]tag[$:/tags/Publish/Jobs]]" variable="job"> +<$list filter="[all[shadows+tiddlers]tag[$:/tags/PublishingJob]]" variable="job">
@@ -42,21 +42,25 @@ Logs: <$view tiddler=<> field="list"/>
-
+<$vars sitemap={{{ [get[sitemap]] }}}> -<$list filter="[all[shadows+tiddlers]tag]" variable="route"> +
+ +

Sitemap: <$link to=<>><$view tiddler=<> field="caption"><$text text=<>/>

+ +<$list filter="[get[list]enlist-input[]]" variable="route">
-

Route: <$link to=<>><$view tiddler=<> field="caption"/>

+

Route: <$link to=<>><$view tiddler=<> field="caption"><$text text=<>/>

-job-type: <$view tiddler=<> field="job-type"/> +job-type: <$view tiddler=<> field="route-type"/> -path: <$edit-text tiddler=<> size="50" field="path"/> +path: <$edit-text tiddler=<> size="50" field="route-path-filter"/> -filter: <$edit-text tiddler=<> size="50" field="filter"/> +filter: <$edit-text tiddler=<> size="50" field="route-tiddler-filter"/> -template: <$edit-text tiddler=<> size="50" field="template"/> +template: <$edit-text tiddler=<> size="50" field="route-template"/>
@@ -64,6 +68,8 @@ template: <$edit-text tiddler=<> size="50" field="template"/>
+ + diff --git a/core/ui/PageControls/publish.tid b/core/ui/PageControls/publish.tid index c16e417bb..b4df28057 100644 --- a/core/ui/PageControls/publish.tid +++ b/core/ui/PageControls/publish.tid @@ -4,20 +4,8 @@ caption: {{$:/core/images/publish}} {{$:/language/Buttons/Publish/Caption}} description: {{$:/language/Buttons/Publish/Hint}} \define publish-actions() -<$list filter="[all[shadows+tiddlers]tag[$:/tags/Publish/Jobs]]" variable="job"> - <$set name="publisher-name" value={{{ [get[publisher]] }}}> - <$action-sendmessage $message="tm-publish-start" publisher-name=<> publish-params-title=<>/> - <$list filter="[all[shadows+tiddlers]tag]" variable="route"> - <$action-sendmessage $message="tm-publish-route" - caption={{{ [get[caption]] }}} - route-type={{{ [get[job-type]] }}} - path={{{ [get[path]] }}} - filter={{{ [get[filter]] }}} - template={{{ [get[template]] }}} - /> - - <$action-sendmessage $message="tm-publish-end"/> - +<$list filter="[all[shadows+tiddlers]tag[$:/tags/PublishingJob]has[enabled]field:enabled[yes]]" variable="job"> + <$action-sendmessage $message="tm-publish" job=<>/> \end diff --git a/core/wiki/publishing-jobs/Default.tid b/core/wiki/publishing-jobs/Default.tid new file mode 100644 index 000000000..6f540188e --- /dev/null +++ b/core/wiki/publishing-jobs/Default.tid @@ -0,0 +1,9 @@ +title: $:/config/PublishingJobs/Default +tags: $:/tags/PublishingJob +caption: Demo static site +publisher: filesystem +sitemap: $:/core/sitemaps/StaticSite +jszip-output-filename: myzipfile.zip +baseurl: https://example.com +enabled: yes +export-filter: [!is[image]!is[system]prefix[T]] [is[image]!is[system]] diff --git a/core/wiki/routes/StaticSite/HTML.tid b/core/wiki/routes/StaticSite/HTML.tid new file mode 100644 index 000000000..f7fef58be --- /dev/null +++ b/core/wiki/routes/StaticSite/HTML.tid @@ -0,0 +1,7 @@ +title: $:/core/routes/StaticSite/HTML +caption: Static HTML +tags: $:/tags/Route +route-type: render +route-path-filter: [addprefix[static/]addsuffix[.html]] +route-tiddler-filter: [!is[system]!is[image]] +route-template: $:/core/templates/static.tiddler.html diff --git a/core/wiki/routes/StaticSite/Images.tid b/core/wiki/routes/StaticSite/Images.tid index d7e3c307c..998d1e20b 100644 --- a/core/wiki/routes/StaticSite/Images.tid +++ b/core/wiki/routes/StaticSite/Images.tid @@ -1,6 +1,6 @@ -title: $:/core/publishing-jobs/Static/Images +title: $:/core/routes/StaticSite/Images caption: Images -tags: $:/core/publishing-jobs/Static/Job -job-type: save -path: images/$title_slugify$.$type_extension$ -filter: [is[image]] +tags: $:/tags/Route +route-type: save +route-path-filter: [slugify[]addprefix[images/]addsuffix] +route-tiddler-filter: [is[image]] diff --git a/core/wiki/routes/StaticSite/Index.tid b/core/wiki/routes/StaticSite/Index.tid index 7e9f6e3fe..60a9281b2 100644 --- a/core/wiki/routes/StaticSite/Index.tid +++ b/core/wiki/routes/StaticSite/Index.tid @@ -1,6 +1,6 @@ -title: $:/core/publishing-jobs/Static/Index +title: $:/core/routes/StaticSite/Index caption: Index -tags: $:/core/publishing-jobs/Static/Job -job-type: render -path: index.html -template: $:/core/save/all +tags: $:/tags/Route +route-type: render +route-path-filter: index.html +route-template: $:/core/save/all diff --git a/core/wiki/routes/StaticSite/Job.tid b/core/wiki/routes/StaticSite/Job.tid deleted file mode 100644 index 40ea6b192..000000000 --- a/core/wiki/routes/StaticSite/Job.tid +++ /dev/null @@ -1,6 +0,0 @@ -title: $:/core/publishing-jobs/Static/Job -tags: $:/tags/Publish/Jobs -caption: Demo Static Site -publisher: jszip -jszip-output-filename: myzipfile.zip -baseurl: https://example.com diff --git a/core/wiki/routes/StaticSite/StaticHTML.tid b/core/wiki/routes/StaticSite/StaticHTML.tid deleted file mode 100644 index 59f265f0a..000000000 --- a/core/wiki/routes/StaticSite/StaticHTML.tid +++ /dev/null @@ -1,9 +0,0 @@ -title: $:/core/publishing-jobs/Static/StaticHTML -caption: Static HTML -tags: $:/core/publishing-jobs/Static/Job -job-type: render -path: static/$title_slugify$.html -filter: [!is[system]!is[image]] -template: $:/core/templates/static.tiddler.html -tv-wikilink-template: $title_slugify$.html -tv-image-template: images/$title_slugify$.$type_extension$ diff --git a/core/wiki/routes/StaticSite/Styles.tid b/core/wiki/routes/StaticSite/Styles.tid index 3d06692c5..91ef47c7d 100644 --- a/core/wiki/routes/StaticSite/Styles.tid +++ b/core/wiki/routes/StaticSite/Styles.tid @@ -1,6 +1,6 @@ -title: $:/core/publishing-jobs/Static/Styles +title: $:/core/routes/StaticSite/Styles caption: Styles -tags: $:/core/publishing-jobs/Static/Job -job-type: render -path: static/static.css -template: $:/core/templates/static.template.css +tags: $:/tags/Route +route-type: render +route-path-filter: static/static.css +route-template: $:/core/templates/static.template.css diff --git a/core/wiki/sitemaps/StaticSite.tid b/core/wiki/sitemaps/StaticSite.tid new file mode 100644 index 000000000..eee2969f0 --- /dev/null +++ b/core/wiki/sitemaps/StaticSite.tid @@ -0,0 +1,7 @@ +title: $:/core/sitemaps/StaticSite +caption: Static site map +description: The original TiddlyWiki 5 static file layout +tags: $:/tags/SiteMap +list: $:/core/routes/StaticSite/Index $:/core/routes/StaticSite/HTML $:/core/routes/StaticSite/Images $:/core/routes/StaticSite/Styles +var-tv-wikilink-template-filter: [slugify[]addsuffix[.html]] +var-tv-image-template-filter: [slugify[]addprefix[../images/]addsuffix] diff --git a/editions/tw5.com/tiddlers/publishing/Publishing.tid b/editions/tw5.com/tiddlers/publishing/Publishing.tid index 2bab62676..aa27b15b1 100644 --- a/editions/tw5.com/tiddlers/publishing/Publishing.tid +++ b/editions/tw5.com/tiddlers/publishing/Publishing.tid @@ -12,23 +12,38 @@ TiddlyWiki has pluggable ''publisher modules'' that provide the means to publish * The JSZip publisher packs the output files in a ZIP file which is automatically downloaded by the browser * The GitHub publisher uploads the output files to a GitHub repository suitable for GitHub Pages -A ''job'' defines a self-contained group of files to be published, and specifies the ''publisher module'' to be used along with any parameters. +A ''publishing job'' describes a self-contained publishing operation. Jobs are defined as configuration tiddlers with the following fields: -Jobs consist of one or more ''routes'' which each define a related set of files that are to be treated in the same way. +* ''title'' -- by convention, prefixed with `$:/config/PublishingJobs/` +* ''caption'' -- human readable short name for the publishing job +* ''tags'' -- `$:/tags/PublishingJob` +* ''enabled'' -- must be set to `yes` for the publishing job to be recognised +* ''export-filter'' -- a filter defining which tiddlers are to be exported +* ''publisher'' -- the name of the publisher module to be used +* ''<publisher-name>-<parameter-name>'' -- parameters required by the publisher module +* ''sitemap'' -- title of the site map tiddler to be used +* ''var-<variable-name>'' -- custom variables to be provided to the output templates -Routes consist of the following information: +A ''site map'' describes the layout and types of files in a publishing job. Site maps are defined as configuration tiddlers with the following fields: -* A filter defining the tiddlers included in the route -* A ''parameterised path'' defining how the ''output path'' is derived from the field values of a particular tiddler -* A ''route type'' which can be set to "save" or "render": +* ''title'' -- by convention, the prefix `$:/core/sitemaps/` is used for sitemaps defined in the core, `$:/plugins///sitemaps/` is used for sitemaps defined in other plugins, and `$:/config/sitemaps/` for user defined sitemaps +* ''caption'' -- human readable short name for the sitemap +* ''description'' -- longer human readable description for the sitemap +* ''tags'' -- `$:/tags/SiteMap` +* ''list'' -- list of titles of routes making up this sitemap +* ''var-<variable-name>'' -- custom variables to be provided to the output templates + +A ''route'' describes how a group of one or more files is to be created during the export. Routes are defined as configuration tiddlers with the following fields: + +* ''title'' -- by convention, the prefix `$:/core/routes/` is used for sitemaps defined in the core, `$:/plugins///routes/` is used for sitemaps defined in other plugins, and `$:/config/routes/` for user defined sitemaps +* ''caption'' -- human readable short name for the route +* ''tags'' -- `$:/tags/Route` +* ''route-tiddler-filter'' -- a filter defining the tiddlers included in the route +* ''route-path-filter'' - a filter defining how the ''output path'' is derived from the field values of a particular tiddler +* ''route-template'' -- optional title of a tiddler used as a template for "render" route types +* ''route-type'' which can be set to "save" or "render": ** ''"save"'' indicates that the raw tiddler is to be saved, without any rendering -** ''"render"'' indicates that the tiddler is to be rendered through a specified ''template'' +** ''"render"'' indicates that the tiddler is to be rendered through a specified template +* ''var-<variable-name>'' -- custom variables to be provided to the output template -Parameterised paths are strings which may contain optional tokens of the format `fieldname_functionname`. These tokens are replaced by the value of the specified field passed through the specified encoding function. The available encoding functions are: - -* ''encoded'' -- applies URI encoding to the value -* ''doubleencoded'' -- applies double URI encoding to the value -* ''slugify'' -- applies the [[slugify Operator]] to the value -* ''extension'' -- interprets the value as a content type and returns the associated file extension - -For backwards compatibility, the field "uri" is accepted as a synonym for "title". +The route tiddler filter is passed the tiddlers resulting from the job export filter. In order to respect the restrictions of the job export filter, route filters must be carefully constructed to ensure they pull their titles from the incoming list. diff --git a/plugins/tiddlywiki/jszip/jszip-publisher.js b/plugins/tiddlywiki/jszip/jszip-publisher.js index 4974d7610..edb5b7acc 100644 --- a/plugins/tiddlywiki/jszip/jszip-publisher.js +++ b/plugins/tiddlywiki/jszip/jszip-publisher.js @@ -30,7 +30,7 @@ exports.create = function(params) { return new JSZipPublisher(params); }; -function JSZipPublisher(params) { +function JSZipPublisher(params,publisherHandler,publishingJob,) { this.params = params; this.zip = new JSZip(); console.log("JSZipPublisher",params); @@ -38,12 +38,13 @@ function JSZipPublisher(params) { JSZipPublisher.prototype.publishStart = function(callback) { console.log("publishStart"); + // Returns a list of the previously published files, but always "none" for ZIP files callback([]); }; JSZipPublisher.prototype.publishFile = function(item,callback) { - this.zip.file(item.path,item.text); - callback(); + this.zip.file(item.path,item.text,{base64: item.isBase64}); + callback(null); }; JSZipPublisher.prototype.publishEnd = function(callback) { @@ -54,7 +55,7 @@ JSZipPublisher.prototype.publishEnd = function(callback) { document.body.appendChild(link); link.click(); document.body.removeChild(link); - callback(); + callback(null); }; })(); diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index ed24cb1e6..6c741e256 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -1819,6 +1819,20 @@ html body.tc-body.tc-single-tiddler-window { border: 1px solid <>; } +.tc-modal-progress { + padding-left: 1em; + padding-right: 1em; +} + +.tc-modal-progress-percent { + text-align: center; +} + +.tc-modal-progress-bar { + height: 10px; + background: red; +} + @media (max-width: 55em) { .tc-modal { position: fixed; @@ -2934,7 +2948,7 @@ Publishing UI background-color: #ffdddd; } -.tc-publishing-job-routes { +.tc-publishing-sitemap { margin: 0.25em; padding: 0.25em; border: 1px solid black; @@ -2948,31 +2962,6 @@ Publishing UI background-color: #ddffdd; } -/* -Progress bar -*/ - -.tc-progress-bar-wrapper { - position: fixed; - width: 50%; - height: 50px; - top: 50%; - left: 50%; - margin-top: -25px; - margin-left: -25%; - background: #ffffee; -} - -.tc-progress-bar-text, -.tc-progress-bar-percent { - text-align: center; -} - -.tc-progress-bar { - height: 10px; - background: red; -} - /* ** Utility classes for SVG icons */