Add support for the plugin library

We create a system bag to contain each plugin/theme/language. It seems wasteful because it results in lots of bags, but the semantics are exactly right and so it seems like the right approach
This commit is contained in:
Jeremy Ruston 2024-04-14 18:41:34 +01:00
parent 131a5abeb8
commit 9ba4556250
4 changed files with 141 additions and 26 deletions

View File

@ -3,7 +3,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-index.js
type: application/javascript
module-type: mws-route
GET /
GET /?show_system=true
\*/
(function() {
@ -31,6 +31,7 @@ exports.handler = function(request,response,state) {
// Render the html
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{
variables: {
"show-system": state.queryParameters.show_system || "off",
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-index",
"bag-list": JSON.stringify(bagList),
"recipe-list": JSON.stringify(recipeList)

View File

@ -46,30 +46,112 @@ function setupStore() {
}
function loadStore(store) {
const path = require("path");
const path = require("path"),
fs = require("fs");
// Performance timing
console.time("mws-initial-load");
// Copy plugins
var makePluginBagName = function(type,publisher,name) {
return "$:/" + type + "/" + (publisher ? publisher + "/" : "") + name;
},
savePlugin = function(pluginFields,type,publisher,name) {
const bagName = makePluginBagName(type,publisher,name);
const result = store.createBag(bagName,pluginFields.description || "(no description)",{allowPrivilegedCharacters: true});
store.saveBagTiddler(pluginFields,bagName);
},
collectPlugins = function(folder,type,publisher) {
var pluginFolders = $tw.utils.getSubdirectories(folder) || [];
for(var p=0; p<pluginFolders.length; p++) {
const pluginFolderName = pluginFolders[p];
if(!$tw.boot.excludeRegExp.test(pluginFolderName)) {
var pluginFields = $tw.loadPluginFolder(path.resolve(folder,"./" + pluginFolderName));
if(pluginFields && pluginFields.title) {
savePlugin(pluginFields,type,publisher,pluginFolderName);
}
}
}
},
collectPublisherPlugins = function(folder,type) {
var publisherFolders = $tw.utils.getSubdirectories(folder) || [];
for(var t=0; t<publisherFolders.length; t++) {
const publisherFolderName = publisherFolders[t];
if(!$tw.boot.excludeRegExp.test(publisherFolderName)) {
collectPlugins(path.resolve(folder,"./" + publisherFolderName),type,publisherFolderName);
}
}
};
$tw.utils.each($tw.getLibraryItemSearchPaths($tw.config.pluginsPath,$tw.config.pluginsEnvVar),function(folder) {
collectPublisherPlugins(folder,"plugin");
});
$tw.utils.each($tw.getLibraryItemSearchPaths($tw.config.themesPath,$tw.config.themesEnvVar),function(folder) {
collectPublisherPlugins(folder,"theme");
});
$tw.utils.each($tw.getLibraryItemSearchPaths($tw.config.languagesPath,$tw.config.languagesEnvVar),function(folder) {
collectPlugins(folder,"language");
});
// Copy TiddlyWiki core editions
function copyEdition(options) {
console.log(`Copying edition ${options.tiddlersPath}`);
store.createBag(options.bagName,options.bagDescription);
store.createRecipe(options.recipeName,[options.bagName],options.recipeDescription);
store.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,options.tiddlersPath),options.bagName);
// Read the tiddlywiki.info file
const wikiInfoPath = path.resolve($tw.boot.corePath,$tw.config.editionsPath,options.wikiPath,$tw.config.wikiInfo);
let wikiInfo;
if(fs.existsSync(wikiInfoPath)) {
wikiInfo = $tw.utils.parseJSONSafe(fs.readFileSync(wikiInfoPath,"utf8"),function() {return null;});
}
if(wikiInfo) {
// Create the bag
store.createBag(options.bagName,options.bagDescription);
// Add plugins to the recipe list
const recipeList = [];
const processPlugins = function(type,plugins) {
$tw.utils.each(plugins,function(pluginName) {
const parts = pluginName.split("/");
let publisher, name;
if(parts.length === 2) {
publisher = parts[0];
name = parts[1];
} else {
name = parts[0];
}
recipeList.push(makePluginBagName(type,publisher,name));
});
};
processPlugins("plugin",wikiInfo.plugins);
processPlugins("theme",wikiInfo.themes);
processPlugins("language",wikiInfo.languages);
// Create the recipe
recipeList.push(options.bagName);
store.createRecipe(options.recipeName,recipeList,options.recipeDescription);
store.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,options.wikiPath,$tw.config.wikiTiddlersSubDir),options.bagName);
}
}
copyEdition({
bagName: "docs",
bagDescription: "TiddlyWiki Documentation from https://tiddlywiki.com",
recipeName: "docs",
recipeDescription: "TiddlyWiki Documentation from https://tiddlywiki.com",
tiddlersPath: "tw5.com/tiddlers"
wikiPath: "tw5.com"
});
copyEdition({
bagName: "dev-docs",
bagDescription: "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev",
recipeName: "dev-docs",
recipeDescription: "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev",
tiddlersPath: "dev/tiddlers"
wikiPath: "dev"
});
copyEdition({
bagName: "tour",
bagDescription: "TiddlyWiki Interactive Tour from https://tiddlywiki.com",
recipeName: "tour",
recipeDescription: "TiddlyWiki Interactive Tour from https://tiddlywiki.com",
wikiPath: "tour"
});
// copyEdition({
// bagName: "full",
// bagDescription: "TiddlyWiki Fully Loaded Edition from https://tiddlywiki.com",
// recipeName: "full",
// recipeDescription: "TiddlyWiki Fully Loaded Edition from https://tiddlywiki.com",
// wikiPath: "full"
// });
// Create bags and recipes
store.createBag("bag-alpha","A test bag");
store.createBag("bag-beta","Another test bag");

View File

@ -75,7 +75,7 @@ SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) {
/*
Returns null if a bag/recipe name is valid, or a string error message if not
*/
SqlTiddlerStore.prototype.validateItemName = function(name) {
SqlTiddlerStore.prototype.validateItemName = function(name,allowPrivilegedCharacters) {
if(typeof name !== "string") {
return "Not a valid string";
}
@ -83,8 +83,14 @@ SqlTiddlerStore.prototype.validateItemName = function(name) {
return "Too long";
}
// Removed ~ from this list temporarily
if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) {
return "Invalid character(s)";
if(allowPrivilegedCharacters) {
if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) {
return "Invalid character(s)";
}
} else {
if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) {
return "Invalid character(s)";
}
}
return null;
};
@ -92,14 +98,14 @@ SqlTiddlerStore.prototype.validateItemName = function(name) {
/*
Returns null if the argument is an array of valid bag/recipe names, or a string error message if not
*/
SqlTiddlerStore.prototype.validateItemNames = function(names) {
SqlTiddlerStore.prototype.validateItemNames = function(names,allowPrivilegedCharacters) {
if(!$tw.utils.isArray(names)) {
return "Not a valid array";
}
var errors = [];
for(const name of names) {
const result = this.validateItemName(name);
if(result) {
const result = this.validateItemName(name,allowPrivilegedCharacters);
if(result && errors.indexOf(result) === -1) {
errors.push(result);
}
}
@ -184,10 +190,16 @@ SqlTiddlerStore.prototype.listBags = function() {
return this.sqlTiddlerDatabase.listBags();
};
SqlTiddlerStore.prototype.createBag = function(bag_name,description) {
/*
Options include:
allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name
*/
SqlTiddlerStore.prototype.createBag = function(bag_name,description,options) {
options = options || {};
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
const validationBagName = self.validateItemName(bag_name);
const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters);
if(validationBagName) {
return {message: validationBagName};
}
@ -203,18 +215,19 @@ SqlTiddlerStore.prototype.listRecipes = function() {
/*
Returns null on success, or {message:} on error
Options include:
allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name
*/
SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description) {
SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description,options) {
bag_names = bag_names || [];
description = description || "";
const validationRecipeName = this.validateItemName(recipe_name);
options = options || {};
const validationRecipeName = this.validateItemName(recipe_name,options.allowPrivilegedCharacters);
if(validationRecipeName) {
return {message: validationRecipeName};
}
const validationBagNames = this.validateItemNames(bag_names);
if(validationBagNames) {
return {message: validationBagNames};
}
if(bag_names.length === 0) {
return {message: "Recipes must contain at least one bag"};
}

View File

@ -1,5 +1,11 @@
title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
\function .hide.system()
[<show-system>match[on]]
[all[]!prefix[$:/]]
\end
\procedure bagPill(element-tag:"span",is-topmost:"yes")
\whitespace trim
<$genesis $type=<<element-tag>> class={{{ mws-bag-pill [<is-topmost>match[yes]then[mws-bag-pill-topmost]] +[join[ ]] }}}>
@ -45,9 +51,9 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
</div>
<div class="mws-wiki-card-meta">
<%if true %>
<ol class="mws-horizontal-list">
<$list filter="[<recipe-info>jsonget[bag_names]]" variable="bag-name" counter="counter">
<$transclude $variable="bagPill" is-topmost={{{ [<counter-last>match[yes]] }}} element-tag="li"/>
<ol class="mws-vertical-list">
<$list filter="[<recipe-info>jsonget[bag_names]reverse[]] :filter[.hide.system[]]" variable="bag-name" counter="counter">
<$transclude $variable="bagPill" is-topmost={{{ [<counter-first>match[yes]] }}} element-tag="li"/>
</$list>
</ol>
<%else%>
@ -96,7 +102,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
! Bags
<ul class="mws-vertical-list">
<$list filter="[<bag-list>jsonindexes[]] :sort[<currentTiddler>jsonget[bag_name]]" variable="bag-index" counter="counter">
<$list filter="[<bag-list>jsonindexes[]] :filter[<bag-list>jsonget<currentTiddler>,[bag_name].hide.system[]] :sort[<bag-list>jsonget<currentTiddler>,[bag_name]]" variable="bag-index" counter="counter">
<li class="mws-wiki-card">
<$let
bag-info={{{ [<bag-list>jsonextract<bag-index>] }}}
@ -131,3 +137,16 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
<input type="submit" value="Create or Update Bag" formmethod="post"/>
</div>
</form>
! Advanced
<form id="checkboxForm" action="." method="GET">
<%if [<show-system>match[on]] %>
<input type="checkbox" id="chkShowSystem" name="show_system" value="on" checked="checked"/>
<%else%>
<input type="checkbox" id="chkShowSystem" name="show_system" value="on"/>
<%endif%>
<label for="chkShowSystem">Show system bags</label>
<button type="submit">Update</button>
</form>