Created
July 1, 2013 19:52
-
-
Save rissem/5903967 to your computer and use it in GitHub Desktop.
create a meteor style templates from an html file cobbled together from http://github.com/meteor/meteor
This file contains hidden or 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
| fs = require("fs"); | |
| var Handlebars = {}; | |
| /* Our format: | |
| * | |
| * A 'template' is an array. Each element in it is either | |
| * - a literal string to echo | |
| * - an escaped substition: ['{', invocation] | |
| * - an unescaped substition: ['!', invocation] | |
| * - a (conditional or iterated) block: | |
| * ['#', invocation, template_a, template_b] | |
| * (the second template is optional) | |
| * - a partial: ['>', partial_name] (partial_name is a string) | |
| * | |
| * An 'invocation' is an array: one or more 'values', then an optional | |
| * hash (of which the keys are strings, and the values are 'values'.) | |
| * | |
| * An 'identifier' is: | |
| * - [depth, key, key, key..] | |
| * Eg, '../../a.b.c' would be [2, 'a', 'b', 'c']. 'a' would be [0, 'a']. | |
| * And 'this' or '.' would be [0]. | |
| * | |
| * A 'value' is either an identifier, or a string, int, or bool. | |
| * | |
| * You should provide a block helper 'with' since we will emit calls | |
| * to it (if the user passes the second 'context' argument to a | |
| * partial.) | |
| */ | |
| Handlebars.to_json_ast = function (code) { | |
| // We need handlebars and underscore, but this is bundle time, so | |
| // we load them using 'require'. | |
| // If we're in a unit test right now, we're actually in the server | |
| // run-time environment; we have '_' but not 'require'. | |
| // This is all very hacky. | |
| var req = (typeof require === 'undefined' ? | |
| Npm.require : require); | |
| var path = req('path'); | |
| var _ = req("underscore"); | |
| var ast = req("handlebars").parse(code); | |
| // Recreate Handlebars.Exception to properly report error messages | |
| // and stack traces. (https://github.com/wycats/handlebars.js/issues/226) | |
| makeHandlebarsExceptionsVisible(req); | |
| var identifier = function (node) { | |
| if (node.type !== "ID") | |
| throw new Error("got ast node " + node.type + " for identifier"); | |
| // drop node.isScoped. this is true if there was a 'this' or '.' | |
| // anywhere in the path. vanilla handlebars will turn off | |
| // helpers lookup if isScoped is true, but this is too restrictive | |
| // for us. | |
| var ret = [node.depth]; | |
| // we still want to turn off helper lookup if path starts with 'this.' | |
| // as in {{this.foo}}, which means it has to look different from {{foo}} | |
| // in our AST. signal the presence of 'this' in our AST using an empty | |
| // path segment. | |
| if (/^this\./.test(node.original)) | |
| ret.push(''); | |
| return ret.concat(node.parts); | |
| }; | |
| var value = function (node) { | |
| // Work around handlebars.js Issue #422 - Negative integers for | |
| // helpers get trapped as ID. handlebars doesn't support floating | |
| // point, just integers. | |
| if (node.type === 'ID' && /^-\d+$/.test(node.string)) { | |
| // Reconstruct node | |
| node.type = 'INTEGER'; | |
| node.integer = node.string; | |
| } | |
| var choices = { | |
| ID: function (node) {return identifier(node);}, | |
| STRING: function (node) {return node.string;}, | |
| INTEGER: function (node) {return +node.integer;}, | |
| BOOLEAN: function (node) {return (node.bool === 'true');} | |
| }; | |
| if (!(node.type in choices)) | |
| throw new Error("got ast node " + node.type + " for value"); | |
| return choices[node.type](node); | |
| }; | |
| var hash = function (node) { | |
| if (node.type !== "hash") | |
| throw new Error("got ast node " + node.type + " for hash"); | |
| var ret = {}; | |
| _.each(node.pairs, function (p) { | |
| ret[p[0]] = value(p[1]); | |
| }); | |
| return ret; | |
| }; | |
| var invocation = function (node) { | |
| if (node.type !== "mustache") | |
| throw new Error("got ast node " + node.type + " for invocation"); | |
| var ret = [node.id]; | |
| ret = ret.concat(node.params); | |
| ret = _.map(ret, value); | |
| if (node.hash) | |
| ret.push(hash(node.hash)); | |
| return ret; | |
| }; | |
| var template = function (nodes) { | |
| var ret = []; | |
| if (!nodes) | |
| return []; | |
| var choices = { | |
| mustache: function (node) { | |
| ret.push([node.escaped ? '{' : '!', invocation(node)]); | |
| }, | |
| partial: function (node) { | |
| var id = identifier(node.id); | |
| if (id.length !== 2 || id[0] !== 0) | |
| // XXX actually should just get the literal string the | |
| // entered, and avoid identifier parsing | |
| throw new Error("Template names shouldn't contain '.' or '/'"); | |
| var x = ['>', id[1]]; | |
| if (node.context) | |
| x = ['#', [[0, 'with'], identifier(node.context)], [x]]; | |
| ret.push(x); | |
| }, | |
| block: function (node) { | |
| var x = ['#', invocation(node.mustache), | |
| template(node.program.statements)]; | |
| if (node.program.inverse) | |
| x.push(template(node.program.inverse.statements)); | |
| ret.push(x); | |
| }, | |
| inverse: function (node) { | |
| ret.push(['#', invocation(node.mustache), | |
| node.program.inverse && | |
| template(node.program.inverse.statements) || [], | |
| template(node.program.statements)]); | |
| }, | |
| content: function (node) {ret.push(node.string);}, | |
| comment: function (node) {} | |
| }; | |
| _.each(nodes, function (node) { | |
| if (!(node.type in choices)) | |
| throw new Error("got ast node " + node.type + " in template"); | |
| choices[node.type](node); | |
| }); | |
| return ret; | |
| }; | |
| if (ast.type !== "program") | |
| throw new Error("got ast node " + node.type + " at toplevel"); | |
| return template(ast.statements); | |
| }; | |
| var makeHandlebarsExceptionsVisible = function (req) { | |
| req("handlebars").Exception = function(message) { | |
| this.message = message; | |
| // In Node, if we don't do this we don't see the message displayed | |
| // nor the right stack trace. | |
| Error.captureStackTrace(this, arguments.callee); | |
| }; | |
| req("handlebars").Exception.prototype = new Error(); | |
| req("handlebars").Exception.prototype.name = 'Handlebars.Exception'; | |
| }; | |
| var html_scanner = { | |
| // Scan a template file for <head>, <body>, and <template> | |
| // tags and extract their contents. | |
| // | |
| // This is a primitive, regex-based scanner. It scans | |
| // top-level tags, which are allowed to have attributes, | |
| // and ignores top-level HTML comments. | |
| scan: function (contents, source_name) { | |
| var rest = contents; | |
| var index = 0; | |
| var advance = function(amount) { | |
| rest = rest.substring(amount); | |
| index += amount; | |
| }; | |
| var parseError = function(msg) { | |
| var lineNumber = contents.substring(0, index).split('\n').length; | |
| var line = contents.split('\n')[lineNumber - 1]; | |
| var info = "line "+lineNumber+", file "+source_name + "\n" + line; | |
| return new Error((msg || "Parse error")+" - "+info); | |
| }; | |
| var results = html_scanner._initResults(); | |
| var rOpenTag = /^((<(template|head|body)\b)|(<!--)|(<!DOCTYPE|{{!)|$)/i; | |
| while (rest) { | |
| // skip whitespace first (for better line numbers) | |
| advance(rest.match(/^\s*/)[0].length); | |
| var match = rOpenTag.exec(rest); | |
| if (! match) | |
| throw parseError(); // unknown text encountered | |
| var matchToken = match[1]; | |
| var matchTokenTagName = match[3]; | |
| var matchTokenComment = match[4]; | |
| var matchTokenUnsupported = match[5]; | |
| advance(match.index + match[0].length); | |
| if (! matchToken) | |
| break; // matched $ (end of file) | |
| if (matchTokenComment === '<!--') { | |
| // top-level HTML comment | |
| var commentEnd = /--\s*>/.exec(rest); | |
| if (! commentEnd) | |
| throw parseError("unclosed HTML comment"); | |
| advance(commentEnd.index + commentEnd[0].length); | |
| continue; | |
| } | |
| if (matchTokenUnsupported) { | |
| switch (matchTokenUnsupported.toLowerCase()) { | |
| case '<!doctype': | |
| throw parseError( | |
| "Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)"); | |
| case '{{!': | |
| throw new parseError( | |
| "Can't use '{{! }}' outside a template. Use '<!-- -->'."); | |
| } | |
| throw new parseError(); | |
| } | |
| // otherwise, a <tag> | |
| var tagName = matchTokenTagName.toLowerCase(); | |
| var tagAttribs = {}; // bare name -> value dict | |
| var rTagPart = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/; | |
| var attr; | |
| // read attributes | |
| while ((attr = rTagPart.exec(rest))) { | |
| var attrToken = attr[1]; | |
| var attrKey = attr[3]; | |
| var attrValue = attr[5]; | |
| advance(attr.index + attr[0].length); | |
| if (attrToken === '>') | |
| break; | |
| // XXX we don't HTML unescape the attribute value | |
| // (e.g. to allow "abcd"efg") or protect against | |
| // collisions with methods of tagAttribs (e.g. for | |
| // a property named toString) | |
| attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim | |
| tagAttribs[attrKey] = attrValue; | |
| } | |
| if (! attr) // didn't end on '>' | |
| throw new parseError("Parse error in tag"); | |
| // find </tag> | |
| var end = (new RegExp('</'+tagName+'\\s*>', 'i')).exec(rest); | |
| if (! end) | |
| throw new parseError("unclosed <"+tagName+">"); | |
| var tagContents = rest.slice(0, end.index); | |
| advance(end.index + end[0].length); | |
| // act on the tag | |
| html_scanner._handleTag(results, tagName, tagAttribs, tagContents, | |
| parseError); | |
| } | |
| return results; | |
| }, | |
| _initResults: function() { | |
| var results = {}; | |
| results.head = ''; | |
| results.body = ''; | |
| results.js = ''; | |
| return results; | |
| }, | |
| _handleTag: function (results, tag, attribs, contents, parseError) { | |
| // trim the tag contents. | |
| // this is a courtesy and is also relied on by some unit tests. | |
| contents = contents.match(/^[ \t\r\n]*([\s\S]*?)[ \t\r\n]*$/)[1]; | |
| // do we have 1 or more attribs? | |
| var hasAttribs = false; | |
| for(var k in attribs) { | |
| if (attribs.hasOwnProperty(k)) { | |
| hasAttribs = true; | |
| break; | |
| } | |
| } | |
| if (tag === "head") { | |
| if (hasAttribs) | |
| throw parseError("Attributes on <head> not supported"); | |
| results.head += contents; | |
| return; | |
| } | |
| // <body> or <template> | |
| var code = 'Handlebars.json_ast_to_func(' + | |
| JSON.stringify(Handlebars.to_json_ast(contents)) + ')'; | |
| if (tag === "template") { | |
| var name = attribs.name; | |
| if (! name) | |
| throw parseError("Template has no 'name' attribute"); | |
| results.js += "Meteor._def_template(" + JSON.stringify(name) + "," | |
| + code + ");\n"; | |
| } else { | |
| // <body> | |
| if (hasAttribs) | |
| throw parseError("Attributes on <body> not supported"); | |
| results.js += "Meteor.startup(function(){" + | |
| "document.body.appendChild(Spark.render(" + | |
| "Meteor._def_template(null," + code + ")));});"; | |
| } | |
| } | |
| }; | |
| var contents = fs.readFileSync("/tmp/test.html", "utf-8"); | |
| var results = html_scanner.scan(contents, "test.html"); | |
| console.log("RESULTS", results); | |
| // If we are running at bundle time, set module.exports. | |
| // For unit testing in server environment, don't. | |
| if (typeof module !== 'undefined') | |
| module.exports = html_scanner; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment