Files
meteor/packages/spacebars-compiler/optimizer.js
2014-06-24 00:27:34 -07:00

187 lines
6.0 KiB
JavaScript

// 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 `<table><tr>...`, 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;
};