Add support for an integrated plugin library

Fixes #1450

Provides support for an integrated plugin library that can be used to
install plugins from tiddlywiki.com directly to wikis hosted online or
offline. See the Plugins tab of Control Panel.

Todo:

* Error checking(eg libraryserver.js HTTP GET)
* Translatability
* Documentation
** $:/tags/ServerConnection
** savelibrarytiddlers command
This commit is contained in:
Jermolene 2015-02-09 19:04:11 +00:00
parent aae56f20af
commit 8643278a45
16 changed files with 472 additions and 39 deletions

View File

@ -38,6 +38,8 @@ Palette/Editor/Reset/Caption: reset
Palette/HideEditor/Caption: hide editor
Palette/Prompt: Current palette:
Palette/ShowEditor/Caption: show editor
Plugins/Add/Hint: Install new plugins
Plugins/Add/Caption: Add
Plugins/Caption: Plugins
Plugins/Disable/Caption: disable
Plugins/Disable/Hint: Disable this plugin when reloading page
@ -45,6 +47,8 @@ Plugins/Disabled/Status: (disabled)
Plugins/Empty/Hint: None
Plugins/Enable/Caption: enable
Plugins/Enable/Hint: Enable this plugin when reloading page
Plugins/Installed/Hint: Currently installed plugins
Plugins/Installed/Caption: Installed
Plugins/Language/Prompt: Languages
Plugins/Plugin/Prompt: Plugins
Plugins/Theme/Prompt: Themes

View File

@ -14,6 +14,7 @@ Encryption/ConfirmClearPassword: Do you wish to clear the password? This will re
Encryption/PromptSetPassword: Set a new password for this TiddlyWiki
InvalidFieldName: Illegal characters in field name "<$text text=<<fieldName>>/>". Fields can only contain lowercase letters, digits and the characters underscore (`_`), hyphen (`-`) and period (`.`)
MissingTiddler/Hint: Missing tiddler "<$text text=<<currentTiddler>>/>" - click {{$:/core/images/edit-button}} to create
OfficialPluginLibrary: Official ~TiddlyWiki Plugin Library
PluginReloadWarning: Please save {{$:/core/ui/Buttons/save-wiki}} and reload {{$:/core/ui/Buttons/refresh}} to allow changes to plugins to take effect
RecentChanges/DateFormat: DDth MMM YYYY
SystemTiddler/Tooltip: This is a system tiddler

View File

