Introduce refactored wiki parser and renderer

This is a half-way through a big refactoring of the parsing and
rendering infrastructure. The main change is to separate the parse and
render trees, which makes the code a lot cleaner. The new parser isn't
yet functional enough to replace the existing parser so for the moment
you have to manually invoke it with `$tw.testNewParser()` in your
browser console. I really ought to use branches for this kind of
thing...
This commit is contained in:
Jeremy Ruston
2012-12-13 21:34:31 +00:00
parent 916ca8eecf
commit d338a54370
28 changed files with 2837 additions and 33 deletions
@@ -0,0 +1,151 @@
/*\
title: $:/core/modules/rendertree/renderers/element.js
type: application/javascript
module-type: wikirenderer
Element renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Element renderer
*/
var ElementRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Compute our dependencies
this.dependencies = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var tr = $tw.utils.parseTextReference(attribute.textReference);
if(tr.title) {
self.dependencies[tr.title] = true;
} else {
self.dependencies[renderContext.tiddlerTitle] = true;
}
}
});
// Compute our attributes
this.computeAttributes();
// Create the renderers for the child nodes
this.children = this.renderTree.createRenderers(this.renderContext,this.parseTreeNode.children);
};
ElementRenderer.prototype.computeAttributes = function() {
this.attributes = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
self.attributes[name] = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle);
} else { // String attribute
self.attributes[name] = attribute.value;
}
});
};
ElementRenderer.prototype.render = function(type) {
var isHtml = type === "text/html",
output = [],attr,a,v;
if(isHtml) {
output.push("<",this.parseTreeNode.tag);
if(this.attributes) {
attr = [];
for(a in this.attributes) {
attr.push(a);
}
attr.sort();
for(a=0; a<attr.length; a++) {
v = this.attributes[attr[a]];
if(v !== undefined) {
if($tw.utils.isArray(v)) {
v = v.join(" ");
} else if(typeof v === "object") {
var s = [];
for(var p in v) {
s.push(p + ":" + v[p] + ";");
}
v = s.join("");
}
output.push(" ",attr[a],"='",$tw.utils.htmlEncode(v),"'");
}
}
}
if(!this.children || this.children.length === 0) {
output.push("/");
}
output.push(">");
}
if(this.children && this.children.length > 0) {
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
if(isHtml) {
output.push("</",this.parseTreeNode.tag,">");
}
}
return output.join("");
};
ElementRenderer.prototype.renderInDom = function() {
// Create the element
this.domNode = document.createElement(this.parseTreeNode.tag);
// Assign the attributes
this.assignAttributes();
// Render any child nodes
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.domNode.appendChild(node.renderInDom());
}
});
// Assign any specified event handlers
$tw.utils.addEventListeners(this.domNode,this.parseTreeNode.events);
// Return the dom node
return this.domNode;
};
ElementRenderer.prototype.assignAttributes = function() {
var self = this;
$tw.utils.each(this.attributes,function(v,a) {
if(v !== undefined) {
if($tw.utils.isArray(v)) { // Ahem, could there be arrays other than className?
self.domNode.className = v.join(" ");
} else if (typeof v === "object") { // ...or objects other than style?
for(var p in v) {
self.domNode.style[$tw.utils.unHyphenateCss(p)] = v[p];
}
} else {
self.domNode.setAttribute(a,v);
}
}
});
};
ElementRenderer.prototype.refreshInDom = function(changes) {
// Check if any of our dependencies have changed
if($tw.utils.checkDependencies(this.dependencies,changes)) {
// Update our attributes
this.computeAttributes();
this.assignAttributes();
}
// Refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changes);
}
});
};
exports.element = ElementRenderer
})();
@@ -0,0 +1,35 @@
/*\
title: $:/core/modules/rendertree/renderers/entity.js
type: application/javascript
module-type: wikirenderer
Entity renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Entity renderer
*/
var EntityRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
EntityRenderer.prototype.render = function(type) {
return type === "text/html" ? this.parseTreeNode.entity : $tw.utils.entityDecode(this.parseTreeNode.entity);
};
EntityRenderer.prototype.renderInDom = function() {
return document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
};
exports.entity = EntityRenderer
})();
@@ -0,0 +1,101 @@
/*\
title: $:/core/modules/rendertree/renderers/macrocall.js
type: application/javascript
module-type: wikirenderer
Macro call renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Macro call renderer
*/
var MacroCallRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Find the macro definition
var macro,childTree;
if($tw.utils.hop(this.renderTree.parser.macroDefinitions,this.parseTreeNode.name)) {
macro = this.renderTree.parser.macroDefinitions[this.parseTreeNode.name];
}
// Insert an error message if we couldn't find the macro
if(!macro) {
childTree = [{type: "text", text: "<<Undefined macro: " + this.parseTreeNode.name + ">>"}];
} else {
// Substitute the macro parameters
var text = this.substituteParameters(macro.text,this.parseTreeNode,macro);
// Parse the text
childTree = this.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",text).tree;
}
// Create the renderers for the child nodes
this.children = this.renderTree.createRenderers(this.renderContext,childTree);
};
/*
Expand the parameters in a block of text
*/
MacroCallRenderer.prototype.substituteParameters = function(text,macroCallParseTreeNode,macroDefinition) {
var nextAnonParameter = 0; // Next candidate anonymous parameter in macro call
// Step through each of the parameters in the macro definition
for(var p=0; p<macroDefinition.params.length; p++) {
// Check if we've got a macro call parameter with the same name
var paramInfo = macroDefinition.params[p],
paramValue;
for(var m=0; m<macroCallParseTreeNode.params.length; m++) {
if(macroCallParseTreeNode.params[m].name === paramInfo.name) {
paramValue = macroCallParseTreeNode.params[m].value;
}
}
// If not, use the next available anonymous macro call parameter
if(!paramValue && macroCallParseTreeNode.params.length > 0) {
while(macroCallParseTreeNode.params[nextAnonParameter].name && nextAnonParameter < macroCallParseTreeNode.params.length-1) {
nextAnonParameter++;
}
if(!macroCallParseTreeNode.params[nextAnonParameter].name) {
paramValue = macroCallParseTreeNode.params[nextAnonParameter].value;
nextAnonParameter++;
}
}
// If we've still not got a value, use the default, if any
paramValue = paramValue || paramInfo["default"] || "";
// Replace any instances of this parameter
text = text.replace(new RegExp("\\$" + $tw.utils.escapeRegExp(paramInfo.name) + "\\$","mg"),paramValue);
}
return text;
};
MacroCallRenderer.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
MacroCallRenderer.prototype.renderInDom = function() {
// Create the element
this.domNode = document.createElement("macrocall");
this.domNode.setAttribute("data-macro-name",this.parseTreeNode.name);
// Render any child nodes
var self = this;
$tw.utils.each(this.children,function(node,index) {
if(node.renderInDom) {
self.domNode.appendChild(node.renderInDom());
}
});
// Return the dom node
return this.domNode;
};
exports.macrocall = MacroCallRenderer
})();
+37
View File
@@ -0,0 +1,37 @@
/*\
title: $:/core/modules/rendertree/renderers/raw.js
type: application/javascript
module-type: wikirenderer
Raw HTML renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Raw HTML renderer
*/
var RawRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
RawRenderer.prototype.render = function(type) {
return this.parseTreeNode.html;
};
RawRenderer.prototype.renderInDom = function() {
var domNode = document.createElement("div");
domNode.innerHTML = this.parseTreeNode.html;
return domNode;
};
exports.raw = RawRenderer
})();
+35
View File
@@ -0,0 +1,35 @@
/*\
title: $:/core/modules/rendertree/renderers/text.js
type: application/javascript
module-type: wikirenderer
Text renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Text renderer
*/
var TextRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
TextRenderer.prototype.render = function(type) {
return type === "text/html" ? $tw.utils.htmlEncode(this.parseTreeNode.text) : this.parseTreeNode.text;
};
TextRenderer.prototype.renderInDom = function() {
return document.createTextNode(this.parseTreeNode.text);
};
exports.text = TextRenderer
})();
+151
View File
@@ -0,0 +1,151 @@
/*\
title: $:/core/modules/rendertree/renderers/widget.js
type: application/javascript
module-type: wikirenderer
Widget renderer.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Widget renderer
*/
var WidgetRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Compute the default dependencies
this.dependencies = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var tr = self.renderTree.wiki.parseTextReference(attribute.textReference);
if(tr.title) {
self.dependencies[tr.title] = true;
} else {
self.dependencies[renderContext.tiddlerTitle] = true;
}
}
});
// Compute our attributes
this.attributes = {};
this.computeAttributes();
// Create the widget object
var WidgetClass = this.renderTree.parser.vocabulary.widgetClasses[this.parseTreeNode.tag];
if(WidgetClass) {
this.widget = new WidgetClass(this);
} else {
// Error if we couldn't find the widget
this.children = this.renderTree.createRenderers(this.renderContext,[
{type: "text", text: "Unknown widget type '" + this.parseTreeNode.tag + "'"}
]);
}
};
WidgetRenderer.prototype.computeAttributes = function() {
var changedAttributes = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var value = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle);
if(self.attributes[name] !== value) {
self.attributes[name] = value;
changedAttributes[name] = true;
}
} else { // String attribute
if(self.attributes[name] !== attribute.value) {
self.attributes[name] = attribute.value;
changedAttributes[name] = true;
}
}
});
return changedAttributes;
};
WidgetRenderer.prototype.hasAttribute = function(name) {
return $tw.utils.hop(this.attributes,name);
};
WidgetRenderer.prototype.getAttribute = function(name,defaultValue) {
if($tw.utils.hop(this.attributes,name)) {
return this.attributes[name];
} else {
return defaultValue;
}
};
WidgetRenderer.prototype.render = function(type) {
// Render the widget if we've got one
if(this.widget && this.widget.render) {
return this.widget.render(type);
}
};
WidgetRenderer.prototype.renderInDom = function() {
// Create the wrapper element
this.domNode = document.createElement("widget");
this.domNode.setAttribute("data-widget-type",this.parseTreeNode.tag);
this.domNode.setAttribute("data-widget-attr",JSON.stringify(this.attributes));
// Render the widget if we've got one
if(this.widget && this.widget.renderInDom) {
this.widget.renderInDom(this.domNode);
}
// Return the dom node
return this.domNode;
};
WidgetRenderer.prototype.refreshInDom = function(changedTiddlers) {
// Refresh if the widget cleared the depencies hashmap to indicate that it should always be refreshed, or if any of our dependencies have changed
if(!this.dependencies || $tw.utils.checkDependencies(this.dependencies,changedTiddlers)) {
// Update our attributes
var changedAttributes = this.computeAttributes();
// Refresh the widget
if(this.widget && this.widget.refreshInDom) {
this.widget.refreshInDom(changedAttributes,changedTiddlers);
return;
}
}
// If the widget itself didn't need refreshing, just refresh any child nodes
var self = this;
$tw.utils.each(this.children,function(node,index) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
};
WidgetRenderer.prototype.getContextTiddlerTitle = function() {
return this.renderContext ? this.renderContext.tiddlerTitle : undefined;
};
/*
Check for render context recursion by returning true if the members of a proposed new render context are already present in the render context chain
*/
WidgetRenderer.prototype.checkContextRecursion = function(newRenderContext) {
var context = this.renderContext;
while(context) {
var match = true;
for(var member in newRenderContext) {
if($tw.utils.hop(newRenderContext,member)) {
if(newRenderContext[member] && newRenderContext[member] !== context[member]) {
match = false;
}
}
}
if(match) {
return true;
}
context = context.parentContext;
}
return false;
};
exports.widget = WidgetRenderer
})();