From 551ebdc0050b199b3bd5451d556c6ca37495a79b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 17 May 2013 10:12:25 +0100 Subject: [PATCH] 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. --- core/modules/commands/savetiddlers.js | 6 +- .../parsers/wikiparser/rules/typedblock.js | 7 +- core/modules/rendertree/renderers/element.js | 48 +------ core/modules/rendertree/renderers/entity.js | 6 +- .../modules/rendertree/renderers/macrocall.js | 12 +- core/modules/rendertree/renderers/raw.js | 6 +- core/modules/rendertree/renderers/text.js | 6 +- core/modules/rendertree/wikirendertree.js | 15 +-- core/modules/utils/dom/styles.js | 4 +- core/modules/utils/fakedom.js | 122 ++++++++++++++++++ .../widgets/edit/editors/bitmapeditor.js | 2 +- .../widgets/edit/editors/texteditor.js | 6 +- core/modules/widgets/link.js | 6 +- .../modules/widgets/list/listviews/classic.js | 2 +- .../widgets/list/listviews/scroller.js | 2 +- .../widgets/view/viewers/htmlwikified.js | 6 +- .../widgets/view/viewers/relativedate.js | 4 +- core/modules/wiki.js | 14 +- 18 files changed, 167 insertions(+), 107 deletions(-) create mode 100644 core/modules/utils/fakedom.js diff --git a/core/modules/commands/savetiddlers.js b/core/modules/commands/savetiddlers.js index d4eab787c..18d432432 100644 --- a/core/modules/commands/savetiddlers.js +++ b/core/modules/commands/savetiddlers.js @@ -39,9 +39,11 @@ Command.prototype.execute = function() { parser = wiki.parseTiddler(template), tiddlers = wiki.filterTiddlers(filter); $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(); - 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"); }); return null; diff --git a/core/modules/parsers/wikiparser/rules/typedblock.js b/core/modules/parsers/wikiparser/rules/typedblock.js index f5ecbea67..0355b1f90 100644 --- a/core/modules/parsers/wikiparser/rules/typedblock.js +++ b/core/modules/parsers/wikiparser/rules/typedblock.js @@ -63,14 +63,17 @@ exports.parse = function() { return parser.tree; } else { // Otherwise, render to the rendertype and return in a
 tag
-		var renderTree = new $tw.WikiRenderTree(parser,{wiki: $tw.wiki});
+		var renderTree = new $tw.WikiRenderTree(parser,{wiki: $tw.wiki, document: $tw.document});
 		renderTree.execute();
+		var container = $tw.document.createElement("div");
+		renderTree.renderInDom(container);
+		var text = renderType === "text/html" ? container.innerHTML : container.textContent;
 		return [{
 			type: "element",
 			tag: "pre",
 			children: [{
 				type: "text",
-				text: renderTree.render(renderType)
+				text: text
 			}]
 		}];
 	}
diff --git a/core/modules/rendertree/renderers/element.js b/core/modules/rendertree/renderers/element.js
index ce0317a63..3e2ef93fe 100644
--- a/core/modules/rendertree/renderers/element.js
+++ b/core/modules/rendertree/renderers/element.js
@@ -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\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("");
-		}
-	}
-	return output.join("");
-};
-
 ElementRenderer.prototype.renderInDom = function() {
 	// Check if our widget is providing an element
 	if(this.widget.tag) {
 		// 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
 		$tw.utils.addEventListeners(this.domNode,this.widget.events);
 		// Assign the attributes
@@ -184,8 +142,8 @@ ElementRenderer.prototype.renderInDom = function() {
 				self.domNode.appendChild(node.renderInDom());
 			}
 		});
