1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-27 03:57:21 +00:00

Major refactoring of rendering mechanism

We now use a fake DOM implementation on the server to let us share more
rendering code between the text output vs. DOM output paths.
This commit is contained in:
Jeremy Ruston 2013-05-17 10:12:25 +01:00
parent bf4fede34e
commit 551ebdc005
18 changed files with 167 additions and 107 deletions

View File

@ -39,9 +39,11 @@ Command.prototype.execute = function() {
parser = wiki.parseTiddler(template), parser = wiki.parseTiddler(template),
tiddlers = wiki.filterTiddlers(filter); tiddlers = wiki.filterTiddlers(filter);
$tw.utils.each(tiddlers,function(title) { $tw.utils.each(tiddlers,function(title) {
var renderTree = new $tw.WikiRenderTree(parser,{wiki: wiki, context: {tiddlerTitle: title}}); var renderTree = new $tw.WikiRenderTree(parser,{wiki: wiki, context: {tiddlerTitle: title}, document: $tw.document});
renderTree.execute(); renderTree.execute();
var text = renderTree.render(type); var container = $tw.document.createElement("div");
renderTree.renderInDom(container);
var text = type === "text/html" ? container.innerHTML : container.textContent;
fs.writeFileSync(path.resolve(pathname,encodeURIComponent(title) + extension),text,"utf8"); fs.writeFileSync(path.resolve(pathname,encodeURIComponent(title) + extension),text,"utf8");
}); });
return null; return null;

View File

@ -63,14 +63,17 @@ exports.parse = function() {
return parser.tree; return parser.tree;
} else { } else {
// Otherwise, render to the rendertype and return in a <PRE> tag // Otherwise, render to the rendertype and return in a <PRE> tag
var renderTree = new $tw.WikiRenderTree(parser,{wiki: $tw.wiki}); var renderTree = new $tw.WikiRenderTree(parser,{wiki: $tw.wiki, document: $tw.document});
renderTree.execute(); renderTree.execute();
var container = $tw.document.createElement("div");
renderTree.renderInDom(container);
var text = renderType === "text/html" ? container.innerHTML : container.textContent;
return [{ return [{
type: "element", type: "element",
tag: "pre", tag: "pre",
children: [{ children: [{
type: "text", type: "text",
text: renderTree.render(renderType) text: text
}] }]
}]; }];
} }

View File

@ -126,53 +126,11 @@ ElementRenderer.prototype.getAttribute = function(name,defaultValue) {
} }
}; };
ElementRenderer.prototype.render = function(type) {
var isHtml = type === "text/html",
output = [],attr,a,v;
if(isHtml) {
output.push("<",this.widget.tag);
if(this.widget.attributes) {
attr = [];
for(a in this.widget.attributes) {
attr.push(a);
}
attr.sort();
for(a=0; a<attr.length; a++) {
v = this.widget.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),"'");
}
}
}
output.push(">\n");
}
if($tw.config.htmlVoidElements.indexOf(this.widget.tag) === -1) {
$tw.utils.each(this.widget.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
if(isHtml) {
output.push("</",this.widget.tag,">");
}
}
return output.join("");
};
ElementRenderer.prototype.renderInDom = function() { ElementRenderer.prototype.renderInDom = function() {
// Check if our widget is providing an element // Check if our widget is providing an element
if(this.widget.tag) { if(this.widget.tag) {
// Create the element // Create the element
this.domNode = document.createElementNS(this.namespace,this.widget.tag); this.domNode = this.renderTree.document.createElementNS(this.namespace,this.widget.tag);
// Assign any specified event handlers // Assign any specified event handlers
$tw.utils.addEventListeners(this.domNode,this.widget.events); $tw.utils.addEventListeners(this.domNode,this.widget.events);
// Assign the attributes // Assign the attributes
@ -184,8 +142,8 @@ ElementRenderer.prototype.renderInDom = function() {
self.domNode.appendChild(node.renderInDom()); self.domNode.appendChild(node.renderInDom());
} }
}); });
// Call postRenderInDom if the widget provides it // Call postRenderInDom if the widget provides it and we're in the browser
if(this.widget.postRenderInDom) { if($tw.browser && this.widget.postRenderInDom) {
this.widget.postRenderInDom(); this.widget.postRenderInDom();
} }
// Return the dom node // Return the dom node

