From ca7b62a5f6e9f5528fd3e3491456cca4a1d64923 Mon Sep 17 00:00:00 2001 From: Jermolene Date: Sun, 27 Jan 2019 10:57:56 +0000 Subject: [PATCH] Introduce "innerwiki" plugin From the readme: This plugin enables TiddlyWiki to embed a modified copy of itself (an "innerwiki"). The primary motivation is to be able to produce screenshot illustrations that are automatically up-to-date with the appearance of TiddlyWiki as it changes over time, or to produce the same screenshot in different languages --- bin/build-site.sh | 10 + .../innerwikidemo/tiddlers/HelloThere.tid | 12 + .../tiddlers/system/DefaultTiddlers.tid | 5 + .../tiddlers/system/SiteSubtitle.tid | 3 + .../tiddlers/system/SiteTitle.tid | 3 + editions/innerwikidemo/tiddlywiki.info | 23 ++ .../tiddlers/plugins/Innerwiki Plugin.tid | 11 + plugins/tiddlywiki/innerwiki/data.js | 56 ++++ plugins/tiddlywiki/innerwiki/doc/docs.tid | 61 +++++ .../tiddlywiki/innerwiki/doc/example-data.tid | 5 + plugins/tiddlywiki/innerwiki/doc/examples.tid | 74 +++++ .../innerwiki/doc/inner-example.tid | 7 + plugins/tiddlywiki/innerwiki/doc/readme.tid | 25 ++ plugins/tiddlywiki/innerwiki/innerwiki.js | 256 ++++++++++++++++++ plugins/tiddlywiki/innerwiki/plugin.info | 7 + plugins/tiddlywiki/innerwiki/screenshot.js | 83 ++++++ plugins/tiddlywiki/innerwiki/styles.tid | 9 + plugins/tiddlywiki/innerwiki/template.tid | 14 + 18 files changed, 664 insertions(+) create mode 100644 editions/innerwikidemo/tiddlers/HelloThere.tid create mode 100644 editions/innerwikidemo/tiddlers/system/DefaultTiddlers.tid create mode 100644 editions/innerwikidemo/tiddlers/system/SiteSubtitle.tid create mode 100644 editions/innerwikidemo/tiddlers/system/SiteTitle.tid create mode 100644 editions/innerwikidemo/tiddlywiki.info create mode 100644 editions/tw5.com/tiddlers/plugins/Innerwiki Plugin.tid create mode 100644 plugins/tiddlywiki/innerwiki/data.js create mode 100644 plugins/tiddlywiki/innerwiki/doc/docs.tid create mode 100644 plugins/tiddlywiki/innerwiki/doc/example-data.tid create mode 100644 plugins/tiddlywiki/innerwiki/doc/examples.tid create mode 100644 plugins/tiddlywiki/innerwiki/doc/inner-example.tid create mode 100644 plugins/tiddlywiki/innerwiki/doc/readme.tid create mode 100644 plugins/tiddlywiki/innerwiki/innerwiki.js create mode 100644 plugins/tiddlywiki/innerwiki/plugin.info create mode 100644 plugins/tiddlywiki/innerwiki/screenshot.js create mode 100644 plugins/tiddlywiki/innerwiki/styles.tid create mode 100644 plugins/tiddlywiki/innerwiki/template.tid diff --git a/bin/build-site.sh b/bin/build-site.sh index 26c6ce980..6772ca648 100755 --- a/bin/build-site.sh +++ b/bin/build-site.sh @@ -224,6 +224,16 @@ node $TW5_BUILD_TIDDLYWIKI \ # ###################################################### +# /plugins/tiddlywiki/innerwiki/index.html Demo wiki with Innerwiki plugin + +node $TW5_BUILD_TIDDLYWIKI \ + ./editions/innerwikidemo \ + --verbose \ + --load $TW5_BUILD_OUTPUT/build.tid \ + --output $TW5_BUILD_OUTPUT \ + --rendertiddler $:/core/save/all plugins/tiddlywiki/innerwikidemo/index.html text/plain \ + || exit 1 + # /plugins/tiddlywiki/dynaview/index.html Demo wiki with DynaView plugin # /plugins/tiddlywiki/dynaview/empty.html Empty wiki with DynaView plugin diff --git a/editions/innerwikidemo/tiddlers/HelloThere.tid b/editions/innerwikidemo/tiddlers/HelloThere.tid new file mode 100644 index 000000000..84a0ad99f --- /dev/null +++ b/editions/innerwikidemo/tiddlers/HelloThere.tid @@ -0,0 +1,12 @@ +title: HelloThere + +This is a demo of TiddlyWiki 5's ''innerwiki'' plugin. + +To try these examples under Node.js: + +# Install Puppeteer, as described in the [[readme|$:/plugins/tiddlywiki/innerwiki/readme]] +# Execute the following command in the root of the TiddlyWiki 5 repo: + +``` +./tiddlywiki.js editions/innerwikidemo --screenshot '[[$:/plugins/tiddlywiki/innerwiki/examples]]' 4 +``` diff --git a/editions/innerwikidemo/tiddlers/system/DefaultTiddlers.tid b/editions/innerwikidemo/tiddlers/system/DefaultTiddlers.tid new file mode 100644 index 000000000..a77d187c1 --- /dev/null +++ b/editions/innerwikidemo/tiddlers/system/DefaultTiddlers.tid @@ -0,0 +1,5 @@ +title: $:/DefaultTiddlers + +[[$:/plugins/tiddlywiki/innerwiki/readme]] +[[$:/plugins/tiddlywiki/innerwiki/examples]] +[[$:/plugins/tiddlywiki/innerwiki/docs]] \ No newline at end of file diff --git a/editions/innerwikidemo/tiddlers/system/SiteSubtitle.tid b/editions/innerwikidemo/tiddlers/system/SiteSubtitle.tid new file mode 100644 index 000000000..b25021be1 --- /dev/null +++ b/editions/innerwikidemo/tiddlers/system/SiteSubtitle.tid @@ -0,0 +1,3 @@ +title: $:/SiteSubtitle + +Embedding wikis within wikis \ No newline at end of file diff --git a/editions/innerwikidemo/tiddlers/system/SiteTitle.tid b/editions/innerwikidemo/tiddlers/system/SiteTitle.tid new file mode 100644 index 000000000..b83549310 --- /dev/null +++ b/editions/innerwikidemo/tiddlers/system/SiteTitle.tid @@ -0,0 +1,3 @@ +title: $:/SiteTitle + +Innerwiki Demo \ No newline at end of file diff --git a/editions/innerwikidemo/tiddlywiki.info b/editions/innerwikidemo/tiddlywiki.info new file mode 100644 index 000000000..45b47a0bc --- /dev/null +++ b/editions/innerwikidemo/tiddlywiki.info @@ -0,0 +1,23 @@ +{ + "description": "Innerwiki Plugin Demo", + "plugins": [ + "tiddlywiki/innerwiki" + ], + "themes": [ + "tiddlywiki/vanilla", + "tiddlywiki/snowwhite" + ], + "build": { + "index": [ + "--rendertiddler", + "$:/core/save/all", + "index.html", + "text/plain" + ], + "save-screenshots": [ + "--screenshot", + "[[$:/plugins/tiddlywiki/innerwiki/examples]]", + "4" + ] + } +} \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/plugins/Innerwiki Plugin.tid b/editions/tw5.com/tiddlers/plugins/Innerwiki Plugin.tid new file mode 100644 index 000000000..11779f535 --- /dev/null +++ b/editions/tw5.com/tiddlers/plugins/Innerwiki Plugin.tid @@ -0,0 +1,11 @@ +created: 20190127104143725 +modified: 20190127104143725 +tags: OfficialPlugins +title: Innerwiki Plugin +type: text/vnd.tiddlywiki + +The Innerwiki Plugin enables TiddlyWiki to embed a modified copy of itself (an "innerwiki"). The primary motivation is to be able to produce screenshot illustrations that are automatically up-to-date with the appearance of TiddlyWiki as it changes over time, or to produce the same screenshot in different languages. + +In the browser, innerwikis are displayed as an embedded iframe. Under Node.js [[Google's Puppeteer|https://pptr.dev/]] is used to load the innerwikis as off-screen web pages and then snapshot them as a PNG image. + +See the demo at [ext[https://tiddlywiki.com/plugins/tiddlywiki/innerwiki|plugins/tiddlywiki/innerwiki]] diff --git a/plugins/tiddlywiki/innerwiki/data.js b/plugins/tiddlywiki/innerwiki/data.js new file mode 100644 index 000000000..4f2067934 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/data.js @@ -0,0 +1,56 @@ +/*\ +title: $:/plugins/tiddlywiki/innerwiki/data.js +type: application/javascript +module-type: widget + +Widget to represent a single item of data + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var DataWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +DataWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +DataWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + this.renderChildren(parent,nextSibling); +}; + +/* +Compute the internal state of the widget +*/ +DataWidget.prototype.execute = function() { + // Construct the child widgets + this.makeChildWidgets(); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +DataWidget.prototype.refresh = function(changedTiddlers) { + // Refresh our attributes + var changedAttributes = this.computeAttributes(); + // Refresh our children + return this.refreshChildren(changedTiddlers); +}; + +exports.data = DataWidget; + +})(); diff --git a/plugins/tiddlywiki/innerwiki/doc/docs.tid b/plugins/tiddlywiki/innerwiki/doc/docs.tid new file mode 100644 index 000000000..6b840adc3 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/doc/docs.tid @@ -0,0 +1,61 @@ +title: $:/plugins/tiddlywiki/innerwiki/docs + +! `<$innerwiki>` widget + +The `<$innerwiki>` widget encapsulates an embedded wiki. It starts as a blank copy of the current wiki and can have additional payload tiddlers added via embedded `<$data>` widgets (see below). + +It supports the following attributes: + +|!Attribute |!Description | +|template |Specifies the template to be used to generate the base wiki (defaults to $:/plugins/tiddlywiki/innerwiki/template) | +|width |Width in pixels of the virtual screen for rendering the embedded wiki | +|height |Height in pixels of the virtual screen for rendering the embedded wiki | +|style |CSS style definitions to be added to the DIV wrapper around the IFRAME containing the embedded wiki | +|class |CSS classes to be added to the DIV wrapper around the IFRAME containing the embedded wiki | +|filename |Base filename for saving a screenshot of the embedded wiki under Node.js (excludes file extension) | +|clipLeft |Position in pixels of the left edge of the clip rectangle (optional) | +|clipTop |Position in pixels of the top edge of the clip rectangle (optional) | +|clipWidth |Width in pixels of the clip rectangle (optional) | +|clipHeight |Height in pixels of the clip rectangle (optional) | + +! `<$data>` widget + +The `<$data>` widget is used within the `<$innerwiki>` widget to specify payload tiddlers to be added to the innerwiki. + +It supports the following attributes: + +|!Attribute |!Description | +|$tiddler |The title of a tiddler to be used as a payload tiddler (optional) | +|$filter |A filter string identifying tiddlers to be used as payload tiddlers (optional) | +|//any attribute
not starting
with $// |Field values to be assigned to the payload tiddler(s) | + +It can be used in three different ways: + +* Without the `$tiddler` or `$filter` attributes, the remaining attributes provide the fields for a single payload tiddler +* With the `$tiddler` attribute present, the payload tiddler takes its fields from that tiddler with the remaining attributes overriding those fields +* With the `$filter` attribute present, the payload is a copy of all of the tiddlers identified by the filter, with the remaining attributes overriding those fields of each one + +This example injects a copy of the "HelloThere" tiddler with the addition of the field "custom" set to "Alpha": + +``` +<$data $tiddler="HelloThere" custom="Alpha"/> +``` + +This example injects all image tiddlers with the addition of the field "custom" set to "Beta": + +``` +<$data $filter="[is[image]]" custom="Beta"/> +``` + +! `screenshot` command + +Saves PNG screenshots of the `<$innerwiki>` widgets rendered by a set of tiddlers identified by a filter. + +``` +--screenshot +``` + +* ''filter'': a filter identifying the tiddlers to be rendered, from which the individual `<$innerwiki>` widgets are screenshotted +* ''deviceScaleFactor'': a scale factor for the screenshot (optional; defaults to 1) + +A deviceScaleFactor of 4 or 5 gives high quality screenshots suitable for print use. diff --git a/plugins/tiddlywiki/innerwiki/doc/example-data.tid b/plugins/tiddlywiki/innerwiki/doc/example-data.tid new file mode 100644 index 000000000..218d5dbb5 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/doc/example-data.tid @@ -0,0 +1,5 @@ +title: $:/plugins/tiddlywiki/innerwiki/example-data + +<$data title="$:/SiteTitle" text="Innerwiki Demo"/> +<$data title="$:/SiteSubtitle" text="Wikis spawning wikis"/> + diff --git a/plugins/tiddlywiki/innerwiki/doc/examples.tid b/plugins/tiddlywiki/innerwiki/doc/examples.tid new file mode 100644 index 000000000..64fbc8888 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/doc/examples.tid @@ -0,0 +1,74 @@ +title: $:/plugins/tiddlywiki/innerwiki/examples + +\define example(text) +<$codeblock code=<<__text__>>/> + +Renders as: + +$text$ +\end + +!! Browser + +The innerwiki widget specifies the dimensions of the virtual screen used to render the wiki (in pixels) and CSS styles to apply to it. Nested `<$data>` widgets are used to specify individual payload tiddlers to be loaded into the wiki. In this example, we initialise the innerwiki with two tiddlers "HelloThere" and "$:/DefaultTiddlers": + +< + <$data title="HelloThere" text="This tiddler is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> +""">> + +Note that the "screenshot" is a shrunken but fully interactive TiddlyWiki. + +!! Node.js + +To render these examples as a PNG bitmap under Node.js, execute the following at the command prompt: + +``` +tiddlywiki mywiki --screenshot $:/plugins/tiddlywiki/innerwiki/examples +``` + +The screenshots will be saved as `screenshot-1.png` etc in the `./output` folder of the wiki. + +!! Clipping + +A clipping rectangle can be applied to limit the area of the wiki that is displayed. For example: + +< + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> +""">> + +!! Transcluding payload tiddlers + +This example shows how the `<$data>` widget can be transcluded from other tiddlers (see $:/plugins/tiddlywiki/innerwiki/example-data): + +< + {{$:/plugins/tiddlywiki/innerwiki/example-data}} + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> +""">> + +!! Customising the wiki state + +By injecting the right payload tiddlers, the innerwiki can be initialised to any desired state. In this example we inject a configuration tiddler to make the "more" page control button visible, and a state tiddler to cause the dropdown to appear: + +< + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> + <$data title="$:/config/PageControlButtons/Visibility/$:/core/ui/Buttons/more-page-actions" text="show"/> + <$data title="$:/state/popup/more--1600698846" text="(151,144,21,25)"/> +""">> + +!! Inception + +An innerwiki can itself contain an inner-innerwiki: + +< + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere $:/plugins/tiddlywiki/innerwiki/inner-example"/> + <$data $tiddler="$:/plugins/tiddlywiki/innerwiki"/> +""">> + +(You can see the innerwiki here: $:/plugins/tiddlywiki/innerwiki/inner-example) + +Note the way that the innerwiki plugin has to be explicitly added to the innerwiki. diff --git a/plugins/tiddlywiki/innerwiki/doc/inner-example.tid b/plugins/tiddlywiki/innerwiki/doc/inner-example.tid new file mode 100644 index 000000000..dd5da6832 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/doc/inner-example.tid @@ -0,0 +1,7 @@ +title: $:/plugins/tiddlywiki/innerwiki/inner-example + +<$innerwiki width="1200" height="400" style="width:100%;"> + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> + <$data title="$:/palette" text="$:/palettes/SolarFlare"/> + \ No newline at end of file diff --git a/plugins/tiddlywiki/innerwiki/doc/readme.tid b/plugins/tiddlywiki/innerwiki/doc/readme.tid new file mode 100644 index 000000000..b188d3054 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/doc/readme.tid @@ -0,0 +1,25 @@ +title: $:/plugins/tiddlywiki/innerwiki/readme + +!! Introduction + +This plugin enables TiddlyWiki to embed a modified copy of itself (an "innerwiki"). The primary motivation is to be able to produce screenshot illustrations that are automatically up-to-date with the appearance of TiddlyWiki as it changes over time, or to produce the same screenshot in different languages. + +In the browser, innerwikis are displayed as an embedded iframe. Under Node.js [[Google's Puppeteer|https://pptr.dev/]] is used to load the innerwikis as off-screen web pages and then snapshot them as a PNG image. + +!! Warnings + +The `<$innerwiki>` widget is relatively slow and resource intensive: each time it is refreshed it will entirely rebuild the iframe containing the innerwiki. + +The `<$innerwiki>` widget does not automatically resize with its container (for example, try hiding the sidebar in this wiki). + +!! Prequisites + +In order to take screenshots under Node.js, Google Puppeteer must first be installed from [[npm|https://npmjs.org//]]. For most situations, it should be installed at the root of the TiddlyWiki 5 repo: + +``` +cd Jermolene/TiddlyWiki5 +npm install puppeteer +``` + +However, if you're working in a different repo that uses npm to install TiddlyWiki 5 then you should install Puppeteer into the same repo. The general rule is that the `node_modules` folder containing Puppeteer should be contained within an ancestor of the folder containing TiddlyWiki's `tiddlywiki.js` file. + diff --git a/plugins/tiddlywiki/innerwiki/innerwiki.js b/plugins/tiddlywiki/innerwiki/innerwiki.js new file mode 100644 index 000000000..7c1cdc629 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/innerwiki.js @@ -0,0 +1,256 @@ +/*\ +title: $:/plugins/tiddlywiki/innerwiki/innerwiki.js +type: application/javascript +module-type: widget + +Widget to display an innerwiki in an iframe + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var DEFAULT_INNERWIKI_TEMPLATE = "$:/plugins/tiddlywiki/innerwiki/template"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + DataWidget = require("$:/plugins/tiddlywiki/innerwiki/data.js").data; + +var InnerWikiWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +InnerWikiWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +InnerWikiWidget.prototype.render = function(parent,nextSibling) { + var self = this; + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + // Create wrapper + var domWrapper = this.document.createElement("div"); + var classes = (this.innerWikiClass || "").split(" "); + classes.push("tc-innerwiki-wrapper"); + domWrapper.className = classes.join(" "); + domWrapper.style = this.innerWikiStyle; + // If we're on the real DOM, adjust the wrapper and iframe + if(!this.document.isTiddlyWikiFakeDom) { + domWrapper.style.overflow = "hidden"; + domWrapper.style.position = "relative"; + domWrapper.style.boxSizing = "content-box"; + // Create iframe + var domIFrame = this.document.createElement("iframe"); + domIFrame.className = "tc-innerwiki-iframe"; + domIFrame.style.position = "absolute"; + domIFrame.style.maxWidth = "none"; + domIFrame.style.border = "none"; + domIFrame.width = this.innerWikiWidth; + domIFrame.height = this.innerWikiHeight; + domWrapper.appendChild(domIFrame); + } + // Insert wrapper into the DOM + parent.insertBefore(domWrapper,nextSibling); + this.renderChildren(domWrapper,null); + this.domNodes.push(domWrapper); + // If we're on the real DOM, finish the initialisation that needs us to be in the DOM + if(!this.document.isTiddlyWikiFakeDom) { + // Write the HTML + domIFrame.contentWindow.document.open(); + domIFrame.contentWindow.document.write(this.createInnerHTML()); + domIFrame.contentWindow.document.close(); + // Scale the iframe and adjust the height of the wrapper + var clipLeft = self.innerWikiClipLeft, + clipTop = self.innerWikiClipTop, + clipWidth = self.innerWikiClipWidth, + clipHeight = self.innerWikiClipHeight, + translateX = -clipLeft, + translateY = -clipTop, + scale = domWrapper.clientWidth / clipWidth; + domIFrame.style.transformOrigin = (-translateX) + "px " + (-translateY) + "px"; + domIFrame.style.transform = "translate(" + translateX + "px," + translateY + "px) scale(" + scale + ")"; + domWrapper.style.height = (clipHeight * scale) + "px"; + } +}; + +/* +Create the HTML of the innerwiki +*/ +InnerWikiWidget.prototype.createInnerHTML = function() { + // Get the HTML of the iframe + var html = this.wiki.renderTiddler("text/plain",this.innerWikiTemplate); + // Insert the overlay tiddlers + var SPLIT_MARKER = "\n", + IMPLANT_PREFIX = "<" + "script>\n$tw.preloadTiddlerArray(", + IMPLANT_SUFFIX = ");\n\n", + parts = html.split(SPLIT_MARKER), + tiddlers = this.findDataWidgets(this.children); + if(parts.length === 2) { + html = parts[0] + IMPLANT_PREFIX + JSON.stringify(tiddlers) + IMPLANT_SUFFIX + SPLIT_MARKER + parts[1]; + } + return html; +}; + +/* +Find the child data widgets +*/ +InnerWikiWidget.prototype.findDataWidgets = function(children) { + var self = this, + results = []; + $tw.utils.each(children,function(child) { + if(child instanceof DataWidget) { + var item = Object.create(null); + $tw.utils.each(child.attributes,function(value,name) { + item[name] = value; + }); + Array.prototype.push.apply(results,self.readDataWidget(child)); + } + if(child.children) { + results = results.concat(self.findDataWidgets(child.children)); + } + }); + return results; +}; + +/* +Read the value(s) from a data widget +*/ +InnerWikiWidget.prototype.readDataWidget = function(dataWidget) { + // Start with a blank object + var item = Object.create(null); + // Read any attributes not prefixed with $ + $tw.utils.each(dataWidget.attributes,function(value,name) { + if(name.charAt(0) !== "$") { + item[name] = value; + } + }); + // Deal with $tiddler or $filter attributes + var titles; + if(dataWidget.hasAttribute("$tiddler")) { + titles = [dataWidget.getAttribute("$tiddler")]; + } else if(dataWidget.hasAttribute("$filter")) { + titles = this.wiki.filterTiddlers(dataWidget.getAttribute("$filter")); + } + if(titles) { + var results = []; + $tw.utils.each(titles,function(title,index) { + var tiddler = $tw.wiki.getTiddler(title), + fields; + if(tiddler) { + fields = tiddler.getFieldStrings(); + } + results.push($tw.utils.extend({},item,fields)); + }) + return results; + } else { + return [item]; + } +}; + +/* +Compute the internal state of the widget +*/ +InnerWikiWidget.prototype.execute = function() { + var parseStringAsNumber = function(num,defaultValue) { + num = parseInt(num + "",10); + if(!isNaN(num)) { + return num; + } else { + return parseInt(defaultValue + "",10); + } + }; + // Get our parameters + this.innerWikiTemplate = this.getAttribute("template",DEFAULT_INNERWIKI_TEMPLATE); + this.innerWikiWidth = parseStringAsNumber(this.getAttribute("width"),800); + this.innerWikiHeight = parseStringAsNumber(this.getAttribute("height"),600); + this.innerWikiStyle = this.getAttribute("style"); + this.innerWikiClass = this.getAttribute("class"); + this.innerWikiFilename = this.getAttribute("filename"); + this.innerWikiClipLeft = parseStringAsNumber(this.getAttribute("clipLeft"),0); + this.innerWikiClipTop = parseStringAsNumber(this.getAttribute("clipTop"),0); + this.innerWikiClipWidth = parseStringAsNumber(this.getAttribute("clipWidth"),this.innerWikiWidth); + this.innerWikiClipHeight = parseStringAsNumber(this.getAttribute("clipHeight"),this.innerWikiHeight); + // Construct the child widgets + this.makeChildWidgets(); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +InnerWikiWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.template || changedAttributes.width || changedAttributes.height || changedAttributes.style || changedAttributes.class) { + this.refreshSelf(); + return true; + } else { + return false; + } +}; + +/* +Use Puppeteer to save a screenshot to a file +*/ +InnerWikiWidget.prototype.saveScreenshot = function(options,callback) { + var self = this, + basepath = options.basepath || ".", + deviceScaleFactor = options.deviceScaleFactor || 1; + // Don't do anything if we don't have a filename + if(!this.innerWikiFilename) { + return callback(null); + } + var path = require("path"), + filepath = path.resolve(basepath,this.innerWikiFilename) + ".png"; + $tw.utils.createFileDirectories(filepath); + console.log("Taking screenshot",filepath); + // Fire up Puppeteer + var puppeteer; + try { + puppeteer = require("puppeteer"); + } catch(e) { + throw "Google Puppeteer not found"; + } + // Take screenshots + puppeteer.launch().then(async browser => { + // NOTE: Copying Google's sample code by using new fangled promises "await" + const page = await browser.newPage(); + await page.setContent(self.createInnerHTML(),{ + waitUntil: "domcontentloaded" + }); + await page.setViewport({ + width: Math.trunc(self.innerWikiWidth), + height: Math.trunc(self.innerWikiHeight), + deviceScaleFactor: deviceScaleFactor + }); + // PDF generation isn't great: there's no clipping, and pagination is hard to control + // await page.emulateMedia("screen"); + // await page.pdf({ + // scale: 0.5, + // width: self.innerWikiWidth + "px", + // height: self.innerWikiHeight + "px", + // path: filepath + ".pdf", + // printBackground: true + // }); + await page.screenshot({ + path: filepath, + clip: { + x: self.innerWikiClipLeft, + y: self.innerWikiClipTop, + width: self.innerWikiClipWidth, + height: self.innerWikiClipHeight + }, + type: "png" + }); + await browser.close(); + callback(null); + }); +}; + +exports.innerwiki = InnerWikiWidget; + +})(); diff --git a/plugins/tiddlywiki/innerwiki/plugin.info b/plugins/tiddlywiki/innerwiki/plugin.info new file mode 100644 index 000000000..07b1e550d --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/innerwiki", + "description": "Innerwiki: programmable screenshots", + "author": "Jeremy Ruston", + "plugin-type": "plugin", + "list": "readme docs examples" +} diff --git a/plugins/tiddlywiki/innerwiki/screenshot.js b/plugins/tiddlywiki/innerwiki/screenshot.js new file mode 100644 index 000000000..428dfb59e --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/screenshot.js @@ -0,0 +1,83 @@ +/*\ +title: $:/plugins/tiddlywiki/innerwiki/screenshot.js +type: application/javascript +module-type: command + +Commands to render tiddlers identified by a filter and save any screenshots identified by <$innerwiki> widgets + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var InnerWikiWidget = require("$:/plugins/tiddlywiki/innerwiki/innerwiki.js").innerwiki; + +exports.info = { + name: "screenshot", + synchronous: false +}; + +var Command = function(params,commander,callback) { + this.params = params; + this.commander = commander; + this.callback = callback; +}; + +Command.prototype.execute = function() { + var self = this; + if(this.params.length < 1) { + return "Missing filter"; + } + var filter = this.params[0], + deviceScaleFactor = parseInt(this.params[1],10) || 1, + tiddlers = this.commander.wiki.filterTiddlers(filter); + // Render each tiddler into a widget tree + var innerWikiWidgets = []; + $tw.utils.each(tiddlers,function(title) { + var parser = self.commander.wiki.parseTiddler(title), + variables = {currentTiddler: title}, + widgetNode = self.commander.wiki.makeWidget(parser,{variables: variables}), + container = $tw.fakeDocument.createElement("div"); + widgetNode.render(container,null); + // Find any innerwiki widgets + Array.prototype.push.apply(innerWikiWidgets,self.findInnerWikiWidgets(widgetNode)); + }); + // Asynchronously tell each innerwiki widget to save a screenshot + var processNextInnerWikiWidget = function() { + if(innerWikiWidgets.length > 0) { + var widget = innerWikiWidgets[0]; + innerWikiWidgets.shift(); + widget.saveScreenshot({ + basepath: self.commander.outputPath, + deviceScaleFactor: deviceScaleFactor + },function(err) { + if(err) { + self.callback(err); + } + processNextInnerWikiWidget(); + }); + } else { + self.callback(null); + } + }; + processNextInnerWikiWidget(); + return null; +}; + +Command.prototype.findInnerWikiWidgets = function(widgetNode) { + var self = this, + results = []; + if(widgetNode.saveScreenshot) { + results.push(widgetNode) + } + $tw.utils.each(widgetNode.children,function(childWidget) { + Array.prototype.push.apply(results,self.findInnerWikiWidgets(childWidget)); + }); + return results; +}; + +exports.Command = Command; + +})(); diff --git a/plugins/tiddlywiki/innerwiki/styles.tid b/plugins/tiddlywiki/innerwiki/styles.tid new file mode 100644 index 000000000..8ebccb995 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/styles.tid @@ -0,0 +1,9 @@ +title: $:/plugins/tiddlywiki/innerwiki/styles +tags: [[$:/tags/Stylesheet]] + +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline + +.tc-innerwiki-wrapper { + border: 1px solid #666; + <> +} diff --git a/plugins/tiddlywiki/innerwiki/template.tid b/plugins/tiddlywiki/innerwiki/template.tid new file mode 100644 index 000000000..4c4a36c70 --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/template.tid @@ -0,0 +1,14 @@ +title: $:/plugins/tiddlywiki/innerwiki/template + +\define saveTiddlerFilter() +$:/boot/boot.css +$:/boot/boot.js +$:/boot/bootprefix.js +$:/core +$:/library/sjcl.js +$:/plugins/tiddlywiki/innerwiki +$:/plugins/tiddlywiki/railroad +$:/themes/tiddlywiki/snowwhite +$:/themes/tiddlywiki/vanilla +\end +{{$:/core/templates/tiddlywiki5.html}}