-		// Call postRenderInDom if the widget provides it
-		if(this.widget.postRenderInDom) {
+		// Call postRenderInDom if the widget provides it and we're in the browser
+		if($tw.browser && this.widget.postRenderInDom) {
 			this.widget.postRenderInDom();
 		}
 		// Return the dom node
diff --git a/core/modules/rendertree/renderers/entity.js b/core/modules/rendertree/renderers/entity.js
index e755aa4c3..7556513f7 100644
--- a/core/modules/rendertree/renderers/entity.js
+++ b/core/modules/rendertree/renderers/entity.js
@@ -22,12 +22,8 @@ var EntityRenderer = function(renderTree,parentRenderer,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() {
-	return document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
+	return this.renderTree.document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
 };
 
 exports.entity = EntityRenderer
diff --git a/core/modules/rendertree/renderers/macrocall.js b/core/modules/rendertree/renderers/macrocall.js
index 4e25ab90a..7e4ae7288 100644
--- a/core/modules/rendertree/renderers/macrocall.js
+++ b/core/modules/rendertree/renderers/macrocall.js
@@ -69,19 +69,9 @@ MacroCallRenderer.prototype.substituteParameters = function(text,macroCallParseT
 	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(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);
 	// Render any child nodes
 	var self = this;
diff --git a/core/modules/rendertree/renderers/raw.js b/core/modules/rendertree/renderers/raw.js
index 00def1e5b..98c77e788 100644
--- a/core/modules/rendertree/renderers/raw.js
+++ b/core/modules/rendertree/renderers/raw.js
@@ -22,12 +22,8 @@ var RawRenderer = function(renderTree,parentRenderer,parseTreeNode) {
 	this.parseTreeNode = parseTreeNode;
 };
 
-RawRenderer.prototype.render = function(type) {
-	return this.parseTreeNode.html;
-};
-
 RawRenderer.prototype.renderInDom = function() {
-	var domNode = document.createElement("div");
+	var domNode = this.renderTree.document.createElement("div");
 	domNode.innerHTML = this.parseTreeNode.html;
 	return domNode;
 };
diff --git a/core/modules/rendertree/renderers/text.js b/core/modules/rendertree/renderers/text.js
index 3ac0e8684..b915d938c 100644
--- a/core/modules/rendertree/renderers/text.js
+++ b/core/modules/rendertree/renderers/text.js
@@ -22,12 +22,8 @@ var TextRenderer = function(renderTree,parentRenderer,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() {
-	return document.createTextNode(this.parseTreeNode.text);
+	return this.renderTree.document.createTextNode(this.parseTreeNode.text);
 };
 
 exports.text = TextRenderer
diff --git a/core/modules/rendertree/wikirendertree.js b/core/modules/rendertree/wikirendertree.js
index 73841a93a..b5f7ddf1c 100644
--- a/core/modules/rendertree/wikirendertree.js
+++ b/core/modules/rendertree/wikirendertree.js
@@ -20,6 +20,7 @@ Options include:
 	wiki: mandatory reference to wiki associated with this render tree
 	context: optional hashmap of context variables (see below)
 	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:
 	tiddlerTitle: title of the tiddler providing the context
 	templateTitle: title of the tiddler providing the current template
@@ -30,6 +31,7 @@ var WikiRenderTree = function(parser,options) {
 	this.wiki = options.wiki;
 	this.context = options.context || {};
 	this.parentRenderer = options.parentRenderer;
+	this.document = options.document || (typeof(document) === "object" ? document : null);
 	// Hashmap of the renderer classes
 	if(!this.rendererClasses) {
 		WikiRenderTree.prototype.rendererClasses = $tw.modules.applyMethods("wikirenderer");
@@ -64,19 +66,6 @@ WikiRenderTree.prototype.createRenderer = function(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
 */
diff --git a/core/modules/utils/dom/styles.js b/core/modules/utils/dom/styles.js
index 7d35df3c7..68dd2dd2c 100644
--- a/core/modules/utils/dom/styles.js
+++ b/core/modules/utils/dom/styles.js
@@ -37,7 +37,9 @@ StylesheetManager.prototype.addStylesheet = function(title) {
 	var parser = this.wiki.parseTiddler(title),
 		renderTree = new $tw.WikiRenderTree(parser,{wiki: this.wiki, context: {tiddlerTitle: title}});
 	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
 	var styleNode = document.createElement("style");
 	styleNode.setAttribute("type","text/css");
diff --git a/core/modules/utils/fakedom.js b/core/modules/utils/fakedom.js
new file mode 100644
index 000000000..d4e96d1ce
--- /dev/null
+++ b/core/modules/utils/fakedom.js
@@ -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\n");
+		if($tw.config.htmlVoidElements.indexOf(this.tag) === -1) {
+			output.push(this.innerHTML);
+			output.push("");
+		}
+		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;
+
+})();
diff --git a/core/modules/widgets/edit/editors/bitmapeditor.js b/core/modules/widgets/edit/editors/bitmapeditor.js
index 7f145c7ab..7818f6523 100644
--- a/core/modules/widgets/edit/editors/bitmapeditor.js
+++ b/core/modules/widgets/edit/editors/bitmapeditor.js
@@ -49,7 +49,7 @@ BitmapEditor.prototype.postRenderInDom = function() {
 		var ctx = canvas.getContext("2d");
 		ctx.drawImage(currImage,0,0);
 		// 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.height = currImage.height;
 		ctx = self.currCanvas.getContext("2d");
diff --git a/core/modules/widgets/edit/editors/texteditor.js b/core/modules/widgets/edit/editors/texteditor.js
index 3dbbbabdc..54ff839e5 100644
--- a/core/modules/widgets/edit/editors/texteditor.js
+++ b/core/modules/widgets/edit/editors/texteditor.js
@@ -127,12 +127,12 @@ TextEditor.prototype.fixHeight = function() {
 		$tw.utils.nextTick(function() {
 			// Resize the textarea to fit its content
 			var textarea = self.editWidget.children[0].domNode,
-				scrollTop = document.body.scrollTop;
+				scrollTop = self.editWidget.renderer.renderTree.document.body.scrollTop;
 			textarea.style.height = "auto";
 			var newHeight = Math.max(textarea.scrollHeight + textarea.offsetHeight - textarea.clientHeight,MIN_TEXT_AREA_HEIGHT);
 			if(newHeight !== textarea.offsetHeight) {
 				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() {
-	if(document.activeElement !== this.editWidget.children[0].domNode) {
+	if(this.editWidget.renderer.renderTree.document.activeElement !== this.editWidget.children[0].domNode) {
 		var editInfo = this.getEditInfo();
 		this.editWidget.children[0].domNode.value = editInfo.value;
 	}
diff --git a/core/modules/widgets/link.js b/core/modules/widgets/link.js
index cd8c3cb56..5537af5c0 100644
--- a/core/modules/widgets/link.js
+++ b/core/modules/widgets/link.js
@@ -123,10 +123,10 @@ LinkWidget.prototype.handleDragStartEvent = function(event) {
 		// Set the dragging class on the element being dragged
 		$tw.utils.addClass(event.target,"tw-tiddlylink-dragging");
 		// 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.appendChild(document.createTextNode(this.to));
-		document.body.appendChild(this.dragImage);
+		this.dragImage.appendChild(this.listWidget.renderer.renderTree.document.createTextNode(this.to));
+		this.listWidget.renderer.renderTree.document.body.appendChild(this.dragImage);
 		// Set the data transfer properties
 		var dataTransfer = event.dataTransfer;
 		dataTransfer.effectAllowed = "copy";
diff --git a/core/modules/widgets/list/listviews/classic.js b/core/modules/widgets/list/listviews/classic.js
index ba73aca0a..5427786c5 100644
--- a/core/modules/widgets/list/listviews/classic.js
+++ b/core/modules/widgets/list/listviews/classic.js
@@ -24,7 +24,7 @@ ClassicListView.prototype.navigateTo = function(historyInfo) {
 	var listElementNode = this.listWidget.children[listElementIndex],
 		targetElement = listElementNode.domNode;
 	// 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);
 	targetElement.dispatchEvent(scrollEvent);
 };
diff --git a/core/modules/widgets/list/listviews/scroller.js b/core/modules/widgets/list/listviews/scroller.js
index 30a4ef68d..26d116e6d 100644
--- a/core/modules/widgets/list/listviews/scroller.js
+++ b/core/modules/widgets/list/listviews/scroller.js
@@ -21,7 +21,7 @@ ScrollerListView.prototype.navigateTo = function(historyInfo) {
 		listElementNode = this.listWidget.children[listElementIndex],
 		targetElement = listElementNode.domNode;
 	// 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);
 	targetElement.dispatchEvent(scrollEvent);
 };
diff --git a/core/modules/widgets/view/viewers/htmlwikified.js b/core/modules/widgets/view/viewers/htmlwikified.js
index c7900ce42..2572a1422 100644
--- a/core/modules/widgets/view/viewers/htmlwikified.js
+++ b/core/modules/widgets/view/viewers/htmlwikified.js
@@ -23,9 +23,11 @@ HtmlWikifiedViewer.prototype.render = function() {
 	// Parse the field text
 	var wiki = this.viewWidget.renderer.renderTree.wiki,
 		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();
-	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
 	this.viewWidget.tag = "pre";
 	this.viewWidget.attributes = {
diff --git a/core/modules/widgets/view/viewers/relativedate.js b/core/modules/widgets/view/viewers/relativedate.js
index 8f2c5fbf4..4cc939d73 100644
--- a/core/modules/widgets/view/viewers/relativedate.js
+++ b/core/modules/widgets/view/viewers/relativedate.js
@@ -51,7 +51,7 @@ RelativeDateViewer.prototype.setTimer = function() {
 	if(this.relativeDate.updatePeriod < 24 * 60 * 60 * 1000) {
 		window.setTimeout(function() {
 			// 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);
 			}
 		},this.relativeDate.updatePeriod);
@@ -67,7 +67,7 @@ RelativeDateViewer.prototype.update = function() {
 		while(this.viewWidget.renderer.domNode.hasChildNodes()) {
 			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();
 	}
 };
diff --git a/core/modules/wiki.js b/core/modules/wiki.js
index 66aa40685..4149eb290 100644
--- a/core/modules/wiki.js
+++ b/core/modules/wiki.js
@@ -553,11 +553,13 @@ Parse text in a specified format and render it into another format
 	textType: content type of the input text
 	text: input text
 */
-exports.renderText = function(outputType,textType,text) {
+exports.renderText = function(outputType,textType,text,context) {
 	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();
-	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) {
 	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();
-	return renderTree.render(outputType);
+	var container = $tw.document.createElement("div");
+	renderTree.renderInDom(container)
+	return outputType === "text/html" ? container.innerHTML : container.textContent;
 };
 
 /*