@ -0,0 +1,66 @@
/*\
title: $:/core/modules/commands/savelibrarytiddlers.js
type: application/javascript
module-type: command
Command to save the subtiddlers of a bundle tiddler as a series of JSON files
--savelibrarytiddlers <tiddler> <pathname> <skinnylisting>
The tiddler identifies the bundle tiddler that contains the subtiddlers.
The pathname specifies the pathname to the folder in which the JSON files should be saved. The filename is the URL encoded title of the subtiddler.
The skinnylisting specifies the title of the tiddler to which a JSON catalogue of the subtiddlers will be saved. The JSON file contains the same data as the bundle tiddler but with the `text` field removed.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.info = {
name: "savelibrarytiddlers",
synchronous: true
};
var Command = function(params,commander,callback) {
this.params = params;
this.commander = commander;
this.callback = callback;
};
Command.prototype.execute = function() {
if(this.params.length < 2) {
return "Missing filename";
}
var self = this,
fs = require("fs"),
path = require("path"),
containerTitle = this.params[0],
basepath = this.params[1],
skinnyListTitle = this.params[2];
// Get the container tiddler as data
var containerData = self.commander.wiki.getTiddlerData(containerTitle,undefined);
if(!containerData) {
return "'" + containerTitle + "' is not a tiddler bundle";
}
// Save each JSON file and collect the skinny data
var skinnyList = [];
$tw.utils.each(containerData.tiddlers,function(tiddler,title) {
var pathname = path.resolve(self.commander.outputPath,basepath + encodeURIComponent(title) + ".json");
$tw.utils.createFileDirectories(pathname);
fs.writeFileSync(pathname,JSON.stringify(tiddler,null,$tw.config.preferences.jsonSpaces),"utf8");
skinnyList.push($tw.utils.extend({},tiddler,{text: undefined}));
});
// Save the catalogue tiddler
if(skinnyListTitle) {
self.commander.wiki.setTiddlerData(skinnyListTitle,skinnyList);
}
return null;
};
exports.Command = Command;
})();

View File

@ -0,0 +1,155 @@
/*\
title: $:/core/modules/browser-messaging.js
type: application/javascript
module-type: startup
Browser message handling
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Export name and synchronous status
exports.name = "browser-messaging";
exports.platforms = ["browser"];
exports.after = ["startup"];
exports.synchronous = true;
/*
Load a specified url as an iframe and call the callback when it is loaded. If the url is already loaded then the existing iframe instance is used
*/
function loadIFrame(url,callback) {
// Check if iframe already exists
var iframeInfo = $tw.browserMessaging.iframeInfoMap[url];
if(iframeInfo) {
// We've already got the iframe
callback(null,iframeInfo);
} else {
// Create the iframe and save it in the list
var iframe = document.createElement("iframe"),
iframeInfo = {
url: url,
status: "loading",
domNode: iframe
};
$tw.browserMessaging.iframeInfoMap[url] = iframeInfo;
saveIFrameInfoTiddler(iframeInfo);
// Add the iframe to the DOM and hide it
iframe.style.display = "none";
document.body.appendChild(iframe);
// Set up onload
iframe.onload = function() {
iframeInfo.status = "loaded";
saveIFrameInfoTiddler(iframeInfo);
callback(null,iframeInfo);
};
iframe.onerror = function() {
callback("Cannot load iframe");
};
try {
iframe.src = url;
} catch(ex) {
callback(ex);
}
}
}
function saveIFrameInfoTiddler(iframeInfo) {
$tw.wiki.addTiddler(new $tw.Tiddler($tw.wiki.getCreationFields(),{
title: "$:/temp/ServerConnection/" + iframeInfo.url,
text: iframeInfo.status,
tags: ["$:/tags/ServerConnection"],
url: iframeInfo.url
},$tw.wiki.getModificationFields()));
}
exports.startup = function() {
// Initialise the store of iframes we've created
$tw.browserMessaging = {
iframeInfoMap: {} // Hashmap by URL of {url:,status:"loading/loaded",domNode:}
};
// Listen for widget messages to control loading the plugin library
$tw.rootWidget.addEventListener("tm-load-plugin-library",function(event) {
var paramObject = event.paramObject || {},
url = paramObject.url;
if(url) {
loadIFrame(url,function(err,iframeInfo) {
if(err) {
alert("Error loading plugin library: " + url);
} else {
iframeInfo.domNode.contentWindow.postMessage({
verb: "GET",
url: "recipes/default/tiddlers.json",
cookies: {
type: "save-info",
infoTitlePrefix: paramObject.infoTitlePrefix || "$:/temp/RemoteAssetInfo/",
url: url
}
},"*");
}
});
}
});
$tw.rootWidget.addEventListener("tm-load-plugin-from-library",function(event) {
var paramObject = event.paramObject || {},
url = paramObject.url,
title = paramObject.title;
if(url && title) {
loadIFrame(url,function(err,iframeInfo) {
if(err) {
alert("Error loading plugin library: " + url);
} else {
iframeInfo.domNode.contentWindow.postMessage({
verb: "GET",
url: "recipes/default/tiddlers/" + encodeURIComponent(title) + ".json",
cookies: {
type: "save-tiddler",
url: url
}
},"*");
}
});
}
});
// Listen for window messages from other windows
window.addEventListener("message",function listener(event){
console.log("browser-messaging: ",document.location.toString())
console.log("browser-messaging: Received message from",event.origin);
console.log("browser-messaging: Message content",event.data);
switch(event.data.verb) {
case "GET-RESPONSE":
if(event.data.status.charAt(0) === "2") {
if(event.data.cookies) {
if(event.data.cookies.type === "save-info") {
var tiddlers = JSON.parse(event.data.body);
$tw.utils.each(tiddlers,function(tiddler) {
$tw.wiki.addTiddler(new $tw.Tiddler($tw.wiki.getCreationFields(),tiddler,{
title: event.data.cookies.infoTitlePrefix + event.data.cookies.url + "/" + tiddler.title,
"original-title": tiddler.title,
text: "",
type: "text/vnd.tiddlywiki",
"original-type": tiddler.type,
"plugin-type": undefined,
"original-plugin-type": tiddler["plugin-type"],
"module-type": undefined,
"original-module-type": tiddler["module-type"],
tags: ["$:/tags/RemoteAssetInfo"],
"original-tags": $tw.utils.stringifyList(tiddler.tags || []),
"server-url": event.data.cookies.url
},$tw.wiki.getModificationFields()));
});
} else if(event.data.cookies.type === "save-tiddler") {
var tiddler = JSON.parse(event.data.body);
$tw.wiki.addTiddler(new $tw.Tiddler(tiddler));
}
}
}
break;
}
},false);
};
})();

View File

