Comparing version 1.6.0 to 2.0.0
444
compiler.js
"use strict"; | ||
var utilities = require("./utilities"); | ||
var CodeBlock = utilities.CodeBlock; | ||
function Scope() { | ||
this.used = {}; | ||
} | ||
var POSSIBLE_COMMENT = /\/\/|<!--/; | ||
Scope.prototype.createName = function(prefix) { | ||
var name; | ||
var i = 1; | ||
function addPossibleConflicts(possibleConflicts, code) { | ||
// It isn’t possible to refer to a local variable and create a conflict | ||
// in strict mode without clearly (or nearly so) specifying the variable’s name. | ||
// Since we won’t be using any name but output_*, other letter and digit | ||
// characters are not a concern. As for eval – that obviously isn’t possible to work around. | ||
var JS_IDENTIFIER = /(?:[a-zA-Z_]|\\u[0-9a-fA-F])(?:\w|\\u[0-9a-fA-F])*/g; | ||
do { | ||
name = "__" + prefix + i; | ||
i++; | ||
} while(this.used[name]); | ||
var m; | ||
this.used[name] = true; | ||
while (m = JS_IDENTIFIER.exec(code)) { | ||
possibleConflicts[JSON.parse('"' + m + '"')] = true; | ||
} | ||
} | ||
return name; | ||
}; | ||
function passThrough(compiler, context, node) { | ||
node.children.forEach(function(child) { | ||
compileNode(compiler, context, child); | ||
}); | ||
} | ||
Scope.prototype.generateCode = function() { | ||
var names = Object.keys(this.used); | ||
function Scope() { | ||
this.used = {}; | ||
} | ||
if(names.length === 0) { | ||
return ""; | ||
Scope.prototype.getName = function(name) { | ||
while (this.used.hasOwnProperty(name)) { | ||
name += "_"; | ||
} | ||
return "var " + names.join(", ") + ";"; | ||
this.used[name] = true; | ||
return name; | ||
}; | ||
@@ -38,42 +46,38 @@ | ||
var nodeHandlers = { | ||
root: function() {}, | ||
element: function(node, context) { | ||
if(!context.content) { | ||
throw node.unexpected; | ||
var transform = { | ||
root: passThrough, | ||
block: passThrough, | ||
element: function(compiler, context, node) { | ||
var name = node.name.toLowerCase(); | ||
var isVoid = voidTags.indexOf(name) !== -1; | ||
var newContext = { | ||
attributes: new CodeBlock().addText("<" + name), | ||
content: !isVoid && new CodeBlock(), | ||
classes: new CodeBlock() | ||
}; | ||
node.children.forEach(function(child) { | ||
compileNode(compiler, newContext, child); | ||
}); | ||
context.content.addBlock(newContext.attributes); | ||
if (newContext.classes.parts.length) { | ||
context.content.addText(" class=\""); | ||
context.content.addBlock(newContext.classes); | ||
context.content.addText("\""); | ||
} | ||
var isVoid = voidTags.indexOf(node.name) !== -1; | ||
context.content.addText(">"); | ||
return { | ||
attributes: new utilities.CodeContext(null, [ | ||
{ | ||
type: "text", | ||
value: "<" + node.name | ||
} | ||
]), | ||
content: isVoid ? null : new utilities.CodeContext(null), | ||
scope: context.scope, | ||
parent: context, | ||
done: function() { | ||
this.parent.content.addContext(this.attributes); | ||
this.parent.content.addText(">"); | ||
context.content.addBlock(newContext.content); | ||
if(!isVoid) { | ||
this.parent.content.addContext(this.content); | ||
this.parent.content.addText("</" + node.name + ">"); | ||
} | ||
} | ||
}; | ||
}, | ||
string: function(node, context) { | ||
if(!context.content) { | ||
throw node.unexpected; | ||
if (!isVoid) { | ||
context.content.addText("</" + name + ">"); | ||
} | ||
context.content.addContext(node.content); | ||
}, | ||
attribute: function(node, context) { | ||
if(!context.attributes) { | ||
throw node.unexpected; | ||
attribute: function(compiler, context, node) { | ||
if (!context.attributes) { | ||
throw new SyntaxError("Unexpected attribute"); // TODO: Where? | ||
} | ||
@@ -83,229 +87,199 @@ | ||
if(node.value !== null) { | ||
if (node.value) { | ||
context.attributes.addText("=\""); | ||
context.attributes.addContext(node.value.content); | ||
context.attributes.addBlock(node.value.value); | ||
context.attributes.addText("\""); | ||
} | ||
}, | ||
code: function(node, context) { | ||
return { | ||
content: new utilities.CodeContext(), | ||
scope: context.scope, | ||
parent: context, | ||
done: function() { | ||
this.parent.content.addCode(node.code.trimLeft() + "\n"); | ||
if(this.content.parts.length !== 0) { | ||
this.parent.content.addCode("{"); | ||
this.parent.content.addContext(this.content); | ||
this.parent.content.addCode("}\n"); | ||
} else { | ||
this.parent.content.addContext(this.content); | ||
for (var i = 0; i < node.value.value.parts.length; i++) { | ||
var part = node.value.value.parts[i]; | ||
if (part.type === "expression") { | ||
addPossibleConflicts(compiler.possibleConflicts, part.value); | ||
} | ||
} | ||
}; | ||
} | ||
}; | ||
} | ||
}, | ||
string: function(compiler, context, node) { | ||
context.content.addBlock(node.value); | ||
function adjustContext(context, node) { | ||
var handler = nodeHandlers[node.type]; | ||
for (var i = 0; i < node.value.parts.length; i++) { | ||
var part = node.value.parts[i]; | ||
if(!handler) { | ||
throw new Error("Unknown type: " + node.type); | ||
} | ||
if (part.type === "expression") { | ||
addPossibleConflicts(compiler.possibleConflicts, part.value); | ||
} | ||
} | ||
}, | ||
class: function(compiler, context, node) { | ||
if (!context.classes) { | ||
throw new SyntaxError("Unexpected class"); // TODO: Where? | ||
} | ||
return handler(node, context); | ||
} | ||
context.classes.addText(" " + node.value); | ||
}, | ||
code: function(compiler, context, node) { | ||
if (node.children.length) { | ||
context.content.addCode(node.code + (POSSIBLE_COMMENT.test(node.code) ? "\n{" : " {")); | ||
function compileNode(node, context) { | ||
var newContext = adjustContext(context, node); | ||
var children = node.children; | ||
var newContext = { | ||
content: context.content | ||
}; | ||
if(newContext) { | ||
context = newContext; | ||
} | ||
node.children.forEach(function(child) { | ||
compileNode(compiler, newContext, child); | ||
}); | ||
if(children) { | ||
for(var i = 0; i < children.length; i++) { | ||
var child = children[i]; | ||
compileNode(child, context); | ||
context.content.addCode("}"); | ||
} else { | ||
context.content.addCode(node.code + (POSSIBLE_COMMENT.test(node.code) ? "\n;" : ";")); | ||
} | ||
} | ||
if(newContext) { | ||
newContext.done(); | ||
} | ||
} | ||
addPossibleConflicts(compiler.possibleConflicts, node.code); | ||
}, | ||
include: function(compiler, context, node) { | ||
var subtree = compiler.options.load(node.template); | ||
function compile(tree) { | ||
var context = { | ||
content: new utilities.CodeContext(), | ||
scope: new Scope(), | ||
done: function() {} | ||
}; | ||
compileNode(compiler, context, subtree); | ||
}, | ||
if: function(compiler, context, node) { | ||
var condition = POSSIBLE_COMMENT.test(node.condition) ? node.condition + "\n" : node.condition; | ||
context.scope.used.__output = true; | ||
var newContext = { | ||
content: new CodeBlock(), | ||
attributes: context.attributes && new CodeBlock(), | ||
classes: context.classes && new CodeBlock() | ||
}; | ||
compileNode(tree, context); | ||
var elseContext; | ||
var staticContent = context.content.generateStatic(); | ||
node.children.forEach(function(child) { | ||
compileNode(compiler, newContext, child); | ||
}); | ||
if(staticContent !== null) { | ||
return function() { | ||
return staticContent; | ||
}; | ||
} | ||
if (node.elif.length) { | ||
node.else = { | ||
children: [ | ||
{ | ||
type: "if", | ||
condition: node.elif[0].condition, | ||
elif: node.elif.slice(1), | ||
else: node.else, | ||
children: node.elif[0].children | ||
} | ||
] | ||
}; | ||
} | ||
var functionBody = | ||
context.scope.generateCode() + | ||
"\n__output = '" + | ||
context.content.generateCode("text") + | ||
"\nreturn __output;"; | ||
if (node.else) { | ||
elseContext = { | ||
content: new CodeBlock(), | ||
attributes: context.attributes && new CodeBlock(), | ||
classes: context.classes && new CodeBlock() | ||
}; | ||
var compiled = new Function("__util, data", functionBody); | ||
node.else.children.forEach(function(child) { | ||
compileNode(compiler, elseContext, child); | ||
}); | ||
} | ||
return function(data) { | ||
return compiled(utilities, data); | ||
}; | ||
} | ||
var conditionName = compiler.scope.getName("condition"); | ||
(context.attributes || context.content).addCode("var " + conditionName + " = (" + condition + ");"); | ||
condition = conditionName; | ||
nodeHandlers.doctype = function(node, context) { | ||
return { | ||
parent: context, | ||
done: function() { | ||
this.parent.content.addText("<!DOCTYPE html>"); | ||
if (newContext.attributes && newContext.attributes.parts.length) { | ||
context.attributes.addCode("if (" + condition + ") {"); | ||
context.attributes.addBlock(newContext.attributes); | ||
context.attributes.addCode("}"); | ||
if (elseContext && elseContext.attributes.parts.length) { | ||
context.attributes.addCode("else {"); | ||
context.attributes.addBlock(elseContext.attributes); | ||
context.attributes.addCode("}"); | ||
} | ||
} | ||
}; | ||
}; | ||
nodeHandlers.include = function(node, context) { | ||
return { | ||
attributes: context.attributes, | ||
content: context.content, | ||
scope: context.scope, | ||
parent: context, | ||
done: function() {} | ||
}; | ||
}; | ||
if (newContext.classes && newContext.classes.parts.length) { | ||
context.classes.addCode("if (" + condition + ") {"); | ||
context.classes.addBlock(newContext.classes); | ||
context.classes.addCode("}"); | ||
nodeHandlers.block = function(node, context) { | ||
return { | ||
attributes: context.attributes, | ||
content: context.content, | ||
scope: context.scope, | ||
parent: context, | ||
done: function() {} | ||
}; | ||
}; | ||
if (elseContext && elseContext.classes.parts.length) { | ||
context.classes.addCode("else {"); | ||
context.classes.addBlock(elseContext.classes); | ||
context.classes.addCode("}"); | ||
} | ||
} | ||
nodeHandlers.extends = function(node, context) { | ||
return { | ||
parent: context, | ||
done: function() {} | ||
}; | ||
}; | ||
if (newContext.content.parts.length) { | ||
context.content.addCode("if (" + condition + ") {"); | ||
context.content.addBlock(newContext.content); | ||
context.content.addCode("}"); | ||
nodeHandlers.if = function(node, context) { | ||
var conditionName = context.scope.createName("condition"); | ||
if(node.elif.length > 0) { | ||
node.else = { | ||
children: [{ | ||
type: "if", | ||
condition: node.elif[0].condition, | ||
children: node.elif[0].children, | ||
elif: node.elif.slice(1), | ||
else: node.else | ||
}] | ||
if (elseContext && elseContext.content.parts.length) { | ||
context.content.addCode("else {"); | ||
context.content.addBlock(elseContext.content); | ||
context.content.addCode("}"); | ||
} | ||
} | ||
}, | ||
for: function(compiler, context, node) { | ||
var newContext = { | ||
content: context.content | ||
}; | ||
} | ||
return { | ||
attributes: new utilities.CodeContext(), | ||
content: new utilities.CodeContext(), | ||
scope: context.scope, | ||
parent: context, | ||
done: function() { | ||
var elseContext; | ||
var index = compiler.scope.getName("i"); | ||
var collectionName = compiler.scope.getName("collection"); | ||
var collection = POSSIBLE_COMMENT.test(node.collection) ? node.collection + "\n" : node.collection; | ||
if(node.else) { | ||
elseContext = { | ||
attributes: new utilities.CodeContext(), | ||
content: new utilities.CodeContext(), | ||
scope: context.scope, | ||
parent: context | ||
}; | ||
context.content.addCode("var " + collectionName + " = (" + collection + ");"); | ||
context.content.addCode("for (var " + index + " = 0; " + index + " < " + collectionName + ".length; " + index + "++) {"); | ||
context.content.addCode("var " + node.variable + " = " + collectionName + "[" + index + "];"); | ||
for(var i = 0; i < node.else.children.length; i++) { | ||
var child = node.else.children[i]; | ||
node.children.forEach(function(child) { | ||
compileNode(compiler, newContext, child); | ||
}); | ||
compileNode(child, elseContext); | ||
} | ||
} | ||
context.content.addCode("}"); | ||
} | ||
}; | ||
if(this.attributes.parts.length === 0 && (!node.else || elseContext.attributes.parts.length === 0)) { | ||
this.parent.content.addCode(conditionName + " = (" + node.condition + "\n);\n"); | ||
} else { | ||
this.parent.attributes.addCode(conditionName + " = (" + node.condition + "\n);\n"); | ||
this.parent.attributes.addCode("if(" + conditionName + ") {\n"); | ||
this.parent.attributes.addContext(this.attributes); | ||
this.parent.attributes.addCode("}\n"); | ||
function compileNode(compiler, context, node) { | ||
var transformer = transform[node.type]; | ||
if(node.else && elseContext.attributes.parts.length !== 0) { | ||
this.parent.attributes.addCode("else {\n"); | ||
this.parent.attributes.addContext(elseContext.attributes); | ||
this.parent.attributes.addCode("}\n"); | ||
} | ||
} | ||
if (!transformer) { | ||
throw new Error("Unknown node type " + node.type + "."); | ||
} | ||
if(this.content.parts.length !== 0 || node.else) { | ||
this.parent.content.addCode("if(" + conditionName + ") {\n"); | ||
this.parent.content.addContext(this.content); | ||
this.parent.content.addCode("}\n"); | ||
} | ||
transformer(compiler, context, node); | ||
} | ||
if(node.else && elseContext.content.parts.length !== 0) { | ||
this.parent.content.addCode("else {\n"); | ||
this.parent.content.addContext(elseContext.content); | ||
this.parent.content.addCode("}\n"); | ||
} | ||
function compile(tree, options) { | ||
var scope = new Scope(); | ||
this.scope.used[conditionName] = false; | ||
} | ||
var compiler = { | ||
scope: scope, | ||
possibleConflicts: scope.used, | ||
options: options | ||
}; | ||
}; | ||
nodeHandlers.for = function(node, context) { | ||
var indexName = context.scope.createName("index"); | ||
var collectionName = context.scope.createName("collection"); | ||
var context = { | ||
content: new CodeBlock() | ||
}; | ||
if(context.scope.used[node.variableName]) { | ||
throw new SyntaxError("Name " + node.variableName + " is in use in a containing scope."); // TODO: Rename variable (requires esprima and escodegen as dependencies) or use a wrapping function (slower). | ||
} | ||
compileNode(compiler, context, tree); | ||
context.scope.used[node.variableName] = true; | ||
var outputVariable = scope.getName("output"); | ||
return { | ||
content: new utilities.CodeContext(), | ||
scope: context.scope, | ||
parent: context, | ||
done: function() { | ||
this.parent.content.addCode( | ||
collectionName + " = (" + node.collection + "\n);\n" + | ||
"for(" + indexName + " = 0; " + indexName + " < " + collectionName + ".length; " + indexName + "++) {\n" + | ||
node.variableName + " = " + collectionName + "[" + indexName + "];\n" | ||
); | ||
this.parent.content.addContext(this.content); | ||
this.parent.content.addCode("}\n"); | ||
var code = | ||
"'use strict';\n\n" + | ||
utilities.escapeAttributeValue + "\n" + | ||
utilities.escapeContent + "\n\n" + | ||
"var " + outputVariable + " = '" + context.content.toCode(outputVariable, "text") + | ||
"\n\nreturn " + outputVariable + ";"; | ||
this.scope.used[node.variableName] = false; | ||
this.scope.used[collectionName] = false; | ||
this.scope.used[indexName] = false; | ||
} | ||
}; | ||
}; | ||
return new Function("data", code); | ||
} | ||
module.exports.constructor = { name: "razorleaf.compiler" }; | ||
module.exports.compile = compile; | ||
module.exports.utilities = utilities; | ||
module.exports.nodeHandlers = nodeHandlers; | ||
module.exports.transform = transform; |
{ | ||
"name": "razorleaf", | ||
"version": "1.6.0", | ||
"version": "2.0.0", | ||
"main": "razorleaf.js", | ||
@@ -10,9 +10,6 @@ "files": [ | ||
"utilities.js", | ||
"cli.js" | ||
"unicode.js" | ||
], | ||
"description": "A template engine for HTML", | ||
"keywords": ["template", "bulbasaur"], | ||
"bin": { | ||
"leaf": "cli.js" | ||
}, | ||
"keywords": ["template"], | ||
"repository": { | ||
@@ -26,8 +23,3 @@ "type": "git", | ||
"homepage": "https://charmander.me/razorleaf/", | ||
"licenses": [ | ||
{ | ||
"type": "CC0", | ||
"url": "https://creativecommons.org/publicdomain/zero/1.0/" | ||
} | ||
] | ||
"license": "MIT" | ||
} |
1189
parser.js
"use strict"; | ||
var utilities = require("./utilities"); | ||
var CodeBlock = utilities.CodeBlock; | ||
var IDENTIFIER_CHARACTER = /[\w-]/; | ||
var JS_IDENTIFIER_CHARACTER = /\w/; // Others are not included for simplicity’s sake. | ||
var HEX = /[\da-fA-F]/; | ||
var DIGIT = /\d/; | ||
var IDENTIFIER = /[\w-]/; | ||
var RECOGNIZABLE = /[!-~]/; | ||
var POSSIBLE_COMMENT = /\/\/|<!--/; | ||
var BLOCK_OR_TEMPLATE_NAME = /\S/; | ||
var JS_IDENTIFIER = /\w/; | ||
function addBlockAction(tree, blockName, action) { | ||
if(tree.blockActions.hasOwnProperty(blockName)) { | ||
tree.blockActions[blockName].push(action); | ||
} else { | ||
tree.blockActions[blockName] = [action]; | ||
var singleCharEscapes = { | ||
"\\": "\\\\", | ||
n: "\n", | ||
r: "\r", | ||
t: "\t", | ||
v: "\v", | ||
f: "\f", | ||
b: "\b", | ||
0: "\0" | ||
}; | ||
function isExpression(js) { | ||
try { | ||
new Function("'use strict'; (" + js + "\n)"); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
var specialBlocks = {}; | ||
function describe(c) { | ||
if (RECOGNIZABLE.test(c)) { | ||
return c; | ||
} | ||
return require("./unicode")[c.charCodeAt(0)] || JSON.stringify(c); | ||
} | ||
var states = { | ||
content: function(c) { | ||
if(c === " ") { | ||
return states.content; | ||
indent: function(parser, c) { | ||
if (c === null && parser.indentString) { | ||
parser.warn("Trailing whitespace"); | ||
return; | ||
} | ||
if(c === "\n") { | ||
this.indent = 0; | ||
if (c === "\n") { | ||
if (parser.indentString) { | ||
parser.warn("Whitespace-only line"); | ||
} | ||
parser.indentString = ""; | ||
return states.indent; | ||
} | ||
if(c === "#") { | ||
return states.comment; | ||
if (c === "\t") { | ||
if (parser.indentType && parser.indentType.indentCharacter !== "\t") { | ||
throw parser.error("Unexpected tab indent; indent was already determined to be " + parser.indentType.name + " by line " + parser.indentType.determined.line + ", character " + parser.indentType.determined.character); | ||
} | ||
} else if (c !== " ") { | ||
if (!parser.indentString) { | ||
parser.indent = 0; | ||
} else if (parser.indentType) { | ||
if (parser.indentType.indentCharacter === "\t") { | ||
var i = parser.indentString.indexOf(" "); | ||
parser.indent = i === -1 ? parser.indentString.length : i; | ||
} else { | ||
var level = parser.indentString.length / parser.indentType.spaces; | ||
if (level !== (level | 0)) { | ||
throw parser.error("Invalid indent level " + level + "; indent was determined to be " + parser.indentType.name + " by line " + parser.indentType.determined.line + ", character " + parser.indentType.determined.character); | ||
} | ||
parser.indent = level; | ||
} | ||
} else { | ||
parser.indent = 1; | ||
if (parser.indentString.charAt(0) === "\t") { | ||
parser.indentType = { | ||
indentCharacter: "\t", | ||
name: "one tab" | ||
}; | ||
} else { | ||
parser.indentType = { | ||
indentCharacter: " ", | ||
name: parser.indentString.length + " space" + (parser.indentString.length === 1 ? "" : "s"), | ||
spaces: parser.indentString.length | ||
}; | ||
} | ||
parser.indentType.determined = { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
}; | ||
} | ||
if (parser.indent > parser.context.indent + 1) { | ||
throw parser.error("Excessive indent " + parser.indent + "; expected " + (parser.context.indent + 1) + " or smaller"); | ||
} | ||
while (parser.context.indent >= parser.indent) { | ||
parser.context = parser.context.parent; | ||
} | ||
return states.content(parser, c); | ||
} | ||
if(c === "%") { | ||
this.context = { | ||
type: "code", | ||
code: "", | ||
parent: this.context, | ||
indent: this.indent, | ||
children: [], | ||
unexpected: this.error("Code here is not valid") | ||
}; | ||
this.context.parent.children.push(this.context); | ||
return states.code; | ||
parser.indentString += c; | ||
return states.indent; | ||
}, | ||
content: function(parser, c) { | ||
if (c === null) { | ||
return; | ||
} | ||
if(c === "\"") { | ||
this.context = { | ||
type: "string", | ||
content: new utilities.CodeContext("escapeContent"), | ||
current: "", | ||
parent: this.context, | ||
unterminated: this.error("Expected end of string before end of input, starting"), | ||
unexpected: this.error("A string here is not valid") | ||
}; | ||
this.context.parent.children.push(this.context); | ||
return states.string; | ||
if (parser.context.type === "attribute") { | ||
if (c === "!") { | ||
throw parser.error("Attributes cannot have raw strings for values"); | ||
} | ||
if (c !== " " && c !== '"') { | ||
parser.context = parser.context.parent; | ||
} | ||
} | ||
if(c === "!" && this.peek() === "\"") { | ||
this.skip(); | ||
this.context = { | ||
type: "string", | ||
content: new utilities.CodeContext(null), | ||
current: "", | ||
parent: this.context, | ||
unterminated: this.error("Expected end of string before end of input, starting"), | ||
unexpected: this.error("A string here is not valid") | ||
}; | ||
this.context.parent.children.push(this.context); | ||
if (c === "\n") { | ||
parser.indentString = ""; | ||
return states.indent; | ||
} | ||
if (c === " ") { | ||
return states.content; | ||
} | ||
if (c === ".") { | ||
parser.identifier = ""; | ||
return states.className; | ||
} | ||
if (c === "!") { | ||
parser.string = new CodeBlock(); | ||
parser.escapeFunction = null; | ||
return states.rawString; | ||
} | ||
if (c === '"') { | ||
parser.string = new CodeBlock(); | ||
parser.escapeFunction = parser.context.type === "attribute" ? "escapeAttributeValue" : "escapeContent"; | ||
return states.string; | ||
} | ||
if(IDENTIFIER_CHARACTER.test(c)) { | ||
this.context = { | ||
name: c, | ||
parent: this.context, | ||
indent: this.indent, | ||
unexpected: this.prepareError() | ||
}; | ||
this.context.parent.children.push(this.context); | ||
return states.identifier; | ||
if (c === "#") { | ||
return states.comment; | ||
} | ||
throw this.error("Unexpected " + c); | ||
if (c === "%") { | ||
parser.code = ""; | ||
return states.code; | ||
} | ||
if (IDENTIFIER.test(c)) { | ||
parser.identifier = ""; | ||
return states.identifier(parser, c); | ||
} | ||
throw parser.error("Unexpected " + describe(c)); | ||
}, | ||
code: function(c) { | ||
if(c === "\n") { | ||
this.indent = 0; | ||
return states.indent; | ||
comment: function(parser, c) { | ||
if (c === null || c === "\n") { | ||
return states.content(parser, c); | ||
} | ||
this.context.code += c; | ||
return states.comment; | ||
}, | ||
code: function(parser, c) { | ||
if (c === null || c === "\n") { | ||
parser.context = { | ||
type: "code", | ||
code: parser.code.trim(), | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
parser.context.parent.children.push(parser.context); | ||
return states.content(parser, c); | ||
} | ||
parser.code += c; | ||
return states.code; | ||
}, | ||
comment: function(c) { | ||
if(c === "\n") { | ||
this.indent = 0; | ||
return states.indent; | ||
identifier: function(parser, c) { | ||
if (c === ":") { | ||
return states.possibleAttribute; | ||
} | ||
return states.comment; | ||
if (c !== null && IDENTIFIER.test(c)) { | ||
parser.identifier += c; | ||
return states.identifier; | ||
} | ||
if (keywords.hasOwnProperty(parser.identifier)) { | ||
return keywords[parser.identifier](parser, c); | ||
} | ||
parser.context = { | ||
type: "element", | ||
name: parser.identifier, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
parser.context.parent.children.push(parser.context); | ||
return states.content(parser, c); | ||
}, | ||
indent: function(c) { | ||
if(c === "\n") { | ||
this.indent = 0; | ||
return states.indent; | ||
className: function(parser, c) { | ||
if (c !== null && IDENTIFIER.test(c)) { | ||
parser.identifier += c; | ||
return states.className; | ||
} | ||
if(c !== "\t") { | ||
while(this.indent <= this.context.indent) { | ||
this.context = this.context.parent; | ||
if (!parser.identifier) { | ||
throw parser.error("Expected class name"); | ||
} | ||
parser.context.children.push({ | ||
type: "class", | ||
value: parser.identifier, | ||
parent: parser.context, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}); | ||
if(this.indent > this.context.indent + 1) { | ||
throw this.error("Excessive indent"); | ||
return states.content(parser, c); | ||
}, | ||
possibleAttribute: function(parser, c) { | ||
if (c !== null && IDENTIFIER.test(c)) { | ||
parser.identifier += ":" + c; | ||
return states.identifier; | ||
} | ||
if (c === ":") { | ||
parser.identifier += ":"; | ||
return states.possibleAttribute; | ||
} | ||
parser.context = { | ||
type: "attribute", | ||
name: parser.identifier, | ||
value: null, | ||
parent: parser.context, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
return this.pass(states.content); | ||
parser.context.parent.children.push(parser.context); | ||
return states.content; | ||
}, | ||
rawString: function(parser, c) { | ||
if (c !== '"') { | ||
throw parser.error("Expected beginning quote of raw string, not " + describe(c)); | ||
} | ||
this.indent++; | ||
return states.indent; | ||
return states.string; | ||
}, | ||
string: function(c) { | ||
if(c === "\"") { | ||
if(this.context.current) { | ||
this.context.content.addText(this.context.current); | ||
string: function(parser, c) { | ||
if (c === null) { | ||
throw parser.error("Expected end of string before end of file"); | ||
} | ||
if (c === '"') { | ||
var string = { | ||
type: "string", | ||
value: parser.string, | ||
parent: parser.context, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
if (parser.context.type === "attribute") { | ||
parser.context.value = string; | ||
parser.context = parser.context.parent; | ||
} else { | ||
parser.context.children.push(string); | ||
} | ||
this.context = this.context.parent; | ||
return states.content; | ||
} | ||
if(c === "\\") { | ||
this.context.current += c; | ||
return states.escaped; | ||
if (c === "#") { | ||
return states.stringPound; | ||
} | ||
if(c === "#" && this.peek() === "{") { | ||
if(this.context.current) { | ||
this.context.content.addText(this.context.current); | ||
this.context.current = ""; | ||
} | ||
this.context = { | ||
type: "interpolation", | ||
value: "", | ||
parent: this.context, | ||
unterminated: this.error("Expected end of interpolated section before end of input, starting") | ||
}; | ||
this.skip(); | ||
return states.interpolation; | ||
if (c === "\\") { | ||
return states.escape; | ||
} | ||
this.context.current += c; | ||
if (parser.escapeFunction) { | ||
parser.string.addText(utilities[parser.escapeFunction](c)); | ||
} else { | ||
parser.string.addText(c); | ||
} | ||
return states.string; | ||
}, | ||
escaped: function(c) { | ||
this.context.current += c; | ||
return states.string; | ||
stringPound: function(parser, c) { | ||
if (c === "{") { | ||
parser.interpolation = ""; | ||
return states.interpolation; | ||
} | ||
parser.string.addText("#"); | ||
return states.string(parser, c); | ||
}, | ||
interpolation: function(c) { | ||
if(c === "\\") { | ||
if(this.peek() === "}") { | ||
this.skip(); | ||
this.context.value += "}"; | ||
return states.interpolation; | ||
} | ||
} else if(c === "}") { | ||
this.context.parent.content.addExpression(this.context.value); | ||
this.context = this.context.parent; | ||
interpolation: function(parser, c) { | ||
if (c === null) { | ||
throw parser.error("Interpolated section never resolves to a valid JavaScript expression"); // TODO: Where did it start? | ||
} | ||
if (c === "}" && isExpression(parser.interpolation)) { | ||
var interpolation = POSSIBLE_COMMENT.test(parser.interpolation) ? parser.interpolation + "\n" : parser.interpolation; | ||
parser.string.addExpression(parser.escapeFunction, interpolation); | ||
return states.string; | ||
} | ||
this.context.value += c; | ||
parser.interpolation += c; | ||
return states.interpolation; | ||
}, | ||
identifier: function(c) { | ||
if(c === ":") { | ||
if(!IDENTIFIER_CHARACTER.test(this.peek())) { | ||
this.context.type = "attribute"; | ||
this.context.value = null; | ||
this.context.unexpected = this.context.unexpected("An attribute here is not valid"); | ||
escape: function(parser, c) { | ||
if (c === null) { | ||
throw parser.error("Expected escape character"); | ||
} | ||
return states.attributeValue; | ||
} | ||
} else if(!IDENTIFIER_CHARACTER.test(c)) { | ||
this.context.type = "element"; | ||
this.context.children = []; | ||
this.context.unexpected = this.context.unexpected("An element here is not valid"); | ||
if (c === "#" || c === '"') { | ||
parser.string.addText(c); | ||
return states.string; | ||
} | ||
if(specialBlocks.hasOwnProperty(this.context.name)) { | ||
var specialBlock = specialBlocks[this.context.name]; | ||
if (c === "x") { | ||
return states.escapeX1; | ||
} | ||
specialBlock.begin.call(this); | ||
if (c === "u") { | ||
return states.escapeU1; | ||
} | ||
return this.pass(specialBlock.initialState); | ||
} | ||
if (singleCharEscapes.hasOwnProperty(c)) { | ||
parser.string.addText(singleCharEscapes[c]); | ||
return states.string; | ||
} | ||
return this.pass(states.content); | ||
// TODO: Allow LineTerminator to be escaped? | ||
return states.string(parser, c); | ||
}, | ||
escapeX1: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
this.context.name += c; | ||
return states.identifier; | ||
parser.charCode = parseInt(c, 16) << 4; | ||
return states.escapeX2; | ||
}, | ||
attributeValue: function(c) { | ||
if(c === " ") { | ||
return states.attributeValue; | ||
escapeX2: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
if(c === "!") { | ||
throw this.error("Attributes cannot have raw strings as values"); | ||
var escapedCharacter = String.fromCharCode(parser.charCode | parseInt(c, 16)); | ||
if (parser.escapeFunction) { | ||
parser.string.addText(utilities[parser.escapeFunction](escapedCharacter)); | ||
} else { | ||
parser.string.addText(escapedCharacter); | ||
} | ||
if(c === "\"") { | ||
var attribute = this.context; | ||
return states.string; | ||
}, | ||
escapeU1: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
attribute.value = this.context = { | ||
type: "string", | ||
content: new utilities.CodeContext("escapeAttributeValue"), | ||
current: "", | ||
parent: attribute.parent, | ||
unterminated: this.error("Expected end of string before end of input, starting") | ||
}; | ||
parser.charCode = parseInt(c, 16) << 12; | ||
return states.escapeU2; | ||
}, | ||
escapeU2: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
return states.string; | ||
parser.charCode |= parseInt(c, 16) << 8; | ||
return states.escapeU3; | ||
}, | ||
escapeU3: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
this.context = this.context.parent; | ||
parser.charCode |= parseInt(c, 16) << 4; | ||
return states.escapeU4; | ||
}, | ||
escapeU4: function(parser, c) { | ||
if (c === null || !HEX.test(c)) { | ||
throw parser.error("Expected hexadecimal digit"); | ||
} | ||
return this.pass(states.content); | ||
var escapedCharacter = String.fromCharCode(parser.charCode | parseInt(c, 16)); | ||
if (parser.escapeFunction) { | ||
parser.string.addText(utilities[parser.escapeFunction](escapedCharacter)); | ||
} else { | ||
parser.string.addText(escapedCharacter); | ||
} | ||
return states.string; | ||
} | ||
}; | ||
function parse(template) { | ||
var i; | ||
var c; | ||
var state = states.content; | ||
var keywords = { | ||
doctype: function(parser, c) { | ||
parser.context.children.push({ | ||
type: "string", | ||
value: new CodeBlock().addText("<!DOCTYPE html>"), | ||
parent: parser.context, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}); | ||
var line = 1; | ||
var lineStart = 0; | ||
return states.content(parser, c); | ||
}, | ||
include: function(parser, c) { | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
var root = { | ||
type: "root", | ||
children: [], | ||
includes: [], | ||
extends: null, | ||
blocks: {}, | ||
blockActions: {}, | ||
indent: -1 | ||
}; | ||
if (c !== null && BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.identifier = ""; | ||
return identifier(parser, c); | ||
} | ||
template += "\n"; | ||
throw parser.error("Expected name of included template, not " + describe(c)); | ||
}; | ||
var parser = { | ||
template: template, | ||
context: root, | ||
root: root, | ||
indent: 0, | ||
pass: function(state) { | ||
return state.call(parser, c); | ||
}, | ||
peek: function(count) { | ||
return count === undefined ? template.charAt(i + 1) : template.substr(i + 1, count); | ||
}, | ||
skip: function(count) { | ||
if(count === undefined) { | ||
count = 1; | ||
var identifier = function(parser, c) { | ||
if (c === null || !BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.context.children.push({ | ||
type: "include", | ||
template: parser.identifier, | ||
parent: parser.context, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}); | ||
return states.content(parser, c); | ||
} | ||
for(var j = 0; j < count; j++) { | ||
i++; | ||
parser.identifier += c; | ||
return identifier; | ||
}; | ||
if(template.charAt(i) === "\n") { | ||
parser.beginLine(); | ||
} | ||
return leadingWhitespace(parser, c); | ||
}, | ||
extends: function(parser, c) { | ||
if (parser.root.children.length || parser.root.extends) { | ||
throw parser.error("extends must appear first in a template"); | ||
} | ||
parser.root.children = { | ||
push: function() { | ||
throw parser.error("A template that extends another can only contain block actions directly"); | ||
} | ||
}, | ||
error: function(message) { | ||
var details = message + " at line " + line + ", character " + (i - lineStart + 1) + "."; | ||
}; | ||
return new SyntaxError(details); | ||
}, | ||
prepareError: function() { | ||
var location = " at line " + line + ", character " + (i - lineStart + 1) + "."; | ||
parser.root.blockActions = {}; | ||
return function(message) { | ||
return new SyntaxError(message + location); | ||
}; | ||
}, | ||
beginLine: function() { | ||
line++; | ||
lineStart = i + 1; | ||
} | ||
}; | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
for(i = 0; i < template.length; i++) { | ||
c = template.charAt(i); | ||
if (c !== null && BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.identifier = ""; | ||
return identifier(parser, c); | ||
} | ||
if(c === "\n") { | ||
parser.beginLine(); | ||
} | ||
throw parser.error("Expected name of parent template, not " + describe(c)); | ||
}; | ||
state = state.call(parser, c); | ||
} | ||
var identifier = function(parser, c) { | ||
if (c === null || !BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.root.extends = parser.identifier; | ||
switch(state) { | ||
case states.indent: | ||
break; | ||
return states.content(parser, c); | ||
} | ||
case states.string: | ||
case states.interpolation: | ||
throw parser.context.unterminated; | ||
parser.identifier += c; | ||
return identifier; | ||
}; | ||
default: | ||
// If this error is thrown, an extension to the parser has most likely parsed incorrectly. | ||
throw new Error("Parsing bug: expected final state to be indent."); | ||
} | ||
return leadingWhitespace(parser, c); | ||
}, | ||
block: function(parser, c) { | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
return root; | ||
} | ||
if (c !== null && BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.identifier = ""; | ||
return identifier(parser, c); | ||
} | ||
specialBlocks.doctype = { | ||
begin: function() { | ||
var parser = this; | ||
throw parser.error("Expected name of block, not " + describe(c)); | ||
}; | ||
this.context.type = "doctype"; | ||
delete this.context.name; | ||
var identifier = function(parser, c) { | ||
if (c === null || !BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
if (parser.root.blocks.hasOwnProperty(parser.identifier)) { | ||
throw parser.error("A block named “" + parser.identifier + "” has already been defined"); | ||
} | ||
this.context.children = { | ||
push: function() { | ||
throw parser.error("doctype element cannot have content"); | ||
parser.context = { | ||
type: "block", | ||
name: parser.identifier, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent | ||
}; | ||
parser.context.parent.children.push(parser.context); | ||
parser.root.blocks[parser.identifier] = parser.context; | ||
return states.content(parser, c); | ||
} | ||
parser.identifier += c; | ||
return identifier; | ||
}; | ||
return leadingWhitespace(parser, c); | ||
}, | ||
initialState: states.content | ||
}; | ||
replace: function(parser, c) { | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
specialBlocks.if = { | ||
begin: function() { | ||
this.context.type = "if"; | ||
this.context.condition = ""; | ||
this.context.elif = []; | ||
if (c !== null && BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
parser.identifier = ""; | ||
return identifier(parser, c); | ||
} | ||
delete this.context.name; | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.if.condition); | ||
} | ||
throw parser.error("Expected name of block to replace, not " + describe(c)); | ||
}; | ||
return whitespace; | ||
}, | ||
condition: function condition(c) { | ||
if(c === "\n") { | ||
return this.pass(states.content); | ||
} | ||
var identifier = function(parser, c) { | ||
if (c === null || !BLOCK_OR_TEMPLATE_NAME.test(c)) { | ||
var newBlock = { | ||
type: "block", | ||
name: parser.identifier, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent | ||
}; | ||
this.context.condition += c; | ||
return condition; | ||
} | ||
}; | ||
var action = function(block) { | ||
block.children = newBlock.children; | ||
}; | ||
specialBlocks.elif = { | ||
begin: function() { | ||
this.context.parent.children.pop(); | ||
this.context.type = "elif"; | ||
this.context.condition = ""; | ||
delete this.context.name; | ||
if (parser.root.blockActions.hasOwnProperty(parser.identifier)) { | ||
parser.root.blockActions[parser.identifier].push(action); | ||
} else { | ||
parser.root.blockActions[parser.identifier] = [action]; | ||
} | ||
var previous = this.context.parent.children[this.context.parent.children.length - 1]; | ||
parser.context = newBlock; | ||
if(!previous || previous.type !== "if" || previous.else) { | ||
throw this.error("Unexpected elif"); | ||
} | ||
return states.content(parser, c); | ||
} | ||
previous.elif.push(this.context); | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.elif.condition); | ||
} | ||
parser.identifier += c; | ||
return identifier; | ||
}; | ||
return whitespace; | ||
return leadingWhitespace(parser, c); | ||
}, | ||
condition: function condition(c) { | ||
if(c === "\n") { | ||
return this.pass(states.content); | ||
} | ||
if: function(parser, c) { | ||
var condition_ = ""; | ||
this.context.condition += c; | ||
return condition; | ||
} | ||
}; | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
specialBlocks.else = { | ||
begin: function() { | ||
this.context.parent.children.pop(); | ||
if (c === null) { | ||
throw parser.error("Expected condition, not end of file"); | ||
} | ||
var previous = this.context.parent.children[this.context.parent.children.length - 1]; | ||
return condition(parser, c); | ||
}; | ||
if(!previous || previous.type !== "if" || previous.else) { | ||
throw this.error("Unexpected else"); | ||
} | ||
var condition = function(parser, c) { | ||
if (c === null || c === "\n") { | ||
parser.context = { | ||
type: "if", | ||
condition: condition_, | ||
elif: [], | ||
else: null, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
previous.else = this.context; | ||
this.context.type = "else"; | ||
delete this.context.name; | ||
parser.context.parent.children.push(parser.context); | ||
return states.content(parser, c); | ||
} | ||
condition_ += c; | ||
return condition; | ||
}; | ||
return leadingWhitespace(parser, c); | ||
}, | ||
initialState: function() { | ||
return this.pass(states.content); | ||
} | ||
}; | ||
elif: function(parser, c) { | ||
var condition_ = ""; | ||
specialBlocks.for = { | ||
begin: function() { | ||
this.context.type = "for"; | ||
this.context.variableName = ""; | ||
this.context.collection = ""; | ||
delete this.context.name; | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.for.variableName); | ||
var previous = parser.context.children && parser.context.children[parser.context.children.length - 1]; | ||
if (!previous || previous.type !== "if" || previous.else) { | ||
throw parser.error("Unexpected elif"); | ||
} | ||
return whitespace; | ||
}, | ||
variableName: function variableName(c) { | ||
if(!JS_IDENTIFIER_CHARACTER.test(c)) { | ||
if(!this.context.variableName) { | ||
throw this.error("Expected variable name"); | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
if(c !== " " || this.peek(3) !== "in ") { | ||
throw this.error("Expected in"); | ||
if (c === null) { | ||
throw parser.error("Expected condition, not end of file"); | ||
} | ||
this.skip(3); | ||
return condition(parser, c); | ||
}; | ||
return specialBlocks.for.whitespace; | ||
} | ||
var condition = function(parser, c) { | ||
if (c === null || c === "\n") { | ||
var elif = { | ||
type: "elif", | ||
condition: condition_, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
this.context.variableName += c; | ||
return variableName; | ||
previous.elif.push(elif); | ||
parser.context = elif; | ||
return states.content(parser, c); | ||
} | ||
condition_ += c; | ||
return condition; | ||
}; | ||
return leadingWhitespace(parser, c); | ||
}, | ||
whitespace: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.for.collection); | ||
else: function(parser, c) { | ||
var previous = parser.context.children && parser.context.children[parser.context.children.length - 1]; | ||
if (!previous || previous.type !== "if" || previous.else) { | ||
throw parser.error("Unexpected else"); | ||
} | ||
return whitespace; | ||
previous.else = { | ||
type: "else", | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
parser.context = previous.else; | ||
return states.content(parser, c); | ||
}, | ||
collection: function collection(c) { | ||
if(c === "\n") { | ||
return this.pass(states.content); | ||
} | ||
for: function(parser, c) { | ||
var collection_ = ""; | ||
this.context.collection += c; | ||
return collection; | ||
} | ||
}; | ||
var leadingWhitespace = function(parser, c) { | ||
if (c === " ") { | ||
return leadingWhitespace; | ||
} | ||
specialBlocks.include = { | ||
begin: function() { | ||
var parser = this; | ||
if (c !== null && JS_IDENTIFIER.test(c)) { | ||
if (DIGIT.test(c)) { | ||
throw parser.error("Expected name of loop variable, not " + describe(c)); | ||
} | ||
this.context.type = "include"; | ||
this.context.template = ""; | ||
delete this.context.name; | ||
parser.identifier = ""; | ||
return identifier(parser, c); | ||
} | ||
this.context.children = { | ||
push: function() { | ||
throw parser.error("include element cannot have content"); | ||
throw parser.error("Expected name of loop variable, not " + describe(c)); | ||
}; | ||
var identifier = function(parser, c) { | ||
if (c === null || (!JS_IDENTIFIER.test(c) && c !== " ")) { | ||
throw parser.error("Expected in"); | ||
} | ||
if (c === " ") { | ||
return whitespace1; | ||
} | ||
parser.identifier += c; | ||
return identifier; | ||
}; | ||
this.root.includes.push(this.context); | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.include.template); | ||
} | ||
var whitespace1 = function(parser, c) { | ||
if (c === " ") { | ||
return whitespace1; | ||
} | ||
return whitespace; | ||
}, | ||
template: function template(c) { | ||
if(c === "\n") { | ||
return this.pass(states.content); | ||
} | ||
if (c === "o") { | ||
return of1; | ||
} | ||
this.context.template += c; | ||
return template; | ||
} | ||
}; | ||
throw parser.error("Expected of"); | ||
}; | ||
specialBlocks.block = { | ||
begin: function() { | ||
this.context.type = "block"; | ||
this.context.name = ""; | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
var duplicatesExistingNameError = this.prepareError(); | ||
this.context.duplicatesExistingName = function() { | ||
return duplicatesExistingNameError("A block named “" + this.name + "” already exists in this context"); | ||
}; | ||
var of1 = function(parser, c) { | ||
if (c === "f") { | ||
return of2; | ||
} | ||
return this.pass(specialBlocks.block.name); | ||
} | ||
throw parser.error("Expected of"); | ||
}; | ||
return whitespace; | ||
}, | ||
name: function name(c) { | ||
if(c === "\n") { | ||
if(this.root.blocks.hasOwnProperty(this.context.name)) { | ||
throw this.context.duplicatesExistingName(); | ||
var of2 = function(parser, c) { | ||
if (c === null) { | ||
throw parser.error("Expected loop collection expression"); | ||
} | ||
this.root.blocks[this.context.name] = this.context; | ||
return this.pass(states.content); | ||
} | ||
if (IDENTIFIER.test(c)) { | ||
throw parser.error("Expected of"); | ||
} | ||
this.context.name += c; | ||
return name; | ||
if (c === " ") { | ||
return whitespace2; | ||
} | ||
return collection(parser, c); | ||
}; | ||
var whitespace2 = function(parser, c) { | ||
if (c === null) { | ||
throw parser.error("Expected loop collection expression"); | ||
} | ||
if (c === " ") { | ||
return whitespace2; | ||
} | ||
return collection(parser, c); | ||
}; | ||
var collection = function(parser, c) { | ||
if (c === null || c === "\n") { | ||
parser.context = { | ||
type: "for", | ||
variable: parser.identifier, | ||
collection: collection_, | ||
parent: parser.context, | ||
children: [], | ||
indent: parser.indent, | ||
position: { | ||
line: parser.position.line, | ||
character: parser.position.character | ||
} | ||
}; | ||
parser.context.parent.children.push(parser.context); | ||
return states.content(parser, c); | ||
} | ||
collection_ += c; | ||
return collection; | ||
}; | ||
return leadingWhitespace(parser, c); | ||
} | ||
}; | ||
specialBlocks.replace = { | ||
begin: function() { | ||
this.context.parent.children.pop(); | ||
this.context.type = "replace-block"; | ||
this.context.name = ""; | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
var replacesNonExistentError = this.prepareError(); | ||
this.context.replacesNonExistentBlock = function() { | ||
return replacesNonExistentError("Block " + this.name + " does not exist in a parent template"); | ||
}; | ||
function parse(template, options) { | ||
var i; | ||
return this.pass(specialBlocks.replace.name); | ||
var eof = false; | ||
var root = { | ||
type: "root", | ||
children: [], | ||
indent: -1, | ||
extends: null, | ||
blockActions: null, | ||
blocks: {} | ||
}; | ||
var parser = Object.seal({ | ||
context: root, | ||
root: root, | ||
indentString: "", | ||
indent: null, | ||
indentType: null, | ||
identifier: null, | ||
raw: null, | ||
string: null, | ||
escapeFunction: null, | ||
interpolation: null, | ||
charCode: null, | ||
code: null, | ||
position: { | ||
line: 1, | ||
character: 0 | ||
}, | ||
error: function(message) { | ||
var where = eof ? "EOF" : "line " + parser.position.line + ", character " + parser.position.character; | ||
return new SyntaxError(message + " at " + where + " in " + options.name + "."); | ||
}, | ||
warn: function(message) { | ||
if (options.debug) { | ||
var where = eof ? "EOF" : "line " + parser.position.line + ", character " + parser.position.character; | ||
console.warn("⚠ %s at %s in %s.", message, where, options.name); | ||
} | ||
} | ||
}); | ||
return whitespace; | ||
}, | ||
name: function name(c) { | ||
var replaceBlock = this.context; | ||
var state = states.indent; | ||
if(c === "\n") { | ||
addBlockAction(this.root, replaceBlock.name, function(block) { | ||
block.children = replaceBlock.children; | ||
}); | ||
for (i = 0; i < template.length; i++) { | ||
var c = template.charAt(i); | ||
return this.pass(states.content); | ||
if (c === "\n") { | ||
parser.position.line++; | ||
parser.position.character = 0; | ||
} | ||
this.context.name += c; | ||
return name; | ||
state = state(parser, c); | ||
parser.position.character++; | ||
} | ||
}; | ||
specialBlocks.extends = { | ||
begin: function() { | ||
this.context.type = "extends"; | ||
eof = true; | ||
state(parser, null); | ||
if(this.root.extends !== null) { | ||
throw this.error("A template cannot extend more than one template"); | ||
} | ||
if (root.extends) { | ||
var parentTemplate = options.load(root.extends); | ||
var blockName; | ||
if(this.root.children.length !== 1) { | ||
throw this.error("extends must appear at the beginning of a template"); | ||
} | ||
for (blockName in root.blocks) { | ||
if (root.blocks.hasOwnProperty(blockName)) { | ||
if (parentTemplate.blocks.hasOwnProperty(blockName)) { | ||
throw new SyntaxError("Parent template " + root.extends + " already contains a block named “" + blockName + "”."); | ||
} | ||
this.root.extends = ""; | ||
delete this.context.name; | ||
}, | ||
initialState: function whitespace(c) { | ||
if(c !== " ") { | ||
return this.pass(specialBlocks.extends.template); | ||
parentTemplate.blocks[blockName] = root.blocks[blockName]; | ||
} | ||
} | ||
return whitespace; | ||
}, | ||
template: function template(c) { | ||
if(c === "\n") { | ||
return this.pass(states.content); | ||
for (blockName in root.blockActions) { | ||
if (root.blockActions.hasOwnProperty(blockName)) { | ||
if (!parentTemplate.blocks.hasOwnProperty(blockName)) { | ||
throw new SyntaxError("There is no block named “" + blockName + "”."); | ||
} | ||
var block = parentTemplate.blocks[blockName]; | ||
var actions = root.blockActions[blockName]; | ||
for (i = 0; i < actions.length; i++) { | ||
var action = actions[i]; | ||
action(block); | ||
} | ||
} | ||
} | ||
this.root.extends += c; | ||
return template; | ||
return parentTemplate; | ||
} | ||
}; | ||
return root; | ||
} | ||
module.exports.constructor = { name: "razorleaf.parser" }; | ||
module.exports.parse = parse; | ||
module.exports.states = states; | ||
module.exports.specialBlocks = specialBlocks; | ||
module.exports.keywords = keywords; |
114
razorleaf.js
@@ -6,98 +6,56 @@ "use strict"; | ||
function isContained(element) { | ||
while(element.parent) { | ||
element = element.parent; | ||
var path = require("path"); | ||
var fs = require("fs"); | ||
if(element.type === "block") { | ||
return true; | ||
} | ||
} | ||
function combine() { | ||
var result = {}; | ||
return false; | ||
} | ||
for (var i = 0; i < arguments.length; i++) { | ||
var obj = arguments[i]; | ||
function loadExtends(tree, visited, options) { | ||
if(tree.extends) { | ||
if(visited.indexOf(tree.extends) !== -1) { | ||
throw new Error("Circular extension: ⤷ " + visited.slice(visited.indexOf(tree.extends)).join(" → ") + " ⤴"); | ||
} | ||
for(var i = 0; i < tree.children.length; i++) { | ||
var child = tree.children[i]; | ||
if(child.type !== "extends" && child.type !== "replace-block") { | ||
throw child.unexpected; | ||
for (var k in obj) { | ||
if (obj.hasOwnProperty(k)) { | ||
result[k] = obj[k]; | ||
} | ||
} | ||
} | ||
var extendTree = parser.parse(options.include(tree.extends)); | ||
return result; | ||
} | ||
visited.push(tree.extends); | ||
var newTree = loadExtends(extendTree, visited, options); | ||
loadIncludes(extendTree, visited, options); | ||
visited.pop(); | ||
var defaults = { | ||
debug: false, | ||
name: "<Razor Leaf template>" | ||
}; | ||
for(var name in tree.blocks) { | ||
if(tree.blocks.hasOwnProperty(name)) { | ||
if(newTree.blocks.hasOwnProperty(name)) { | ||
throw tree.blocks[name].duplicatesExistingName(); | ||
} | ||
function compile(template, options) { | ||
options = combine(defaults, options); | ||
newTree.blocks[name] = tree.blocks[name]; | ||
} | ||
} | ||
for(var name in newTree.blocks) { | ||
if(tree.blockActions.hasOwnProperty(name)) { | ||
var block = newTree.blocks[name]; | ||
tree.blockActions[name].forEach(function(action) { | ||
action(block); | ||
}); | ||
} | ||
} | ||
return newTree; | ||
} | ||
return tree; | ||
var tree = parser.parse(template, options); | ||
return compiler.compile(tree, options); | ||
} | ||
function loadIncludes(tree, visited, options) { | ||
tree.includes.forEach(function(include) { | ||
if(visited.indexOf(include.template) !== -1) { | ||
throw new Error("Circular inclusion: ⤷ " + visited.slice(visited.indexOf(include.template)).join(" → ") + " ⤴"); | ||
function DirectoryLoader(root, options) { | ||
var loader = this; | ||
var loaderOptions = { | ||
load: function(name) { | ||
return parser.parse(loader.read(name), combine(defaults, loaderOptions, loader.options)); | ||
} | ||
}; | ||
var includeTree = parser.parse(options.include(include.template)); | ||
this.root = root; | ||
visited.push(include.template); | ||
tree = loadExtends(includeTree, visited, options); | ||
loadIncludes(includeTree, visited, options); | ||
visited.pop(); | ||
include.children = includeTree.children; | ||
}); | ||
this.options = combine(loaderOptions, options); | ||
} | ||
function compile(template, options) { | ||
var tree = parser.parse(template); | ||
DirectoryLoader.prototype.read = function(name) { | ||
return fs.readFileSync(path.join(this.root, name + ".leaf"), "utf-8"); | ||
}; | ||
tree = loadExtends(tree, [], options); | ||
loadIncludes(tree, [], options); | ||
DirectoryLoader.prototype.load = function(name) { | ||
return compile(this.read(name), this.options); | ||
}; | ||
for(var name in tree.blocks) { | ||
if(tree.blockActions.hasOwnProperty(name)) { | ||
var block = tree.blocks[name]; | ||
tree.blockActions[name].forEach(function(action) { | ||
action(block); | ||
}); | ||
} | ||
} | ||
return compiler.compile(tree); | ||
} | ||
module.exports.constructor = { name: "razorleaf" }; | ||
module.exports.compile = compile; | ||
module.exports.utilities = compiler.utilities; | ||
module.exports.DirectoryLoader = DirectoryLoader; |
@@ -1,7 +0,7 @@ | ||
[![Build Status](https://travis-ci.org/charmander/razorleaf.png)](https://travis-ci.org/charmander/razorleaf) | ||
![Status] | ||
Razor Leaf is a template engine for JavaScript with a convenient | ||
indentation-based syntax. It aims, like [Jade], to reduce the redundancy | ||
inherent in HTML — but with simpler rules, a sparser syntax, and a few further | ||
features not found in larger libraries. | ||
indentation-based syntax. It aims to reduce the redundancy inherent in HTML | ||
with simple rules, a sparse syntax, and a few further features not found | ||
in larger libraries. | ||
@@ -49,3 +49,3 @@ ## Syntax | ||
Strings can also contain interpolated sections, delimited by `#{` and `}`. | ||
Both delimiters can be escaped with a backslash. | ||
`#{` can be escaped with a leading backslash; `}` doesn’t require escaping. | ||
@@ -89,4 +89,3 @@ ``` | ||
Hierarchy in Razor Leaf is defined using indentation. Indentation *must* use | ||
tabs, and not spaces. For example: | ||
Hierarchy in Razor Leaf is defined using indentation. For example: | ||
@@ -134,3 +133,3 @@ ``` | ||
``` | ||
% if(i < 5) | ||
% if (i < 5) | ||
!"#{i}" | ||
@@ -142,4 +141,4 @@ ``` | ||
```javascript | ||
if(i < 5) { | ||
__output += i; | ||
if (i < 5) { | ||
output += i; | ||
} | ||
@@ -156,4 +155,4 @@ ``` | ||
- **`else`**: Can immediately follow an `if` or an `elif`. | ||
- **`for (identifier) in (collection)`**: Includes its content for each element | ||
in the array or array-like object *`collection`*. | ||
- **`for (identifier) of (collection)`**: Includes its content for each element | ||
of the array or array-like object *`collection`*. | ||
- **`include (name)`**: Loads and includes another template. | ||
@@ -168,2 +167,11 @@ - **`extends (name)`**: Loads another template and replaces its blocks. | ||
### `new razorleaf.DirectoryLoader(root, [options])` | ||
Creates a loader that maps template names to files with the `.leaf` extension | ||
in the directory located at *`root`*. | ||
#### `razorleaf.DirectoryLoader.prototype.load(name)` | ||
Returns a template object loaded from the root directory. | ||
### `razorleaf.compile(template, [options])` | ||
@@ -176,13 +184,6 @@ | ||
- **`include(name)`**: A function that should return the template represented | ||
by `name`, as given by any `include` statements in a template. This is | ||
optional if template inclusion is not used. | ||
- **`debug`**: If `true`, warnings will be printed. (In a later version, this will enable error rewriting.) | ||
- **`load(name)`**: A function that returns a parsed template represented by `name`. | ||
This is filled automatically by most loaders. | ||
## leaf | ||
`leaf` is a utility to compile static template files to HTML. It can currently | ||
be passed any number of paths to compile, and will write the result to an HTML | ||
file of the same name. (If the path ends in `.leaf`, it is replaced | ||
with `.html`.) | ||
[Jade]: http://jade-lang.com/ | ||
[Status]: https://charmander.me/razorleaf/status.svg |
208
utilities.js
"use strict"; | ||
var push = Array.prototype.push; | ||
var amp = /&/g; | ||
var quot = /"/g; | ||
var lt = /</g; | ||
var gt = />/g; | ||
function escapeLiteral(text) { | ||
return text.replace(/\\/g, "\\\\") | ||
.replace(/'/g, "\\'") | ||
.replace(/\r/g, "\\r") | ||
.replace(/\n/g, "\\n") | ||
.replace(/\u2028/g, "\\u2028") | ||
.replace(/\u2029/g, "\\u2029"); | ||
} | ||
var utilities = { | ||
escapeAttributeValue: function(value) { | ||
return ("" + value).replace(amp, "&") | ||
.replace(quot, """); | ||
}, | ||
escapeContent: function(content) { | ||
return ("" + content).replace(amp, "&") | ||
.replace(lt, "<") | ||
.replace(gt, ">"); | ||
}, | ||
CodeContext: CodeContext | ||
}; | ||
function escapeAttributeValue(value) { | ||
return ("" + value).replace(/&/g, "&") | ||
.replace(/"/g, """); | ||
} | ||
function escapeStringLiteral(string) { | ||
var result = ""; | ||
var escaped = false; | ||
for(var i = 0; i < string.length; i++) { | ||
var c = string.charAt(i); | ||
if(escaped) { | ||
escaped = false; | ||
result += c; | ||
} else if(c === "\\") { | ||
escaped = true; | ||
result += c; | ||
} else if(c === "\n") { | ||
result += "\\n"; | ||
} else if(c === "\r") { | ||
result += "\\r"; | ||
} else if(c === "\u2028") { | ||
result += "\\u2028"; | ||
} else if(c === "\u2029") { | ||
result += "\\u2029"; | ||
} else if(c === "'") { | ||
result += "\\'"; | ||
} else { | ||
result += c; | ||
} | ||
} | ||
return result; | ||
function escapeContent(value) { | ||
return ("" + value).replace(/&/g, "&") | ||
.replace(/</g, "<") | ||
.replace(/>/g, ">"); | ||
} | ||
function CodeContext(escapeFunction, initialParts) { | ||
this.parts = initialParts || []; | ||
this.escapeFunction = escapeFunction; | ||
function CodeBlock() { | ||
this.parts = []; | ||
} | ||
CodeContext.prototype.addCode = function(code) { | ||
this.parts.push({type: "code", value: code}); | ||
CodeBlock.prototype.addText = function(text) { | ||
this.parts.push({ | ||
type: "text", | ||
value: text | ||
}); | ||
return this; | ||
}; | ||
CodeContext.prototype.addText = function(text) { | ||
if(this.escapeFunction) { | ||
text = utilities[this.escapeFunction](text); | ||
} | ||
CodeBlock.prototype.addExpression = function(escapeFunction, expression) { | ||
this.parts.push({ | ||
type: "expression", | ||
escapeFunction: escapeFunction, | ||
value: expression | ||
}); | ||
this.parts.push({type: "text", value: text}); | ||
return this; | ||
}; | ||
CodeContext.prototype.addExpression = function(expression) { | ||
this.parts.push({type: "expression", value: expression, escapeFunction: this.escapeFunction}); | ||
}; | ||
CodeBlock.prototype.addCode = function(code) { | ||
this.parts.push({ | ||
type: "code", | ||
value: code | ||
}); | ||
CodeContext.prototype.addContext = function(context) { | ||
push.apply(this.parts, context.parts); | ||
return this; | ||
}; | ||
CodeContext.prototype.generateStatic = function() { | ||
var isStatic = function(part) { | ||
return part.type === "text"; | ||
}; | ||
CodeBlock.prototype.addBlock = function(block) { | ||
Array.prototype.push.apply(this.parts, block.parts); | ||
if(!this.parts.every(isStatic)) { | ||
return null; | ||
} | ||
return this.parts.map(function(part) { | ||
return part.value; | ||
}).join(""); | ||
return this; | ||
}; | ||
CodeContext.prototype.generateCode = function(initial) { | ||
var current = initial || "code"; | ||
var generated = ""; | ||
CodeBlock.prototype.toCode = function(outputVariable, initialState) { | ||
var code = ""; | ||
var currentType = initialState; | ||
for(var i = 0; i < this.parts.length; i++) { | ||
for (var i = 0; i < this.parts.length; i++) { | ||
var part = this.parts[i]; | ||
switch(part.type) { | ||
case "code": | ||
if(current === "text") { | ||
generated += "';\n"; | ||
} else if(current === "expression") { | ||
generated += ";\n"; | ||
} | ||
switch (part.type) { | ||
case "text": | ||
if (currentType === "code") { | ||
code += outputVariable + " += '"; | ||
} else if (currentType === "expression") { | ||
code += " + '"; | ||
} | ||
generated += part.value; | ||
current = "code"; | ||
code += escapeLiteral(part.value); | ||
currentType = "text"; | ||
break; | ||
break; | ||
case "text": | ||
if(current === "code") { | ||
generated += "__output += '"; | ||
} else if(current === "expression") { | ||
generated += " + '"; | ||
} | ||
case "expression": | ||
if (currentType === "code") { | ||
code += outputVariable + " += "; | ||
} else if (currentType === "expression") { | ||
code += " + "; | ||
} else { | ||
code += "' + "; | ||
} | ||
generated += escapeStringLiteral(part.value); | ||
current = "text"; | ||
if (part.escapeFunction) { | ||
code += part.escapeFunction + "((" + part.value + "))"; | ||
} else { | ||
code += "(" + part.value + ")"; | ||
} | ||
break; | ||
case "expression": | ||
if(current === "code") { | ||
generated += "__output += "; | ||
} else if(current === "text") { | ||
generated += "' + "; | ||
} else { | ||
generated += " + "; | ||
} | ||
currentType = "expression"; | ||
break; | ||
if(part.escapeFunction) { | ||
generated += "__util." + part.escapeFunction + "((" + part.value + "))"; | ||
} else { | ||
generated += "(" + part.value + ")"; | ||
} | ||
case "code": | ||
if (currentType === "text") { | ||
code += "';\n"; | ||
} else if (currentType === "expression") { | ||
code += ";\n"; | ||
} | ||
current = "expression"; | ||
code += part.value + "\n"; | ||
currentType = "code"; | ||
break; | ||
break; | ||
default: | ||
throw new Error("Unknown part type"); | ||
default: | ||
throw new Error("Unknown part type " + part.type + "."); | ||
} | ||
} | ||
if(current === "text") { | ||
generated += "';"; | ||
} else if(current === "expression") { | ||
generated += ";"; | ||
if (currentType === "text") { | ||
code += "';"; | ||
} else if (currentType === "expression") { | ||
code += ";"; | ||
} | ||
return generated; | ||
return code; | ||
}; | ||
module.exports = utilities; | ||
module.exports.constructor = { name: "razorleaf.utilities" }; | ||
module.exports.escapeAttributeValue = escapeAttributeValue; | ||
module.exports.escapeContent = escapeContent; | ||
module.exports.CodeBlock = CodeBlock; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
968535
25568
183
3