Created
April 30, 2014 03:58
-
-
Save mrzafod/0965d99615db6b9e77c9 to your computer and use it in GitHub Desktop.
improvement for meteor-jade/plugin/compiler.js
This file contains 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
// ---------------------------------------------------------------------------- | |
// THIS ALLOWS YOU TO DEFINE JADE SYNTAX AS FOLLOW | |
// ---------------------------------------------------------------------------- | |
// rules: you can use a helper inside jade with brackets, you can pass any arguments (i.e. another helpers) inside brackets | |
// Samples: | |
// +if anyHelper(arg1, arg2, arg3, ...) | |
// div(class=anyHelper(arg1, arg2, arg3, ...)) | |
// h1= anyHelper(arg1, arg2, arg3, ...) | |
// h1 #{anyHelper(arg1, arg2, arg3, ...)} | |
// ---------------------------------------------------------------------------- | |
Compiler = function(tree, filename) { | |
var self = this; | |
self.tree = tree; | |
self.filename = filename; | |
self.head = null; | |
self.body = null; | |
self.templates = {}; | |
} | |
_.extend(Compiler.prototype, { | |
compile: function () { | |
var self = this; | |
self.visitBlock(self.tree, 0); | |
return { | |
head: self.head, | |
body: self.body, | |
templates: self.templates | |
} | |
}, | |
visitBlock: function (block, level) { | |
if (_.isUndefined(block) || _.isNull(block) || ! _.has(block, 'nodes')) | |
return []; | |
var self = this; | |
var buffer = []; | |
var nodes = block.nodes; | |
var currentNode, elseNode, stack; | |
for (var i = 0; i < nodes.length; i++) { | |
currentNode = nodes[i]; | |
// If the node is a Mixin (ie Component), we check if there are some | |
// `else if` and `else` blocks after it and if so, we groups thoses | |
// nodes by two with the following transformation: | |
// if a if a | |
// else if b else | |
// else => if b | |
// else | |
if (currentNode.type === "Mixin") { | |
// Create the stack [nodeIf, nodeElseIf..., nodeElse] | |
stack = []; | |
while (currentNode.name === "if" && nodes[i+1] && | |
nodes[i+1].type === "Mixin" && nodes[i+1].name === "else if") | |
stack.push(nodes[++i]); | |
if (nodes[i+1] && nodes[i+1].type === "Mixin" && | |
nodes[i+1].name === "else") | |
stack.push(nodes[++i]); | |
// Transform the stack | |
elseNode = stack.shift(); | |
if (elseNode && elseNode.name === "else if") { | |
elseNode.name = "if"; | |
elseNode = { | |
name: "else", | |
type: "Mixin", | |
block: { nodes: [elseNode].concat(stack) }, | |
call: false | |
} | |
} | |
} | |
buffer.push(self.visitNode(currentNode, elseNode, level + 1)); | |
} | |
return buffer; | |
}, | |
visitNode: function(node, elseNode, level) { | |
var self = this; | |
var attrs = self.visitAttributes(node.attrs); | |
var content = (node.code) ? self.visitCode(node.code) : | |
self.visitBlock(node.block, level); | |
var elseContent = self.visitBlock(elseNode && elseNode.block, level); | |
if (level === 1) | |
return self.registerRootNode(node, content); | |
else | |
return self['visit' + node.type](node, attrs, content, elseContent); | |
}, | |
visitCode: function(code) { | |
// XXX Need to improve this for "anonymous helpers" | |
return [ HTMLTools.Special(this.lookup(code.val, code.escape)) ]; | |
}, | |
// We interpret "Mixins" as "Components" | |
// Thanks to our customize Lexer, `if`, `unless`, `with` and `each` are | |
// retrieved as Mixins by the parser | |
visitMixin: function(node, attrs, content, elseContent) { | |
var self = this; | |
var componentName = node.name; | |
if (componentName === "else") | |
self.throwError("Unexpected else block", node); | |
var spacebarsSymbol = content.length === 0 ? ">" : "#"; | |
var args = (node.args || "").replace(/\(/ig, " ").replace(/\)/ig, " ").replace(/\,/ig, " "); | |
var mustache = "{{" + spacebarsSymbol + componentName + " " + args + "}}"; | |
var tag = Spacebars.TemplateTag.parse(mustache); | |
// Optimize arrays | |
if (content.length === 1) | |
tag.content = content[0]; | |
else if (content.length > 1) | |
tag.content = content; | |
if (elseContent.length === 1) | |
tag.elseContent = elseContent[0]; | |
else if (elseContent.length > 1) | |
tag.elseContent = elseContent; | |
return HTMLTools.Special(tag); | |
}, | |
visitTag: function(node, attrs, content) { | |
var self = this; | |
var tagName = node.name.toLowerCase(); | |
if (! HTML.isTagEnsured(tagName)) | |
self.throwError("Unknow tag: " + tagName, node); | |
if (! _.isEmpty(attrs)) | |
content.unshift(attrs); | |
return HTML[tagName.toUpperCase()].apply(null, content); | |
}, | |
visitText: function(node) { | |
var self = this; | |
return node.val ? self.parseText(node.val) : null; | |
}, | |
parseText: function(text) { | |
// The parser doesn't parse the #{expression} syntax. Let's do it. | |
// Since we rely on the Spacebars parser for this, we support the | |
// {{mustache}} syntax as well. | |
var self = this; | |
var jadeExpression = /#\{\s*((\.{1,2}\/)*[\w\.-]+)\s*\}/g; | |
text = text.replace(jadeExpression, "{{$1}}"); | |
return Spacebars.parse(text); | |
}, | |
visitComment: function (comment) { | |
// If buffer boolean is true we want to display this comment in the DOM | |
if (comment.buffer) | |
return HTML.Comment(comment.val); | |
}, | |
visitBlockComment: function (comment) { | |
var self = this; | |
comment.val = "\n" + _.pluck(comment.block.nodes, "val").join("\n") + "\n"; | |
return self.visitComment(comment); | |
}, | |
visitFilter: function (filter, attrs, content) { | |
var self = this; | |
if (Filters[filter.name]) | |
return self.parseText(Filters[filter.name](content.join("\n"))); | |
else | |
self.throwError("Unknowed filter " + filter.name, filter); | |
}, | |
visitAttributes: function (attrs) { | |
// The jade parser provide an attribute tree of this type: | |
// [{name: "class", val: "val1", escaped: true}, {name: "id" val: "val2"}] | |
// Let's transform that into: | |
// {"class": "val1", id: "val2"} | |
// Moreover if an "id" or "class" attribute is used more than once we need | |
// to concatenate the values. | |
if (_.isUndefined(attrs)) | |
return; | |
if (_.isString(attrs)) | |
return attrs; | |
var self = this; | |
var dict = {}; | |
var concatAttributes = function(a, b) { | |
if (_.isString(a) && _.isString(b)) | |
return a + b; | |
if (_.isUndefined(a)) | |
return b; | |
if (! _.isArray(a)) a = [a]; | |
if (! _.isArray(b)) b = [b]; | |
return a.concat(b); | |
}; | |
_.each(attrs, function (attr) { | |
var val = attr.val; | |
var key = attr.name; | |
// XXX We need a better handler for JavaScript code | |
if (/^('|")/.test(val) && val.slice(-1) === val.slice(0, 1)) | |
// First case this is a string | |
val = self.parseText(val.slice(1, -1)); | |
else if (val === true) | |
// For cases like <input required> Spacebars compiler expect required | |
// attriute to have the value `""` but Jade parser returns `true` | |
val = ""; | |
else | |
// Otherwise this is some code we need to evaluate | |
val = HTMLTools.Special(self.lookup(val, attr.escaped)); | |
if (key === "$dyn") | |
key = "$specials"; | |
// If a user has defined such kind of tag: div.myClass(class="myClass2") | |
// we need to concatenate classes (and ids) | |
if ((key === "class" || key === "id") && dict[key]) | |
val = [" ", val]; | |
dict[key] = concatAttributes(dict[key], val); | |
}); | |
return dict; | |
}, | |
lookup: function (val, escape) { | |
val = val.replace(/\(/ig, " ").replace(/\)/ig, " ").replace(/\,/ig, " "); | |
if (escape) | |
spacebarsSymbol = "{{" + val + "}}"; | |
else | |
spacebarsSymbol = "{{{" + val + "}}}"; | |
return Spacebars.TemplateTag.parse(spacebarsSymbol); | |
}, | |
registerRootNode: function(node, result) { | |
// XXX This is mostly the same code as the `templating` core package | |
// The `templating` package should be more generic to allow others templates | |
// engine to use its methods. | |
var self = this; | |
// Don't use an array if there is only one node | |
if (result.length === 1) | |
result = result[0]; | |
// Ignore top level comments | |
if (node.type === "Comment" || node.type === "BlockComment" || | |
node.type === "TAG" && _.isUndefined(node.name)) { | |
} | |
// Doctypes | |
else if (node.type === "Doctype") { | |
self.throwError("Meteor sets the doctype for you", node); | |
} | |
// There are two specials templates: head and body | |
else if (node.name === "body" || node.name === "head") { | |
var template = node.name; | |
if (self[template] !== null) | |
self.throwError(template + " is set twice", node); | |
if (node.attrs.length !== 0) | |
self.throwError("Attributes on " + template + " not supported", node); | |
self[template] = result; | |
} | |
// Templates | |
else if (node.name === "template") { | |
if (node.attrs.length !== 1 || node.attrs[0].name !== 'name') | |
self.throwError('Templates must only have a "name" attribute', node); | |
var name = self.visitAttributes(node.attrs).name; | |
if (name === "content") | |
self.throwError('Template can\'t be named "content"', node); | |
if (_.has(self.templates, name)) | |
self.throwError('Template "' + name + '" is set twice', node); | |
self.templates[name] = result; | |
} | |
// Otherwise this is an error, we do not allow tags, mixins, if, etc. | |
// outside templates | |
else | |
self.throwError(node.type + ' must be in a template', node); | |
}, | |
throwError: function (message, node) { | |
message = message || "Syntax error"; | |
if (node.line) | |
message += " on line " + node.line; | |
throw new Error(message); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment