Skip to content
Browse files

Moved things around.. finished up a bunch. Should have committed a lo…

…t along the way...
  • Loading branch information...
1 parent 629dbed commit 1856828e1d0720972f71f8d1b2fb7551111f3629 RayMorgan committed Aug 31, 2010
Showing with 835 additions and 18 deletions.
  1. +9 −0 compiler.js
  2. +103 −0 lib/mu.js
  3. +14 −0 lib/mu/errors.js
  4. +12 −18 { → lib/mu}/parser.js
  5. +161 −0 lib/mu/renderer.js
  6. +85 −0 lib/mu/stream.js
  7. +300 −0 mu.js
  8. +81 −0 test.js
  9. +70 −0 try.js
View
9 compiler.js
@@ -0,0 +1,9 @@
+var sys = require('sys');
+
+exports.compile = function (tokens) {
+ return compile(tokens);
+}
+
+function compile(tokens) {
+ var compiled = {strings: [], buffers: [], tokens: tokens, }
+}
View
103 lib/mu.js
@@ -0,0 +1,103 @@
+var sys = require('sys'),
+ fs = require('fs'),
+ path = require('path'),
+ parser = require('./mu/parser'),
+ renderer = require('./mu/renderer'),
+ Stream = require('./mu/stream'),
+ errors = require('./mu/errors');
+
+var mu = module.exports = {};
+
+mu.root = process.cwd();
+mu.cache = {};
+
+mu.fs = function (filename, callback) {
+ fs.readFile(path.join(mu.root, filename), 'utf8', callback);
+}
+
+mu.compile = function(filename, callback) {
+ var parsed;
+
+ mu.fs(filename, function (err, contents) {
+ if (err) {
+ callback(new Error('file_not_found'));//errors.fileNotFound(mu.root, filename, err)));
+ }
+
+ parsed = parser.parse(contents);
+ mu.cache[filename] = parsed;
+ callback(undefined, parsed);
+ });
+}
+
+mu.compileText = function (name, template, callback) {
+ var parsed;
+
+ callback = callback || function() { /* noop */ };
+
+ if (typeof template === 'undefined') {
+ template = name;
+ name = undefined;
+ }
+
+ parsed = parser.parse(template);
+ if (name) {
+ mu.cache[name] = parsed;
+ }
+
+ callback(undefined, parsed); // this is to unify the API
+ return parsed;
+}
+
+mu.render = function (filename, view) {
+ var stream;
+
+ if (mu.cache[filename]) {
+ stream = new Stream();
+ process.nextTick(function () {
+ renderer.render(mu.cache[filename].tokens, view, mu.cache, stream, function () {
+ stream.emit('end');
+ });
+ });
+ return stream;
+ } else {
+ throw new Error('template_not_in_cache'); //errors.templateNotInCache(filename)));
+ }
+}
+
+mu.renderText = function (template, view, partials, callback) {
+ var name, parsed, tokens;
+
+ if (typeof callback === 'undefined') {
+ callback = partials;
+ partials = {};
+ }
+
+ partials = shallowCopy(partials);
+ partials.__proto__ = mu.cache;
+
+ for (name in partials) {
+ if (partials.hasOwnProperty(name) && !partials[name].tokens) {
+ partials[name] = parser.parse(partials[name]);
+ }
+ }
+
+ parsed = parser.parse(template);
+ tokens = parsed.tokens;
+
+ renderer.render(tokens, view, partials, callback);
+}
+
+
+/// Private API
+
+function shallowCopy(obj) {
+ var o = {};
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ o[key] = obj[key];
+ }
+ }
+
+ return o;
+}
View
14 lib/mu/errors.js
@@ -0,0 +1,14 @@
+exports.fileNotFound = function (root, filename, error) {
+ return {
+ rootError: error,
+ key: 'file_not_found',
+ message: 'File not found to compile: ' + path.join(root, filename)
+ }
+};
+
+exports.templateNotInCache = function (filename) {
+ return {
+ key: 'template_not_in_cache',
+ message: filename + ' was not found in mu\'s cache. Has it been compiled?'
+ }
+};
View
30 parser.js → lib/mu/parser.js
@@ -1,6 +1,7 @@
-var sys = require('sys');
+var sys = require('sys'),
+ Buffer = require('buffer').Buffer;
-exports.tokenize = function (template, options) {
+exports.parse = function (template, options) {
var parser = new Parser(template, options);
return parser.tokenize();
}
@@ -11,6 +12,7 @@ function Parser(template, options) {
this.sections = [];
this.tokens = ['multi'];
+ this.partials = [];
this.buffer = this.template;
this.state = 'static'; // 'static' or 'tag'
@@ -27,7 +29,7 @@ Parser.prototype = {
throw new Error('Encountered an unclosed section.');
}
- return this.tokens;
+ return {partials: this.partials, tokens: this.tokens};
},
setTag: function (tags) {
@@ -43,8 +45,11 @@ Parser.prototype = {
}
var content = this.buffer.substring(0, index);
+ buffer = new Buffer(Buffer.byteLength(content));
+
if (content !== '') {
- this.tokens.push(['static', content]);
+ buffer.write(content, 'utf8', 0);
+ this.tokens.push(['static', content, buffer]);
}
this.buffer = this.buffer.substring(index + this.otag.length);
@@ -86,6 +91,7 @@ Parser.prototype = {
case '>':
case '<':
this.tokens.push(['mustache', 'partial', content]);
+ this.partials.push(content);
break;
case '{':
@@ -126,8 +132,8 @@ Parser.prototype = {
break;
}
- this.buffer = remainder;
- this.state = 'static';
+ this.buffer = remainder;
+ this.state = 'static';
}
}
@@ -150,15 +156,3 @@ function e(text) {
return text.replace(arguments.callee.sRE, '\\$1');
}
-
-
-(function test () {
-
- var template =
- //"{{title}}: {{= <% %> }} <% top %> <%= {{ }} %> " +
- "Hello {{!dude}} {{name}} {{{cool}}} " +
- "{{#day}} {{^foo}}bar {{bar}}{{/foo}} {{/day}}!";
-
- sys.puts(sys.inspect(exports.tokenize(template), false, 10));
-
-}());
View
161 lib/mu/renderer.js
@@ -0,0 +1,161 @@
+var parser = require('./parser'),
+ sys = require('sys'),
+ baseProto = ({}).__proto__;
+
+exports.render = render;
+
+function render(tokens, context, partials, stream, callback) {
+ if (tokens[0] !== 'multi') {
+ throw new Error('WTF did you give me?');
+ }
+
+ var i = 1;
+
+ function next() {
+
+ if (stream.paused) {
+ stream.on('resume', function () {
+ process.nextTick(next);
+ });
+ return;
+ }
+
+ var token = tokens[i++];
+
+ if (!token) {
+ return callback ? callback() : true;
+ }
+
+ switch (token[0]) {
+ case 'static':
+ stream.write(token[2]);
+ return next();
+
+ case 'mustache':
+ switch (token[1]) {
+ case 'utag': // Unescaped Tag
+ stream.write(s(normalize(context, token[2])));
+ return next();
+
+ case 'etag': // Escaped Tag
+ stream.write(escape(s(normalize(context, token[2]))));
+ return next();
+
+ case 'section':
+ if (normalize(context, token[2])) {
+ return section(context, token[2], token[3], partials, stream, next);
+ } else {
+ return next();
+ }
+
+ case 'inverted_section':
+ if (!normalize(context, token[2])) {
+ return section(context, token[2], token[3], partials, stream, next);
+ } else {
+ return next();
+ }
+
+ case 'partial':
+ var partial = partials[token[2]];
+ if (partial) {
+ return render(partial.tokens, context, partials, stream, next);
+ } else {
+ return next();
+ }
+ }
+
+ }
+ }
+
+ next();
+}
+
+function s(val) {
+ return typeof val === 'undefined' ? '' : val.toString();
+}
+
+function escape(string) {
+ return string.replace(/[&<>"]/g, escapeReplace);
+}
+
+function normalize(view, name) {
+ var val = view[name];
+
+ if (typeof(val) === 'function') {
+ val = view[name]();
+ }
+
+ return val;
+}
+
+function section(view, name, tokens, partials, stream, callback) {
+ var val = normalize(view, name);
+
+ if (typeof val === 'boolean') {
+ return val ? render(tokens, val, partials, stream, callback) : callback();
+ }
+
+ if (val instanceof Array) {
+ var i = 0;
+
+ (function next() {
+ // if (stream.paused) {
+ // stream.on('resume', function () {
+ // next();
+ // });
+ // return;
+ // }
+
+ var item = val[i++];
+
+ if (item) {
+ var proto = insertProto(item, view);
+ render(tokens, item, partials, stream, next);
+ proto.__proto__ = baseProto;
+ } else {
+ callback();
+ }
+
+ }());
+
+ return;
+ }
+
+ if (typeof val === 'object') {
+ var proto = insertProto(val, view);
+ render(tokens, val, partials, stream, callback);
+ proto.__proto__ = baseProto;
+ return;
+ }
+
+ return callback();
+}
+
+
+//
+//
+//
+function insertProto(obj, newProto, replaceProto) {
+ replaceProto = replaceProto || baseProto;
+ var proto = obj.__proto__;
+ while (proto !== replaceProto) {
+ obj = proto;
+ proto = proto.__proto__;
+ }
+
+ obj.__proto__ = newProto;
+ return obj;
+}
+
+//
+//
+//
+function escapeReplace(char) {
+ switch (char) {
+ case '<': return '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ case '"': return '&quot;';
+ default: return char;
+ }
+}
View
85 lib/mu/stream.js
@@ -0,0 +1,85 @@
+var EventEmitter = require('events').EventEmitter;
+
+module.exports = Stream;
+
+function Stream() {
+ //this.events = {data: [], end: [], resume: [], error: []};
+
+ this.paused = false;
+ this.closed = false;
+ this.buffered = [];
+ this.emitter = new EventEmitter();
+}
+
+Stream.prototype = {
+ addListener: function (eventName, listener) {
+ this.emitter.on(eventName, listener);
+ return this;
+ },
+
+ removeListener: function (eventName, listener) {
+ this.emitter.removeListener(eventName, listener);
+ return this;
+ },
+
+ removeAllListeners: function (eventName) {
+ this.emitter.removeAllListeners(eventName);
+ return this;
+ },
+
+ emit: function (eventName, data) {
+ this.emitter.emit(eventName, data);
+ },
+
+ write: function (data) {
+ if (this.closed) return false;
+
+ if (this.paused) {
+ this.buffered.push(data);
+ return false;
+ } else {
+ this.emit('data', data);
+ return true;
+ }
+ },
+
+ pause: function () {
+ this.paused = true;
+ },
+
+ resume: function () {
+ var buffer
+
+ this.paused = false;
+ while (!this.paused && this.buffered.length) {
+ buffer = this.buffered.shift();
+ this.emit('data', buffer);
+ }
+
+ if (!this.paused) {
+ this.emit('resume');
+ this.removeAllListeners('resume');
+
+ if (!this.paused) {
+ if (this.closed) {
+ this.end();
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ end: function () {
+ this.closed = true;
+
+ if (this.buffered.length === 0) {
+ this.emit('end');
+ }
+ }
+};
+
+Stream.prototype.on = Stream.prototype.addListener;
+
View
300 mu.js
@@ -0,0 +1,300 @@
+var sys = require('sys'),
+ Buffer = require('buffer').Buffer;
+ baseProto = ({}).__proto__;
+
+exports.run = run;
+exports.tokenize = tokenize;
+
+function run(tokens, context, callback) {
+ if (tokens[0] !== 'multi') {
+ throw new Error('WTF did you give me?');
+ }
+
+ var i = 1;
+
+ function next() {
+ var token = tokens[i++];
+
+ if (!token) {
+ return callback ? callback() : sys.print("\n");
+ }
+
+ switch (token[0]) {
+ case 'static':
+ sys.print(token[2]);
+ next();
+ break;
+
+ case 'mustache':
+ switch (token[1]) {
+ case 'utag': // Unescaped Tag
+ sys.print(s(normalize(context, token[2])));
+ return next();
+
+ case 'etag': // Escaped Tag
+ sys.print(escape(s(normalize(context, token[2]))));
+ return next();
+
+ case 'section':
+ if (normalize(context, token[2])) {
+ return section(context, token[2], token[3], next);
+ } else {
+ return next();
+ }
+
+ case 'inverted_section':
+ if (!normalize(context, token[2])) {
+ return section(context, token[2], token[3], next);
+ } else {
+ return next();
+ }
+ }
+
+ }
+ }
+
+ next();
+}
+
+function s(val) {
+ return typeof val === 'undefined' ? '' : val.toString();
+}
+
+function escape(string) {
+ return string.replace(/[&<>"]/g, escapeReplace);
+}
+
+function normalize(view, name) {
+ var val = view[name];
+
+ if (typeof(val) === 'function') {
+ val = view[name]();
+ }
+
+ return val;
+}
+
+function section(view, name, tokens, callback) {
+ var val = normalize(view, name);
+
+ if (typeof val === 'boolean') {
+ return val ? run(tokens, val, callback) : callback();
+ }
+
+ if (val instanceof Array) {
+ var i = 0;
+
+ (function next() {
+ var item = val[i++];
+
+ if (item) {
+ var proto = insertProto(item, view);
+ run(tokens, item, next);
+ proto.__proto__ = baseProto;
+ } else {
+ callback();
+ }
+
+ }());
+
+ return;
+ }
+
+ if (typeof val === 'object') {
+ var proto = insertProto(val, view);
+ run(tokens, val, callback);
+ proto.__proto__ = baseProto;
+ return;
+ }
+
+ return callback();
+}
+
+
+
+//
+// Parser
+//
+
+function tokenize(template, options) {
+ var parser = new Parser(template, options);
+ return parser.tokenize();
+}
+
+function Parser(template, options) {
+ this.template = template;
+ this.options = options || {};
+
+ this.sections = [];
+ this.tokens = ['multi'];
+ this.buffer = this.template;
+ this.state = 'static'; // 'static' or 'tag'
+
+ this.setTag(['{{', '}}']);
+}
+
+Parser.prototype = {
+ tokenize: function () {
+ while (this.buffer) {
+ this.state === 'static' ? this.scanText() : this.scanTag();
+ }
+
+ if (this.sections.length) {
+ throw new Error('Encountered an unclosed section.');
+ }
+
+ return this.tokens;
+ },
+
+ setTag: function (tags) {
+ this.otag = tags[0] || '{{';
+ this.ctag = tags[1] || '}}';
+ },
+
+ scanText: function () {
+ var index = this.buffer.indexOf(this.otag);
+
+ if (index === -1) {
+ index = this.buffer.length;
+ }
+
+ var content = this.buffer.substring(0, index);
+ buffer = new Buffer(Buffer.byteLength(content));
+
+ if (content !== '') {
+ buffer.write(content, 'utf8', 0);
+ this.tokens.push(['static', content, buffer]);
+ }
+
+ this.buffer = this.buffer.substring(index + this.otag.length);
+ this.state = 'tag';
+ },
+
+ scanTag: function () {
+ var ctag = this.ctag,
+ matcher =
+ "^" +
+ "\\s*" + // Skip any whitespace
+
+ "(#|\\^|/|=|!|<|>|&|\\{)?" + // Check for a tag type and capture it
+ "\\s*" + // Skip any whitespace
+ "([^(?:\\}?" + e(ctag) + ")]+)" + // Capture the text inside of the tag
+ "\\s*" + // Skip any whitespace
+ "\\}?" + // Skip balancing '}' if it exists
+ e(ctag) + // Find the close of the tag
+
+ "(.*)$" // Capture the rest of the string
+ ;
+ matcher = new RegExp(matcher);
+
+ var match = this.buffer.match(matcher);
+
+ if (!match) {
+ throw new Error('Encountered an unclosed tag: "' + this.otag + this.buffer + '"');
+ }
+
+ var sigil = match[1],
+ content = match[2].trim(),
+ remainder = match[3];
+
+ switch (sigil) {
+ case undefined:
+ this.tokens.push(['mustache', 'etag', content]);
+ break;
+
+ case '>':
+ case '<':
+ this.tokens.push(['mustache', 'partial', content]);
+ break;
+
+ case '{':
+ case '&':
+ this.tokens.push(['mustache', 'utag', content]);
+ break;
+
+ case '!':
+ // Ignore comments
+ break;
+
+ case '=':
+ sys.puts("Changing tag: " + content)
+ this.setTag(content.split(' '));
+ break;
+
+ case '#':
+ case '^':
+ var type = sigil === '#' ? 'section' : 'inverted_section';
+ block = ['multi'];
+
+ this.tokens.push(['mustache', type, content, block]);
+ this.sections.push([content, this.tokens]);
+ this.tokens = block;
+ break;
+
+ case '/':
+ var res = this.sections.pop() || [],
+ name = res[0],
+ tokens = res[1];
+
+ this.tokens = tokens;
+ if (!name) {
+ throw new Error('Closing unopened ' + name);
+ } else if (name !== content) {
+ throw new Error("Unclosed section " + name);
+ }
+ break;
+ }
+
+ this.buffer = remainder;
+ this.state = 'static';
+
+ }
+}
+
+
+//
+// Used to escape RegExp strings
+//
+function e(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');
+}
+
+//
+//
+//
+function insertProto(obj, newProto, replaceProto) {
+ replaceProto = replaceProto || baseProto;
+ var proto = obj.__proto__;
+ while (proto !== replaceProto) {
+ obj = proto;
+ proto = proto.__proto__;
+ }
+
+ obj.__proto__ = newProto;
+ return obj;
+}
+
+//
+//
+//
+function escapeReplace(char) {
+ switch (char) {
+ case '<': return '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ case '"': return '&quot;';
+ default: return char;
+ }
+}
+
View
81 test.js
@@ -0,0 +1,81 @@
+var sys = require('sys'),
+ ctag = '}}',
+
+ matcher =
+ "^" +
+ "\\s*" + // Skip any whitespace
+
+ "(#|^|/|=|!|<|>|&|\\{)?" + // Check for a tag type and capture it
+ "\\s*" + // Skip any whitespace
+ "([^(?:\\}?" + e(ctag) + ")]+)" + // Capture the text inside of the tag
+ "\\s*" + // Skip any whitespace
+ "\\}?" + // Skip balancing '}' if it exists
+ e(ctag) + // Find the close of the tag
+
+ "(.*)$" // Capture the rest of the string
+ ;
+
+ matcher = new RegExp(matcher);
+
+ // balancedMather = new RegExp(
+ // "^" +
+ // "\\s*" + // Skip any whitespace
+ //
+ //
+ // '^\\s*(\\{)\\s*([^\\s}]*)\\s*\\}' + e(ctag) + '(.*)$'
+ // ),
+
+var tests = {
+ // " #foo }}} bar #baz.": [" #foo }}} bar #baz.", "#", "foo ", " bar #baz."],
+
+ " #foo }} bar #baz.": ["#", "foo ", " bar #baz."],
+ " foo }} bar #baz.": [undefined, "foo ", " bar #baz."],
+ "foo }} bar #baz.": [undefined, "foo ", " bar #baz."],
+ " foo}} bar #baz.": [undefined, "foo", " bar #baz."],
+ "foo}} bar #baz.": [undefined, "foo", " bar #baz."],
+ "foo bar}} bar #baz.": [undefined, "foo bar", " bar #baz."],
+
+ "{ foo }}} bar #baz.": ["{", "foo ", " bar #baz."],
+ "{foo}}} bar #baz.": ["{", "foo", " bar #baz."],
+ "&foo}} bar #baz.": ["&", "foo", " bar #baz."],
+
+ "foo}}}} bar {{baz}}.":[undefined, "foo", "} bar {{baz}}."],
+};
+
+
+for (var k in tests) {
+ assert(k, k.match(matcher), [k].concat(tests[k]));
+}
+
+sys.puts('All Tests passed')
+
+function assert(name, val1, val2) {
+ if (val1) {
+ for (var i = 0; i < val1.length; i++) {
+ if (val1[i] != val2[i]) {
+ throw new Error('[' + val1.join(',') + '] does not equal: [' + val2.join(',') + ']');
+ }
+ }
+ } else {
+ throw new Error("Matching '" + name + "' was null.");
+ }
+}
+
+//[ #foo }}} bar #baz.,, #foo }, bar #baz.,,,]
+//[ #foo }}} bar #baz.,#,foo , bar #baz.]
+
+
+function e(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');
+}
View
70 try.js
@@ -0,0 +1,70 @@
+var sys = require('sys'),
+ mu = require('./lib/mu');
+
+var template = "ÏHello{{#user}} {{name}}{{/user}}! " +
+ "Hello{{^user}} {{name}}{{/user}}! " +
+ "Names: {{#names}}{{name}} {{/names}} " +
+ "{{tag}} - {{{tag}}} " +
+ "{{goodness}} {{#admin}}(is admin){{/admin}}" +
+ "{{>foo.html}}" + '/\\/\\//';
+
+// TODO: Newlines break things
+
+//var tokens = mu.tokenize(template);
+var view = {
+ user: {name: "Jim"},
+ names: [
+ {name: 'Jim'},
+ {name: 'Ray'},
+ {name: 'Frank'}
+ ],
+ tag: "<html>",
+ goodness: Math.floor(Math.random() * 10),
+ admin: function () { return this.goodness > 5; }
+ };
+
+var partials = {
+ //'foo.html': 'Hello from foo.html.'
+}
+
+mu.compileText('foo.html', 'Hello from foo');
+mu.compileText('bar.html', template);
+
+// sys.puts(sys.inspect(tokens, false, 10));
+
+//mu.renderText(template, view, partials, function () {});
+
+// mu.render('bar.html', view, function (err, stream) {
+// if (err) {
+// console.log(err)
+// }
+//
+// stream
+// .on('data', function () {})
+// .on('end', function () {})
+// .on('error', function (err) {});
+// });
+
+var stream = mu.render('bar.html', view)
+ .on('data', function (data) {
+ sys.print(data);
+ //stream.pause();
+ //setTimeout(function () { stream.resume(); }, 1000);
+ })
+ .on('end', function () { sys.print('\nDONE\n'); })
+ .on('error', function (err) {});
+
+
+
+
+// mu.render(template, partials, view) # => Stream
+// mu.render()
+
+// mu.fs = function (resume) {
+// var file = read file;
+//
+// resume(file);
+// }
+
+
+// mu.render('foo.html', {name: 'Jim'});

0 comments on commit 1856828

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