Skip to content

Instantly share code, notes, and snippets.

@rgrove
Created November 7, 2012 01:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rgrove/4028896 to your computer and use it in GitHub Desktop.
Save rgrove/4028896 to your computer and use it in GitHub Desktop.
An old, aborted attempt at a performant Mustache parser.
(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: {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
'`': '&#x60;'
},
/**
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