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
This commit is contained in:
Jermolene 2019-01-27 10:57:56 +00:00
parent 353821f442
commit ca7b62a5f6
18 changed files with 664 additions and 0 deletions

View File

@ -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

View File

@ -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
```

View File

@ -0,0 +1,5 @@
title: $:/DefaultTiddlers
[[$:/plugins/tiddlywiki/innerwiki/readme]]
[[$:/plugins/tiddlywiki/innerwiki/examples]]
[[$:/plugins/tiddlywiki/innerwiki/docs]]

View File

@ -0,0 +1,3 @@
title: $:/SiteSubtitle
Embedding wikis within wikis

View File

@ -0,0 +1,3 @@
title: $:/SiteTitle
Innerwiki Demo

View File

@ -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"
]
}
}

View File

@ -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]]

View File

@ -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;
})();

View File

@ -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<br>not starting<br>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> <deviceScaleFactor>
```
* ''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.

View File

@ -0,0 +1,5 @@
title: $:/plugins/tiddlywiki/innerwiki/example-data
<$data title="$:/SiteTitle" text="Innerwiki Demo"/>
<$data title="$:/SiteSubtitle" text="Wikis spawning wikis"/>

View File

@ -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":
<<example """<$innerwiki width="1200" height="400" style="width:100%;" filename="screenshot-1">
<$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:
<<example """<$innerwiki width="1200" height="400" style="width:100%;" clipLeft="500" clipTop="100" clipWidth="600" clipHeight="300" filename="screenshot-2">
<$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):
<<example """<$innerwiki width="600" height="400" style="width:100%;" filename="screenshot-3">
{{$:/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:
<<example """<$innerwiki template="$:/plugins/tiddlywiki/innerwiki/template" filename="screenshot-4" width="1200" height="400" clipLeft="500" clipTop="100" clipWidth="600" clipHeight="300" style="width:100%;">
<$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:
<<example """<$innerwiki width="1200" height="600" style="width:100%;" filename="screenshot-5">
<$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.

View File

@ -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>

View File

@ -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.

View File

@ -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 = "<!--~~ Boot" + " kernel ~~-->\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;
})();

View File

@ -0,0 +1,7 @@
{
"title": "$:/plugins/tiddlywiki/innerwiki",
"description": "Innerwiki: programmable screenshots",
"author": "Jeremy Ruston",
"plugin-type": "plugin",
"list": "readme docs examples"
}

View File

@ -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;
})();

View File

@ -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;
<<box-shadow "2px 2px 5px rgba(0, 0, 0, 0.5)">>
}

View File

@ -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}}