@ -0,0 +1,85 @@
title: $:/core/ui/ControlPanel/Plugins/Add
tags: $:/tags/ControlPanel/Plugins
caption: {{$:/language/ControlPanel/Plugins/Add/Caption}}
\define lingo-base() $:/language/ControlPanel/Plugins/
\define install-plugin-button()
<$button>
<$action-sendmessage $message="tm-load-plugin-from-library" url={{!!url}} title={{$(assetInfo)$!!original-title}}/>
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""install""">
reinstall
</$list>
</$button>
\end
\define display-plugin-info()
<tr>
<td>
<<install-plugin-button>>
<br>
</td>
<td>
''<$view tiddler=<<assetInfo>> field="description"/>''
<br>
<$view tiddler=<<assetInfo>> field="original-title"/>
</td>
<td>
<$view tiddler=<<assetInfo>> field="version"/>
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion">
<br>
<em>
Installed:
<br>
<$text text=<<installedVersion>>/>
</em>
</$list>
</td>
</tr>
\end
\define load-plugin-library-button()
<$button>
<$action-sendmessage $message="tm-load-plugin-library" url={{!!url}} infoTitlePrefix="$:/temp/RemoteAssetInfo/"/>
open plugin library
</$button>
\end
\define display-server-connection()
<$list filter="[all[tiddlers+shadows]tag[$:/tags/ServerConnection]suffix{!!url}]" variable="connectionTiddler" emptyMessage=<<load-plugin-library-button>>>
Search: <$edit-text tiddler="""$:/temp/RemoteAssetSearch/$(currentTiddler)$""" default="" type="search" tag="input" focus="true"/>
<$select tiddler="$:/temp/RemoteAssetCategory/$(currentTiddler)$" default="plugin">
<option value="plugin">Plugins</option>
<option value="theme">Themes</option>
<option value="language">Languages</option>
</$select>
<$set name="pluginType" filter="[[$:/temp/RemoteAssetCategory/$(currentTiddler)$]is[tiddler]]" value={{$:/temp/RemoteAssetCategory/$(currentTiddler)$}} emptyValue="plugin">
<table class="tc-plugin-library-listing">
<tbody>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/RemoteAssetInfo]server-url{!!url}original-plugin-type<pluginType>search{$:/temp/RemoteAssetSearch/$(currentTiddler)$}sort[description]]" variable="assetInfo">
<<display-plugin-info>>
</$list>
</tbody>
</table>
</$set>
</$list>
\end
\define plugin-library-listing()
<$list filter="[all[tiddlers+shadows]tag[$:/tags/PluginLibrary]]">
<div class="tc-plugin-library">
!! <$transclude field="caption"><$view field="title"/></$transclude>
<$view field="url"/>
<<display-server-connection>>
</div>
</$list>
\end
<div>
<<plugin-library-listing>>
</div>

View File

@ -1,6 +1,6 @@
title: $:/core/ui/ControlPanel/Plugins
tags: $:/tags/ControlPanel
caption: {{$:/language/ControlPanel/Plugins/Caption}}
title: $:/core/ui/ControlPanel/Plugins/Installed
tags: $:/tags/ControlPanel/Plugins
caption: {{$:/language/ControlPanel/Plugins/Installed/Caption}}
\define lingo-base() $:/language/ControlPanel/Plugins/
\define popup-state-macro()

View File

@ -0,0 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: http://tiddlywiki.com/library/index.html
caption: {{$:/language/OfficialPluginLibrary}}

View File

@ -0,0 +1,2 @@
title: $:/tags/ControlPanel/Plugins
list: [[$:/core/ui/ControlPanel/Plugins/Installed]] [[$:/core/ui/ControlPanel/Plugins/Add]]

View File

@ -0,0 +1,17 @@
{
"description": "TiddlyWiki Plugin Library",
"plugins": [
"tiddlywiki/pluginlibrary"
],
"themes": [
],
"includeWikis": [
],
"build": {
"library": [
"--makelibrary","$:/UpgradeLibrary",
"--savelibrarytiddlers","$:/UpgradeLibrary","library/recipes/default/tiddlers/","$:/UpgradeLibrary/List",
"--savetiddler","$:/UpgradeLibrary/List","library/recipes/default/tiddlers.json",
"--rendertiddler","$:/plugins/tiddlywiki/pluginlibrary/library.template.html","library/index.html","text/plain"]
}
}

View File

@ -0,0 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: http://tiddlywiki.com/prerelease/library/index.html
url: file:///Users/jeremyruston/git/Jermolene/jermolene.github.io/prerelease/library/index.html
url: http://0.0.0.0:8080/prerelease/library/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)

View File

@ -16,23 +16,6 @@
"tiddlywiki/readonly"
],
"languages": [
"da-DK",
"el-GR",
"en-US",
"en-GB",
"de-AT",
"de-DE",
"es-ES",
"fr-FR",
"nl-NL",
"zh-Hans",
"zh-Hant",
"hi-IN",
"pa-IN",
"it-IT",
"ja-JP",
"cs-CZ",
"ru-RU"
],
"includeWikis": [
"../tw5.com"

View File

@ -17,25 +17,6 @@
"tiddlywiki/readonly"
],
"languages": [
"da-DK",
"el-GR",
"en-US",
"en-GB",
"de-AT",
"de-DE",
"es-ES",
"fr-FR",
"ia-IA",
"nl-NL",
"zh-Hans",
"zh-Hant",
"hi-IN",
"pa-IN",
"pt-PT",
"it-IT",
"ja-JP",
"cs-CZ",
"ru-RU"
],
"build": {
"index": [

View File

@ -0,0 +1,4 @@
title: $:/plugins/tiddlywiki/pluginlibrary/asset-list-json
`var assetList = `<$view tiddler="$:/UpgradeLibrary/List"/>`;
`

View File

@ -0,0 +1,27 @@
title: $:/plugins/tiddlywiki/pluginlibrary/library.template.html
\rules only filteredtranscludeinline transcludeinline
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="application-name" content="TiddlyWiki Plugin Library" />
<meta name="application-version" content="v0.0.0" />
<meta name="copyright" content="Copyright 2015 Jeremy Ruston" />
<link id="faviconLink" rel="shortcut icon" href="favicon.ico">
<title>Plugin Library</title>
<script>
{{$:/plugins/tiddlywiki/pluginlibrary/asset-list-json}}
{{$:/plugins/tiddlywiki/pluginlibrary/libraryserver.js}}
</script>
</head>
<body>
<h1>HelloThere</h1>
<p>This is the TiddlyWiki plugin library. It is not intended to be opened directly in the browser.</p>
<p>See <a href="http://tiddlywiki.com/" target="_blank">http://tiddlywiki.com/</a> for details of how to install plugins.</p>
</body>
</html>

View File

@ -0,0 +1,90 @@
/*\
title: $:/plugins/tiddlywiki/pluginlibrary/libraryserver.js
type: application/javascript
module-type: library
A simple HTTP-over-window.postMessage implementation of a standard TiddlyWeb-compatible server. It uses real HTTP to load the individual tiddler JSON files.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Listen for window messages
window.addEventListener("message",function listener(event){
console.log("plugin library: Received message from",event.origin);
console.log("plugin library: Message content",event.data);
switch(event.data.verb) {
case "GET":
if(event.data.url === "recipes/default/tiddlers.json") {
// Route for recipes/default/tiddlers.json
event.source.postMessage({
verb: "GET-RESPONSE",
status: "200",
cookies: event.data.cookies,
url: event.data.url,
type: "application/json",
body: JSON.stringify(assetList,null,4)
},"*");
} else if(event.data.url.indexOf("recipes/default/tiddlers/") === 0) {
var url = "recipes/default/tiddlers/" + encodeURIComponent(removePrefix(event.data.url,"recipes/default/tiddlers/"));
// Route for recipes/default/tiddlers/<uri-encoded-tiddler-title>.json
httpGet(url,function(err,responseText) {
if(err) {
event.source.postMessage({
verb: "GET-RESPONSE",
status: "404",
cookies: event.data.cookies,
url: event.data.url,
type: "text/plain",
body: "Not found"
},"*");
} else {
event.source.postMessage({
verb: "GET-RESPONSE",
status: "200",
cookies: event.data.cookies,
url: event.data.url,
type: "application/json",
body: responseText
},"*");
}
});
} else {
event.source.postMessage({
verb: "GET-RESPONSE",
status: "404",
cookies: event.data.cookies,
url: event.data.url,
type: "text/plain",
body: "Not found"
},"*");
}
break;
}
},false);
// Helper to remove string prefixes
function removePrefix(string,prefix) {
if(string.indexOf(prefix) === 0) {
return string.substr(prefix.length);
} else {
return string;
}
}
// Helper for HTTP GET
function httpGet(url,callback) {
var http = new XMLHttpRequest();
http.open("GET",url,true);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
callback(null,http.responseText);
}
};
http.send();
}
})();

View File

@ -0,0 +1,6 @@
{
"title": "$:/plugins/tiddlywiki/pluginlibrary",
"description": "Plugin for building the TiddlyWiki Plugin Library",
"author": "JeremyRuston",
"core-version": ">=5.0.0"
}