View File

@ -22,12 +22,8 @@ var EntityRenderer = function(renderTree,parentRenderer,parseTreeNode) {
this.parseTreeNode = parseTreeNode; 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() { EntityRenderer.prototype.renderInDom = function() {
return document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity)); return this.renderTree.document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
}; };
exports.entity = EntityRenderer exports.entity = EntityRenderer

View File

@ -69,19 +69,9 @@ MacroCallRenderer.prototype.substituteParameters = function(text,macroCallParseT
return text; 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() { MacroCallRenderer.prototype.renderInDom = function() {
// Create the element // Create the element
this.domNode = document.createElement(this.parseTreeNode.isBlock ? "div" : "span"); this.domNode = this.renderTree.document.createElement(this.parseTreeNode.isBlock ? "div" : "span");
this.domNode.setAttribute("data-macro-name",this.parseTreeNode.name); this.domNode.setAttribute("data-macro-name",this.parseTreeNode.name);
// Render any child nodes // Render any child nodes
var self = this; var self = this;

View File

@ -22,12 +22,8 @@ var RawRenderer = function(renderTree,parentRenderer,parseTreeNode) {
this.parseTreeNode = parseTreeNode; this.parseTreeNode = parseTreeNode;
}; };
RawRenderer.prototype.render = function(type) {
return this.parseTreeNode.html;
};
RawRenderer.prototype.renderInDom = function() { RawRenderer.prototype.renderInDom = function() {
var domNode = document.createElement("div"); var domNode = this.renderTree.document.createElement("div");
domNode.innerHTML = this.parseTreeNode.html; domNode.innerHTML = this.parseTreeNode.html;
return domNode; return domNode;
}; };

View File

@ -22,12 +22,8 @@ var TextRenderer = function(renderTree,parentRenderer,parseTreeNode) {
this.parseTreeNode = parseTreeNode; 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() { TextRenderer.prototype.renderInDom = function() {
return document.createTextNode(this.parseTreeNode.text); return this.renderTree.document.createTextNode(this.parseTreeNode.text);
}; };
exports.text = TextRenderer exports.text = TextRenderer

View File

@ -20,6 +20,7 @@ Options include:
wiki: mandatory reference to wiki associated with this render tree wiki: mandatory reference to wiki associated with this render tree
context: optional hashmap of context variables (see below) context: optional hashmap of context variables (see below)
parentRenderer: optional reference to a parent renderer node for the context chain parentRenderer: optional reference to a parent renderer node for the context chain
document: optional document object to use instead of global document
Context variables include: Context variables include:
tiddlerTitle: title of the tiddler providing the context tiddlerTitle: title of the tiddler providing the context
templateTitle: title of the tiddler providing the current template templateTitle: title of the tiddler providing the current template
@ -30,6 +31,7 @@ var WikiRenderTree = function(parser,options) {
this.wiki = options.wiki; this.wiki = options.wiki;
this.context = options.context || {}; this.context = options.context || {};
this.parentRenderer = options.parentRenderer; this.parentRenderer = options.parentRenderer;
this.document = options.document || (typeof(document) === "object" ? document : null);
// Hashmap of the renderer classes // Hashmap of the renderer classes
if(!this.rendererClasses) { if(!this.rendererClasses) {
WikiRenderTree.prototype.rendererClasses = $tw.modules.applyMethods("wikirenderer"); WikiRenderTree.prototype.rendererClasses = $tw.modules.applyMethods("wikirenderer");
@ -64,19 +66,6 @@ WikiRenderTree.prototype.createRenderer = function(parentRenderer,parseTreeNode)
return new RenderNodeClass(this,parentRenderer,parseTreeNode); return new RenderNodeClass(this,parentRenderer,parseTreeNode);
}; };
/*
Render as a string
*/
WikiRenderTree.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.rendererTree,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
/* /*
Render to the DOM Render to the DOM
*/ */

View File

@ -37,7 +37,9 @@ StylesheetManager.prototype.addStylesheet = function(title) {
var parser = this.wiki.parseTiddler(title), var parser = this.wiki.parseTiddler(title),
renderTree = new $tw.WikiRenderTree(parser,{wiki: this.wiki, context: {tiddlerTitle: title}}); renderTree = new $tw.WikiRenderTree(parser,{wiki: this.wiki, context: {tiddlerTitle: title}});
renderTree.execute(); renderTree.execute();
var text = renderTree.render("text/plain"); var container = $tw.document.createElement("div");
renderTree.renderInDom(container);
var text = container.textContent;
// Create a style element and put it in the document // Create a style element and put it in the document
var styleNode = document.createElement("style"); var styleNode = document.createElement("style");
styleNode.setAttribute("type","text/css"); styleNode.setAttribute("type","text/css");

View File

@ -0,0 +1,122 @@
/*\
title: $:/core/modules/utils/fakedom.js
type: application/javascript
module-type: global
A barebones implementation of DOM interfaces needed by the rendering mechanism.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var TW_TextNode = function(text) {
this.textContent = text;
}
var TW_Element = function(tag) {
this.tag = tag;
this.attributes = {};
this.isRaw = false;
this.children = [];
}
TW_Element.prototype.setAttribute = function(name,value) {
if(this.isRaw) {
throw "Cannot setAttribute on a raw TW_Element";
}
this.attributes[name] = value;
};
TW_Element.prototype.setAttributeNS = function(namespace,name,value) {
this.setAttribute(name,value);
};
TW_Element.prototype.appendChild = function(node) {
this.children.push(node);
node.parentNode = this;
};
TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
// Do nothing
};
Object.defineProperty(TW_Element.prototype, "outerHTML", {
get: function() {
var output = [],attr,a,v;
output.push("<",this.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) {
output.push(" ",attr[a],"='",$tw.utils.htmlEncode(v),"'");
}
}
}
output.push(">\n");
if($tw.config.htmlVoidElements.indexOf(this.tag) === -1) {
output.push(this.innerHTML);
output.push("</",this.tag,">");
}
return output.join("");
}
});
Object.defineProperty(TW_Element.prototype, "innerHTML", {
get: function() {
if(this.isRaw) {
return this.rawHTML;
} else {
var b = [];
$tw.utils.each(this.children,function(node) {
if(node instanceof TW_Element) {
b.push(node.outerHTML);
} else if(node instanceof TW_TextNode) {
b.push($tw.utils.htmlEncode(node.textContent));
}
});
return b.join("");
}
},
set: function(value) {
this.isRaw = true;
this.rawHTML = value;
}
});
Object.defineProperty(TW_Element.prototype, "textContent", {
get: function() {
if(this.isRaw) {
throw "Cannot get textContent on a raw TW_Element";
} else {
var b = [];
$tw.utils.each(this.children,function(node) {
b.push(node.textContent);
});
return b.join("");
}
}
});
var document = {
createElementNS: function(namespace,tag) {
return new TW_Element(tag);
},
createElement: function(tag) {
return new TW_Element(tag);
},
createTextNode: function(text) {
return new TW_TextNode(text);
},
};
exports.document = document;
})();

View File

@ -49,7 +49,7 @@ BitmapEditor.prototype.postRenderInDom = function() {
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext("2d");
ctx.drawImage(currImage,0,0); ctx.drawImage(currImage,0,0);
// And also copy the current bitmap to the off-screen canvas // And also copy the current bitmap to the off-screen canvas
self.currCanvas = document.createElement("canvas"); self.currCanvas = self.editWidget.renderer.renderTree.document.createElement("canvas");
self.currCanvas.width = currImage.width; self.currCanvas.width = currImage.width;
self.currCanvas.height = currImage.height; self.currCanvas.height = currImage.height;
ctx = self.currCanvas.getContext("2d"); ctx = self.currCanvas.getContext("2d");

View File

@ -127,12 +127,12 @@ TextEditor.prototype.fixHeight = function() {
$tw.utils.nextTick(function() { $tw.utils.nextTick(function() {
// Resize the textarea to fit its content // Resize the textarea to fit its content
var textarea = self.editWidget.children[0].domNode, var textarea = self.editWidget.children[0].domNode,
scrollTop = document.body.scrollTop; scrollTop = self.editWidget.renderer.renderTree.document.body.scrollTop;
textarea.style.height = "auto"; textarea.style.height = "auto";
var newHeight = Math.max(textarea.scrollHeight + textarea.offsetHeight - textarea.clientHeight,MIN_TEXT_AREA_HEIGHT); var newHeight = Math.max(textarea.scrollHeight + textarea.offsetHeight - textarea.clientHeight,MIN_TEXT_AREA_HEIGHT);
if(newHeight !== textarea.offsetHeight) { if(newHeight !== textarea.offsetHeight) {
textarea.style.height = newHeight + "px"; textarea.style.height = newHeight + "px";
document.body.scrollTop = scrollTop; self.editWidget.renderer.renderTree.document.body.scrollTop = scrollTop;
} }
}); });
} }
@ -143,7 +143,7 @@ TextEditor.prototype.postRenderInDom = function() {
}; };
TextEditor.prototype.refreshInDom = function() { TextEditor.prototype.refreshInDom = function() {
if(document.activeElement !== this.editWidget.children[0].domNode) { if(this.editWidget.renderer.renderTree.document.activeElement !== this.editWidget.children[0].domNode) {
var editInfo = this.getEditInfo(); var editInfo = this.getEditInfo();
this.editWidget.children[0].domNode.value = editInfo.value; this.editWidget.children[0].domNode.value = editInfo.value;
} }

View File

@ -123,10 +123,10 @@ LinkWidget.prototype.handleDragStartEvent = function(event) {
// Set the dragging class on the element being dragged // Set the dragging class on the element being dragged
$tw.utils.addClass(event.target,"tw-tiddlylink-dragging"); $tw.utils.addClass(event.target,"tw-tiddlylink-dragging");
// Create the drag image element // Create the drag image element
this.dragImage = document.createElement("div"); this.dragImage = this.listWidget.renderer.renderTree.document.createElement("div");
this.dragImage.className = "tw-tiddler-dragger"; this.dragImage.className = "tw-tiddler-dragger";
this.dragImage.appendChild(document.createTextNode(this.to)); this.dragImage.appendChild(this.listWidget.renderer.renderTree.document.createTextNode(this.to));
document.body.appendChild(this.dragImage); this.listWidget.renderer.renderTree.document.body.appendChild(this.dragImage);
// Set the data transfer properties // Set the data transfer properties
var dataTransfer = event.dataTransfer; var dataTransfer = event.dataTransfer;
dataTransfer.effectAllowed = "copy"; dataTransfer.effectAllowed = "copy";

View File

@ -24,7 +24,7 @@ ClassicListView.prototype.navigateTo = function(historyInfo) {
var listElementNode = this.listWidget.children[listElementIndex], var listElementNode = this.listWidget.children[listElementIndex],
targetElement = listElementNode.domNode; targetElement = listElementNode.domNode;
// Scroll the node into view // Scroll the node into view
var scrollEvent = document.createEvent("Event"); var scrollEvent = this.listWidget.renderer.renderTree.document.createEvent("Event");
scrollEvent.initEvent("tw-scroll",true,true); scrollEvent.initEvent("tw-scroll",true,true);
targetElement.dispatchEvent(scrollEvent); targetElement.dispatchEvent(scrollEvent);
}; };

View File

@ -21,7 +21,7 @@ ScrollerListView.prototype.navigateTo = function(historyInfo) {
listElementNode = this.listWidget.children[listElementIndex], listElementNode = this.listWidget.children[listElementIndex],
targetElement = listElementNode.domNode; targetElement = listElementNode.domNode;
// Scroll the node into view // Scroll the node into view
var scrollEvent = document.createEvent("Event"); var scrollEvent = this.listWidget.renderer.renderTree.document.createEvent("Event");
scrollEvent.initEvent("tw-scroll",true,true); scrollEvent.initEvent("tw-scroll",true,true);
targetElement.dispatchEvent(scrollEvent); targetElement.dispatchEvent(scrollEvent);
}; };

View File

@ -23,9 +23,11 @@ HtmlWikifiedViewer.prototype.render = function() {
// Parse the field text // Parse the field text
var wiki = this.viewWidget.renderer.renderTree.wiki, var wiki = this.viewWidget.renderer.renderTree.wiki,
parser = wiki.parseText("text/vnd.tiddlywiki",this.value), parser = wiki.parseText("text/vnd.tiddlywiki",this.value),
renderTree = new $tw.WikiRenderTree(parser,{wiki: wiki, parentRenderer: this.viewWidget.renderer}); renderTree = new $tw.WikiRenderTree(parser,{wiki: wiki, parentRenderer: this.viewWidget.renderer, document: this.viewWidget.renderer.renderTree.document});
renderTree.execute(); renderTree.execute();
var text = renderTree.render("text/html"); var container = this.viewWidget.renderer.renderTree.document.createElement("div");
renderTree.renderInDom(container)
var text = container.innerHTML;
// Set the element details // Set the element details
this.viewWidget.tag = "pre"; this.viewWidget.tag = "pre";
this.viewWidget.attributes = { this.viewWidget.attributes = {

View File

@ -51,7 +51,7 @@ RelativeDateViewer.prototype.setTimer = function() {
if(this.relativeDate.updatePeriod < 24 * 60 * 60 * 1000) { if(this.relativeDate.updatePeriod < 24 * 60 * 60 * 1000) {
window.setTimeout(function() { window.setTimeout(function() {
// Only call the update function if the dom node is still in the document // Only call the update function if the dom node is still in the document
if($tw.utils.domContains(document,self.viewWidget.renderer.domNode)) { if($tw.utils.domContains(self.listWidget.renderer.renderTree.document,self.viewWidget.renderer.domNode)) {
self.update.call(self); self.update.call(self);
} }
},this.relativeDate.updatePeriod); },this.relativeDate.updatePeriod);
@ -67,7 +67,7 @@ RelativeDateViewer.prototype.update = function() {
while(this.viewWidget.renderer.domNode.hasChildNodes()) { while(this.viewWidget.renderer.domNode.hasChildNodes()) {
this.viewWidget.renderer.domNode.removeChild(this.viewWidget.renderer.domNode.firstChild); this.viewWidget.renderer.domNode.removeChild(this.viewWidget.renderer.domNode.firstChild);
} }
this.viewWidget.renderer.domNode.appendChild(document.createTextNode(this.relativeDate.description)); this.viewWidget.renderer.domNode.appendChild(this.viewWidget.renderer.renderTree.document.createTextNode(this.relativeDate.description));
this.setTimer(); this.setTimer();
} }
}; };

View File

@ -553,11 +553,13 @@ Parse text in a specified format and render it into another format
textType: content type of the input text textType: content type of the input text
text: input text text: input text
*/ */
exports.renderText = function(outputType,textType,text) { exports.renderText = function(outputType,textType,text,context) {
var parser = this.parseText(textType,text), var parser = this.parseText(textType,text),
renderTree = new $tw.WikiRenderTree(parser,{wiki: this}); renderTree = new $tw.WikiRenderTree(parser,{wiki: this, context: context, document: $tw.document});
renderTree.execute(); renderTree.execute();
return renderTree.render(outputType); var container = $tw.document.createElement("div");
renderTree.renderInDom(container)
return outputType === "text/html" ? container.innerHTML : container.textContent;
}; };
/* /*
@ -567,9 +569,11 @@ Parse text from a tiddler and render it into another format
*/ */
exports.renderTiddler = function(outputType,title,context) { exports.renderTiddler = function(outputType,title,context) {
var parser = this.parseTiddler(title), var parser = this.parseTiddler(title),
renderTree = new $tw.WikiRenderTree(parser,{wiki: this, context: context}); renderTree = new $tw.WikiRenderTree(parser,{wiki: this, context: context, document: $tw.document});
renderTree.execute(); renderTree.execute();
return renderTree.render(outputType); var container = $tw.document.createElement("div");
renderTree.renderInDom(container)
return outputType === "text/html" ? container.innerHTML : container.textContent;
}; };
/* /*