// Optimize parts of an HTMLjs tree into raw HTML strings when they don't // contain template tags. var constant = function (value) { return function () { return value; }; }; var OPTIMIZABLE = { NONE: 0, PARTS: 1, FULL: 2 }; // We can only turn content into an HTML string if it contains no template // tags and no "tricky" HTML tags. If we can optimize the entire content // into a string, we return OPTIMIZABLE.FULL. If the we are given an // unoptimizable node, we return OPTIMIZABLE.NONE. If we are given a tree // that contains an unoptimizable node somewhere, we return OPTIMIZABLE.PARTS. // // For example, we always create SVG elements programmatically, since SVG // doesn't have innerHTML. If we are given an SVG element, we return NONE. // However, if we are given a big tree that contains SVG somewhere, we // return PARTS so that the optimizer can descend into the tree and optimize // other parts of it. var CanOptimizeVisitor = HTML.Visitor.extend(); CanOptimizeVisitor.def({ visitNull: constant(OPTIMIZABLE.FULL), visitPrimitive: constant(OPTIMIZABLE.FULL), visitComment: constant(OPTIMIZABLE.FULL), visitCharRef: constant(OPTIMIZABLE.FULL), visitRaw: constant(OPTIMIZABLE.FULL), visitObject: constant(OPTIMIZABLE.NONE), visitFunction: constant(OPTIMIZABLE.NONE), visitArray: function (x) { for (var i = 0; i < x.length; i++) if (this.visit(x[i]) !== OPTIMIZABLE.FULL) return OPTIMIZABLE.PARTS; return OPTIMIZABLE.FULL; }, visitTag: function (tag) { var tagName = tag.tagName; if (tagName === 'textarea') { // optimizing into a TEXTAREA's RCDATA would require being a little // more clever. return OPTIMIZABLE.NONE; } else if (! (HTML.isKnownElement(tagName) && ! HTML.isKnownSVGElement(tagName))) { // foreign elements like SVG can't be stringified for innerHTML. return OPTIMIZABLE.NONE; } else if (tagName === 'table') { // Avoid ever producing HTML containing `...`, because the // browser will insert a TBODY. If we just `createElement("table")` and // `createElement("tr")`, on the other hand, no TBODY is necessary // (assuming IE 8+). return OPTIMIZABLE.NONE; } var children = tag.children; for (var i = 0; i < children.length; i++) if (this.visit(children[i]) !== OPTIMIZABLE.FULL) return OPTIMIZABLE.PARTS; if (this.visitAttributes(tag.attrs) !== OPTIMIZABLE.FULL) return OPTIMIZABLE.PARTS; return OPTIMIZABLE.FULL; }, visitAttributes: function (attrs) { if (attrs) { var isArray = HTML.isArray(attrs); for (var i = 0; i < (isArray ? attrs.length : 1); i++) { var a = (isArray ? attrs[i] : attrs); if ((typeof a !== 'object') || (a instanceof HTMLTools.TemplateTag)) return OPTIMIZABLE.PARTS; for (var k in a) if (this.visit(a[k]) !== OPTIMIZABLE.FULL) return OPTIMIZABLE.PARTS; } } return OPTIMIZABLE.FULL; } }); var getOptimizability = function (content) { return (new CanOptimizeVisitor).visit(content); }; var toRaw = function (x) { return HTML.Raw(HTML.toHTML(x)); }; var TreeTransformer = HTML.TransformingVisitor.extend(); TreeTransformer.def({ visitAttributes: function (attrs/*, ...*/) { // pass template tags through by default if (attrs instanceof HTMLTools.TemplateTag) return attrs; return HTML.TransformingVisitor.prototype.visitAttributes.apply( this, arguments); } }); // Replace parts of the HTMLjs tree that have no template tags (or // tricky HTML tags) with HTML.Raw objects containing raw HTML. var OptimizingVisitor = TreeTransformer.extend(); OptimizingVisitor.def({ visitNull: toRaw, visitPrimitive: toRaw, visitComment: toRaw, visitCharRef: toRaw, visitArray: function (array) { var optimizability = getOptimizability(array); if (optimizability === OPTIMIZABLE.FULL) { return toRaw(array); } else if (optimizability === OPTIMIZABLE.PARTS) { return TreeTransformer.prototype.visitArray.call(this, array); } else { return array; } }, visitTag: function (tag) { var optimizability = getOptimizability(tag); if (optimizability === OPTIMIZABLE.FULL) { return toRaw(tag); } else if (optimizability === OPTIMIZABLE.PARTS) { return TreeTransformer.prototype.visitTag.call(this, tag); } else { return tag; } }, visitChildren: function (children) { // don't optimize the children array into a Raw object! return TreeTransformer.prototype.visitArray.call(this, children); }, visitAttributes: function (attrs) { return attrs; } }); // Combine consecutive HTML.Raws. Remove empty ones. var RawCompactingVisitor = TreeTransformer.extend(); RawCompactingVisitor.def({ visitArray: function (array) { var result = []; for (var i = 0; i < array.length; i++) { var item = array[i]; if ((item instanceof HTML.Raw) && ((! item.value) || (result.length && (result[result.length - 1] instanceof HTML.Raw)))) { // two cases: item is an empty Raw, or previous item is // a Raw as well. In the latter case, replace the previous // Raw with a longer one that includes the new Raw. if (item.value) { result[result.length - 1] = HTML.Raw( result[result.length - 1].value + item.value); } } else { result.push(item); } } return result; } }); // Replace pointless Raws like `HTMl.Raw('foo')` that contain no special // characters with simple strings. var RawReplacingVisitor = TreeTransformer.extend(); RawReplacingVisitor.def({ visitRaw: function (raw) { var html = raw.value; if (html.indexOf('&') < 0 && html.indexOf('<') < 0) { return html; } else { return raw; } } }); SpacebarsCompiler.optimize = function (tree) { tree = (new OptimizingVisitor).visit(tree); tree = (new RawCompactingVisitor).visit(tree); tree = (new RawReplacingVisitor).visit(tree); return tree; };