Introduce genesis widget (#6961)

* Initial Commit

* Fix version number

* Fix docs date
This commit is contained in:
Jeremy Ruston 2022-09-24 14:07:42 +01:00 committed by GitHub
parent dd66fcc759
commit 4e9267ea58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 288 additions and 2 deletions

View File

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

View File

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

View File

@ -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=<<myvar>>/>)</$genesis>
<$genesis $type="$let" $$myvar="Kitten">(<$text text=<<$myvar>>/>)</$genesis>
+
title: ExpectedResult
<p>(Kitten)(Kitten)</p>

View File

@ -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=<<myvar>>/>|<$text text=<<other>>/>)</$genesis>
<$genesis $type="$let" $names="$myvar $other" $values="Kitten Donkey" $$myvar="Shark">(<$text text=<<$myvar>>/>|<$text text=<<$other>>/>)</$genesis>
+
title: ExpectedResult
<p>(Kitten|Donkey)(Kitten|Donkey)</p>

View File

@ -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>
<$genesis $type="div" class="tc-thing" label="Squeak">Mouse</$genesis>
+
title: ExpectedResult
<p><div>Mouse</div><div class="tc-thing" label="Squeak">Mouse</div></p>

View File

@ -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</$genesis>'/>

View File

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