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"/>
+$innerwiki>""">>
+
+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"/>
+$innerwiki>""">>
+
+!! 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"/>
+$innerwiki>""">>
+
+!! 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)"/>
+$innerwiki>""">>
+
+!! 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"/>
+$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"/>
+$innerwiki>
\ 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" + "script>\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}}