Created
November 7, 2012 01:13
-
-
Save rgrove/4028896 to your computer and use it in GitHub Desktop.
An old, aborted attempt at a performant Mustache parser.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
// -- Constructor -------------------------------------------------------------- | |
function Stache(template) { | |
this.parsed = false; | |
this._template = template; | |
} | |
// -- Static Functions --------------------------------------------------------- | |
Stache.render = function (template, view, partials) { | |
return new Stache(template).render(view, partials); | |
}; | |
Stache.prototype = { | |
// -- Public Prototype Properties ------------------------------------------ | |
HTML_CHARS: { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
"'": ''', | |
'/': '/', | |
'`': '`' | |
}, | |
/** | |
Regex that matches zero or more whitespace characters from the beginning of | |
the string until (and including) the first newline. | |
@property REGEX_BLANK_AFTER | |
@type {RegExp} | |
**/ | |
REGEX_BLANK_AFTER: /^[^\n\S]*\n/, | |
/** | |
Regex that matches a newline followed by zero or more whitespace characters | |
at the end of the string. | |
@property REGEX_BLANK_BEFORE | |
@type {RegExp} | |
**/ | |
REGEX_BLANK_BEFORE: /\r?\n[^\n\S]*$/, | |
/** | |
Global regex with the following subpattern matches: | |
1. Tag opener ({{, or {{{) | |
2. Sigil (optional) | |
3. Tag name | |
4. Tag closer (}}, or }}}) | |
@property REGEX_TAG | |
@type {RegExp} | |
**/ | |
REGEX_TAG: /(\{{2,3})([!#\^&<>\/])?\s*([\s\S]+?)\s*(\}{2,3})/g, | |
/** | |
If these tag types are the only non-whitespace content on a line, the line | |
will be removed from the output. | |
@property REMOVE_LINES | |
@type {Object} | |
**/ | |
REMOVE_LINES: { | |
'!': true, | |
'#': true, | |
'^': true, | |
'>': true, | |
'/': true | |
}, | |
// -- Public Prototype Functions ------------------------------------------- | |
parse: function (template) { | |
var self = this, | |
next, node, prev, state; | |
this._reset(); | |
this._template = template; | |
state = this._state; | |
template.replace(this.REGEX_TAG, function (tag, open, operator, name, close, offset) { | |
var before = offset > state.offset ? | |
template.substring(state.offset, offset) : ''; | |
self._parseTag(tag, open, operator, name, close, offset, before); | |
state.offset = offset + tag.length; | |
}); | |
if (state.offset < template.length) { | |
prev = this._tree.children[this._tree.children.length - 1] || this._tree; | |
node = { | |
content: template.substr(state.offset), | |
prev : prev, | |
type : 'static' | |
}; | |
prev.next = node; | |
this._tree.children.push(node); | |
} | |
// Walk the tree and remove standalone lines left behind by blocks, | |
// comments, and partials. | |
node = this._tree; | |
while (node && (node = node.next)) { | |
if (Stache.DUMP) { | |
var dump = {}; | |
dump[node.type] = node.tag || node.content; | |
console.log(dump); | |
} | |
if (!this.REMOVE_LINES[node.type]) { continue; } | |
prev = node.prev; | |
next = node.next; | |
if ((this.REMOVE_LINES[prev.type] || prev.type === 'root') | |
&& (next && next.type === 'static')) { | |
next.content = next.content.replace(this.REGEX_BLANK_AFTER, ''); | |
} else if (prev.type === 'static') { | |
if (next && next.type === 'static' | |
&& ~prev.content.search(this.REGEX_BLANK_BEFORE) | |
&& ~next.content.search(this.REGEX_BLANK_AFTER)) { | |
prev.content = prev.content.replace(this.REGEX_BLANK_BEFORE, '\n'); | |
next.content = next.content.replace(this.REGEX_BLANK_AFTER, ''); | |
} else if (!next) { | |
prev.content = prev.content.replace(this.REGEX_BLANK_BEFORE, ''); | |
} | |
} | |
} | |
// XXX: For debugging. Remove. | |
if (Stache.DUMP) { | |
process.exit(); | |
} | |
// XXX: End debugging. | |
this.parsed = true; | |
return this; | |
}, | |
render: function (view, partials, root) { | |
var output = [], | |
i, len, node; | |
function buffer(chunk) { | |
output.push(chunk); | |
} | |
if (!this.parsed) { | |
this.parse(this._template); | |
} | |
partials || (partials = {}); | |
root || (root = this._tree); | |
view || (view = {}); | |
len = root.children.length; | |
if (len) { | |
for (i = 0; i < len; ++i) { | |
node = root.children[i]; | |
if (node.type === 'static') { | |
if (node.content) { | |
buffer(node.content); | |
} | |
} else if (node.render) { | |
node.render.call(this, node, view, partials, buffer); | |
} | |
} | |
} else if (root === this._tree) { | |
buffer(this._template); | |
} | |
return output.join(''); | |
}, | |
// -- Protected Prototype Functions ---------------------------------------- | |
_escapeHTML: function (html) { | |
var self = this; | |
return html.replace(/[&<>"'\/`]/g, function (match) { | |
return self.HTML_CHARS[match]; | |
}); | |
}, | |
_getStringValue: function (value) { | |
return value ? this._getValue(value, '').toString() : ''; | |
}, | |
_getValue: function (value, defaultValue, recursed) { | |
var type; | |
if (value) { | |
type = typeof value; | |
if (type === 'function') { | |
if (!recursed) { | |
return this._getValue(value(), defaultValue, true); | |
} | |
} else if (type === 'object' && Array.isArray(value)) { | |
return value.length ? value : defaultValue; | |
} | |
return value; | |
} | |
return defaultValue; | |
}, | |
_parseTag: function (tag, open, operator, name, close, offset, before) { | |
var state = this._state, | |
parent = state.stack[state.stack.length - 1] || this._tree, | |
prev = parent.children[parent.children.length - 1] || parent, | |
template = this._template, | |
indent, node; | |
// TODO: ignore escaped tags | |
node = { | |
children : [], | |
name : name, // contents of the tag, minus operator | |
parent : parent, // parent node, or the root of the tree | |
prev : prev, | |
tag : tag, // raw unparsed tag including mustaches | |
type : operator, | |
unescaped: operator === '&' || (open === '{{{' && close === '}}}') // true if this node's replacement should not be escaped | |
}; | |
if (before) { | |
before = { | |
content: before, | |
next : node, | |
prev : prev, | |
type : 'static' | |
}; | |
prev.next = before; | |
prev = node.prev = before; | |
parent.children.push(before); | |
} else { | |
prev.next = node; | |
} | |
switch (operator) { | |
case '#': // Section open | |
node.render = this._renderSection; | |
state.stack.push(node); | |
break; | |
case '^': // Inverted section open | |
node.inverted = true; | |
node.render = this._renderSection; | |
state.stack.push(node); | |
break; | |
case '/': // Section close | |
parent = node.parent = parent.parent; | |
state.stack.pop(); | |
break; | |
case '>': // Partial | |
case '<': | |
if (prev.type === 'static' | |
&& (indent = prev.content.match(/\n([^\n\S])$/))) { | |
node.indent = indent[1]; | |
} | |
node.render = this._renderPartial; | |
node.type = '>'; | |
break; | |
case '!': // Comment | |
// Do nothing. | |
break; | |
default: // Variable | |
node.render = this._renderVariable; | |
node.type = 'variable'; | |
state.lastOffset = node.end; | |
} | |
parent.children.push(node); | |
}, | |
_renderPartial: function (node, context, partials, buffer) { | |
var content = partials[node.name], | |
indent; | |
if (content) { | |
if (node.indent) { | |
content = content.replace(/^(.)/gm, node.indent + '$1'); | |
} | |
buffer(Stache.render(content, context, partials)); | |
} | |
}, | |
_renderSection: function (node, context, partials, buffer) { | |
var self = this, | |
value = this._getValue(context[node.name]); | |
if (!!value === !node.inverted && node.children.length) { | |
switch (typeof value) { | |
case 'object': | |
if (Array.isArray(value)) { | |
value.forEach(function (context) { | |
buffer(self.render(context, partials, node)); | |
}); | |
} else { | |
context = merge(context, value); | |
buffer(this.render(context, partials, node)); | |
} | |
break; | |
case 'function': | |
// TODO | |
break; | |
default: | |
buffer(this.render(context, partials, node)); | |
} | |
} | |
}, | |
_renderVariable: function (node, context, partials, buffer) { | |
var value = this._getStringValue(context[node.name]); | |
if (value) { | |
buffer(node.unescaped ? value : this._escapeHTML(value)); | |
} | |
}, | |
_reset: function () { | |
this.parsed = false; | |
this._state = { | |
offset: 0, | |
stack : [] | |
}; | |
this._tree = { | |
children: [], | |
type : 'root' | |
}; | |
} | |
}; | |
// -- Private Functions -------------------------------------------------------- | |
/** | |
Returns a new object containing a deep merge of the enumerable properties of all | |
passed objects. Properties in later arguments take precedence over properties | |
with the same name in earlier arguments. Object values are deep-cloned. Array | |
values are _not_ deep-cloned. | |
@method merge | |
@param {object} obj* One or more objects to merge. | |
@return {object} New object with merged values from all other objects. | |
**/ | |
function merge() { | |
var args = Array.prototype.slice.call(arguments), | |
target = {}; | |
args.unshift(target); | |
mix.apply(this, args); | |
return target; | |
} | |
/** | |
Like `merge()`, but augments the first passed object with a deep merge of the | |
enumerable properties of all other passed objects, rather than returning a | |
brand new object. | |
@method mix | |
@param {object} target Object to receive mixed-in properties. | |
@param {object} obj* One or more objects to mix into _target_. | |
@return {object} Reference to the same _target_ object that was passed in. | |
**/ | |
function mix() { | |
var args = Array.prototype.slice.call(arguments), | |
target = args.shift(), | |
i, key, keys, len, source, value; | |
while ((source = args.shift())) { | |
keys = Object.keys(source); | |
for (i = 0, len = keys.length; i < len; ++i) { | |
key = keys[i]; | |
value = source[key]; | |
if (typeof value === 'object' && !Array.isArray(value)) { | |
typeof target[key] === 'object' || (target[key] = {}); | |
mix(target[key], value); | |
} else { | |
target[key] = value; | |
} | |
} | |
} | |
return target; | |
} | |
if (typeof module !== 'undefined' && module.exports) { | |
module.exports = Stache; | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment