Skip to content

Commit

Permalink
Can now call functions with static arguments from your Mustache templ…
Browse files Browse the repository at this point in the history
…ate! Yay. Also: renovated parsing.js to use dicts instead of lists.
  • Loading branch information
chbrown committed Aug 17, 2011
1 parent 05acbaa commit 85072c3
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 175 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,2 +1,4 @@
.DS_Store .DS_Store
test/spec/ test/spec/
notes.txt

2 changes: 1 addition & 1 deletion benchmarks/million_complex.js
Expand Up @@ -38,7 +38,7 @@ console.time('Total time');
// var i = 0, d = new Date(); // var i = 0, d = new Date();
// (function go() { // (function go() {
// if (i++ < RUNS) { // if (i++ < RUNS) {
// mu.render('complex.html', js).on('end', function () { go(); }); // mu.render('complex.html', js).once('end', function () { go(); });
// } // }
// }()) // }())
// process.addListener('exit', function () { // process.addListener('exit', function () {
Expand Down
2 changes: 1 addition & 1 deletion demo.js
Expand Up @@ -29,5 +29,5 @@ Mu.render('simple.html', ctx, {chunkSize: 10}, function (err, output) {
output.resume(); output.resume();
}, 500); }, 500);
}) })
.addListener('end', function () { sys.puts("\n\nDONE"); }); .once('end', function () { sys.puts("\n\nDONE"); });
}); });
2 changes: 1 addition & 1 deletion lib/cli.js
Expand Up @@ -10,7 +10,7 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk) { process.stdin.on('data', function(chunk) {
input += chunk; input += chunk;
}); });
process.stdin.on('end', function() { process.stdin.once('end', function() {


// template = template.toString('utf8'); // template = template.toString('utf8');
// var parsed = parser.parse(template); // var parsed = parser.parse(template);
Expand Down
126 changes: 41 additions & 85 deletions lib/parsing.js
Expand Up @@ -31,7 +31,7 @@ exports.hitCache = function(name) {
} }
return CACHE[name]; return CACHE[name];
}; };
exports.root = function(directory, resetCache) { exports.root = function(directory, doResetCache) {
// This is synchronous // This is synchronous
if (directory === undefined) { if (directory === undefined) {
// GETTER // GETTER
Expand All @@ -40,7 +40,7 @@ exports.root = function(directory, resetCache) {
else { else {
// SETTER // SETTER
ROOT = path.join(directory, '.'); ROOT = path.join(directory, '.');
if (resetCache === undefined || resetCache === true) { if (doResetCache === undefined || doResetCache === true) {
resetCache('.'); // and yes, even this is synchronous resetCache('.'); // and yes, even this is synchronous
} }
} }
Expand Down Expand Up @@ -74,14 +74,6 @@ function resetCache(local) {
}); });
}); });
} }
// var escapeRegex = function(text) {
// // thank you Simon Willison
// if (!arguments.callee.sRE) {
// var specials = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\']
// arguments.callee.sRE = new RegExp('(\\' + specials.join('|\\') + ')', 'g')
// }
// return text.replace(arguments.callee.sRE, '\\$1')
// }


