Skip to content
Browse files

Can now call functions with static arguments from your Mustache templ…

…ate! Yay. Also: renovated parsing.js to use dicts instead of lists.
  • Loading branch information...
1 parent 05acbaa commit 85072c33f5f3295d4eda09fd80ec4b69edb6ff89 @chbrown committed Aug 17, 2011
View
2 .gitignore
@@ -1,2 +1,4 @@
.DS_Store
test/spec/
+notes.txt
+
View
2 benchmarks/million_complex.js
@@ -38,7 +38,7 @@ console.time('Total time');
// var i = 0, d = new Date();
// (function go() {
// 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 () {
View
2 demo.js
@@ -29,5 +29,5 @@ Mu.render('simple.html', ctx, {chunkSize: 10}, function (err, output) {
output.resume();
}, 500);
})
- .addListener('end', function () { sys.puts("\n\nDONE"); });
+ .once('end', function () { sys.puts("\n\nDONE"); });
});
View
2 lib/cli.js
@@ -10,7 +10,7 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk) {
input += chunk;
});
-process.stdin.on('end', function() {
+process.stdin.once('end', function() {
// template = template.toString('utf8');
// var parsed = parser.parse(template);
View
126 lib/parsing.js
@@ -31,7 +31,7 @@ exports.hitCache = function(name) {
}
return CACHE[name];
};
-exports.root = function(directory, resetCache) {
+exports.root = function(directory, doResetCache) {
// This is synchronous
if (directory === undefined) {
// GETTER
@@ -40,7 +40,7 @@ exports.root = function(directory, resetCache) {
else {
// SETTER
ROOT = path.join(directory, '.');
- if (resetCache === undefined || resetCache === true) {
+ if (doResetCache === undefined || doResetCache === true) {
resetCache('.'); // and yes, even this is synchronous
}
}
@@ -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) {
this.buffer_string = template_string; // = this.template_string
@@ -129,22 +121,9 @@ Parser.prototype = {
var raw = this.buffer_string.substring(0, index); //.replace(newlineRegExp, '\n'),
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) {
this.state = 'eos'; // stop
this.buffer_string = '';
@@ -164,85 +143,62 @@ Parser.prototype = {
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 match = tag.match(/(\W*)(.+)/);
- // var symbol = match[1];
- // var variable = match[2];
-
- // 1-long: (command)
- // yield
- // 2-long: (command, variable)
- // everything else, except for sections
- // 3-long: (command, variable, block)
- // sections and inverted_sections
+ // node.t is a string; e.g. 'escaped', 'unescaped', 'partial', 'section', etc.
+ // node.val (optional, highly recommended) is a string, which must be an object in the context, possibly a function.
+ // node.block (optional) is a list of nodes
+ // node.arguments (optional) is a list of strings, potentially empty.
- var top = this.stackTop(), top_last;
+ var new_node, top = this.stackTop();
if (tag.match(/^\w/)) {
- top.push(['escaped', tag]);
+ new_node = {t: 'escaped', val: tag};
} else if (tag[0] === '&') {
- top.push(['unescaped', tag.slice(1)]);
+ new_node = {t: 'unescaped', val: tag.slice(1)};
} else if (tag[0] === '{') {
- top.push(['unescaped', tag.slice(1)]);
+ new_node = {t: 'unescaped', val: tag.slice(1)};
close_tag_length++;
} else if (tag[0] === '>') {
- top.push(['partial', tag.slice(1)]);
+ new_node = {t: 'partial', val: tag.slice(1)};
} else if (tag[0] === '<') {
- top.push(['yield']);
+ new_node = {t: 'yield'};
} else if (tag[0] === '=') {
// e.g. "{{=<% %>=}}" but not "{{=<? ?>}}"
var parts = tag.slice(1, -1).split(' ');
this.setTags(parts[0], parts[1]);
} else if (tag[0] === '#' || tag[0] === '^') {
- var name = tag[0] === '#' ? 'section' : 'inverted_section';
- // 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)]);
+ new_node = {t: tag[0] === '#' ? 'section' : 'inverted_section', val: tag.slice(1)};
this.stack.push([]);
} else if (tag[0] === '/') {
- var popped_top = this.stack.pop();
+ var block = this.stack.pop();
top = this.stackTop();
- top_last = top[top.length - 1]; // get the declaration of the (inverted_)section
- top_last[2] = popped_top; // 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);
- //}
+ var top_last_node = top[top.length - 1]; // get the declaration of the (inverted_)section
+ top_last_node.block = block; // add as block
} else if (tag[0] === '!') {
// nothing
} else {
console.error("The tag '" + tag + "' is not recognized mustache/amulet syntax.");
- // if (variable) {
- // top.push(['escaped', variable])
- // }
+ // if (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"
// (that is, act like asap=false for this variable, even if asap=true at the renderer level)
@@ -259,5 +215,5 @@ Parser.prototype = {
}
walk(this.tokens);
// return this.stack[this.stack.length - 1];
- },
+ }
};
View
42 lib/rendering.js
@@ -124,31 +124,28 @@ function(tokens, yield_names, context, callback) {
var i = 0;
(function next() {
try {
- var token = tokens[i++];
+ var node = tokens[i++];
- if (token === undefined) {
- // we're done! (with this scope)
+ if (node === undefined) {
+ // we're done! (with this scope, at least)
return callback(undefined);
}
- if (token === null) {
- // this might have been a token that was optimized out by a parsing post-processor, so just ignore it
+ if (node === null) {
+ // 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");
return next();
}
- // token = (command, variable, block)
- var command = token[0];
- var variable = token[1];
- if (command === 'raw') {
- renderer.outputWrite(variable);
+ if (node.t === 'raw') {
+ renderer.outputWrite(node.val);
return next();
}
- else if (command === 'partial') {
- // what about partials with variables for names?
- var partial_tokens = parsing.hitCache(variable);
+ else if (node.t === 'partial') {
+ // xxx: what about partials with context lookups (variables) for names?
+ var partial_tokens = parsing.hitCache(node.val);
return renderer.renderTokens(partial_tokens, [], context, next);
}
- else if (command === 'yield') {
+ else if (node.t === 'yield') {
if (yield_names[0] === undefined) {
throw new Error('Cannot yield nothing');
}
@@ -158,12 +155,12 @@ function(tokens, yield_names, context, callback) {
else {
(function bump() {
var item = context,
- splits = variable.split('.'),
+ splits = node.val.split('.'),
next_item = null;
for (var i = 0, len = splits.length; i < len; i++) { // foreach split
next_item = item[splits[i]];
if (typeof(next_item) === 'function') {
- item = next_item.apply(item); // item(block) ?(block) ? allow parameters?
+ item = next_item.apply(item, node.arguments);
}
else {
item = next_item;
@@ -190,29 +187,26 @@ function(tokens, yield_names, context, callback) {
item = '';
}
- if (command === 'unescaped') {
+ if (node.t === 'unescaped') {
renderer.outputWrite(item.toString());
return next();
}
- else if (command === 'escaped') {
+ else if (node.t === 'escaped') {
renderer.outputWrite(escapeHtml(item.toString()));
return next();
}
- else if (command === 'section' || command === 'inverted_section') {
- var enabled = command === 'inverted_section' ? !item : item;
+ else if (node.t === 'section' || node.t === 'inverted_section') {
+ var enabled = node.t === 'inverted_section' ? !item : item;
if (enabled) {
- var block = token[2];
function sectionRender(item, context, callback) {
if (typeof(item) !== 'object') {
// for strings, numbers, booleans, etc.
item = {'_': item};
}
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
- // stck.pop()
callback();
});
}
View
62 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.');
+ }
+ })();
+});
View
1 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"}]}
View
13 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>
View
33 tests/lib.js
@@ -1,34 +1,53 @@
-var Stream = require('stream').Stream;
-var EventEmitter = require('events').EventEmitter;
+var fs = require('fs'),
+ path = require('path'),
+ exec = require('child_process').exec,
+ Stream = require('stream').Stream,
+ EventEmitter = require('events').EventEmitter;
var StringStream = function() {
this.buffer = '';
this.writable = true;
-}
+};
StringStream.prototype = new EventEmitter();
StringStream.prototype.write = function(chunk) {
if (this.writable)
this.buffer += chunk;
else
throw new Error("Cannot write to unwritable StringStream.");
-}
+};
StringStream.prototype.end = function(arg0) {
this.writable = false;
this.emit('end', this.buffer);
-}
+};
StringStream.prototype.ondata = function(chunk) {
if (this.writable)
this.buffer += chunk;
else
throw new Error("Cannot write to unwritable StringStream.");
-}
+};
StringStream.prototype.onend = function() {
this.writable = false;
this.emit('end', this.buffer);
-}
+};
exports.StringStream = StringStream;
+// node.js sucks at reading yaml
+exports.yaml2json = function(yaml, json, callback) {
+ if (!path.existsSync(json)) {
+ console.log("Json doesn't exist. Converting.");
+ return exec('yaml2json ' + yaml + ' > ' + json, callback);
+ }
+ else {
+ yaml_stats = fs.statSync(yaml);
+ json_stats = fs.statSync(json);
+ if (yaml_stats.mtime > json_stats.mtime) {
+ console.log('Yaml is newer than the json. Re-converting.');
+ return exec('yaml2json ' + yaml + ' > ' + json, callback);
+ }
+ }
+ return callback();
+};
// THE BELOW DOESN'T WORK!
// function StringStream() {
View
88 tests/local.js
@@ -2,67 +2,45 @@ var fs = require('fs'),
path = require('path'),
amulet = require('../lib/amulet'),
Stream = require('stream').Stream,
- // EventEmitter = require('events').EventEmitter,
Buffer = require('buffer').Buffer,
- StringStream = require('./lib').StringStream,
- exec = require('child_process').exec;
+ yaml2json = require('./lib').yaml2json;
// amulet.root(path.join(__dirname, 'examples'));
var ignore_whitespace = true;
-console.log('Simple specs: [ignore_whitespace = ' + ignore_whitespace + ']');
+console.log('ignore_whitespace = ' + ignore_whitespace);
-var simple_spec = null;
-
-function start(callback) {
- var simple_spec_json = fs.readFileSync('simple_spec.json');
- simple_spec = JSON.parse(simple_spec_json);
- callback();
-}
-var i = 0;
-function next() {
- if (i < simple_spec.tests.length) {
- var spec = simple_spec.tests[i++];
- process.stdout.write(' Spec: ' + spec.description + ' ');
- amulet.parseTemplate(spec.description, spec.template);
- try {
- var context = eval('(' + spec.context + ')');
- }
- catch (e) {
- console.error('Reading context failed', e, spec.context);
- }
-
- // var string_stream = new StringStream();
- amulet.renderString(spec.description, context, function(err, output) { // process.stdout
- // var output = string_stream.buffer;
- if (output == spec.output) {
- process.stdout.write('[Success]\n');
- }
- else if (ignore_whitespace && output.replace(/\s+/g, '') == spec.output.replace(/\s+/g, '')) {
- process.stdout.write('[Success]\n');
+yaml2json('local_spec.yaml', 'local_spec.json', function() {
+ var tests = JSON.parse(fs.readFileSync('local_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 + ')');
}
- else {
- process.stdout.write('[Failed]' +
- '\n Expected output:\n' + spec.output +
- '\n Actual output:\n' + output + '\n');
+ catch (e) {
+ console.error('Reading context failed', e, spec.context);
}
- next();
- });
- }
- else {
- console.log('Done with simple_spec tests');
- }
-}
+ 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');
+ }
-// node.js sucks at reading yaml
-yaml_stats = fs.statSync('simple_spec.yaml');
-json_stats = fs.statSync('simple_spec.json');
-if (yaml_stats.mtime > json_stats.mtime) {
- console.log('Yaml is newer than the json -- re-converting.');
- exec('yaml2json simple_spec.yaml > simple_spec.json', function (error, stdout, stderr) {
- start(next);
- });
-}
-else {
- start(next);
-}
+ next();
+ });
+ }
+ else {
+ console.log('Done.');
+ }
+ })();
+});
+
View
0 tests/simple_spec.json → tests/local_spec.json
File renamed without changes.
View
0 tests/simple_spec.yaml → tests/local_spec.yaml
File renamed without changes.
View
2 tests/run_mu_spec.js
@@ -78,7 +78,7 @@ function spec_repo_available() {
renderer.stream.on('data', function(data) {
rendered += data
})
- renderer.stream.on('end', function() {
+ renderer.stream.once('end', function() {
// console.log(' string_stream ended')
})

0 comments on commit 85072c3

Please sign in to comment.
Something went wrong with that request. Please try again.