From 4e9267ea58c3081b9845dcebd96bbd25819d0143 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 24 Sep 2022 14:07:42 +0100 Subject: [PATCH] Introduce genesis widget (#6961) * Initial Commit * Fix version number * Fix docs date --- core/modules/utils/parsetree.js | 18 ++- core/modules/widgets/genesis.js | 108 ++++++++++++++++++ .../tests/data/genesis-widget/DollarSigns.tid | 14 +++ .../genesis-widget/MultipleAttributes.tid | 14 +++ .../tests/data/genesis-widget/Simple.tid | 14 +++ .../tiddlers/widgets/GenesisWidget.tid | 29 +++++ .../jasmine/run-wiki-based-tests.js | 93 +++++++++++++++ 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 core/modules/widgets/genesis.js create mode 100644 editions/test/tiddlers/tests/data/genesis-widget/DollarSigns.tid create mode 100644 editions/test/tiddlers/tests/data/genesis-widget/MultipleAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/genesis-widget/Simple.tid create mode 100644 editions/tw5.com/tiddlers/widgets/GenesisWidget.tid create mode 100644 plugins/tiddlywiki/jasmine/run-wiki-based-tests.js diff --git a/core/modules/utils/parsetree.js b/core/modules/utils/parsetree.js index e056b0fdd..5bab10706 100644 --- a/core/modules/utils/parsetree.js +++ b/core/modules/utils/parsetree.js @@ -12,12 +12,26 @@ Parse tree utility functions. /*global $tw: false */ "use strict"; +/* +Add attribute to parse tree node +Can be invoked as (node,name,value) or (node,attr) +*/ exports.addAttributeToParseTreeNode = function(node,name,value) { - var attribute = {name: name, type: "string", value: value}; + var attribute = typeof name === "object" ? name : {name: name, type: "string", value: value}; + name = attribute.name; node.attributes = node.attributes || {}; + node.orderedAttributes = node.orderedAttributes || []; node.attributes[name] = attribute; - if(node.orderedAttributes) { + var foundIndex = -1; + $tw.utils.each(node.orderedAttributes,function(attr,index) { + if(attr.name === name) { + foundIndex = index; + } + }); + if(foundIndex === -1) { node.orderedAttributes.push(attribute); + } else { + node.orderedAttributes[foundIndex] = attribute; } }; diff --git a/core/modules/widgets/genesis.js b/core/modules/widgets/genesis.js new file mode 100644 index 000000000..51544361e --- /dev/null +++ b/core/modules/widgets/genesis.js @@ -0,0 +1,108 @@ +/*\ +title: $:/core/modules/widgets/genesis.js +type: application/javascript +module-type: widget + +Genesis widget for dynamically creating widgets + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var GenesisWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +GenesisWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +GenesisWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes({filterFn: function(name) { + // Only compute our own attributes which start with a single dollar + return name.charAt(0) === "$" && name.charAt(1) !== "$"; + }}); + this.execute(); + this.renderChildren(parent,nextSibling); +}; + +/* +Compute the internal state of the widget +*/ +GenesisWidget.prototype.execute = function() { + var self = this; + // Collect attributes + this.genesisType = this.getAttribute("$type","element"); + this.genesisRemappable = this.getAttribute("$remappable","yes") === "yes"; + this.genesisNames = this.getAttribute("$names",""); + this.genesisValues = this.getAttribute("$values",""); + // Construct parse tree + var isElementWidget = this.genesisType.charAt(0) !== "$", + nodeType = isElementWidget ? "element" : this.genesisType.substr(1), + nodeTag = isElementWidget ? this.genesisType : undefined; + var parseTreeNodes = [{ + type: nodeType, + tag: nodeTag, + attributes: {}, + orderedAttributes: [], + children: this.parseTreeNode.children || [], + isNotRemappable: !this.genesisRemappable + }]; + // Apply explicit attributes + $tw.utils.each($tw.utils.getOrderedAttributesFromParseTreeNode(this.parseTreeNode),function(attribute) { + var name = attribute.name; + if(name.charAt(0) === "$") { + if(name.charAt(1) === "$") { + // Double $$ is changed to a single $ + name = name.substr(1); + } else { + // Single dollar is ignored + return; + } + } + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],$tw.utils.extend({},attribute,{name: name})); + }); + // Apply attributes in $names/$values + this.attributeNames = []; + this.attributeValues = []; + if(this.genesisNames && this.genesisValues) { + this.attributeNames = this.wiki.filterTiddlers(self.genesisNames,this); + this.attributeValues = this.wiki.filterTiddlers(self.genesisValues,this); + $tw.utils.each(this.attributeNames,function(varname,index) { + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],varname,self.attributeValues[index] || ""); + }); + } + // Construct the child widgets + this.makeChildWidgets(parseTreeNodes); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +GenesisWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(), + filterNames = this.getAttribute("$names",""), + filterValues = this.getAttribute("$values",""), + attributeNames = this.wiki.filterTiddlers(filterNames,this), + attributeValues = this.wiki.filterTiddlers(filterValues,this); + if($tw.utils.count(changedAttributes) > 0 || !$tw.utils.isArrayEqual(this.attributeNames,attributeNames) || !$tw.utils.isArrayEqual(this.attributeValues,attributeValues)) { + this.refreshSelf(); + return true; + } else { + return this.refreshChildren(changedTiddlers); + } +}; + +exports.genesis = GenesisWidget; + +})(); diff --git a/editions/test/tiddlers/tests/data/genesis-widget/DollarSigns.tid b/editions/test/tiddlers/tests/data/genesis-widget/DollarSigns.tid new file mode 100644 index 000000000..ac12bfe38 --- /dev/null +++ b/editions/test/tiddlers/tests/data/genesis-widget/DollarSigns.tid @@ -0,0 +1,14 @@ +title: Genesis/DollarSigns +description: Usage of genesis widget with attributes starting with dollar signs +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$genesis $type="$let" myvar="Kitten">(<$text text=<>/>) +<$genesis $type="$let" $$myvar="Kitten">(<$text text=<<$myvar>>/>) ++ +title: ExpectedResult + +

(Kitten)(Kitten)

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/genesis-widget/MultipleAttributes.tid b/editions/test/tiddlers/tests/data/genesis-widget/MultipleAttributes.tid new file mode 100644 index 000000000..8988c3cc7 --- /dev/null +++ b/editions/test/tiddlers/tests/data/genesis-widget/MultipleAttributes.tid @@ -0,0 +1,14 @@ +title: Genesis/MultipleAttributes +description: Usage of genesis widget with multiple attributes +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$genesis $type="$let" $names="myvar other" $values="Kitten Donkey" myvar={{{ Shark }}}>(<$text text=<>/>|<$text text=<>/>) +<$genesis $type="$let" $names="$myvar $other" $values="Kitten Donkey" $$myvar="Shark">(<$text text=<<$myvar>>/>|<$text text=<<$other>>/>) ++ +title: ExpectedResult + +

(Kitten|Donkey)(Kitten|Donkey)

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/genesis-widget/Simple.tid b/editions/test/tiddlers/tests/data/genesis-widget/Simple.tid new file mode 100644 index 000000000..d9ec67c00 --- /dev/null +++ b/editions/test/tiddlers/tests/data/genesis-widget/Simple.tid @@ -0,0 +1,14 @@ +title: Genesis/Simple +description: Simple usage of genesis widget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$genesis $type="div">Mouse +<$genesis $type="div" class="tc-thing" label="Squeak">Mouse ++ +title: ExpectedResult + +

Mouse
Mouse

\ No newline at end of file diff --git a/editions/tw5.com/tiddlers/widgets/GenesisWidget.tid b/editions/tw5.com/tiddlers/widgets/GenesisWidget.tid new file mode 100644 index 000000000..9b324e8fa --- /dev/null +++ b/editions/tw5.com/tiddlers/widgets/GenesisWidget.tid @@ -0,0 +1,29 @@ +caption: genesis +created: 20220924140702430 +modified: 20220924140702430 +tags: Widgets +title: GenesisWidget +type: text/vnd.tiddlywiki + +! Introduction + +<<.from-version "5.2.4">> The <<.wlink GenesisWidget>> widget allows the dynamic construction of another widget, where the name and attributes of the new widget can be dynamically determined, without needing to be known in advance. + +! Content and Attributes + +The content of the <<.wlink GenesisWidget>> widget is used as the content of the dynamically created widget. + +|!Attribute |!Description | +|$type |The type of widget or element to create (an initial `$` indicates a widget, otherwise an HTML element will be created) | +|$names |An optional filter evaluating to the names of a list of attributes to be applied to the widget | +|$values |An optional filter evaluating to the values corresponding to the list of names specified in `$names` | +|//{other attributes starting with $}// |Other attributes starting with a single dollar sign are reserved for future use | +|//{attributes starting with $$}// |Attributes starting with two dollar signs are appplied as attributes to the output widget, but with the attribute name changed to use a single dollar sign | +|//{attributes not starting with $}// |Any other attributes that do not start with a dollar are applied as attributes to the output widget | + +Note that attributes explicitly specified take precedence over attributes with the same name specified in the `$names` filter. + +! Examples + +<$macrocall $name='wikitext-example-without-html' +src='<$genesis $type="div" class="tc-thing" label="Squeak">Mouse'/> diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js new file mode 100644 index 000000000..5f28db061 --- /dev/null +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -0,0 +1,93 @@ +/*\ +title: $:/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests the wiki based tests + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var TEST_WIKI_TIDDLER_FILTER = "[type[text/vnd.tiddlywiki-multiple]tag[$:/tags/wiki-test-spec]]"; + +var widget = require("$:/core/modules/widgets/widget.js"); + +describe("Wiki-based tests", function() { + + // Step through the test tiddlers + var tests = $tw.wiki.filterTiddlers(TEST_WIKI_TIDDLER_FILTER); + $tw.utils.each(tests,function(title) { + var tiddler = $tw.wiki.getTiddler(title); + it(tiddler.fields.title + ": " + tiddler.fields.description, function() { + // Add our tiddlers + var wiki = new $tw.Wiki(); + wiki.addTiddlers(readMultipleTiddlersTiddler(title)); + // Complain if we don't have the ouput and expected results + if(!wiki.tiddlerExists("Output")) { + throw "Missing 'Output' tiddler"; + } + if(!wiki.tiddlerExists("ExpectedResult")) { + throw "Missing 'ExpectedResult' tiddler"; + } + // Construct the widget node + var text = "{{Output}}\n\n"; + 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")); + refreshWidgetNode(widgetNode,wrapper); + } + // Test the rendering + expect(wrapper.innerHTML).toBe(wiki.getTiddlerText("ExpectedResult")); + }); + }); + + function readMultipleTiddlersTiddler(title) { + var rawTiddlers = $tw.wiki.getTiddlerText(title).split("\n+\n"); + var tiddlers = []; + $tw.utils.each(rawTiddlers,function(rawTiddler) { + var fields = Object.create(null), + split = rawTiddler.split(/\r?\n\r?\n/mg); + if(split.length >= 1) { + fields = $tw.utils.parseFields(split[0],fields); + } + if(split.length >= 2) { + fields.text = split.slice(1).join("\n\n"); + } + tiddlers.push(fields); + }); + return tiddlers; + } + + function createWidgetNode(parser,wiki) { + return wiki.makeWidget(parser); + } + + function parseText(text,wiki,options) { + return wiki.parseText("text/vnd.tiddlywiki",text,options); + } + + function renderWidgetNode(widgetNode) { + $tw.fakeDocument.setSequenceNumber(0); + var wrapper = $tw.fakeDocument.createElement("div"); + widgetNode.render(wrapper,null); +// console.log(require("util").inspect(wrapper,{depth: 8})); + return wrapper; + } + + function refreshWidgetNode(widgetNode,wrapper) { + widgetNode.refresh(widgetNode.wiki.changedTiddlers,wrapper); +// console.log(require("util").inspect(wrapper,{depth: 8})); + } + +}); + +})();