var Parser = function(template_string, options) { var Parser = function(template_string, options) {
this.buffer_string = template_string; // = this.template_string this.buffer_string = template_string; // = this.template_string
Expand Down Expand Up @@ -129,22 +121,9 @@ Parser.prototype = {


var raw = this.buffer_string.substring(0, index); //.replace(newlineRegExp, '\n'), var raw = this.buffer_string.substring(0, index); //.replace(newlineRegExp, '\n'),
if (raw !== '') { if (raw !== '') {
this.stackTop().push(['raw', raw]); this.stackTop().push({t: 'raw', val: raw});
} }



// var buffer = new Buffer(Buffer.byteLength(content))

// var top_last = top[top.length - 1];
// raw = raw.replace(/\n+/g, '\n');
// if (top_last && top_last[0] === 'raw') { // this only happens when delimiters are changed (rare!)
// top_last[1] += raw;
// }
// buffer.write(content, 'utf8', 0)

// var line = this.currentLine + content

// this.currentLine = line.substring(line.lastIndexOf('\n') + 1, line.length)
if (eos) { if (eos) {
this.state = 'eos'; // stop this.state = 'eos'; // stop
this.buffer_string = ''; this.buffer_string = '';
Expand All @@ -164,85 +143,62 @@ Parser.prototype = {
var tag = raw.replace(/^\s+|\s+$/g, ''); // strip all edge whitespace var tag = raw.replace(/^\s+|\s+$/g, ''); // strip all edge whitespace
var close_tag_length = this.close_tag.length; // silly hack for }}} closing tags var close_tag_length = this.close_tag.length; // silly hack for }}} closing tags


// var match = tag.match(/(\W*)(.+)/); // node.t is a string; e.g. 'escaped', 'unescaped', 'partial', 'section', etc.
// var symbol = match[1]; // node.val (optional, highly recommended) is a string, which must be an object in the context, possibly a function.
// var variable = match[2]; // node.block (optional) is a list of nodes

// node.arguments (optional) is a list of strings, potentially empty.
// 1-long: (command)
// yield
// 2-long: (command, variable)
// everything else, except for sections
// 3-long: (command, variable, block)
// sections and inverted_sections


var top = this.stackTop(), top_last; var new_node, top = this.stackTop();
if (tag.match(/^\w/)) { if (tag.match(/^\w/)) {
top.push(['escaped', tag]); new_node = {t: 'escaped', val: tag};
} else if (tag[0] === '&') { } else if (tag[0] === '&') {
top.push(['unescaped', tag.slice(1)]); new_node = {t: 'unescaped', val: tag.slice(1)};
} else if (tag[0] === '{') { } else if (tag[0] === '{') {
top.push(['unescaped', tag.slice(1)]); new_node = {t: 'unescaped', val: tag.slice(1)};
close_tag_length++; close_tag_length++;
} else if (tag[0] === '>') { } else if (tag[0] === '>') {
top.push(['partial', tag.slice(1)]); new_node = {t: 'partial', val: tag.slice(1)};
} else if (tag[0] === '<') { } else if (tag[0] === '<') {
top.push(['yield']); new_node = {t: 'yield'};
} else if (tag[0] === '=') { } else if (tag[0] === '=') {
// e.g. "{{=<% %>=}}" but not "{{=<? ?>}}" // e.g. "{{=<% %>=}}" but not "{{=<? ?>}}"
var parts = tag.slice(1, -1).split(' '); var parts = tag.slice(1, -1).split(' ');
this.setTags(parts[0], parts[1]); this.setTags(parts[0], parts[1]);
} else if (tag[0] === '#' || tag[0] === '^') { } else if (tag[0] === '#' || tag[0] === '^') {
var name = tag[0] === '#' ? 'section' : 'inverted_section'; new_node = {t: tag[0] === '#' ? 'section' : 'inverted_section', val: tag.slice(1)};
// here we shrink the line containing the section {{#variable}}, if possible.
// so, check that this tag occurs on it's own line. This is almost ridiculous.
// XXX: ACTUALLY, NVM, do this in a post-processing walk-step. Be faster that way.
// top_last = top[top.length - 1];
// try {
// if (top_last !== undefined && top_last[0] === 'raw') {
// var remainder_newline_index = remainder.search(/^\s*\n/);
// if (remainder_newline_index !== -1) {
// var previous = top_last[1];
// var previous_newline = previous.search(/\n\s*$/);
// if (previous_newline !== -1) {
// console.log('== Previous:');
// console.log('<< "' + previous.replace(/\n/g, '\\n') + '"');
// previous = previous.slice(0, previous_newline);
// top_last[1] = previous !== '' ? previous + '\n' : '';
// console.log('>> "' + top_last[1].replace(/\n/g, '\\n') + '"');
// console.log('Remainder[:100]:\n "' + remainder.slice(0, 100).replace(/\n/g, '\\n') + '"\n---------==========>\n "' + remainder.substring(remainder_newline_index + 1).replace(/\n/g, '\\n') + '"');
// remainder = remainder.substring(remainder_newline_index + 1);
// }
// }
// }
// }
// catch(e) {
// console.log('Error!!!');
// console.dir(this.stack);
// throw e;
// }
top.push([name, tag.slice(1)]);
this.stack.push([]); this.stack.push([]);
} else if (tag[0] === '/') { } else if (tag[0] === '/') {
var popped_top = this.stack.pop(); var block = this.stack.pop();
top = this.stackTop(); top = this.stackTop();
top_last = top[top.length - 1]; // get the declaration of the (inverted_)section var top_last_node = top[top.length - 1]; // get the declaration of the (inverted_)section
top_last[2] = popped_top; // add as block top_last_node.block = block; // add as block
// Screw it. Close whatever is open and on top of the stack.
//var original_command = next_last[0];
//var original_variable = next_last[1];
//if (original_command !== 'section' && original_command !== 'inverted_section') {
// throw new Error('Closing unopened section with ' + tag.slice(1) + '; ' + original_command + ' should be a section.');
//}
//else if (original_variable !== tag.slice(1)) {
// throw new Error('Unmatched closing section; ' + tag.slice(1) + ' should be ' + original_variable);
//}
} else if (tag[0] === '!') { } else if (tag[0] === '!') {
// nothing // nothing
} else { } else {
console.error("The tag '" + tag + "' is not recognized mustache/amulet syntax."); console.error("The tag '" + tag + "' is not recognized mustache/amulet syntax.");
// if (variable) { // if (variable) { top.push(['escaped', variable]) }
// top.push(['escaped', variable]) }
// }
if (new_node !== undefined) {
// open parens indicates function application
var open_parens_index = new_node.val.indexOf('(');
// the entire following conditional is for functional application.
if (open_parens_index > -1) {
// close_parens_index should be append_to_top.length - 1
var close_parens_index = new_node.val.lastIndexOf(')');

// String.substring takes two indices (not a length)
var argument_part = new_node.val.substring(open_parens_index + 1, close_parens_index - 1);
// trim quotes since arguments can only be strings or ints anyway. At least, for now.
// (xxx: let them use context?)
var argument_array = argument_part.split(',').map(function(s) {
return s.replace(/^\s*['"]?|['"]?\s*$/g, '');
});

new_node.val = new_node.val.slice(0, open_parens_index);
new_node.arguments = argument_array;
}
top.push(new_node);
} }
// todo: syntax for "never wait even if mode is asap" // todo: syntax for "never wait even if mode is asap"
// (that is, act like asap=false for this variable, even if asap=true at the renderer level) // (that is, act like asap=false for this variable, even if asap=true at the renderer level)
Expand All @@ -259,5 +215,5 @@ Parser.prototype = {
} }
walk(this.tokens); walk(this.tokens);
// return this.stack[this.stack.length - 1]; // return this.stack[this.stack.length - 1];
}, }
}; };
42 changes: 18 additions & 24 deletions lib/rendering.js
Expand Up @@ -124,31 +124,28 @@ function(tokens, yield_names, context, callback) {
var i = 0; var i = 0;
(function next() { (function next() {
try { try {
var token = tokens[i++]; var node = tokens[i++];


if (token === undefined) { if (node === undefined) {
// we're done! (with this scope) // we're done! (with this scope, at least)
return callback(undefined); return callback(undefined);
} }
if (token === null) { if (node === null) {
// this might have been a token that was optimized out by a parsing post-processor, so just ignore it // this might have been a node/token that was optimized out by a parsing post-processor, so just ignore it
// console.log("Skipping null token in amulet.rendering.renderTokens"); // console.log("Skipping null token in amulet.rendering.renderTokens");
return next(); return next();
} }


// token = (command, variable, block) if (node.t === 'raw') {
var command = token[0]; renderer.outputWrite(node.val);
var variable = token[1];
if (command === 'raw') {
renderer.outputWrite(variable);
return next(); return next();
} }
else if (command === 'partial') { else if (node.t === 'partial') {
// what about partials with variables for names? // xxx: what about partials with context lookups (variables) for names?
var partial_tokens = parsing.hitCache(variable); var partial_tokens = parsing.hitCache(node.val);
return renderer.renderTokens(partial_tokens, [], context, next); return renderer.renderTokens(partial_tokens, [], context, next);
} }
else if (command === 'yield') { else if (node.t === 'yield') {
if (yield_names[0] === undefined) { if (yield_names[0] === undefined) {
throw new Error('Cannot yield nothing'); throw new Error('Cannot yield nothing');
} }
Expand All @@ -158,12 +155,12 @@ function(tokens, yield_names, context, callback) {
else { else {
(function bump() { (function bump() {
var item = context, var item = context,
splits = variable.split('.'), splits = node.val.split('.'),
next_item = null; next_item = null;
for (var i = 0, len = splits.length; i < len; i++) { // foreach split for (var i = 0, len = splits.length; i < len; i++) { // foreach split
next_item = item[splits[i]]; next_item = item[splits[i]];
if (typeof(next_item) === 'function') { if (typeof(next_item) === 'function') {
item = next_item.apply(item); // item(block) ?(block) ? allow parameters? item = next_item.apply(item, node.arguments);
} }
else { else {
item = next_item; item = next_item;
Expand All @@ -190,29 +187,26 @@ function(tokens, yield_names, context, callback) {
item = ''; item = '';
} }


if (command === 'unescaped') { if (node.t === 'unescaped') {
renderer.outputWrite(item.toString()); renderer.outputWrite(item.toString());
return next(); return next();
} }
else if (command === 'escaped') { else if (node.t === 'escaped') {
renderer.outputWrite(escapeHtml(item.toString())); renderer.outputWrite(escapeHtml(item.toString()));
return next(); return next();
} }
else if (command === 'section' || command === 'inverted_section') { else if (node.t === 'section' || node.t === 'inverted_section') {
var enabled = command === 'inverted_section' ? !item : item; var enabled = node.t === 'inverted_section' ? !item : item;
if (enabled) { if (enabled) {
var block = token[2];
function sectionRender(item, context, callback) { function sectionRender(item, context, callback) {
if (typeof(item) !== 'object') { if (typeof(item) !== 'object') {
// for strings, numbers, booleans, etc. // for strings, numbers, booleans, etc.
item = {'_': item}; item = {'_': item};
} }
replaceProto(item, default_object_proto, context); replaceProto(item, default_object_proto, context);
// stck.push(item)


return renderer.renderTokens(block, [], item, function() { return renderer.renderTokens(node.block, [], item, function() {
replaceProto(item, context, default_object_proto); // put it back replaceProto(item, context, default_object_proto); // put it back
// stck.pop()
callback(); callback();
}); });
} }
Expand Down
62 changes: 62 additions & 0 deletions tests/extended.js
@@ -0,0 +1,62 @@
var fs = require('fs'),
path = require('path'),
amulet = require('../lib/amulet'),
Stream = require('stream').Stream,
Buffer = require('buffer').Buffer,
yaml2json = require('./lib').yaml2json;

var ignore_whitespace = true;
console.log('ignore_whitespace = ' + ignore_whitespace);

String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
String.prototype.titleize = function() {
var result = [];
var parts = this.split(" ");
for (i in parts) {
result.push(capitalize(parts[i]));
}
return result.join(" ");
};
String.prototype.humanize = function() {
return titleize(this.replace('_', ' '));
};
String.prototype.equals = function(test) {
return this.valueOf() === test;
};

yaml2json('extended_spec.yaml', 'extended_spec.json', function() {
var tests = JSON.parse(fs.readFileSync('extended_spec.json')).tests;
var i = 0;
(function next() {
if (i < tests.length) {
var spec = tests[i++], context;
process.stdout.write(' Spec: ' + spec.description + ' ');
amulet.parseTemplate(spec.description, spec.template);
try {
context = eval('(' + spec.context + ')');
}
catch (e) {
console.error('Reading context failed', e, spec.context);
}

amulet.renderString(spec.description, context, function(err, output) {
if (
output == spec.output ||
(ignore_whitespace && output.replace(/\s+/g, '') == spec.output.replace(/\s+/g, ''))
) {
process.stdout.write('[Success]\n');
}
else {
process.stdout.write('[Failed]\n Expected:\n' + spec.output + '\n Actual:\n' + output + '\n');
}

next();
});
}
else {
console.log('Done.');
}
})();
});
1 change: 1 addition & 0 deletions tests/extended_spec.json
@@ -0,0 +1 @@
{"tests": [{"output": "<span color=\"green\">Thanks for paying $314.15!</span>\n", "description": "function calls no. 1", "context": "{ status: 'paid', amount: '$314.15' }\n", "template": "{{#status.equals('owed')}}\n <span color=\"red\">You owe {{amount}}</span>\n{{/}}\n{{#status.equals('paid')}}\n <span color=\"green\">Thanks for paying {{amount}}!</span>\n{{/}}\n"}]}
13 changes: 13 additions & 0 deletions tests/extended_spec.yaml
@@ -0,0 +1,13 @@
tests:
- description: function calls no. 1
context: |
{ status: 'paid', amount: '$314.15' }
template: |
{{#status.equals('owed')}}
<span color="red">You owe {{amount}}</span>
{{/}}
{{#status.equals('paid')}}
<span color="green">Thanks for paying {{amount}}!</span>
{{/}}
output: |
<span color="green">Thanks for paying $314.15!</span>

0 comments on commit 85072c3

Please sign in to comment.