From 778e544f1c24ae2b898d75d99df4a2497deaebd7 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sun, 16 Feb 2025 18:20:00 +0000 Subject: [PATCH 01/21] Initial Commit --- boot/boot.js | 102 +++++++++++------- editions/tw5.com/tiddlywiki.info | 3 +- .../intrinsic-subplugin-tests/plugin.info | 7 ++ .../intrinsic-subplugin-tests/readme.tid | 4 + .../test-subplugin/plugin.info | 7 ++ .../test-subplugin/readme.tid | 3 + .../test-subsubplugin/plugin.info | 7 ++ .../test-subsubplugin/readme.tid | 3 + 8 files changed, 94 insertions(+), 42 deletions(-) create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/plugin.info create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/readme.tid create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/plugin.info create mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/readme.tid diff --git a/boot/boot.js b/boot/boot.js index f1f6e4906..0d59ae982 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1163,7 +1163,7 @@ $tw.Wiki = function(options) { options = options || {}; var self = this, tiddlers = Object.create(null), // Hashmap of tiddlers - tiddlerTitles = null, // Array of tiddler titles + tiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed getTiddlerTitles = function() { if(!tiddlerTitles) { tiddlerTitles = Object.keys(tiddlers).sort(function(a,b) {return a.localeCompare(b);}); @@ -1173,7 +1173,7 @@ $tw.Wiki = function(options) { pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority pluginInfo = Object.create(null), // Hashmap of parsed plugin content shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:} - shadowTiddlerTitles = null, + shadowTiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed getShadowTiddlerTitles = function() { if(!shadowTiddlerTitles) { shadowTiddlerTitles = Object.keys(shadowTiddlers).sort(function(a,b) {return a.localeCompare(b);}); @@ -1500,7 +1500,7 @@ $tw.Wiki = function(options) { }); } }); - shadowTiddlerTitles = null; + shadowTiddlerTitles = null; // Force regeneration of the shadow tiddler titles list this.clearCache(null); this.clearGlobalCache(); $tw.utils.each(indexers,function(indexer) { @@ -1936,7 +1936,7 @@ $tw.boot.excludeRegExp = /^\.DS_Store$|^.*\.meta$|^\..*\.swp$|^\._.*$|^\.git$|^\ /* Load all the tiddlers recursively from a directory, including honouring `tiddlywiki.files` files for drawing in external files. Returns an array of {filepath:,type:,tiddlers: [{..fields...}],hasMetaFile:}. Note that no file information is returned for externally loaded tiddlers, just the `tiddlers` property. */ -$tw.loadTiddlersFromPath = function(filepath,excludeRegExp) { +$tw.loadTiddlersFromPath = function(filepath,excludeRegExp,excludePluginInfo) { excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; var tiddlers = []; if(fs.existsSync(filepath)) { @@ -1946,11 +1946,16 @@ $tw.loadTiddlersFromPath = function(filepath,excludeRegExp) { // Look for a tiddlywiki.files file if(files.indexOf("tiddlywiki.files") !== -1) { Array.prototype.push.apply(tiddlers,$tw.loadTiddlersFromSpecification(filepath,excludeRegExp)); + } else if(files.indexOf("plugin.info") !== -1 && !excludePluginInfo) { + var tiddler = $tw.loadPluginFolder(filepath); + if(tiddler) { + tiddlers.push({tiddlers: [tiddler]}); + } } else { // If not, read all the files in the directory $tw.utils.each(files,function(file) { if(!excludeRegExp.test(file) && file !== "plugin.info") { - tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(filepath + path.sep + file,excludeRegExp)); + tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(path.join(filepath,file),excludeRegExp)); } }); } @@ -2097,54 +2102,69 @@ Load the tiddlers from a plugin folder, and package them up into a proper JSON p */ $tw.loadPluginFolder = function(filepath,excludeRegExp) { excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; - var infoPath = filepath + path.sep + "plugin.info"; - if(fs.existsSync(filepath) && fs.statSync(filepath).isDirectory()) { - // Read the plugin information - if(!fs.existsSync(infoPath) || !fs.statSync(infoPath).isFile()) { - console.log("Warning: missing plugin.info file in " + filepath); + function readPluginFields(filepath) { + if(!(fs.existsSync(filepath) && fs.statSync(filepath).isDirectory())) { return null; } - var pluginInfo = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"),function() {return null;}); - if(!pluginInfo) { - console.log("warning: invalid JSON in plugin.info file at " + infoPath); - pluginInfo = {}; + var infoPath = path.join(filepath,"plugin.info"); + // Read the plugin information file + if(!fs.existsSync(infoPath) || !fs.statSync(infoPath).isFile()) { + return null; } - // Read the plugin files - var pluginFiles = $tw.loadTiddlersFromPath(filepath,excludeRegExp); - // Save the plugin tiddlers into the plugin info - pluginInfo.tiddlers = pluginInfo.tiddlers || Object.create(null); + var pluginFields = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"),{}); + if(!(pluginFields.title && pluginFields.name)) { + return null; + } + // Give the plugin the same version number as the core if it doesn't have one + if(!("version" in pluginFields)) { + pluginFields.version = $tw.packageInfo.version; + } + // Use "plugin" as the plugin-type if we don't have one + if(!("plugin-type" in pluginFields)) { + pluginFields["plugin-type"] = "plugin"; + } + // Set the dependents and type + pluginFields.dependents = pluginFields.dependents || []; + pluginFields.type = "application/json"; + // Deserialise array fields (currently required for the dependents field) + for(var field in pluginFields) { + if($tw.utils.isArray(pluginFields[field])) { + pluginFields[field] = $tw.utils.stringifyList(pluginFields[field]); + } + } + return pluginFields; + } + function readPluginTiddlers(tiddlersPath) { + var pluginFiles = $tw.loadTiddlersFromPath(tiddlersPath,excludeRegExp,true), + pluginTiddlers = {}; + // Save the plugin tiddlers into the plugin payload for(var f=0; f Date: Sun, 16 Feb 2025 19:32:56 +0000 Subject: [PATCH 02/21] Refactor some symbols for clarity All these symbols are inaccessible outside the wiki class closure, and so no backwards compatibility issues --- boot/boot.js | 74 +++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/boot/boot.js b/boot/boot.js index 0d59ae982..9aea44552 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1162,21 +1162,21 @@ enableIndexers - Array of indexer names to enable, or null to use all available $tw.Wiki = function(options) { options = options || {}; var self = this, - tiddlers = Object.create(null), // Hashmap of tiddlers + tiddlerStore = Object.create(null), // Hashmap of tiddlers tiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed getTiddlerTitles = function() { if(!tiddlerTitles) { - tiddlerTitles = Object.keys(tiddlers).sort(function(a,b) {return a.localeCompare(b);}); + tiddlerTitles = Object.keys(tiddlerStore).sort(function(a,b) {return a.localeCompare(b);}); } return tiddlerTitles; }, pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority - pluginInfo = Object.create(null), // Hashmap of parsed plugin content - shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:} + pluginContents = Object.create(null), // Hashmap of parsed plugin content + shadowTiddlerInfo = Object.create(null), // Hashmap by title of {source:, tiddler:} shadowTiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed getShadowTiddlerTitles = function() { if(!shadowTiddlerTitles) { - shadowTiddlerTitles = Object.keys(shadowTiddlers).sort(function(a,b) {return a.localeCompare(b);}); + shadowTiddlerTitles = Object.keys(shadowTiddlerInfo).sort(function(a,b) {return a.localeCompare(b);}); } return shadowTiddlerTitles; }, @@ -1218,7 +1218,7 @@ $tw.Wiki = function(options) { } } // Save the new tiddler - tiddlers[title] = tiddler; + tiddlerStore[title] = tiddler; // Check we've got the title tiddlerTitles = $tw.utils.insertSortedArray(tiddlerTitles || [],title); // Record the new tiddler state @@ -1243,7 +1243,7 @@ $tw.Wiki = function(options) { this.deleteTiddler = function(title) { // Uncomment the following line for detailed logs of all tiddler deletions // console.log("Deleting",title) - if($tw.utils.hop(tiddlers,title)) { + if($tw.utils.hop(tiddlerStore,title)) { // Record the old tiddler state var updateDescriptor = { old: { @@ -1253,7 +1253,7 @@ $tw.Wiki = function(options) { } } // Delete the tiddler - delete tiddlers[title]; + delete tiddlerStore[title]; // Delete it from the list of titles if(tiddlerTitles) { var index = tiddlerTitles.indexOf(title); @@ -1281,11 +1281,11 @@ $tw.Wiki = function(options) { // Get a tiddler from the store this.getTiddler = function(title) { if(title) { - var t = tiddlers[title]; + var t = tiddlerStore[title]; if(t !== undefined) { return t; } else { - var s = shadowTiddlers[title]; + var s = shadowTiddlerInfo[title]; if(s !== undefined) { return s.tiddler; } @@ -1305,7 +1305,7 @@ $tw.Wiki = function(options) { index,titlesLength,title; for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - callback(tiddlers[title],title); + callback(tiddlerStore[title],title); } }; @@ -1320,10 +1320,10 @@ $tw.Wiki = function(options) { index,titlesLength,title; for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - if(tiddlers[title]) { - callback(tiddlers[title],title); + if(tiddlerStore[title]) { + callback(tiddlerStore[title],title); } else { - var shadowInfo = shadowTiddlers[title]; + var shadowInfo = shadowTiddlerInfo[title]; callback(shadowInfo.tiddler,title); } } @@ -1335,13 +1335,13 @@ $tw.Wiki = function(options) { titles = getTiddlerTitles(); for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - callback(tiddlers[title],title); + callback(tiddlerStore[title],title); } titles = getShadowTiddlerTitles(); for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - if(!tiddlers[title]) { - var shadowInfo = shadowTiddlers[title]; + if(!tiddlerStore[title]) { + var shadowInfo = shadowTiddlerInfo[title]; callback(shadowInfo.tiddler,title); } } @@ -1353,35 +1353,35 @@ $tw.Wiki = function(options) { titles = getShadowTiddlerTitles(); for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - if(tiddlers[title]) { - callback(tiddlers[title],title); + if(tiddlerStore[title]) { + callback(tiddlerStore[title],title); } else { - var shadowInfo = shadowTiddlers[title]; + var shadowInfo = shadowTiddlerInfo[title]; callback(shadowInfo.tiddler,title); } } titles = getTiddlerTitles(); for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { title = titles[index]; - if(!shadowTiddlers[title]) { - callback(tiddlers[title],title); + if(!shadowTiddlerInfo[title]) { + callback(tiddlerStore[title],title); } } }; // Test for the existence of a tiddler (excludes shadow tiddlers) this.tiddlerExists = function(title) { - return !!$tw.utils.hop(tiddlers,title); + return !!$tw.utils.hop(tiddlerStore,title); }; // Determines if a tiddler is a shadow tiddler, regardless of whether it has been overridden by a real tiddler this.isShadowTiddler = function(title) { - return $tw.utils.hop(shadowTiddlers,title); + return $tw.utils.hop(shadowTiddlerInfo,title); }; this.getShadowSource = function(title) { - if($tw.utils.hop(shadowTiddlers,title)) { - return shadowTiddlers[title].source; + if($tw.utils.hop(shadowTiddlerInfo,title)) { + return shadowTiddlerInfo[title].source; } return null; }; @@ -1398,22 +1398,22 @@ $tw.Wiki = function(options) { return types; }; - // Read plugin info for all plugins, or just an array of titles. Returns the number of plugins updated or deleted + // Read plugin info for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles this.readPluginInfo = function(titles) { var results = { modifiedPlugins: [], deletedPlugins: [] }; $tw.utils.each(titles || getTiddlerTitles(),function(title) { - var tiddler = tiddlers[title]; + var tiddler = tiddlerStore[title]; if(tiddler) { if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type") && tiddler.fields.text) { - pluginInfo[tiddler.fields.title] = $tw.utils.parseJSONSafe(tiddler.fields.text); + pluginContents[tiddler.fields.title] = $tw.utils.parseJSONSafe(tiddler.fields.text); results.modifiedPlugins.push(tiddler.fields.title); } } else { - if(pluginInfo[title]) { - delete pluginInfo[title]; + if(pluginContents[title]) { + delete pluginContents[title]; results.deletedPlugins.push(title); } } @@ -1423,7 +1423,7 @@ $tw.Wiki = function(options) { // Get plugin info for a plugin this.getPluginInfo = function(title) { - return pluginInfo[title]; + return pluginContents[title]; }; // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected @@ -1437,6 +1437,7 @@ $tw.Wiki = function(options) { self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered pluginTiddlers.push(tiddler); registeredTitles.push(tiddler.fields.title); + // Register any sub-plugins } } }; @@ -1485,14 +1486,14 @@ $tw.Wiki = function(options) { } }); // Now go through the plugins in ascending order and assign the shadows - shadowTiddlers = Object.create(null); + shadowTiddlerInfo = Object.create(null); $tw.utils.each(pluginTiddlers,function(tiddler) { // Extract the constituent tiddlers - if($tw.utils.hop(pluginInfo,tiddler.fields.title)) { - $tw.utils.each(pluginInfo[tiddler.fields.title].tiddlers,function(constituentTiddler,constituentTitle) { + if($tw.utils.hop(pluginContents,tiddler.fields.title)) { + $tw.utils.each(pluginContents[tiddler.fields.title].tiddlers,function(constituentTiddler,constituentTitle) { // Save the tiddler object if(constituentTitle) { - shadowTiddlers[constituentTitle] = { + shadowTiddlerInfo[constituentTitle] = { source: tiddler.fields.title, tiddler: new $tw.Tiddler(constituentTiddler,{title: constituentTitle}) }; @@ -1947,6 +1948,7 @@ $tw.loadTiddlersFromPath = function(filepath,excludeRegExp,excludePluginInfo) { if(files.indexOf("tiddlywiki.files") !== -1) { Array.prototype.push.apply(tiddlers,$tw.loadTiddlersFromSpecification(filepath,excludeRegExp)); } else if(files.indexOf("plugin.info") !== -1 && !excludePluginInfo) { + // Load sub-plugin var tiddler = $tw.loadPluginFolder(filepath); if(tiddler) { tiddlers.push({tiddlers: [tiddler]}); From e8de413fba84620ad3055c1e2a0bb5698a14153d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 17 Feb 2025 16:31:42 +0000 Subject: [PATCH 03/21] More refactoring for clarity --- boot/boot.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/boot/boot.js b/boot/boot.js index 9aea44552..3b94737ec 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1398,7 +1398,7 @@ $tw.Wiki = function(options) { return types; }; - // Read plugin info for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles + // Read plugin contents for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles this.readPluginInfo = function(titles) { var results = { modifiedPlugins: [], @@ -1408,7 +1408,8 @@ $tw.Wiki = function(options) { var tiddler = tiddlerStore[title]; if(tiddler) { if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type") && tiddler.fields.text) { - pluginContents[tiddler.fields.title] = $tw.utils.parseJSONSafe(tiddler.fields.text); + var contents = $tw.utils.parseJSONSafe(tiddler.fields.text); + pluginContents[tiddler.fields.title] = contents; results.modifiedPlugins.push(tiddler.fields.title); } } else { @@ -1437,7 +1438,6 @@ $tw.Wiki = function(options) { self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered pluginTiddlers.push(tiddler); registeredTitles.push(tiddler.fields.title); - // Register any sub-plugins } } }; From c142bfc50dda1231ad962285fed55f22d1711f5a Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 18 Feb 2025 09:14:05 +0000 Subject: [PATCH 04/21] Use "bundled" rather than "intrinsic" --- editions/tw5.com/tiddlywiki.info | 2 +- plugins/tiddlywiki/bundled-subplugin-tests/plugin.info | 7 +++++++ plugins/tiddlywiki/bundled-subplugin-tests/readme.tid | 4 ++++ .../test-subplugin/plugin.info | 0 .../test-subplugin/readme.tid | 0 .../test-subplugin/test-subsubplugin/plugin.info | 0 .../test-subplugin/test-subsubplugin/readme.tid | 0 plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info | 7 ------- plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid | 4 ---- 9 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 plugins/tiddlywiki/bundled-subplugin-tests/plugin.info create mode 100644 plugins/tiddlywiki/bundled-subplugin-tests/readme.tid rename plugins/tiddlywiki/{intrinsic-subplugin-tests => bundled-subplugin-tests}/test-subplugin/plugin.info (100%) rename plugins/tiddlywiki/{intrinsic-subplugin-tests => bundled-subplugin-tests}/test-subplugin/readme.tid (100%) rename plugins/tiddlywiki/{intrinsic-subplugin-tests => bundled-subplugin-tests}/test-subplugin/test-subsubplugin/plugin.info (100%) rename plugins/tiddlywiki/{intrinsic-subplugin-tests => bundled-subplugin-tests}/test-subplugin/test-subsubplugin/readme.tid (100%) delete mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info delete mode 100644 plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index 68e8c49ea..5b547c582 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -8,7 +8,7 @@ "tiddlywiki/confetti", "tiddlywiki/dynannotate", "tiddlywiki/tour", - "tiddlywiki/intrinsic-subplugin-tests" + "tiddlywiki/bundled-subplugin-tests" ], "themes": [ "tiddlywiki/vanilla", diff --git a/plugins/tiddlywiki/bundled-subplugin-tests/plugin.info b/plugins/tiddlywiki/bundled-subplugin-tests/plugin.info new file mode 100644 index 000000000..e96ff06d9 --- /dev/null +++ b/plugins/tiddlywiki/bundled-subplugin-tests/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/bundled-subplugin-tests", + "name": "bundled-subplugin-tests", + "description": "Bundled subplugin tests", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL" +} diff --git a/plugins/tiddlywiki/bundled-subplugin-tests/readme.tid b/plugins/tiddlywiki/bundled-subplugin-tests/readme.tid new file mode 100644 index 000000000..44e146fc6 --- /dev/null +++ b/plugins/tiddlywiki/bundled-subplugin-tests/readme.tid @@ -0,0 +1,4 @@ +title: $:/plugins/tiddlywiki/bundled-subplugin-tests/readme + +Readme from $:/plugins/tiddlywiki/bundled-subplugin-tests + diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/plugin.info b/plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/plugin.info similarity index 100% rename from plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/plugin.info rename to plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/plugin.info diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/readme.tid b/plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/readme.tid similarity index 100% rename from plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/readme.tid rename to plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/readme.tid diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/plugin.info b/plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/test-subsubplugin/plugin.info similarity index 100% rename from plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/plugin.info rename to plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/test-subsubplugin/plugin.info diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/readme.tid b/plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/test-subsubplugin/readme.tid similarity index 100% rename from plugins/tiddlywiki/intrinsic-subplugin-tests/test-subplugin/test-subsubplugin/readme.tid rename to plugins/tiddlywiki/bundled-subplugin-tests/test-subplugin/test-subsubplugin/readme.tid diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info b/plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info deleted file mode 100644 index e48b51d57..000000000 --- a/plugins/tiddlywiki/intrinsic-subplugin-tests/plugin.info +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "$:/plugins/tiddlywiki/intrinsic-subplugin-tests", - "name": "intrinsic-subplugin-tests", - "description": "Intrinsic subplugin tests", - "list": "readme", - "stability": "STABILITY_1_EXPERIMENTAL" -} diff --git a/plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid b/plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid deleted file mode 100644 index 199faeb7a..000000000 --- a/plugins/tiddlywiki/intrinsic-subplugin-tests/readme.tid +++ /dev/null @@ -1,4 +0,0 @@ -title: $:/plugins/tiddlywiki/intrinsic-subplugin-tests/readme - -Readme from $:/plugins/tiddlywiki/intrinsic-subplugin-tests - From 6b0a13b8cc755f652c376ed8edbd55b147c084da Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 18 Feb 2025 09:14:54 +0000 Subject: [PATCH 05/21] Basic loading of bundled subplugins Very basic. Doesn't cope with any changes to the plugins after startup, and doesn't remove sub-plugins when the parent plugin is removed. --- boot/boot.js | 84 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/boot/boot.js b/boot/boot.js index 3b94737ec..499456862 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1170,7 +1170,7 @@ $tw.Wiki = function(options) { } return tiddlerTitles; }, - pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority + pluginTiddlersInfo = [], // Array of {tiddler:,parentPluginTitle:} for all plugins and sub-plugins, ordered by priority pluginContents = Object.create(null), // Hashmap of parsed plugin content shadowTiddlerInfo = Object.create(null), // Hashmap by title of {source:, tiddler:} shadowTiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed @@ -1389,8 +1389,8 @@ $tw.Wiki = function(options) { // Get an array of all the currently recognised plugin types this.getPluginTypes = function() { var types = []; - $tw.utils.each(pluginTiddlers,function(pluginTiddler) { - var pluginType = pluginTiddler.fields["plugin-type"]; + $tw.utils.each(pluginTiddlersInfo,function(pluginTiddlerInfo) { + var pluginType = pluginTiddlerInfo.tiddler.fields["plugin-type"]; if(pluginType && types.indexOf(pluginType) === -1) { types.push(pluginType); } @@ -1430,20 +1430,10 @@ $tw.Wiki = function(options) { // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected this.registerPluginTiddlers = function(pluginType,titles) { var self = this, - registeredTitles = [], - checkTiddler = function(tiddler,title) { - if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { - var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); - if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { - self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered - pluginTiddlers.push(tiddler); - registeredTitles.push(tiddler.fields.title); - } - } - }; + registeredTitles = []; if(titles) { $tw.utils.each(titles,function(title) { - checkTiddler(self.getTiddler(title),title); + checkTiddler(null,title); }); } else { this.each(function(tiddler,title) { @@ -1451,6 +1441,38 @@ $tw.Wiki = function(options) { }); } return registeredTitles; + function checkTiddler(tiddler,title) { + if(!tiddler) { + tiddler = self.getTiddler(title); + } + if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { + var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); + if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { + self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered + pluginTiddlersInfo.push({tiddler: tiddler}); + registeredTitles.push(tiddler.fields.title); + // Register any sub-plugins + var contents = pluginContents[tiddler.fields.title]; + if(contents) { + $tw.utils.each(contents.tiddlers,function(subPluginFields,subPluginTitle) { + checkSubTiddler(title,subPluginTitle); + }); + } + } + } + } + function checkSubTiddler(parentPluginTitle,title) { + var tiddler = new $tw.Tiddler(pluginContents[parentPluginTitle].tiddlers[title]); + if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { + var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); + if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { + self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered + pluginTiddlersInfo.push({tiddler: tiddler,parentPluginTitle: parentPluginTitle}); + registeredTitles.push(tiddler.fields.title); + } + } + + } }; // Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected @@ -1458,11 +1480,12 @@ $tw.Wiki = function(options) { var self = this, unregisteredTitles = []; // Remove any previous registered plugins of this type - for(var t=pluginTiddlers.length-1; t>=0; t--) { - var tiddler = pluginTiddlers[t]; + for(var t=pluginTiddlersInfo.length-1; t>=0; t--) { + var pluginInfo = pluginTiddlersInfo[t], + tiddler = pluginInfo.tiddler; if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) { unregisteredTitles.push(tiddler.fields.title); - pluginTiddlers.splice(t,1); + pluginTiddlersInfo.splice(t,1); } } return unregisteredTitles; @@ -1472,14 +1495,16 @@ $tw.Wiki = function(options) { this.unpackPluginTiddlers = function() { var self = this; // Sort the plugin titles by the `plugin-priority` field, if this field is missing, default to 1 - pluginTiddlers.sort(function(a, b) { - var priorityA = "plugin-priority" in a.fields ? a.fields["plugin-priority"] : 1; - var priorityB = "plugin-priority" in b.fields ? b.fields["plugin-priority"] : 1; + pluginTiddlersInfo.sort(function(a, b) { + var fieldsA = a.tiddler.fields, + fieldsB = b.tiddler.fields, + priorityA = "plugin-priority" in fieldsA ? fieldsA["plugin-priority"] : 1, + priorityB = "plugin-priority" in fieldsB ? fieldsB["plugin-priority"] : 1; if (priorityA !== priorityB) { return priorityA - priorityB; - } else if (a.fields.title < b.fields.title) { + } else if (fieldsA.title < fieldsB.title) { return -1; - } else if (a.fields.title === b.fields.title) { + } else if (fieldsA.title === fieldsB.title) { return 0; } else { return +1; @@ -1487,10 +1512,17 @@ $tw.Wiki = function(options) { }); // Now go through the plugins in ascending order and assign the shadows shadowTiddlerInfo = Object.create(null); - $tw.utils.each(pluginTiddlers,function(tiddler) { + $tw.utils.each(pluginTiddlersInfo,function(tiddlerInfo) { + var tiddler = tiddlerInfo.tiddler, + contents; + if(tiddlerInfo.parentPluginTitle) { + contents = $tw.utils.parseJSONSafe(pluginContents[tiddlerInfo.parentPluginTitle].tiddlers[tiddler.fields.title].text || "null"); + } else { + contents = pluginContents[tiddler.fields.title]; + } // Extract the constituent tiddlers - if($tw.utils.hop(pluginContents,tiddler.fields.title)) { - $tw.utils.each(pluginContents[tiddler.fields.title].tiddlers,function(constituentTiddler,constituentTitle) { + if(contents) { + $tw.utils.each(contents.tiddlers,function(constituentTiddler,constituentTitle) { // Save the tiddler object if(constituentTitle) { shadowTiddlerInfo[constituentTitle] = { From afabe5b7146abd22936b4d6ef8c7465635bcf7a6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Tue, 18 Feb 2025 17:30:11 +0000 Subject: [PATCH 06/21] More efficient checks if a tiddler is a plugin --- boot/boot.js | 16 ++++++++++------ core/modules/tiddler.js | 4 ---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/boot/boot.js b/boot/boot.js index 499456862..b3b43a51d 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1068,6 +1068,10 @@ $tw.Tiddler.prototype.hasField = function(field) { return $tw.utils.hop(this.fields,field); }; +$tw.Tiddler.prototype.isPlugin = function() { + return this.fields.type === "application/json" && this.hasField("plugin-type") && !this.hasField("draft.of"); +}; + /* Compare two tiddlers for equality tiddler: the tiddler to compare @@ -1407,7 +1411,7 @@ $tw.Wiki = function(options) { $tw.utils.each(titles || getTiddlerTitles(),function(title) { var tiddler = tiddlerStore[title]; if(tiddler) { - if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type") && tiddler.fields.text) { + if(tiddler.isPlugin() && tiddler.fields.text) { var contents = $tw.utils.parseJSONSafe(tiddler.fields.text); pluginContents[tiddler.fields.title] = contents; results.modifiedPlugins.push(tiddler.fields.title); @@ -1445,7 +1449,7 @@ $tw.Wiki = function(options) { if(!tiddler) { tiddler = self.getTiddler(title); } - if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { + if(tiddler && tiddler.isPlugin() && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered @@ -1462,16 +1466,16 @@ $tw.Wiki = function(options) { } } function checkSubTiddler(parentPluginTitle,title) { - var tiddler = new $tw.Tiddler(pluginContents[parentPluginTitle].tiddlers[title]); - if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { - var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); + var tiddlerFields = pluginContents[parentPluginTitle].tiddlers[title]; + if(tiddlerFields.type === "application/json" && tiddlerFields["plugin-type"] && !tiddlerFields["draft.of"] && (!pluginType || tiddlerFields["plugin-type"] === pluginType)) { + var tiddler = new $tw.Tiddler(tiddlerFields,{title: title}), + disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered pluginTiddlersInfo.push({tiddler: tiddler,parentPluginTitle: parentPluginTitle}); registeredTitles.push(tiddler.fields.title); } } - } }; diff --git a/core/modules/tiddler.js b/core/modules/tiddler.js index d8d67bf77..5028f813f 100644 --- a/core/modules/tiddler.js +++ b/core/modules/tiddler.js @@ -16,10 +16,6 @@ exports.hasTag = function(tag) { return this.fields.tags && this.fields.tags.indexOf(tag) !== -1; }; -exports.isPlugin = function() { - return this.fields.type === "application/json" && this.hasField("plugin-type"); -}; - exports.isDraft = function() { return this.hasField("draft.of"); }; From 7e2a8238b56acf223dcdb6f51e39445f1e822cf2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Tue, 18 Feb 2025 21:17:34 +0000 Subject: [PATCH 07/21] Clarify comment --- boot/boot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/boot/boot.js b/boot/boot.js index b3b43a51d..f50e8989e 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1402,7 +1402,8 @@ $tw.Wiki = function(options) { return types; }; - // Read plugin contents for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles + // Read plugin contents for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles. + // Clears the pluginContents cache for any plugins that have been deleted this.readPluginInfo = function(titles) { var results = { modifiedPlugins: [], From 55c634692bbeb47cfc208f5412111abe71f05569 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Fri, 21 Feb 2025 09:36:40 +0000 Subject: [PATCH 08/21] Deal with dynamic loading and unloading of plugins More enhancements needed for the test framework too in order to allow events to be processed manually, without relying on the nexttick handler which is awkward in Jasmine --- boot/boot.js | 156 +++++++++--------- core/modules/startup/plugins.js | 62 +------ core/modules/utils/plugins.js | 95 +++++++++++ core/modules/wiki.js | 33 ++-- .../data/plugins/DynamicJSPluginLoad.tid | 36 ++++ .../data/plugins/DynamicSubPluginLoad.tid | 43 +++++ .../data/plugins/DynamicSubPluginUnload.tid | 43 +++++ .../tests/data/plugins/SimpleSubPlugin.tid | 21 +++ .../jasmine/run-wiki-based-tests.js | 17 +- 9 files changed, 350 insertions(+), 156 deletions(-) create mode 100644 core/modules/utils/plugins.js create mode 100644 editions/test/tiddlers/tests/data/plugins/DynamicJSPluginLoad.tid create mode 100644 editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid create mode 100644 editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid create mode 100644 editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid diff --git a/boot/boot.js b/boot/boot.js index f50e8989e..cdb3f93fb 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -1174,8 +1174,8 @@ $tw.Wiki = function(options) { } return tiddlerTitles; }, - pluginTiddlersInfo = [], // Array of {tiddler:<tiddler object>,parentPluginTitle:<title>} for all plugins and sub-plugins, ordered by priority - pluginContents = Object.create(null), // Hashmap of parsed plugin content + pluginTiddlersInfo = [], // Array of tiddler fields for all plugins and sub-plugins, ordered by priority + pluginContents = Object.create(null), // Hashmap by title of {contents:<parsed plugin contents>,fields:<fields>,subPluginTitles:<list>} shadowTiddlerInfo = Object.create(null), // Hashmap by title of {source:, tiddler:} shadowTiddlerTitles = null, // Array of tiddler titles, calculated and cached when needed getShadowTiddlerTitles = function() { @@ -1393,8 +1393,8 @@ $tw.Wiki = function(options) { // Get an array of all the currently recognised plugin types this.getPluginTypes = function() { var types = []; - $tw.utils.each(pluginTiddlersInfo,function(pluginTiddlerInfo) { - var pluginType = pluginTiddlerInfo.tiddler.fields["plugin-type"]; + $tw.utils.each(pluginTiddlersInfo,function(pluginTiddlerFields) { + var pluginType = pluginTiddlerFields["plugin-type"]; if(pluginType && types.indexOf(pluginType) === -1) { types.push(pluginType); } @@ -1402,23 +1402,44 @@ $tw.Wiki = function(options) { return types; }; - // Read plugin contents for all plugins, or just an array of titles. Returns the list of updated plugin titles and the list of deleted plugin titles. - // Clears the pluginContents cache for any plugins that have been deleted + // Read plugin contents for all plugins, or just an array of titles. Returns the list of updated plugin titles, the list of deleted plugin titles and + // a hashmap of the deleted plugin contents. Clears the pluginContents cache for any plugins that have been deleted this.readPluginInfo = function(titles) { - var results = { + var self = this, + results = { modifiedPlugins: [], - deletedPlugins: [] + deletedPlugins: [], + deletedPluginContents: {} }; $tw.utils.each(titles || getTiddlerTitles(),function(title) { - var tiddler = tiddlerStore[title]; + var tiddler = self.getTiddler(title); + if(!tiddler) { + for(var t=0; t<pluginContents.length; t++) { + if(pluginContents[t].subPluginTitles.indexOf(title) !== -1) { + tiddler = new $tw.Tiddler(pluginContents[t].fields); + break; + } + } + } if(tiddler) { if(tiddler.isPlugin() && tiddler.fields.text) { - var contents = $tw.utils.parseJSONSafe(tiddler.fields.text); - pluginContents[tiddler.fields.title] = contents; - results.modifiedPlugins.push(tiddler.fields.title); + var contents = $tw.utils.parseJSONSafe(tiddler.fields.text).tiddlers; + var subPluginTitles = []; + // Read any sub-plugins + $tw.utils.each(contents,function(subPluginFields,subPluginTitle) { + if(subPluginFields["plugin-type"] && subPluginFields["type"] === "application/json") { + var contents = $tw.utils.parseJSONSafe(subPluginFields.text).tiddlers + pluginContents[subPluginTitle] = {contents: contents,fields: subPluginFields}; + results.modifiedPlugins.push(subPluginTitle); + subPluginTitles.push(subPluginTitle) + } + }); + pluginContents[title] = {contents: contents,fields: tiddler.fields,subPluginTitles: subPluginTitles}; + results.modifiedPlugins.push(title); } } else { if(pluginContents[title]) { + results.deletedPluginContents[title] = {tiddlers: pluginContents[title].contents}; delete pluginContents[title]; results.deletedPlugins.push(title); } @@ -1427,57 +1448,43 @@ $tw.Wiki = function(options) { return results; }; + this.isSubPlugin = function(title) { + for(var t=0; t<pluginContents.length; t++) { + if(pluginContents[t].subPluginTitles.indexOf(title) !== -1) { + return true; + } + } + return false; + }; + // Get plugin info for a plugin this.getPluginInfo = function(title) { - return pluginContents[title]; + var pluginContent = pluginContents[title]; + if(pluginContent) { + return {tiddlers: pluginContent.contents, subPluginTitles: pluginContent.subPluginTitles, fields: pluginContent.fields}; + } else { + return null; + } }; // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected this.registerPluginTiddlers = function(pluginType,titles) { var self = this, registeredTitles = []; - if(titles) { - $tw.utils.each(titles,function(title) { - checkTiddler(null,title); - }); - } else { - this.each(function(tiddler,title) { - checkTiddler(tiddler,title); - }); - } - return registeredTitles; - function checkTiddler(tiddler,title) { - if(!tiddler) { - tiddler = self.getTiddler(title); - } - if(tiddler && tiddler.isPlugin() && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { - var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); - if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { - self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered - pluginTiddlersInfo.push({tiddler: tiddler}); - registeredTitles.push(tiddler.fields.title); - // Register any sub-plugins - var contents = pluginContents[tiddler.fields.title]; - if(contents) { - $tw.utils.each(contents.tiddlers,function(subPluginFields,subPluginTitle) { - checkSubTiddler(title,subPluginTitle); - }); + $tw.utils.each(pluginContents,function(pluginContent,pluginTitle) { + var pluginFields = pluginContent.fields; + if(!titles || titles.indexOf(pluginTitle) !== -1) { + if(!pluginType || pluginFields["plugin-type"] === pluginType) { + var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + pluginTitle); + if(pluginTitle === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { + self.unregisterPluginTiddlers(null,[pluginTitle]); // Unregister the plugin if it's already registered + pluginTiddlersInfo.push(pluginFields); + registeredTitles.push(pluginTitle); } } } - } - function checkSubTiddler(parentPluginTitle,title) { - var tiddlerFields = pluginContents[parentPluginTitle].tiddlers[title]; - if(tiddlerFields.type === "application/json" && tiddlerFields["plugin-type"] && !tiddlerFields["draft.of"] && (!pluginType || tiddlerFields["plugin-type"] === pluginType)) { - var tiddler = new $tw.Tiddler(tiddlerFields,{title: title}), - disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); - if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { - self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered - pluginTiddlersInfo.push({tiddler: tiddler,parentPluginTitle: parentPluginTitle}); - registeredTitles.push(tiddler.fields.title); - } - } - } + }); + return registeredTitles; }; // Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected @@ -1486,10 +1493,9 @@ $tw.Wiki = function(options) { unregisteredTitles = []; // Remove any previous registered plugins of this type for(var t=pluginTiddlersInfo.length-1; t>=0; t--) { - var pluginInfo = pluginTiddlersInfo[t], - tiddler = pluginInfo.tiddler; - if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) { - unregisteredTitles.push(tiddler.fields.title); + var pluginFields = pluginTiddlersInfo[t]; + if(pluginFields["plugin-type"] && (!pluginType || pluginFields["plugin-type"] === pluginType) && (!titles || titles.indexOf(pluginFields.title) !== -1)) { + unregisteredTitles.push(pluginFields.title); pluginTiddlersInfo.splice(t,1); } } @@ -1501,15 +1507,13 @@ $tw.Wiki = function(options) { var self = this; // Sort the plugin titles by the `plugin-priority` field, if this field is missing, default to 1 pluginTiddlersInfo.sort(function(a, b) { - var fieldsA = a.tiddler.fields, - fieldsB = b.tiddler.fields, - priorityA = "plugin-priority" in fieldsA ? fieldsA["plugin-priority"] : 1, - priorityB = "plugin-priority" in fieldsB ? fieldsB["plugin-priority"] : 1; + var priorityA = "plugin-priority" in a ? a["plugin-priority"] : 1, + priorityB = "plugin-priority" in b ? b["plugin-priority"] : 1; if (priorityA !== priorityB) { return priorityA - priorityB; - } else if (fieldsA.title < fieldsB.title) { + } else if (a.title < b.title) { return -1; - } else if (fieldsA.title === fieldsB.title) { + } else if (a.title === b.title) { return 0; } else { return +1; @@ -1517,26 +1521,16 @@ $tw.Wiki = function(options) { }); // Now go through the plugins in ascending order and assign the shadows shadowTiddlerInfo = Object.create(null); - $tw.utils.each(pluginTiddlersInfo,function(tiddlerInfo) { - var tiddler = tiddlerInfo.tiddler, - contents; - if(tiddlerInfo.parentPluginTitle) { - contents = $tw.utils.parseJSONSafe(pluginContents[tiddlerInfo.parentPluginTitle].tiddlers[tiddler.fields.title].text || "null"); - } else { - contents = pluginContents[tiddler.fields.title]; - } + $tw.utils.each(pluginTiddlersInfo,function(tiddlerFields) { + var contents = pluginContents[tiddlerFields.title].contents; // Extract the constituent tiddlers - if(contents) { - $tw.utils.each(contents.tiddlers,function(constituentTiddler,constituentTitle) { - // Save the tiddler object - if(constituentTitle) { - shadowTiddlerInfo[constituentTitle] = { - source: tiddler.fields.title, - tiddler: new $tw.Tiddler(constituentTiddler,{title: constituentTitle}) - }; - } - }); - } + $tw.utils.each(contents,function(constituentTiddler,constituentTitle) { + // Save the tiddler object + shadowTiddlerInfo[constituentTitle] = { + source: tiddlerFields.title, + tiddler: new $tw.Tiddler(constituentTiddler,{title: constituentTitle}) + }; + }); }); shadowTiddlerTitles = null; // Force regeneration of the shadow tiddler titles list this.clearCache(null); diff --git a/core/modules/startup/plugins.js b/core/modules/startup/plugins.js index af354c38e..6d86785f9 100644 --- a/core/modules/startup/plugins.js +++ b/core/modules/startup/plugins.js @@ -18,68 +18,8 @@ exports.after = ["load-modules"]; exports.before = ["startup"]; exports.synchronous = true; -var TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE = "$:/status/RequireReloadDueToPluginChange"; - -var PREFIX_CONFIG_REGISTER_PLUGIN_TYPE = "$:/config/RegisterPluginType/"; - exports.startup = function() { - $tw.wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "no"}); - $tw.wiki.addEventListener("change",function(changes) { - // Work out which of the changed tiddlers are plugins that we need to reregister - var changesToProcess = [], - requireReloadDueToPluginChange = false; - $tw.utils.each(Object.keys(changes),function(title) { - var tiddler = $tw.wiki.getTiddler(title), - requiresReload = $tw.wiki.doesPluginRequireReload(title); - if(requiresReload) { - requireReloadDueToPluginChange = true; - } else if(tiddler) { - var pluginType = tiddler.fields["plugin-type"]; - if($tw.wiki.getTiddlerText(PREFIX_CONFIG_REGISTER_PLUGIN_TYPE + (tiddler.fields["plugin-type"] || ""),"no") === "yes") { - changesToProcess.push(title); - } - } - }); - // Issue warning if any of the tiddlers require a reload - if(requireReloadDueToPluginChange) { - $tw.wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "yes"}); - } - // Read or delete the plugin info of the changed tiddlers - if(changesToProcess.length > 0) { - var changes = $tw.wiki.readPluginInfo(changesToProcess); - if(changes.modifiedPlugins.length > 0 || changes.deletedPlugins.length > 0) { - var changedShadowTiddlers = {}; - // Collect the shadow tiddlers of any deleted plugins - $tw.utils.each(changes.deletedPlugins,function(pluginTitle) { - var pluginInfo = $tw.wiki.getPluginInfo(pluginTitle); - if(pluginInfo) { - $tw.utils.each(Object.keys(pluginInfo.tiddlers),function(title) { - changedShadowTiddlers[title] = true; - }); - } - }); - // Collect the shadow tiddlers of any modified plugins - $tw.utils.each(changes.modifiedPlugins,function(pluginTitle) { - var pluginInfo = $tw.wiki.getPluginInfo(pluginTitle); - if(pluginInfo && pluginInfo.tiddlers) { - $tw.utils.each(Object.keys(pluginInfo.tiddlers),function(title) { - changedShadowTiddlers[title] = false; - }); - } - }); - // (Re-)register any modified plugins - $tw.wiki.registerPluginTiddlers(null,changes.modifiedPlugins); - // Unregister any deleted plugins - $tw.wiki.unregisterPluginTiddlers(null,changes.deletedPlugins); - // Unpack the shadow tiddlers - $tw.wiki.unpackPluginTiddlers(); - // Queue change events for the changed shadow tiddlers - $tw.utils.each(Object.keys(changedShadowTiddlers),function(title) { - $tw.wiki.enqueueTiddlerEvent(title,changedShadowTiddlers[title], true); - }); - } - } - }); + $tw.utils.installPluginChangeHandler($tw.wiki); }; })(); diff --git a/core/modules/utils/plugins.js b/core/modules/utils/plugins.js new file mode 100644 index 000000000..782d96b36 --- /dev/null +++ b/core/modules/utils/plugins.js @@ -0,0 +1,95 @@ +/*\ +title: $:/core/modules/utils/plugins.js +type: application/javascript +module-type: utils + +Plugin utilities + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE = "$:/status/RequireReloadDueToPluginChange"; + +var PREFIX_CONFIG_REGISTER_PLUGIN_TYPE = "$:/config/RegisterPluginType/"; + +exports.installPluginChangeHandler = function(wiki) { + wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "no"}); + wiki.addEventListener("change",function(changes) { + // Work out which of the changed tiddlers are plugins that we need to (re)register + var changesToProcess = []; + $tw.utils.each(Object.keys(changes),function(title) { + var tiddler = wiki.getTiddler(title); + if(tiddler) { + // It is a plugin that has been added or modified and is of a type that we need to register + if(tiddler.isPlugin() && $tw.wiki.getTiddlerText(PREFIX_CONFIG_REGISTER_PLUGIN_TYPE + (tiddler.fields["plugin-type"] || ""),"no") === "yes") { + changesToProcess.push(title); + } + } else { + if(wiki.isSubPlugin(title)) { + // It is a sub-plugin + changesToProcess.push(title); + } else { + // It is a plugin that has been deleted + var pluginInfo = wiki.getPluginInfo(title) + if(pluginInfo) { + changesToProcess.push(title); + } + } + } + }); + if(changesToProcess.length > 0) { + // Read the plugin info of the changed tiddlers + var changedPluginInfo = wiki.readPluginInfo(changesToProcess); + if(changedPluginInfo.modifiedPlugins.length > 0 || changedPluginInfo.deletedPlugins.length > 0) { + var changedShadowTiddlers = {}, + requireReloadDueToPluginChange = false; + // Collect the shadow tiddlers of any deleted plugins + $tw.utils.each(changedPluginInfo.deletedPlugins,function(pluginTitle) { + var contents = changedPluginInfo.deletedPluginContents[pluginTitle]; + if(contents && contents.tiddlers) { + $tw.utils.each(Object.keys(contents.tiddlers),function(title) { + changedShadowTiddlers[title] = true; + if(contents.tiddlers[title].type === "application/javascript") { + requireReloadDueToPluginChange = true; + } + }); + } + }); + // Collect the shadow tiddlers of any modified plugins + $tw.utils.each(changedPluginInfo.modifiedPlugins,function(pluginTitle) { + var pluginInfo = wiki.getPluginInfo(pluginTitle); + if(pluginInfo && pluginInfo.tiddlers) { + $tw.utils.each(Object.keys(pluginInfo.tiddlers),function(title) { + changedShadowTiddlers[title] = false; + if(pluginInfo.tiddlers[title].type === "application/javascript") { + requireReloadDueToPluginChange = true; + } + }); + } + }); + // (Re-)register any modified plugins + wiki.registerPluginTiddlers(null,changedPluginInfo.modifiedPlugins); + // Unregister any deleted plugins + wiki.unregisterPluginTiddlers(null,changedPluginInfo.deletedPlugins); + // Unpack the shadow tiddlers + wiki.unpackPluginTiddlers(); + // Queue change events for the changed shadow tiddlers + $tw.utils.each(changedShadowTiddlers,function(status,title) { + wiki.enqueueTiddlerEvent(title,changedShadowTiddlers[title], true); + }); + // Issue warning if any of the tiddlers require a reload + if(requireReloadDueToPluginChange) { + wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "yes"}); + } + } + } + }); +}; + + + +})(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 928a2e847..0fc352910 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -135,6 +135,14 @@ exports.dispatchEvent = function(type /*, args */) { } }; +/* +false (default) to dispatch event tick as usual +true to suppress dispatching the event tick and allow outstanding events to be processed manually +*/ +exports.setDispatchMode = function(mode) { + this.dispatchMode = mode; +} + /* Causes a tiddler to be marked as changed, incrementing the change count, and triggers event handlers. This method should be called after the changes it describes have been made to the wiki.tiddlers[] array. @@ -159,17 +167,18 @@ exports.enqueueTiddlerEvent = function(title,isDeleted,isShadow) { } // Trigger events this.eventListeners = this.eventListeners || {}; - if(!this.eventsTriggered) { - var self = this; - $tw.utils.nextTick(function() { - var changes = self.changedTiddlers; - self.changedTiddlers = Object.create(null); - self.eventsTriggered = false; - if($tw.utils.count(changes) > 0) { - self.dispatchEvent("change",changes); - } - }); - this.eventsTriggered = true; + if(!this.eventsTriggered && !this.dispatchMode) { + $tw.utils.nextTick(this.processOutstandingTiddlerEvents.bind(this)); + } + this.eventsTriggered = true; +}; + +exports.processOutstandingTiddlerEvents = function() { + var changes = this.changedTiddlers; + this.changedTiddlers = Object.create(null); + this.eventsTriggered = false; + if($tw.utils.count(changes) > 0) { + this.dispatchEvent("change",changes); } }; @@ -1743,7 +1752,7 @@ exports.invokeUpgraders = function(titles,tiddlers) { // Determine whether a plugin by title is dynamically loadable exports.doesPluginRequireReload = function(title) { var tiddler = this.getTiddler(title); - if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"]) { + if(tiddler && tiddler.isPlugin()) { if(tiddler.fields["plugin-type"] === "import") { // The import plugin never requires reloading return false; diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicJSPluginLoad.tid b/editions/test/tiddlers/tests/data/plugins/DynamicJSPluginLoad.tid new file mode 100644 index 000000000..00cb3474a --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/DynamicJSPluginLoad.tid @@ -0,0 +1,36 @@ +title: Plugins/DynamicJSPluginLoad +description: Dynamic JS plugin loading +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{$:/status/RequireReloadDueToPluginChange}} ++ +title: Actions + +<$action-createtiddler $basetitle="$:/plugins/tiddlywiki/test-js-plugin" $template="$:/plugins/tiddlywiki/test-js-plugin/raw" type="application/json"/> ++ +title: $:/plugins/tiddlywiki/test-js-plugin/raw +list: readme +name: bundled-subplugin-tests +plugin-type: plugin +type: text/plain + +{ + "tiddlers": { + "$:/plugins/tiddlywiki/test-js-plugin/readme": { + "title": "$:/plugins/tiddlywiki/test-js-plugin/readme", + "text": "Readme from $:/plugins/tiddlywiki/test-js-plugin\n\n" + }, + "$:/plugins/tiddlywiki/test-js-plugin/js": { + "title": "$:/plugins/tiddlywiki/test-js-plugin/js", + "text": "2+2", + "type": "application/javascript" + } + } +} ++ +title: ExpectedResult + +<p>yes</p> \ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid b/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid new file mode 100644 index 000000000..78d5f50cc --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid @@ -0,0 +1,43 @@ +title: Plugins/DynamicSubPluginLoad +description: Dynamic sub-plugin loading +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{$:/plugins/tiddlywiki/test-subplugin/readme}} ++ +title: Actions + +<$action-createtiddler $basetitle="$:/plugins/tiddlywiki/bundled-subplugin-tests" $template="$:/plugins/tiddlywiki/bundled-subplugin-tests/raw" type="application/json"/> ++ +title: $:/plugins/tiddlywiki/bundled-subplugin-tests/raw +list: readme +name: bundled-subplugin-tests +plugin-type: plugin +type: text/plain + +{ + "tiddlers": { + "$:/plugins/tiddlywiki/bundled-subplugin-tests/readme": { + "title": "$:/plugins/tiddlywiki/bundled-subplugin-tests/readme", + "text": "Readme from $:/plugins/tiddlywiki/bundled-subplugin-tests\n\n" + }, + "$:/plugins/tiddlywiki/test-subplugin": { + "title": "$:/plugins/tiddlywiki/test-subplugin", + "name": "test-subplugin", + "description": "Test subplugin", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL", + "version": "5.3.7-prerelease", + "plugin-type": "plugin", + "dependents": "", + "type": "application/json", + "text": "{\"tiddlers\":{\"$:/plugins/tiddlywiki/test-subplugin/readme\":{\"title\":\"$:/plugins/tiddlywiki/test-subplugin/readme\",\"text\":\"Readme from $:/plugins/tiddlywiki/test-subplugin\"},\"$:/plugins/tiddlywiki/test-subsubplugin\":{\"title\":\"$:/plugins/tiddlywiki/test-subsubplugin\",\"name\":\"test-subsubplugin\",\"description\":\"Test subsubplugin\",\"list\":\"readme readme2\",\"stability\":\"STABILITY_1_EXPERIMENTAL\",\"version\":\"5.3.7-prerelease\",\"plugin-type\":\"plugin\",\"dependents\":\"\",\"type\":\"application/json\",\"text\":\"{\\\"tiddlers\\\":{\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\":{\\\"title\\\":\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\",\\\"text\\\":\\\"Readme from $:/plugins/tiddlywiki/test-subsubplugin\\\"}}}\"}}}" + } + } +} ++ +title: ExpectedResult + +<p>Readme from <a class="tc-tiddlylink tc-tiddlylink-shadow" href="#%24%3A%2Fplugins%2Ftiddlywiki%2Ftest-subplugin">$:/plugins/tiddlywiki/test-subplugin</a></p> \ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid b/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid new file mode 100644 index 000000000..c38ab99d5 --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid @@ -0,0 +1,43 @@ +title: Plugins/DynamicSubPluginUnload +description: Dynamic sub-plugin unloading +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-specx]] + +title: Output + +{{$:/plugins/tiddlywiki/bundled-subplugin-tests/readme}} ++ +title: Actions + +<$action-deletetiddler $tiddler="$:/plugins/tiddlywiki/bundled-subplugin-tests"/> ++ +title: $:/plugins/tiddlywiki/bundled-subplugin-tests +list: readme +name: bundled-subplugin-tests +plugin-type: plugin +type: application/json + +{ + "tiddlers": { + "$:/plugins/tiddlywiki/bundled-subplugin-tests/readme": { + "title": "$:/plugins/tiddlywiki/bundled-subplugin-tests/readme", + "text": "Readme from $:/plugins/tiddlywiki/bundled-subplugin-tests\n\n" + }, + "$:/plugins/tiddlywiki/test-subplugin": { + "title": "$:/plugins/tiddlywiki/test-subplugin", + "name": "test-subplugin", + "description": "Test subplugin", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL", + "version": "5.3.7-prerelease", + "plugin-type": "plugin", + "dependents": "", + "type": "application/json", + "text": "{\"tiddlers\":{\"$:/plugins/tiddlywiki/test-subplugin/readme\":{\"title\":\"$:/plugins/tiddlywiki/test-subplugin/readme\",\"text\":\"Readme from $:/plugins/tiddlywiki/test-subplugin\"},\"$:/plugins/tiddlywiki/test-subsubplugin\":{\"title\":\"$:/plugins/tiddlywiki/test-subsubplugin\",\"name\":\"test-subsubplugin\",\"description\":\"Test subsubplugin\",\"list\":\"readme readme2\",\"stability\":\"STABILITY_1_EXPERIMENTAL\",\"version\":\"5.3.7-prerelease\",\"plugin-type\":\"plugin\",\"dependents\":\"\",\"type\":\"application/json\",\"text\":\"{\\\"tiddlers\\\":{\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\":{\\\"title\\\":\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\",\\\"text\\\":\\\"Readme from $:/plugins/tiddlywiki/test-subsubplugin\\\"}}}\"}}}" + } + } +} ++ +title: ExpectedResult + +. \ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid b/editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid new file mode 100644 index 000000000..2e583621a --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid @@ -0,0 +1,21 @@ +title: Plugins/SimpleSubPlugin +description: Simple sub-plugin +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{$:/plugins/tiddlywiki/test-subplugin/readme}} + ++ +title: $:/plugins/tiddlywiki/bundled-subplugin-tests +list: readme +name: bundled-subplugin-tests +plugin-type: plugin +type: application/json + +{"tiddlers":{"$:/plugins/tiddlywiki/bundled-subplugin-tests/readme":{"title":"$:/plugins/tiddlywiki/bundled-subplugin-tests/readme","text":"Readme from $:/plugins/tiddlywiki/bundled-subplugin-tests\n\n"},"$:/plugins/tiddlywiki/test-subplugin":{"title":"$:/plugins/tiddlywiki/test-subplugin","name":"test-subplugin","description":"Test subplugin","list":"readme","stability":"STABILITY_1_EXPERIMENTAL","version":"5.3.7-prerelease","plugin-type":"plugin","dependents":"","type":"application/json","text":"{\"tiddlers\":{\"$:/plugins/tiddlywiki/test-subplugin/readme\":{\"title\":\"$:/plugins/tiddlywiki/test-subplugin/readme\",\"text\":\"Readme from $:/plugins/tiddlywiki/test-subplugin\"},\"$:/plugins/tiddlywiki/test-subsubplugin\":{\"title\":\"$:/plugins/tiddlywiki/test-subsubplugin\",\"name\":\"test-subsubplugin\",\"description\":\"Test subsubplugin\",\"list\":\"readme readme2\",\"stability\":\"STABILITY_1_EXPERIMENTAL\",\"version\":\"5.3.7-prerelease\",\"plugin-type\":\"plugin\",\"dependents\":\"\",\"type\":\"application/json\",\"text\":\"{\\\"tiddlers\\\":{\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\":{\\\"title\\\":\\\"$:/plugins/tiddlywiki/test-subsubplugin/readme\\\",\\\"text\\\":\\\"Readme from $:/plugins/tiddlywiki/test-subsubplugin\\\"}}}\"}}}"}}} ++ +title: ExpectedResult + +<p>Readme from <a class="tc-tiddlylink tc-tiddlylink-shadow" href="#%24%3A%2Fplugins%2Ftiddlywiki%2Ftest-subplugin">$:/plugins/tiddlywiki/test-subplugin</a></p> \ No newline at end of file diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js index 2e854cf46..3f961a1bc 100644 --- a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -24,11 +24,15 @@ describe("Wiki-based tests", function() { var tiddler = $tw.wiki.getTiddler(title); it(tiddler.fields.title + ": " + tiddler.fields.description, function() { // Add our tiddlers - var wiki = new $tw.Wiki(), - coreTiddler = $tw.wiki.getTiddler("$:/core"); + var wiki = new $tw.Wiki(); + // Suppress next tick dispatch for wiki change events + wiki.setDispatchMode(true); + // Add the core plugin + var coreTiddler = $tw.wiki.getTiddler("$:/core") if(coreTiddler) { wiki.addTiddler(coreTiddler); } + // Add other tiddlers wiki.addTiddlers(readMultipleTiddlersTiddler(title)); // Unpack plugin tiddlers wiki.readPluginInfo(); @@ -37,6 +41,8 @@ describe("Wiki-based tests", function() { wiki.addIndexersToWiki(); // Clear changes queue wiki.clearTiddlerEventQueue(); + // Install the plugin change event handler + $tw.utils.installPluginChangeHandler(wiki); // Complain if we don't have the ouput and expected results if(!wiki.tiddlerExists("Output")) { throw "Missing 'Output' tiddler"; @@ -54,7 +60,14 @@ describe("Wiki-based tests", function() { widgetNode.invokeActionString(wiki.getTiddlerText("Actions")); refreshWidgetNode(widgetNode,wrapper); } + // Make sure all wiki events have been cleared + while(wiki.eventsTriggered) { + wiki.processOutstandingTiddlerEvents(); + refreshWidgetNode(widgetNode,wrapper); + } + wiki.processOutstandingTiddlerEvents(); // Test the rendering + refreshWidgetNode(widgetNode,wrapper); expect(wrapper.innerHTML).toBe(wiki.getTiddlerText("ExpectedResult")); } }); From 0c80e1b35cbd86e9a750c5624e8a3771281261ec Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Fri, 21 Feb 2025 10:59:38 +0000 Subject: [PATCH 09/21] Move subplugin tests into their own folder --- .../tests/data/{plugins => subplugins}/DynamicSubPluginLoad.tid | 2 +- .../data/{plugins => subplugins}/DynamicSubPluginUnload.tid | 2 +- .../tests/data/{plugins => subplugins}/SimpleSubPlugin.tid | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename editions/test/tiddlers/tests/data/{plugins => subplugins}/DynamicSubPluginLoad.tid (98%) rename editions/test/tiddlers/tests/data/{plugins => subplugins}/DynamicSubPluginUnload.tid (97%) rename editions/test/tiddlers/tests/data/{plugins => subplugins}/SimpleSubPlugin.tid (98%) diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginLoad.tid similarity index 98% rename from editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid rename to editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginLoad.tid index 78d5f50cc..f751d6ebc 100644 --- a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginLoad.tid +++ b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginLoad.tid @@ -1,4 +1,4 @@ -title: Plugins/DynamicSubPluginLoad +title: SubPlugins/DynamicSubPluginLoad description: Dynamic sub-plugin loading type: text/vnd.tiddlywiki-multiple tags: [[$:/tags/wiki-test-spec]] diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid similarity index 97% rename from editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid rename to editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid index c38ab99d5..d91db8938 100644 --- a/editions/test/tiddlers/tests/data/plugins/DynamicSubPluginUnload.tid +++ b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid @@ -1,4 +1,4 @@ -title: Plugins/DynamicSubPluginUnload +title: SubPlugins/DynamicSubPluginUnload description: Dynamic sub-plugin unloading type: text/vnd.tiddlywiki-multiple tags: [[$:/tags/wiki-test-specx]] diff --git a/editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid b/editions/test/tiddlers/tests/data/subplugins/SimpleSubPlugin.tid similarity index 98% rename from editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid rename to editions/test/tiddlers/tests/data/subplugins/SimpleSubPlugin.tid index 2e583621a..08adc551e 100644 --- a/editions/test/tiddlers/tests/data/plugins/SimpleSubPlugin.tid +++ b/editions/test/tiddlers/tests/data/subplugins/SimpleSubPlugin.tid @@ -1,4 +1,4 @@ -title: Plugins/SimpleSubPlugin +title: SubPlugins/SimpleSubPlugin description: Simple sub-plugin type: text/vnd.tiddlywiki-multiple tags: [[$:/tags/wiki-test-spec]] From 01beac1d7c14412c4d8e392a89ab9ac51c870260 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Fri, 21 Feb 2025 16:47:00 +0000 Subject: [PATCH 10/21] Test for loading language plugins at startup --- core/modules/pluginswitcher.js | 8 ++-- .../tiddlers/tests/data/plugins/Language.tid | 46 +++++++++++++++++++ .../jasmine/run-wiki-based-tests.js | 16 +++++-- 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/plugins/Language.tid diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js index 2915de95a..71d9f3b03 100644 --- a/core/modules/pluginswitcher.js +++ b/core/modules/pluginswitcher.js @@ -61,13 +61,13 @@ PluginSwitcher.prototype.switchPlugins = function() { }; accumulatePlugin(selectedPluginTitle); // Read the plugin info for the incoming plugins - var changes = $tw.wiki.readPluginInfo(plugins); + var changes = this.wiki.readPluginInfo(plugins); // Unregister any existing theme tiddlers - var unregisteredTiddlers = $tw.wiki.unregisterPluginTiddlers(this.pluginType); + var unregisteredTiddlers = this.wiki.unregisterPluginTiddlers(this.pluginType); // Register any new theme tiddlers - var registeredTiddlers = $tw.wiki.registerPluginTiddlers(this.pluginType,plugins); + var registeredTiddlers = this.wiki.registerPluginTiddlers(this.pluginType,plugins,plugins[0] === "$:/languages/fr-FR"); // Unpack the current theme tiddlers - $tw.wiki.unpackPluginTiddlers(); + this.wiki.unpackPluginTiddlers(); // Call the switch handler if(this.onSwitch) { this.onSwitch(plugins); diff --git a/editions/test/tiddlers/tests/data/plugins/Language.tid b/editions/test/tiddlers/tests/data/plugins/Language.tid new file mode 100644 index 000000000..706d54ecf --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/Language.tid @@ -0,0 +1,46 @@ +title: Plugins/Language +description: Loading of correct language plugin at startup +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{First}} ++ +title: $:/language + +$:/languages/fr-FR ++ +title: $:/languages/en-GB +list: readme +name: en-GB +plugin-type: language +type: application/json + +{ + "tiddlers": { + "First": { + "title": "First", + "text": "This is English" + } + } +} ++ +title: $:/languages/fr-FR +list: readme +name: fr-FR +plugin-type: language +type: application/json + +{ + "tiddlers": { + "First": { + "title": "First", + "text": "This is French" + } + } +} ++ +title: ExpectedResult + +<p>This is French</p> \ No newline at end of file diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js index 3f961a1bc..536c51966 100644 --- a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -39,10 +39,19 @@ describe("Wiki-based tests", function() { wiki.registerPluginTiddlers("plugin"); wiki.unpackPluginTiddlers(); wiki.addIndexersToWiki(); - // Clear changes queue - wiki.clearTiddlerEventQueue(); // Install the plugin change event handler $tw.utils.installPluginChangeHandler(wiki); + // Install the language switcher + $tw.languageSwitcher = new $tw.PluginSwitcher({ + wiki: wiki, + pluginType: "language", + controllerTitle: "$:/language", + defaultPlugins: [ + "$:/languages/en-GB" + ] + }); + // Clear changes queue + wiki.clearTiddlerEventQueue(); // Complain if we don't have the ouput and expected results if(!wiki.tiddlerExists("Output")) { throw "Missing 'Output' tiddler"; @@ -53,8 +62,6 @@ describe("Wiki-based tests", function() { var widgetNode = createWidgetNode(parseText(text,wiki),wiki); // Render the widget node to the DOM var wrapper = renderWidgetNode(widgetNode); - // Clear changes queue - wiki.clearTiddlerEventQueue(); // Run the actions if provided if(wiki.tiddlerExists("Actions")) { widgetNode.invokeActionString(wiki.getTiddlerText("Actions")); @@ -65,7 +72,6 @@ describe("Wiki-based tests", function() { wiki.processOutstandingTiddlerEvents(); refreshWidgetNode(widgetNode,wrapper); } - wiki.processOutstandingTiddlerEvents(); // Test the rendering refreshWidgetNode(widgetNode,wrapper); expect(wrapper.innerHTML).toBe(wiki.getTiddlerText("ExpectedResult")); From 4e3d07b16afc4d4b9375f4e070b9e2f559a89e57 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Sat, 22 Feb 2025 10:19:57 +0000 Subject: [PATCH 11/21] Code typo --- core/modules/utils/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/utils/plugins.js b/core/modules/utils/plugins.js index 782d96b36..da8ef0ee3 100644 --- a/core/modules/utils/plugins.js +++ b/core/modules/utils/plugins.js @@ -25,7 +25,7 @@ exports.installPluginChangeHandler = function(wiki) { var tiddler = wiki.getTiddler(title); if(tiddler) { // It is a plugin that has been added or modified and is of a type that we need to register - if(tiddler.isPlugin() && $tw.wiki.getTiddlerText(PREFIX_CONFIG_REGISTER_PLUGIN_TYPE + (tiddler.fields["plugin-type"] || ""),"no") === "yes") { + if(tiddler.isPlugin() && wiki.getTiddlerText(PREFIX_CONFIG_REGISTER_PLUGIN_TYPE + (tiddler.fields["plugin-type"] || ""),"no") === "yes") { changesToProcess.push(title); } } else { From 26b2768294a00717c372822208f4442f4df15c98 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Wed, 26 Feb 2025 17:18:37 +0000 Subject: [PATCH 12/21] Fix testcase export to include plugins --- core/ui/TestCases/actions/Export.tid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/TestCases/actions/Export.tid b/core/ui/TestCases/actions/Export.tid index 79c6eb93a..9a2da64df 100644 --- a/core/ui/TestCases/actions/Export.tid +++ b/core/ui/TestCases/actions/Export.tid @@ -1,4 +1,4 @@ title: $:/core/ui/testcases/actions/Export tags: $:/tags/TestCase/Actions -<$macrocall $name="exportButton" exportFilter="[all[tiddlers]sort[]] -[prefix[$:/state/]] -Description -Narrative -ExpectedResult -[has[plugin-type]]" lingoBase="$:/language/Buttons/ExportTiddlers/"/> \ No newline at end of file +<$macrocall $name="exportButton" exportFilter="[all[tiddlers]sort[]] -[prefix[$:/state/]] -Description -Narrative -ExpectedResult -[[$:/core]]" lingoBase="$:/language/Buttons/ExportTiddlers/"/> \ No newline at end of file From 41cd335cf40558e56d2850ecef7dee2ecec80de1 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Wed, 26 Feb 2025 17:20:03 +0000 Subject: [PATCH 13/21] Add test for dynamically loading a language plugin --- core/modules/pluginswitcher.js | 31 ++++++++++-- core/modules/widgets/testcase.js | 24 +++++++++- core/ui/TestCases/DefaultTemplate.tid | 3 +- .../data/plugins/DynamicLoadLanguage.tid | 47 +++++++++++++++++++ .../jasmine/run-wiki-based-tests.js | 18 +++---- 5 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/plugins/DynamicLoadLanguage.tid diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js index 71d9f3b03..9a7a7a351 100644 --- a/core/modules/pluginswitcher.js +++ b/core/modules/pluginswitcher.js @@ -38,6 +38,7 @@ function PluginSwitcher(options) { } PluginSwitcher.prototype.switchPlugins = function() { + var self = this; // Get the name of the current theme var selectedPluginTitle = this.wiki.getTiddlerText(this.controllerTitle); // If it doesn't exist, then fallback to one of the default themes @@ -61,13 +62,37 @@ PluginSwitcher.prototype.switchPlugins = function() { }; accumulatePlugin(selectedPluginTitle); // Read the plugin info for the incoming plugins - var changes = this.wiki.readPluginInfo(plugins); + var changedPluginInfo = this.wiki.readPluginInfo(plugins); + // Collect the shadow tiddlers of any deleted plugins + var changedShadowTiddlers = {}; + $tw.utils.each(changedPluginInfo.deletedPlugins,function(pluginTitle) { + var contents = changedPluginInfo.deletedPluginContents[pluginTitle]; + if(contents && contents.tiddlers) { + $tw.utils.each(Object.keys(contents.tiddlers),function(title) { + changedShadowTiddlers[title] = true; + }); + } + }); + // Collect the shadow tiddlers of any modified plugins + $tw.utils.each(changedPluginInfo.modifiedPlugins,function(pluginTitle) { + var pluginInfo = self.wiki.getPluginInfo(pluginTitle); + if(pluginInfo && pluginInfo.tiddlers) { + $tw.utils.each(Object.keys(pluginInfo.tiddlers),function(title) { + changedShadowTiddlers[title] = false; + }); + } + }); // Unregister any existing theme tiddlers var unregisteredTiddlers = this.wiki.unregisterPluginTiddlers(this.pluginType); // Register any new theme tiddlers - var registeredTiddlers = this.wiki.registerPluginTiddlers(this.pluginType,plugins,plugins[0] === "$:/languages/fr-FR"); + var registeredTiddlers = this.wiki.registerPluginTiddlers(this.pluginType,plugins); // Unpack the current theme tiddlers - this.wiki.unpackPluginTiddlers(); + this.wiki.unpackPluginTiddlers(this.doDebug); + // Queue change events for the changed shadow tiddlers + $tw.utils.each(changedShadowTiddlers,function(status,title) { + self.wiki.enqueueTiddlerEvent(title,changedShadowTiddlers[title], true); + }); + // Call the switch handler if(this.onSwitch) { this.onSwitch(plugins); diff --git a/core/modules/widgets/testcase.js b/core/modules/widgets/testcase.js index ad1905279..f04a7f75d 100644 --- a/core/modules/widgets/testcase.js +++ b/core/modules/widgets/testcase.js @@ -69,11 +69,24 @@ TestCaseWidget.prototype.render = function(parent,nextSibling) { }); var jsonPayload = JSON.stringify(tiddlers); this.testcaseWiki.addTiddlers(tiddlers); + // Suppress next tick dispatch for wiki change events + this.testcaseWiki.setDispatchMode(true); // Unpack plugin tiddlers this.testcaseWiki.readPluginInfo(); this.testcaseWiki.registerPluginTiddlers("plugin"); this.testcaseWiki.unpackPluginTiddlers(); this.testcaseWiki.addIndexersToWiki(); + // Install the plugin change event handler + $tw.utils.installPluginChangeHandler(this.testcaseWiki); + // Install the language switcher + var languageSwitcher = new $tw.PluginSwitcher({ + wiki: this.testcaseWiki, + pluginType: "language", + controllerTitle: "$:/language", + defaultPlugins: [ + "$:/languages/en-GB" + ] + }); // Generate a `transclusion` variable that depends on the values of the payload tiddlers so that the template can easily make unique state tiddlers this.setVariable("transclusion",$tw.utils.hashString(jsonPayload)); // Generate a `payloadTiddlers` variable that contains the payload in JSON format @@ -95,13 +108,20 @@ TestCaseWidget.prototype.render = function(parent,nextSibling) { } }); testcaseOutputWidget.render(testcaseOutputContainer); + // Install the wiki change event handler + this.testcaseWiki.addEventListener("change",function(changes) { + testcaseOutputWidget.refresh(changes,testcaseOutputContainer); + }); } // Clear changes queue this.testcaseWiki.clearTiddlerEventQueue(); // Run the actions if provided if(this.testcaseWiki.tiddlerExists(this.testcaseTestActions)) { testcaseOutputWidget.invokeActionString(this.testcaseWiki.getTiddlerText(this.testcaseTestActions)); - testcaseOutputWidget.refresh(this.testcaseWiki.changedTiddlers,testcaseOutputContainer); + // Make sure all wiki events have been cleared + while(this.testcaseWiki.eventsTriggered) { + this.testcaseWiki.processOutstandingTiddlerEvents(); + } } // Set up the test result variables var testResult = "", @@ -131,6 +151,8 @@ TestCaseWidget.prototype.render = function(parent,nextSibling) { parentWidget: this }); rootWidget.render(domNode); + // Re-enable next tick dispatch for wiki change events + this.testcaseWiki.setDispatchMode(false); // Trap changes in the wiki and refresh the rendering this.testcaseWiki.addEventListener("change",function(changes) { rootWidget.refresh(changes,domNode); diff --git a/core/ui/TestCases/DefaultTemplate.tid b/core/ui/TestCases/DefaultTemplate.tid index 2967a5157..db5aa0472 100644 --- a/core/ui/TestCases/DefaultTemplate.tid +++ b/core/ui/TestCases/DefaultTemplate.tid @@ -103,7 +103,8 @@ code-body: yes -Description -Narrative -[[$:/temp/testcase/draft-title]] --[has[plugin-type]] +-[[$:/status/RequireReloadDueToPluginChange]] +-[[$:/core]] -[prefix<tf.state>] -[prefix[$:/state/popup/export]] -[prefix[$:/HistoryList]] diff --git a/editions/test/tiddlers/tests/data/plugins/DynamicLoadLanguage.tid b/editions/test/tiddlers/tests/data/plugins/DynamicLoadLanguage.tid new file mode 100644 index 000000000..742f6aa17 --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/DynamicLoadLanguage.tid @@ -0,0 +1,47 @@ +title: Plugins/DynamicLoadLanguage +description: Dynamically loading of language plugin +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{First}} ++ +title: Actions + +<$action-createtiddler $basetitle="$:/languages/fr-FR" $template="$:/languages/fr-FR/raw" type="application/json"/> +<$action-setfield $tiddler="$:/languages/fr-FR" plugin-type="language"/> +<$action-setfield $tiddler="$:/language" text="$:/languages/fr-FR"/> ++ +title: $:/languages/en-GB +list: readme +name: en-GB +plugin-type: language +type: application/json + +{ + "tiddlers": { + "First": { + "title": "First", + "text": "This is English" + } + } +} ++ +title: $:/languages/fr-FR/raw +list: readme +name: fr-FR +type: plain/text + +{ + "tiddlers": { + "First": { + "title": "First", + "text": "This is French" + } + } +} ++ +title: ExpectedResult + +<p>This is French</p> \ No newline at end of file diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js index 536c51966..a14707045 100644 --- a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -39,10 +39,8 @@ describe("Wiki-based tests", function() { wiki.registerPluginTiddlers("plugin"); wiki.unpackPluginTiddlers(); wiki.addIndexersToWiki(); - // Install the plugin change event handler - $tw.utils.installPluginChangeHandler(wiki); // Install the language switcher - $tw.languageSwitcher = new $tw.PluginSwitcher({ + var languageSwitcher = new $tw.PluginSwitcher({ wiki: wiki, pluginType: "language", controllerTitle: "$:/language", @@ -50,6 +48,8 @@ describe("Wiki-based tests", function() { "$:/languages/en-GB" ] }); + // Install the plugin change event handler + $tw.utils.installPluginChangeHandler(wiki,doDebug); // Clear changes queue wiki.clearTiddlerEventQueue(); // Complain if we don't have the ouput and expected results @@ -62,18 +62,19 @@ describe("Wiki-based tests", function() { var widgetNode = createWidgetNode(parseText(text,wiki),wiki); // Render the widget node to the DOM var wrapper = renderWidgetNode(widgetNode); + // Install the wiki change event handler + wiki.addEventListener("change",function(changes) { + widgetNode.refresh(changes,wrapper); + }); // Run the actions if provided if(wiki.tiddlerExists("Actions")) { widgetNode.invokeActionString(wiki.getTiddlerText("Actions")); - refreshWidgetNode(widgetNode,wrapper); } // Make sure all wiki events have been cleared while(wiki.eventsTriggered) { wiki.processOutstandingTiddlerEvents(); - refreshWidgetNode(widgetNode,wrapper); } // Test the rendering - refreshWidgetNode(widgetNode,wrapper); expect(wrapper.innerHTML).toBe(wiki.getTiddlerText("ExpectedResult")); } }); @@ -112,11 +113,6 @@ describe("Wiki-based tests", function() { return wrapper; } - function refreshWidgetNode(widgetNode,wrapper) { - widgetNode.refresh(widgetNode.wiki.changedTiddlers,wrapper); -// console.log(require("util").inspect(wrapper,{depth: 8})); - } - }); })(); From 094428b1abfb079b72eadd965c44dc549d024c8d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Wed, 26 Feb 2025 18:07:39 +0000 Subject: [PATCH 14/21] Remove debug code --- plugins/tiddlywiki/jasmine/run-wiki-based-tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js index a14707045..4654dddc9 100644 --- a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -49,7 +49,7 @@ describe("Wiki-based tests", function() { ] }); // Install the plugin change event handler - $tw.utils.installPluginChangeHandler(wiki,doDebug); + $tw.utils.installPluginChangeHandler(wiki); // Clear changes queue wiki.clearTiddlerEventQueue(); // Complain if we don't have the ouput and expected results From 752c5cfbda7d557adeddfce9dbf1c150ba85564c Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Wed, 26 Feb 2025 20:01:32 +0000 Subject: [PATCH 15/21] Cleaning up --- core/modules/pluginswitcher.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js index 9a7a7a351..2dd47265c 100644 --- a/core/modules/pluginswitcher.js +++ b/core/modules/pluginswitcher.js @@ -53,8 +53,7 @@ PluginSwitcher.prototype.switchPlugins = function() { var tiddler = self.wiki.getTiddler(title); if(tiddler && tiddler.isPlugin() && plugins.indexOf(title) === -1) { plugins.push(title); - var pluginInfo = $tw.utils.parseJSONSafe(self.wiki.getTiddlerText(title)), - dependents = $tw.utils.parseStringArray(tiddler.fields.dependents || ""); + var dependents = $tw.utils.parseStringArray(tiddler.fields.dependents || ""); $tw.utils.each(dependents,function(title) { accumulatePlugin(title); }); @@ -82,17 +81,16 @@ PluginSwitcher.prototype.switchPlugins = function() { }); } }); - // Unregister any existing theme tiddlers + // Unregister any existing theme/language tiddlers var unregisteredTiddlers = this.wiki.unregisterPluginTiddlers(this.pluginType); - // Register any new theme tiddlers + // Register any new theme/language tiddlers var registeredTiddlers = this.wiki.registerPluginTiddlers(this.pluginType,plugins); - // Unpack the current theme tiddlers + // Unpack the current theme/language tiddlers this.wiki.unpackPluginTiddlers(this.doDebug); // Queue change events for the changed shadow tiddlers $tw.utils.each(changedShadowTiddlers,function(status,title) { self.wiki.enqueueTiddlerEvent(title,changedShadowTiddlers[title], true); }); - // Call the switch handler if(this.onSwitch) { this.onSwitch(plugins); From e01c428a17c8ef2ff3c0a1936c055b684ae907ce Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Wed, 26 Feb 2025 21:17:53 +0000 Subject: [PATCH 16/21] Add support for add-on language and theme plugins --- core/modules/pluginswitcher.js | 8 ++- .../data/plugins/LanguageAddonSubPlugin.tid | 57 +++++++++++++++++++ .../tests/data/plugins/LanguageSubPlugin.tid | 45 +++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 editions/test/tiddlers/tests/data/plugins/LanguageAddonSubPlugin.tid create mode 100644 editions/test/tiddlers/tests/data/plugins/LanguageSubPlugin.tid diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js index 2dd47265c..8d80032ab 100644 --- a/core/modules/pluginswitcher.js +++ b/core/modules/pluginswitcher.js @@ -60,6 +60,12 @@ PluginSwitcher.prototype.switchPlugins = function() { } }; accumulatePlugin(selectedPluginTitle); + var selectedPluginTiddler = this.wiki.getTiddler(selectedPluginTitle); + this.wiki.eachTiddlerPlusShadows(function(tiddler,title) { + if(tiddler.isPlugin() && tiddler.fields["plugin-type"] === self.pluginType && tiddler.fields.name === selectedPluginTiddler.fields.name) { + accumulatePlugin(title); + } + }); // Read the plugin info for the incoming plugins var changedPluginInfo = this.wiki.readPluginInfo(plugins); // Collect the shadow tiddlers of any deleted plugins @@ -86,7 +92,7 @@ PluginSwitcher.prototype.switchPlugins = function() { // Register any new theme/language tiddlers var registeredTiddlers = this.wiki.registerPluginTiddlers(this.pluginType,plugins); // Unpack the current theme/language tiddlers - this.wiki.unpackPluginTiddlers(this.doDebug); + this.wiki.unpackPluginTiddlers(); // Queue change events for the changed shadow tiddlers $tw.utils.each(changedShadowTiddlers,function(status,title) { self.wiki.enqueueTiddlerEvent(title,changedShadowTiddlers[title], true); diff --git a/editions/test/tiddlers/tests/data/plugins/LanguageAddonSubPlugin.tid b/editions/test/tiddlers/tests/data/plugins/LanguageAddonSubPlugin.tid new file mode 100644 index 000000000..868bc98b6 --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/LanguageAddonSubPlugin.tid @@ -0,0 +1,57 @@ +title: Plugins/LanguageAddonSubPlugin +description: Loading of correct language subplugin at startup +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{First}} + +{{Second}} ++ +title: $:/language + +$:/languages/fr-FR-subplugin ++ +title: $:/languages/fr-FR-container +list: readme +name: fr-FR +plugin-type: plugin +type: application/json + +{ + "tiddlers": { + "$:/languages/fr-FR-container/readme": { + "title": "$:/languages/fr-FR-container/readme", + "text": "Readme from ~$:/languages/fr-FR-container\n\n" + }, + "$:/languages/fr-FR-subplugin": { + "title": "$:/languages/fr-FR-subplugin", + "name": "fr-FR", + "description": "fr-FR subplugin", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL", + "version": "5.3.7-prerelease", + "plugin-type": "language", + "dependents": "", + "type": "application/json", + "text": "{\"tiddlers\":{\"First\":{\"title\":\"First\",\"text\":\"First from ~$:/languages/fr-FR-subplugin\"}}}" + }, + "$:/languages/fr-FR-subplugin2": { + "title": "$:/languages/fr-FR-subplugin2", + "name": "fr-FR", + "description": "fr-FR subplugin2", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL", + "version": "5.3.7-prerelease", + "plugin-type": "language", + "dependents": "", + "type": "application/json", + "text": "{\"tiddlers\":{\"Second\":{\"title\":\"Second\",\"text\":\"Second from ~$:/languages/fr-FR-subplugin2\"}}}" + } + } +} ++ +title: ExpectedResult + +<p>First from $:/languages/fr-FR-subplugin</p><p>Second from $:/languages/fr-FR-subplugin2</p> \ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/plugins/LanguageSubPlugin.tid b/editions/test/tiddlers/tests/data/plugins/LanguageSubPlugin.tid new file mode 100644 index 000000000..77cdb88a3 --- /dev/null +++ b/editions/test/tiddlers/tests/data/plugins/LanguageSubPlugin.tid @@ -0,0 +1,45 @@ +title: Plugins/LanguageSubPlugin +description: Loading of correct language subplugin at startup +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +{{First}} + +{{$:/languages/fr-FR-container/readme}} ++ +title: $:/language + +$:/languages/fr-FR-subplugin ++ +title: $:/languages/fr-FR-container +list: readme +name: fr-FR +plugin-type: plugin +type: application/json + +{ + "tiddlers": { + "$:/languages/fr-FR-container/readme": { + "title": "$:/languages/fr-FR-container/readme", + "text": "Readme from ~$:/languages/fr-FR-container\n\n" + }, + "$:/languages/fr-FR-subplugin": { + "title": "$:/languages/fr-FR-subplugin", + "name": "fr-FR", + "description": "fr-FR subplugin", + "list": "readme", + "stability": "STABILITY_1_EXPERIMENTAL", + "version": "5.3.7-prerelease", + "plugin-type": "language", + "dependents": "", + "type": "application/json", + "text": "{\"tiddlers\":{\"First\":{\"title\":\"First\",\"text\":\"First from ~$:/languages/fr-FR-subplugin\"}}}" + } + } +} ++ +title: ExpectedResult + +<p>First from $:/languages/fr-FR-subplugin</p><p>Readme from $:/languages/fr-FR-container</p> \ No newline at end of file From 378b9b3627cab368ed216cf5eb856370cfb4b9b0 Mon Sep 17 00:00:00 2001 From: Leilei332 <LeiYiXia29@outlook.com> Date: Thu, 27 Feb 2025 19:00:58 +0800 Subject: [PATCH 17/21] Allow building external core version of main site (#8953) --- bin/build-site.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/build-site.sh b/bin/build-site.sh index f1fe649e7..066d13d1f 100755 --- a/bin/build-site.sh +++ b/bin/build-site.sh @@ -105,6 +105,7 @@ node $TW5_BUILD_TIDDLYWIKI \ fi # /index.html Main site +# /external-(version).html External core version of main site # /favicon.ico Favicon for main site # /static.html Static rendering of default tiddlers # /alltiddlers.html Static rendering of all tiddlers @@ -117,7 +118,7 @@ node $TW5_BUILD_TIDDLYWIKI \ --version \ --load $TW5_BUILD_OUTPUT/build.tid \ --output $TW5_BUILD_OUTPUT \ - --build favicon static index \ + --build favicon static index external-js \ || exit 1 # /empty.html Empty From 9a2534fc5e8d032d0d774f42be89914e412258bc Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Thu, 27 Feb 2025 11:17:35 +0000 Subject: [PATCH 18/21] Fix accidentally disabled test --- .../tests/data/subplugins/DynamicSubPluginUnload.tid | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid index d91db8938..e6a2a8ca7 100644 --- a/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid +++ b/editions/test/tiddlers/tests/data/subplugins/DynamicSubPluginUnload.tid @@ -1,11 +1,13 @@ title: SubPlugins/DynamicSubPluginUnload description: Dynamic sub-plugin unloading type: text/vnd.tiddlywiki-multiple -tags: [[$:/tags/wiki-test-specx]] +tags: [[$:/tags/wiki-test-spec]] title: Output -{{$:/plugins/tiddlywiki/bundled-subplugin-tests/readme}} +<$transclude $tiddler="$:/plugins/tiddlywiki/test-subplugin/readme"> +NO +</$transclude> + title: Actions @@ -40,4 +42,6 @@ type: application/json + title: ExpectedResult -. \ No newline at end of file +<p> +NO +</p> \ No newline at end of file From a4e8308e758732de3ea598045696623c78c52c68 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Thu, 27 Feb 2025 12:48:07 +0000 Subject: [PATCH 19/21] Add comment --- core/modules/pluginswitcher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js index 8d80032ab..f8b759e5a 100644 --- a/core/modules/pluginswitcher.js +++ b/core/modules/pluginswitcher.js @@ -61,6 +61,7 @@ PluginSwitcher.prototype.switchPlugins = function() { }; accumulatePlugin(selectedPluginTitle); var selectedPluginTiddler = this.wiki.getTiddler(selectedPluginTitle); + // Accumulate any other plugins of the same type with the same name this.wiki.eachTiddlerPlusShadows(function(tiddler,title) { if(tiddler.isPlugin() && tiddler.fields["plugin-type"] === self.pluginType && tiddler.fields.name === selectedPluginTiddler.fields.name) { accumulatePlugin(title); From 12ced06d1df64827cb00718cd714fd14452c0540 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Thu, 27 Feb 2025 12:49:24 +0000 Subject: [PATCH 20/21] Add subplugins for codemirror languages The French translation is just for demo purposes. It would be helpful to replace it with some real translations. --- core/language/en-GB.tid | 4 ++- editions/tw5.com/tiddlywiki.info | 2 ++ .../en-GB}/config-language.multids | 0 .../codemirror/language/en-GB/plugin.info | 7 +++++ .../codemirror/language/en-GB/readme.tid | 3 ++ .../language/fr-FR/config-language.multids | 29 +++++++++++++++++++ .../codemirror/language/fr-FR/plugin.info | 7 +++++ .../codemirror/language/fr-FR/readme.tid | 3 ++ 8 files changed, 54 insertions(+), 1 deletion(-) rename plugins/tiddlywiki/codemirror/{ => language/en-GB}/config-language.multids (100%) create mode 100644 plugins/tiddlywiki/codemirror/language/en-GB/plugin.info create mode 100644 plugins/tiddlywiki/codemirror/language/en-GB/readme.tid create mode 100644 plugins/tiddlywiki/codemirror/language/fr-FR/config-language.multids create mode 100644 plugins/tiddlywiki/codemirror/language/fr-FR/plugin.info create mode 100644 plugins/tiddlywiki/codemirror/language/fr-FR/readme.tid diff --git a/core/language/en-GB.tid b/core/language/en-GB.tid index 08e2e4093..aad01e0f4 100644 --- a/core/language/en-GB.tid +++ b/core/language/en-GB.tid @@ -3,5 +3,7 @@ name: en-GB description: English (British) author: JeremyRuston core-version: >=5.0.0" +plugin-type: language +type: application/json -Stub pseudo-plugin for the default language \ No newline at end of file +{tidddlers:{}} \ No newline at end of file diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index 5b547c582..d897c897b 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -8,6 +8,7 @@ "tiddlywiki/confetti", "tiddlywiki/dynannotate", "tiddlywiki/tour", + "tiddlywiki/codemirror", "tiddlywiki/bundled-subplugin-tests" ], "themes": [ @@ -22,6 +23,7 @@ "tiddlywiki/readonly" ], "languages": [ + "fr-FR" ], "build": { "index": [ diff --git a/plugins/tiddlywiki/codemirror/config-language.multids b/plugins/tiddlywiki/codemirror/language/en-GB/config-language.multids similarity index 100% rename from plugins/tiddlywiki/codemirror/config-language.multids rename to plugins/tiddlywiki/codemirror/language/en-GB/config-language.multids diff --git a/plugins/tiddlywiki/codemirror/language/en-GB/plugin.info b/plugins/tiddlywiki/codemirror/language/en-GB/plugin.info new file mode 100644 index 000000000..beb496b50 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/language/en-GB/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/language/codemirror/en-GB", + "name": "en-GB", + "plugin-type": "language", + "description": "English (UK) translations for CodeMirror", + "list": "readme" +} diff --git a/plugins/tiddlywiki/codemirror/language/en-GB/readme.tid b/plugins/tiddlywiki/codemirror/language/en-GB/readme.tid new file mode 100644 index 000000000..4f7688d3a --- /dev/null +++ b/plugins/tiddlywiki/codemirror/language/en-GB/readme.tid @@ -0,0 +1,3 @@ +title: $:/language/codemirror/en-GB/readme + +English (UK) translations for CodeMirror \ No newline at end of file diff --git a/plugins/tiddlywiki/codemirror/language/fr-FR/config-language.multids b/plugins/tiddlywiki/codemirror/language/fr-FR/config-language.multids new file mode 100644 index 000000000..c23df0c11 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/language/fr-FR/config-language.multids @@ -0,0 +1,29 @@ +title: $:/language/codemirror/ + +homeUrl: http://codemirror.net +addOnUrl: http://codemirror.net/doc/manual.html#addons +configUrl: http://codemirror.net/doc/manual.html#config +controlPanel/hint: Tʜᴇꜱᴇ ꜱᴇᴛᴛɪɴɢꜱ ʟᴇᴛ ʏᴏᴜ ᴄᴜꜱᴛᴏᴍɪꜱᴇ ᴛʜᴇ ʙᴇʜᴀᴠɪᴏᴜʀ ᴏꜰ [[CᴏᴅᴇMɪʀʀᴏʀ|$:/plugins/tiddlywiki/codemirror]]. +controlPanel/keyboard: Kᴇʏʙᴏᴀʀᴅ ꜱʜᴏʀᴛᴄᴜᴛꜱ +controlPanel/usage: Uꜱᴀɢᴇ ɪɴꜰᴏʀᴍᴀᴛɪᴏɴ +cursorBlinkRate/hint: Cᴜʀꜱᴏʀ ʙʟɪɴᴋ ʀᴀᴛᴇ +editorFont/hint: Eᴅɪᴛᴏʀ ꜰᴏɴᴛ ꜰᴀᴍɪʟʏ +editorFont/info: Sᴇᴛ ᴛʜᴇ ꜰᴏɴᴛ ꜰᴀᴍɪʟʏ ꜰᴏʀ ᴛʜᴇ ~CᴏᴅᴇMɪʀʀᴏʀ ᴛᴇxᴛ-ᴇᴅɪᴛᴏʀ +indentUnit/hint: Hᴏᴡ ᴍᴀɴʏ ꜱᴘᴀᴄᴇꜱ ᴀ ʙʟᴏᴄᴋ ꜱʜᴏᴜʟᴅ ʙᴇ ɪɴᴅᴇɴᴛᴇᴅ +indentWithTabs/hint: Eɴᴀʙʟᴇ ɪɴᴅᴇɴᴛɪɴɢ ᴡɪᴛʜ ᴛᴀʙꜱ +indentWithTabs/info: Wʜᴇᴛʜᴇʀ, ᴡʜᴇɴ ɪɴᴅᴇɴᴛɪɴɢ, ᴛʜᴇ ꜰɪʀꜱᴛ N*`ᴛᴀʙSɪᴢᴇ` ꜱᴘᴀᴄᴇꜱ ꜱʜᴏᴜʟᴅ ʙᴇ ʀᴇᴘʟᴀᴄᴇᴅ ʙʏ N ᴛᴀʙꜱ. +keyMap/hint: ~CᴏᴅᴇMɪʀʀᴏʀ ᴋᴇʏᴍᴀᴘ +keyMap/info: ~Tʜᴇ Kᴇʏʙᴏᴀʀᴅ KᴇʏMᴀᴘ ᴜꜱᴇᴅ ᴡɪᴛʜɪɴ ᴛʜᴇ ~CᴏᴅᴇMɪʀʀᴏʀ ᴛᴇxᴛ-ᴇᴅɪᴛᴏʀ +lineNumbers/hint: Eɴᴀʙʟᴇ ʟɪɴᴇ ɴᴜᴍʙᴇʀꜱ +lineNumbers/info: Wʜᴇᴛʜᴇʀ ᴛᴏ ꜱʜᴏᴡ ʟɪɴᴇ ɴᴜᴍʙᴇʀꜱ ᴛᴏ ᴛʜᴇ ʟᴇꜰᴛ ᴏꜰ ᴛʜᴇ ᴇᴅɪᴛᴏʀ. +lineWrapping/hint: Eɴᴀʙʟᴇ ʟɪɴᴇ ᴡʀᴀᴘᴘɪɴɢ +lineWrapping/info: Wʜᴇᴛʜᴇʀ CᴏᴅᴇMɪʀʀᴏʀ ꜱʜᴏᴜʟᴅ ꜱᴄʀᴏʟʟ ᴏʀ ᴡʀᴀᴘ ꜰᴏʀ ʟᴏɴɢ ʟɪɴᴇꜱ. Dᴇꜰᴀᴜʟᴛꜱ ᴛᴏ `ꜰᴀʟꜱᴇ` (ꜱᴄʀᴏʟʟ). +showCursorWhenSelecting/hint: Sʜᴏᴡ ᴄᴜʀꜱᴏʀ, ᴡʜᴇɴ ꜱᴇʟᴇᴄᴛɪɴɢ +showCursorWhenSelecting/info: Wʜᴇᴛʜᴇʀ ᴛʜᴇ ᴄᴜʀꜱᴏʀ ꜱʜᴏᴜʟᴅ ʙᴇ ᴅʀᴀᴡɴ ᴡʜᴇɴ ᴀ ꜱᴇʟᴇᴄᴛɪᴏɴ ɪꜱ ᴀᴄᴛɪᴠᴇ. +smartIndent/hint: Eɴᴀʙʟᴇ ꜱᴍᴀʀᴛ ɪɴᴅᴇɴᴛ +smartIndent/info: Wʜᴇᴛʜᴇʀ ᴛᴏ ᴜꜱᴇ ᴛʜᴇ ᴄᴏɴᴛᴇxᴛ-ꜱᴇɴꜱɪᴛɪᴠᴇ ɪɴᴅᴇɴᴛᴀᴛɪᴏɴ ᴛʜᴀᴛ ᴛʜᴇ ᴍᴏᴅᴇ ᴘʀᴏᴠɪᴅᴇꜱ (ᴏʀ ᴊᴜꜱᴛ ɪɴᴅᴇɴᴛ ᴛʜᴇ ꜱᴀᴍᴇ ᴀꜱ ᴛʜᴇ ʟɪɴᴇ ʙᴇꜰᴏʀᴇ). Dᴇꜰᴀᴜʟᴛꜱ ᴛᴏ `ᴛʀᴜᴇ`. +styleActiveLine/hint: Hɪɢʜʟɪɢʜᴛ ᴀᴄᴛɪᴠᴇ ʟɪɴᴇ +styleActiveLine/info: Wʜᴇᴛʜᴇʀ ᴏʀ ɴᴏᴛ ᴛᴏ ʜɪɢʜʟɪɢʜᴛ ᴛʜᴇ ᴀᴄᴛɪᴠᴇ ᴛᴇxᴛ-ᴇᴅɪᴛᴏʀ ʟɪɴᴇ +tabSize/hint: Wɪᴅᴛʜ ᴏꜰ ᴀ ᴛᴀʙ ᴄʜᴀʀᴀᴄᴛᴇʀ +theme/hint: Sᴇʟᴇᴄᴛ ᴀ ᴛʜᴇᴍᴇ +theme/info: Cʜᴏᴏꜱᴇ ʙᴇᴛᴡᴇᴇɴ ~CᴏᴅᴇMɪʀʀᴏʀ ᴛʜᴇᴍᴇꜱ diff --git a/plugins/tiddlywiki/codemirror/language/fr-FR/plugin.info b/plugins/tiddlywiki/codemirror/language/fr-FR/plugin.info new file mode 100644 index 000000000..9f6d8fa12 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/language/fr-FR/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/language/codemirror/fr-FR", + "name": "fr-FR", + "plugin-type": "language", + "description": "French translations for CodeMirror", + "list": "readme" +} diff --git a/plugins/tiddlywiki/codemirror/language/fr-FR/readme.tid b/plugins/tiddlywiki/codemirror/language/fr-FR/readme.tid new file mode 100644 index 000000000..d01ce4b38 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/language/fr-FR/readme.tid @@ -0,0 +1,3 @@ +title: $:/language/codemirror/fr-FR/readme + +French translations for CodeMirror \ No newline at end of file From 9dadd3fafb2ac05c05712d41fa302a0ac169e170 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Thu, 27 Feb 2025 16:54:32 +0000 Subject: [PATCH 21/21] Display subplugins in control panel listing --- core/language/en-GB.tid | 1 + core/modules/filters/is/plugin.js | 36 +++++++++++++++++++ core/ui/ControlPanel/Plugins.tid | 2 +- .../Plugins/Installed/Languages.tid | 2 +- .../Plugins/Installed/Plugins.tid | 2 +- .../ControlPanel/Plugins/Installed/Themes.tid | 2 +- 6 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 core/modules/filters/is/plugin.js diff --git a/core/language/en-GB.tid b/core/language/en-GB.tid index aad01e0f4..e797f7eac 100644 --- a/core/language/en-GB.tid +++ b/core/language/en-GB.tid @@ -5,5 +5,6 @@ author: JeremyRuston core-version: >=5.0.0" plugin-type: language type: application/json +hidden: yes {tidddlers:{}} \ No newline at end of file diff --git a/core/modules/filters/is/plugin.js b/core/modules/filters/is/plugin.js new file mode 100644 index 000000000..fe2b962ce --- /dev/null +++ b/core/modules/filters/is/plugin.js @@ -0,0 +1,36 @@ +/*\ +title: $:/core/modules/filters/is/plugin.js +type: application/javascript +module-type: isfilteroperator + +Filter function for [is[plugin]] + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Export our filter function +*/ +exports.plugin = function(source,prefix,options) { + var results = []; + if(prefix === "!") { + source(function(tiddler,title) { + if(!tiddler.isPlugin()) { + results.push(title); + } + }); + } else { + source(function(tiddler,title) { + if(tiddler.isPlugin()) { + results.push(title); + } + }); + } + return results; +}; + +})(); diff --git a/core/ui/ControlPanel/Plugins.tid b/core/ui/ControlPanel/Plugins.tid index 995a6bdf0..e7ac3bbc5 100644 --- a/core/ui/ControlPanel/Plugins.tid +++ b/core/ui/ControlPanel/Plugins.tid @@ -8,7 +8,7 @@ caption: {{$:/language/ControlPanel/Plugins/Caption}} \whitespace trim <$set name="plugin-type" value="""$type$"""> <$set name="qualified-state" value=<<qualify "$:/state/plugin-info">>> -<$list filter="[!has[draft.of]plugin-type[$type$]sort[name]]" emptyMessage=<<lingo "Empty/Hint">> template="$:/core/ui/Components/plugin-info"/> +<$list filter="[all[tiddlers+shadows]!has[draft.of]plugin-type[$type$]!field:hidden[yes]sort[name]]" emptyMessage=<<lingo "Empty/Hint">> template="$:/core/ui/Components/plugin-info"/> </$set> </$set> \end diff --git a/core/ui/ControlPanel/Plugins/Installed/Languages.tid b/core/ui/ControlPanel/Plugins/Installed/Languages.tid index ddd5396ef..825d31b5c 100644 --- a/core/ui/ControlPanel/Plugins/Installed/Languages.tid +++ b/core/ui/ControlPanel/Plugins/Installed/Languages.tid @@ -1,5 +1,5 @@ title: $:/core/ui/ControlPanel/Plugins/Installed/Languages tags: $:/tags/ControlPanel/Plugins -caption: {{$:/language/ControlPanel/Plugins/Languages/Caption}} (<$count filter="[!has[draft.of]plugin-type[language]]"/>) +caption: {{$:/language/ControlPanel/Plugins/Languages/Caption}} (<$count filter="[all[tiddlers+shadows]is[plugin]!field:hidden[yes]plugin-type[language]]"/>) <<plugin-table language>> diff --git a/core/ui/ControlPanel/Plugins/Installed/Plugins.tid b/core/ui/ControlPanel/Plugins/Installed/Plugins.tid index 879f6cfaa..13053331d 100644 --- a/core/ui/ControlPanel/Plugins/Installed/Plugins.tid +++ b/core/ui/ControlPanel/Plugins/Installed/Plugins.tid @@ -1,5 +1,5 @@ title: $:/core/ui/ControlPanel/Plugins/Installed/Plugins tags: $:/tags/ControlPanel/Plugins -caption: {{$:/language/ControlPanel/Plugins/Plugins/Caption}} (<$count filter="[!has[draft.of]plugin-type[plugin]]"/>) +caption: {{$:/language/ControlPanel/Plugins/Plugins/Caption}} (<$count filter="[all[tiddlers+shadows]is[plugin]plugin-type[plugin]]"/>) <<plugin-table plugin>> diff --git a/core/ui/ControlPanel/Plugins/Installed/Themes.tid b/core/ui/ControlPanel/Plugins/Installed/Themes.tid index 6be33a236..f8eeb7693 100644 --- a/core/ui/ControlPanel/Plugins/Installed/Themes.tid +++ b/core/ui/ControlPanel/Plugins/Installed/Themes.tid @@ -1,5 +1,5 @@ title: $:/core/ui/ControlPanel/Plugins/Installed/Themes tags: $:/tags/ControlPanel/Plugins -caption: {{$:/language/ControlPanel/Plugins/Themes/Caption}} (<$count filter="[!has[draft.of]plugin-type[theme]]"/>) +caption: {{$:/language/ControlPanel/Plugins/Themes/Caption}} (<$count filter="[all[tiddlers+shadows]is[plugin]plugin-type[theme]]"/>) <<plugin-table theme>>