Fixes #349 - Inline Marko template compilation support

Also changed how JavaScript code is generated
This commit is contained in:
Patrick Steele-Idem 2016-08-15 14:54:13 -06:00
parent 78a73e573e
commit c386da875e
164 changed files with 1803 additions and 1380 deletions

View File

@ -23,7 +23,6 @@ var Text = require('./ast/Text');
var ForEach = require('./ast/ForEach');
var ForEachProp = require('./ast/ForEachProp');
var ForRange = require('./ast/ForRange');
var Slot = require('./ast/Slot');
var HtmlComment = require('./ast/HtmlComment');
var SelfInvokingFunction = require('./ast/SelfInvokingFunction');
var ForStatement = require('./ast/ForStatement');
@ -72,6 +71,7 @@ var literalUndefined = new Literal({value: undefined});
var literalTrue = new Literal({value: true});
var literalFalse = new Literal({value: false});
var identifierOut = new Identifier({name: 'out'});
var identifierRequire = new Identifier({name: 'require'});
class Builder {
arrayExpression(elements) {
@ -266,6 +266,11 @@ class Builder {
}
}
htmlLiteral(htmlCode) {
var argument = new Literal({value: htmlCode});
return new Html({argument});
}
identifier(name) {
ok(typeof name === 'string', '"name" should be a string');
@ -425,7 +430,7 @@ class Builder {
require(path) {
path = makeNode(path);
let callee = 'require';
let callee = identifierRequire;
let args = [ path ];
return new FunctionCall({callee, args});
}
@ -462,10 +467,6 @@ class Builder {
return new SelfInvokingFunction({params, args, body});
}
slot(onDone) {
return new Slot({onDone});
}
strictEquality(left, right) {
left = makeNode(left);
right = makeNode(right);

View File

@ -5,10 +5,9 @@ const Node = require('./ast/Node');
const Literal = require('./ast/Literal');
const Identifier = require('./ast/Identifier');
const HtmlElement = require('./ast/HtmlElement');
const Html = require('./ast/Html');
const ok = require('assert').ok;
const Container = require('./ast/Container');
const util = require('util');
const isValidJavaScriptVarName = require('./util/isValidJavaScriptVarName');
const createError = require('raptor-util/createError');
class GeneratorEvent {
@ -19,93 +18,45 @@ class GeneratorEvent {
this.isBefore = true;
this.builder = codegen.builder;
this.context = codegen.context;
this.insertedNodes = null;
}
insertCode(newCode) {
this.codegen.generateStatements(newCode);
if (this.isBefore) {
if (!this.codegen._code.endsWith(this.codegen.currentIndent)) {
this.codegen.writeLineIndent();
}
}
this.insertedNodes = newCode;
}
}
class Slot {
constructor(codegen, slotNode) {
this._content = null;
this._start = codegen._code.length;
codegen.write('/* slot */');
if (slotNode.statement) {
codegen.write('\n');
}
this._end = codegen._code.length;
this.currentIndent = codegen.currentIndent;
this._inFunction = codegen.inFunction;
this._statement = slotNode.statement;
class FinalNodes {
constructor() {
this.nodes = [];
this.nodes._finalNode = true; // Mark the array as a collection of final nodes
this.lastNode = null;
}
setContent(content) {
this._content = content;
push(node) {
if (!node) {
return;
}
generateCode(codegen) {
let content = this._content;
let slotCode;
if (content) {
let isStatement = this._statement;
codegen.currentIndent = this.currentIndent;
codegen.inFunction = this._inFunction;
let capture = codegen._beginCaptureCode();
if (isStatement) {
codegen.generateStatements(content);
} else {
codegen.generateCode(content);
if (node instanceof Html && this.lastNode instanceof Html) {
this.lastNode.append(node);
return;
}
slotCode = capture.end();
if (isStatement && slotCode.startsWith(codegen.currentIndent)) {
slotCode = slotCode.substring(codegen.currentIndent.length);
}
if (node.setFinalNode) {
node.setFinalNode(true);
}
let oldCode = codegen._code;
let beforeCode = oldCode.substring(0, this._start);
let afterCode = oldCode.substring(this._end);
if (slotCode) {
codegen._code = beforeCode + slotCode + afterCode;
} else {
let beforeWhitespaceMatches = beforeCode.match(/[\n]\s*$/);
if (beforeWhitespaceMatches != null) {
let beforeWhitespace = beforeWhitespaceMatches[0];
if (afterCode.startsWith(beforeWhitespace)) {
afterCode = afterCode.substring(beforeWhitespace.length);
}
}
codegen._code = beforeCode + afterCode;
}
this.lastNode = node;
this.nodes.push(node);
}
}
class Generator {
class CodeGenerator {
constructor(context, options) {
options = options || {};
this.root = null;
this._indentStr = options.indent != null ? options.indent : ' ';
this._indentSize = this._indentStr.length;
this._code = '';
this.currentIndent = '';
@ -113,7 +64,7 @@ class Generator {
this._doneListeners = [];
this._bufferedWrites = null;
this.builder = context.builder;
this.outputType = options.output || 'html';
this.context = context;
@ -124,16 +75,6 @@ class Generator {
this.outputType.charAt(0).toUpperCase() +
this.outputType.substring(1) +
'Code';
this._slots = [];
}
beginSlot(slotNode) {
var addSeparator = slotNode.statement;
this._flushBufferedWrites(addSeparator);
let slot = new Slot(this, slotNode);
this._slots.push(slot);
return slot;
}
addVar(name, value) {
@ -156,55 +97,13 @@ class Generator {
return this.context.importModule(varName, path);
}
generateCode(node) {
ok(node != null, '"node" is required');
if (typeof node === 'string' ||
typeof node === 'number' ||
typeof node === 'boolean') {
this.write(node);
return;
} else if (isArray(node)) {
node.forEach(this.generateCode, this);
return;
} else if (node instanceof Container) {
node.forEach((child) => {
if (child.container === node) {
this.generateCode(child);
}
});
return;
}
let oldCurrentNode = this._currentNode;
this._currentNode = node;
let finalNode;
let generateCodeFunc;
var isStatement = node.statement;
var beforeAfterEvent;
if (node.listenerCount('beforeGenerateCode') || node.listenerCount('afterGenerateCode')) {
beforeAfterEvent = new GeneratorEvent(node, this);
}
var isWhitespacePreserved = node.isPreserveWhitespace();
if (beforeAfterEvent) {
beforeAfterEvent.isBefore = true;
beforeAfterEvent.node.emit('beforeGenerateCode', beforeAfterEvent);
if (isWhitespacePreserved) {
this.context.beginPreserveWhitespace();
}
}
if (node.getCodeGenerator) {
generateCodeFunc = node.getCodeGenerator(this.outputType);
if (generateCodeFunc) {
_invokeCodeGenerator(func, node, isMethod) {
try {
finalNode = generateCodeFunc(node, this);
if (isMethod) {
return func.call(node, this);
} else {
return func.call(node, node, this);
}
} catch(err) {
var errorMessage = 'Generating code for ';
@ -222,52 +121,97 @@ class Generator {
throw createError(errorMessage, err /* cause */);
}
}
if (finalNode === node) {
// If the same node was returned then we will generate
// code for the node as normal
finalNode = null;
} else if (finalNode == null) {
// If nothing was returned then don't generate any code
_generateCode(node, finalNodes) {
if (isArray(node)) {
node.forEach((child) => {
this._generateCode(child, finalNodes);
});
return;
} else if (node instanceof Container) {
node.forEach((child) => {
if (child.container === node) {
this._generateCode(child, finalNodes);
}
});
return;
}
if (node == null) {
return;
}
if (typeof node === 'string' || node._finalNode || !(node instanceof Node)) {
finalNodes.push(node);
return;
}
if (node._normalizeChildTextNodes) {
node._normalizeChildTextNodes(this.context);
}
let oldCurrentNode = this._currentNode;
this._currentNode = node;
var beforeAfterEvent;
if (node.listenerCount('beforeGenerateCode') || node.listenerCount('afterGenerateCode')) {
beforeAfterEvent = new GeneratorEvent(node, this);
}
var isWhitespacePreserved = node.isPreserveWhitespace();
if (isWhitespacePreserved) {
this.context.beginPreserveWhitespace();
}
if (beforeAfterEvent) {
beforeAfterEvent.isBefore = true;
beforeAfterEvent.node.emit('beforeGenerateCode', beforeAfterEvent);
if (beforeAfterEvent.insertedNodes) {
this._generateCode(beforeAfterEvent.insertedNodes, finalNodes);
beforeAfterEvent.insertedNodes = null;
}
}
let codeGeneratorFunc;
let generatedCode;
if (node.getCodeGenerator) {
codeGeneratorFunc = node.getCodeGenerator(this.outputType);
if (codeGeneratorFunc) {
node.setCodeGenerator(null);
generatedCode = this._invokeCodeGenerator(codeGeneratorFunc, node, false);
if (generatedCode != null && generatedCode !== node) {
node = null;
this._generateCode(generatedCode, finalNodes);
}
}
}
if (finalNode) {
if (isStatement) {
this.generateStatements(finalNode);
if (node != null) {
codeGeneratorFunc = node.generateCode;
if (!codeGeneratorFunc) {
codeGeneratorFunc = node[this._codegenCodeMethodName];
}
if (codeGeneratorFunc) {
generatedCode = this._invokeCodeGenerator(codeGeneratorFunc, node, true);
if (generatedCode === undefined || generatedCode === node) {
finalNodes.push(node);
} else if (generatedCode === null) {
// If nothing was returned then don't generate any code
} else {
this.generateCode(finalNode);
this._generateCode(generatedCode, finalNodes);
}
} else if (node) {
let generateCodeMethod = node.generateCode;
if (!generateCodeMethod) {
generateCodeMethod = node[this._codegenCodeMethodName];
if (!generateCodeMethod) {
throw new Error('No code codegen for node of type "' +
node.type +
'" (output type: "' + this.outputType + '"). Node: ' + util.inspect(node));
}
}
// The generateCode function can optionally return either of the following:
// - An AST node
// - An array/cointainer of AST nodes
finalNode = generateCodeMethod.call(node, this);
if (finalNode != null) {
if (finalNode === node) {
throw new Error('Invalid node returned. Same node returned: ' + util.inspect(node));
}
if (isStatement) {
this.generateStatements(finalNode);
} else {
this.generateCode(finalNode);
}
finalNodes.push(node);
}
}
@ -275,263 +219,45 @@ class Generator {
beforeAfterEvent.isBefore = false;
beforeAfterEvent.node.emit('afterGenerateCode', beforeAfterEvent);
if (beforeAfterEvent.insertedNodes) {
this._generateCode(beforeAfterEvent.insertedNodes, finalNodes);
beforeAfterEvent.insertedNodes = null;
}
}
if (isWhitespacePreserved) {
this.context.endPreserveWhitespace();
}
}
this._currentNode = oldCurrentNode;
}
getCode() {
this._flushBufferedWrites();
generateCode(node) {
if (!node) {
return null;
}
while(this._doneListeners.length || this._slots.length) {
if (node._finalNode) {
return node;
}
let doneListeners = this._doneListeners;
if (doneListeners.length) {
this._doneListeners = [];
let finalNodes = new FinalNodes();
for (let i=0; i<doneListeners.length; i++) {
let doneListener = doneListeners[i];
doneListener(this);
var isList = typeof node.forEach === 'function';
this._generateCode(node, finalNodes);
finalNodes = finalNodes.nodes;
if (!isList) {
if (finalNodes.length === 0) {
return null;
} else if (finalNodes.length === 1) {
return finalNodes[0];
}
}
let slots = this._slots;
if (slots.length) {
this._slots = [];
for (let i=slots.length-1; i>=0; i--) {
let slot = slots[i];
slot.generateCode(this);
}
}
}
return this._code;
}
generateBlock(body) {
if (!body) {
this.write('{}');
return;
}
if (typeof body === 'function') {
body = body();
}
if (!isArray(body) && !(body instanceof Container)) {
throw new Error('Invalid body');
}
if (body.length === 0) {
this.write('{}');
return;
}
this.write('{\n')
.incIndent();
let oldCodeLength = this._code.length;
this.generateStatements(body);
if (this._bufferedWrites) {
if (this._code.length !== oldCodeLength) {
this._code += '\n';
}
this._flushBufferedWrites();
}
this.decIndent()
.writeLineIndent()
.write('}');
}
generateStatements(nodes) {
ok(nodes, '"nodes" expected');
let firstStatement = true;
if (nodes instanceof Node) {
nodes = [nodes];
}
nodes.forEach((node) => {
if (node instanceof Node) {
node.statement = true;
}
let startCodeLen = this._code.length;
let currentIndent = this.currentIndent;
if (!firstStatement) {
this._write('\n');
}
if (!this._code.endsWith(currentIndent)) {
this.writeLineIndent();
}
let startPos = this._code.length;
if (Array.isArray(node) || (node instanceof Container)) {
this.generateStatements(node);
} else {
this.generateCode(node);
}
if (this._code.length === startPos) {
// No code was generated. Remove any code that was previously added
this._code = this._code.slice(0, startCodeLen);
return;
}
if (this._code.endsWith('\n')) {
// Do nothing
} else if (this._code.endsWith(';')) {
this._code += '\n';
} else if (this._code.endsWith('\n' + this.currentIndent)) {
// Do nothing
} else {
this._code += ';\n';
}
firstStatement = false;
});
}
_beginCaptureCode() {
let oldCode = this._code;
this._code = '';
return {
codegen: this,
end() {
let newCode = this.codegen._code;
this.codegen._code = oldCode;
return newCode;
}
};
}
addWriteLiteral(value) {
if (!(value instanceof Literal)) {
value = new Literal({value});
}
this.addWrite(value);
}
addWrite(output) {
ok(output, '"output" is required');
if (output instanceof Literal) {
let lastWrite = this._bufferedWrites ?
this._bufferedWrites[this._bufferedWrites.length-1] :
null;
if (lastWrite instanceof Literal) {
lastWrite.value += output.value;
return;
}
} else {
if (!(output instanceof Node)) {
throw new Error('Invalid write: ' + JSON.stringify(output, null, 2));
}
}
if (!this._bufferedWrites) {
this._bufferedWrites = [output];
} else {
this._bufferedWrites.push(output);
}
}
_flushBufferedWrites(addSeparator) {
let bufferedWrites = this._bufferedWrites;
if (!bufferedWrites) {
return;
}
this._bufferedWrites = null;
if (!addSeparator && !this._code.endsWith(this.currentIndent)) {
this.writeLineIndent();
}
let len = bufferedWrites.length;
for (let i=0; i<len; i++) {
let write = bufferedWrites[i];
if (i === 0) {
this._write('out.w(');
} else {
this._write(' +\n');
this.writeLineIndent();
this._write(this._indentStr);
}
this.generateCode(write);
}
this._write(');\n');
if (addSeparator) {
this._write('\n' + this.currentIndent);
}
}
write(code) {
if (this._bufferedWrites) {
this._flushBufferedWrites(true /* add separator */);
}
this._code += code;
return this;
}
_write(code) {
this._code += code;
return this;
}
incIndent(count) {
this._flushBufferedWrites(true /* add separator */);
if (count != null) {
for (let i=0; i<count; i++) {
this.currentIndent += ' ';
}
} else {
this.currentIndent += this._indentStr;
}
return this;
}
decIndent(count) {
if (count == null) {
count = this._indentSize;
}
this.currentIndent = this.currentIndent.substring(
0,
this.currentIndent.length - count);
return this;
}
writeLineIndent() {
this._code += this.currentIndent;
return this;
}
writeIndent() {
this._code += this._indentStr;
return this;
return finalNodes;
}
isLiteralNode(node) {
@ -542,94 +268,6 @@ class Generator {
return node instanceof Identifier;
}
writeLiteral(value) {
if (value === null) {
this.write('null');
} else if (value === undefined) {
this.write('undefined');
} else if (typeof value === 'string') {
this.write(JSON.stringify(value));
} else if (value === true) {
this.write('true');
} else if (value === false) {
this.write('false');
} else if (isArray(value)) {
if (value.length === 0) {
this.write('[]');
return;
}
this.write('[\n');
this.incIndent();
for (let i=0; i<value.length; i++) {
let v = value[i];
this.writeLineIndent();
if (v instanceof Node) {
this.generateCode(v);
} else {
this.writeLiteral(v);
}
if (i < value.length - 1) {
this.write(',\n');
} else {
this.write('\n');
}
}
this.decIndent();
this.writeLineIndent();
this.write(']');
} else if (typeof value === 'number') {
this.write(value.toString());
} else if (value instanceof RegExp) {
this.write(value.toString());
} else if (typeof value === 'object') {
let keys = Object.keys(value);
if (keys.length === 0) {
this.write('{}');
return;
}
this.incIndent();
this.write('{\n');
this.incIndent();
for (let i=0; i<keys.length; i++) {
let k = keys[i];
let v = value[k];
this.writeLineIndent();
if (isValidJavaScriptVarName(k)) {
this.write(k + ': ');
} else {
this.write(JSON.stringify(k) + ': ');
}
if (v instanceof Node) {
this.generateCode(v);
} else {
this.writeLiteral(v);
}
if (i < keys.length - 1) {
this.write(',\n');
} else {
this.write('\n');
}
}
this.decIndent();
this.writeLineIndent();
this.write('}');
this.decIndent();
}
}
isPreserveWhitespaceEnabled() {
return false;
}
@ -661,4 +299,4 @@ class Generator {
}
}
module.exports = Generator;
module.exports = CodeGenerator;

268
compiler/CodeWriter.js Normal file
View File

@ -0,0 +1,268 @@
'use strict';
const isArray = Array.isArray;
const Node = require('./ast/Node');
const Literal = require('./ast/Literal');
const Identifier = require('./ast/Identifier');
const ok = require('assert').ok;
const Container = require('./ast/Container');
const isValidJavaScriptVarName = require('./util/isValidJavaScriptVarName');
class CodeWriter {
constructor(options) {
options = options || {};
this.root = null;
this._indentStr = options.indent != null ? options.indent : ' ';
this._indentSize = this._indentStr.length;
this._code = '';
this.currentIndent = '';
this.outputType = options.output || 'html';
this._writeCodeMethodName = 'write' +
this.outputType.charAt(0).toUpperCase() +
this.outputType.substring(1) +
'Code';
}
getCode() {
return this._code;
}
writeBlock(body) {
if (!body) {
this.write('{}');
return;
}
if (typeof body === 'function') {
body = body();
}
if (!body ||
(Array.isArray(body) && body.length === 0) ||
(body instanceof Container && body.length === 0)) {
this.write('{}');
return;
}
this.write('{\n')
.incIndent();
this.writeStatements(body);
this.decIndent()
.writeLineIndent()
.write('}');
}
writeStatements(nodes) {
if (!nodes) {
return;
}
ok(nodes, '"nodes" expected');
let firstStatement = true;
var writeNode = (node) => {
if (Array.isArray(node) || (node instanceof Container)) {
node.forEach(writeNode);
return;
} else {
if (firstStatement) {
firstStatement = false;
} else {
this._write('\n');
}
this.writeLineIndent();
if (typeof node === 'string') {
this._write(node);
} else {
node.statement = true;
this.write(node);
}
if (this._code.endsWith('\n')) {
// Do nothing
} else if (this._code.endsWith(';')) {
this._code += '\n';
} else if (this._code.endsWith('\n' + this.currentIndent)) {
// Do nothing
} else {
this._code += ';\n';
}
}
};
if (nodes instanceof Node) {
writeNode(nodes);
} else {
nodes.forEach(writeNode);
}
}
write(code) {
if (code == null || code === '') {
return;
}
if (code instanceof Node) {
let node = code;
if (!node.writeCode) {
throw new Error('Node does not have a `writeCode` method: ' + JSON.stringify(node, null, 4));
}
node.writeCode(this);
} else if (isArray(code) || code instanceof Container) {
code.forEach(this.write, this);
return;
} else if (typeof code === 'string') {
this._code += code;
} else if (typeof code === 'boolean' || typeof code === 'number') {
this._code += code.toString();
} else {
throw new Error('Illegal argument: ' + JSON.stringify(code));
}
return this;
}
_write(code) {
this._code += code;
return this;
}
incIndent(count) {
if (count != null) {
for (let i=0; i<count; i++) {
this.currentIndent += ' ';
}
} else {
this.currentIndent += this._indentStr;
}
return this;
}
decIndent(count) {
if (count == null) {
count = this._indentSize;
}
this.currentIndent = this.currentIndent.substring(
0,
this.currentIndent.length - count);
return this;
}
writeLineIndent() {
this._code += this.currentIndent;
return this;
}
writeIndent() {
this._code += this._indentStr;
return this;
}
isLiteralNode(node) {
return node instanceof Literal;
}
isIdentifierNode(node) {
return node instanceof Identifier;
}
writeLiteral(value) {
if (value === null) {
this.write('null');
} else if (value === undefined) {
this.write('undefined');
} else if (typeof value === 'string') {
this.write(JSON.stringify(value));
} else if (value === true) {
this.write('true');
} else if (value === false) {
this.write('false');
} else if (isArray(value)) {
if (value.length === 0) {
this.write('[]');
return;
}
this.write('[\n');
this.incIndent();
for (let i=0; i<value.length; i++) {
let v = value[i];
this.writeLineIndent();
if (v instanceof Node) {
this.write(v);
} else {
this.writeLiteral(v);
}
if (i < value.length - 1) {
this.write(',\n');
} else {
this.write('\n');
}
}
this.decIndent();
this.writeLineIndent();
this.write(']');
} else if (typeof value === 'number') {
this.write(value.toString());
} else if (value instanceof RegExp) {
this.write(value.toString());
} else if (typeof value === 'object') {
let keys = Object.keys(value);
if (keys.length === 0) {
this.write('{}');
return;
}
this.incIndent();
this.write('{\n');
this.incIndent();
for (let i=0; i<keys.length; i++) {
let k = keys[i];
let v = value[k];
this.writeLineIndent();
if (isValidJavaScriptVarName(k)) {
this.write(k + ': ');
} else {
this.write(JSON.stringify(k) + ': ');
}
if (v instanceof Node) {
this.write(v);
} else {
this.writeLiteral(v);
}
if (i < keys.length - 1) {
this.write(',\n');
} else {
this.write('\n');
}
}
this.decIndent();
this.writeLineIndent();
this.write('}');
this.decIndent();
}
}
}
module.exports = CodeWriter;

View File

@ -48,8 +48,26 @@ function requireResolve(builder, path) {
return builder.functionCall(requireResolveNode, [ path ]);
}
const helpers = {
'escapeXml': 'x',
'escapeXmlAttr': 'xa',
'escapeScript': 'xs',
'str': 's',
'merge': 'm',
'classAttr': 'ca',
'styleAttr': 'sa',
'attr': 'a',
'attrs': 'as',
'loadTag': 't',
'forEachWithStatusVar': 'fv',
'forEach': 'f',
'forEachProp': 'fp',
'classList': 'cl',
'loadTemplate': 'l'
};
class CompileContext {
constructor(src, filename, builder) {
constructor(src, filename, builder, options) {
ok(typeof src === 'string', '"src" string is required');
ok(filename, '"filename" is required');
@ -61,6 +79,8 @@ class CompileContext {
this.taglibLookup = taglibLookup.buildLookup(this.dirname);
this.data = {};
this.options = options || {};
this._vars = {};
this._uniqueVars = new UniqueVars();
this._staticVars = {};
@ -72,6 +92,19 @@ class CompileContext {
this._macros = null;
this._preserveWhitespace = null;
this._preserveComments = null;
this.inline = this.options.inline === true;
this._helpersIdentifier = null;
if (this.options.preserveWhitespace) {
this.setPreserveWhitespace(true);
}
this._helpers = {};
}
setInline(isInline) {
this.inline = isInline === true;
}
getPosInfo(pos) {
@ -195,10 +228,6 @@ class CompileContext {
return this._staticCode;
}
getEscapeXmlAttrVar() {
return this.addStaticVar('escapeXmlAttr', '__helpers.xa');
}
getTagDef(tagName) {
var taglibLookup = this.taglibLookup;
@ -378,14 +407,8 @@ class CompileContext {
ok(typeof relativePath === 'string', '"path" should be a string');
var builder = this.builder;
// We want to add the following import:
// var loadTemplate = __helpers.t;
// var template = loadTemplate(require.resolve(<templateRequirePath>))
var loadTemplateVar = this.addStaticVar('loadTemplate', '__helpers.l');
var requireResolveTemplate = requireResolve(builder, builder.literal(relativePath));
var loadFunctionCall = builder.functionCall(loadTemplateVar, [ requireResolveTemplate ]);
var loadFunctionCall = builder.functionCall(this.helper('loadTemplate'), [ requireResolveTemplate ]);
var templateVar = this.addStaticVar(removeExt(relativePath), loadFunctionCall);
return templateVar;
}
@ -447,6 +470,61 @@ class CompileContext {
return pathExpression;
}
getStaticNodes() {
let builder = this.builder;
let staticNodes = [];
let staticVars = this.getStaticVars();
let staticVarNodes = Object.keys(staticVars).map((varName) => {
var varInit = staticVars[varName];
return builder.variableDeclarator(varName, varInit);
});
if (staticVarNodes.length) {
staticNodes.push(this.builder.vars(staticVarNodes));
}
var staticCodeArray = this.getStaticCode();
if (staticCodeArray) {
staticNodes = staticNodes.concat(staticCodeArray);
}
return staticNodes;
}
get helpersIdentifier() {
if (!this._helpersIdentifier) {
if (this.inline) {
this._helpersIdentifier = this.importModule('__markoHelpers', 'marko/runtime/helpers');
} else {
// The helpers variable is a parameter of the outer create function
this._helpersIdentifier = this.builder.identifier('__markoHelpers');
}
}
return this._helpersIdentifier;
}
helper(name) {
var helperIdentifier = this._helpers[name];
if (!helperIdentifier) {
var methodName = helpers[name];
if (!methodName) {
throw new Error('Invalid helper: ' + name);
}
var methodIdentifier = this.builder.identifier(methodName);
helperIdentifier = this.addStaticVar(
'marko_' + name,
this.builder.memberExpression(this.helpersIdentifier, methodIdentifier));
this._helpers[name] = helperIdentifier;
}
return helperIdentifier;
}
}
CompileContext.prototype.util = {

View File

@ -1,10 +1,8 @@
'use strict';
var ok = require('assert').ok;
var CodeGenerator = require('./CodeGenerator');
var CompileContext = require('./CompileContext');
var CodeWriter = require('./CodeWriter');
var createError = require('raptor-util/createError');
var config = require('./config');
var extend = require('raptor-util/extend');
const FLAG_TRANSFORMER_APPLIED = 'transformerApply';
@ -61,8 +59,47 @@ function transformTree(rootNode, context) {
return rootNode;
}
function handleErrors(context) {
// If there were any errors then compilation failed.
if (context.hasErrors()) {
var errors = context.getErrors();
var message = 'An error occurred while trying to compile template at path "' + context.filename + '". Error(s) in template:\n';
for (var i = 0, len = errors.length; i < len; i++) {
let error = errors[i];
message += (i + 1) + ') ' + error.toString() + '\n';
}
var error = new Error(message);
error.errors = errors;
throw error;
}
}
class CompiledTemplate {
constructor(ast, context, codeGenerator) {
this.ast = ast;
this.context = context;
this.filename = context.filename;
}
get code() {
// STAGE 3: Generate the code using the final AST
handleErrors(this.context);
// console.log(module.id, 'FINAL AST:' + JSON.stringify(finalAST, null, 4));
var codeWriter = new CodeWriter(this.context.options);
codeWriter.write(this.ast);
handleErrors(this.context);
// Return the generated code as the compiled output:
var compiledSrc = codeWriter.getCode();
return compiledSrc;
}
}
class Compiler {
constructor(options) {
constructor(options, userOptions, inline) {
ok(options, '"options" is required');
this.builder = options.builder;
@ -72,32 +109,14 @@ class Compiler {
ok(this.parser, '"options.parser" is required');
}
compile(src, filename, userOptions) {
compile(src, context) {
ok(typeof src === 'string', '"src" argument should be a string');
ok(filename, '"filename" argument is required');
ok(typeof filename === 'string', '"filename" argument should be a string');
var context = new CompileContext(src, filename, this.builder);
var options = {};
extend(options, config);
if (userOptions) {
extend(options, userOptions);
}
if (options.preserveWhitespace) {
context.setPreserveWhitespace(true);
}
var codeGenerator = new CodeGenerator(context);
// STAGE 1: Parse the template to produce the initial AST
var ast = this.parser.parse(src, context);
// Trim start and end whitespace for the root node
ast._normalizeChildTextNodes(codeGenerator, true /* trim start and end */, true /* force */);
context.root = ast;
// console.log('ROOT', JSON.stringify(ast, null, 2));
@ -105,30 +124,13 @@ class Compiler {
var transformedAST = transformTree(ast, context);
// console.log('transformedAST', JSON.stringify(ast, null, 2));
// Trim start and end whitespace for the root node (again, after the transformation)
transformedAST._normalizeChildTextNodes(codeGenerator, true /* trim start and end */, true /* force */);
handleErrors(context);
// STAGE 3: Generate the code using the final AST
var finalAST = codeGenerator.generateCode(transformedAST);
codeGenerator.generateCode(transformedAST);
handleErrors(context);
// If there were any errors then compilation failed.
if (context.hasErrors()) {
var errors = context.getErrors();
var message = 'An error occurred while trying to compile template at path "' + filename + '". Error(s) in template:\n';
for (var i = 0, len = errors.length; i < len; i++) {
let error = errors[i];
message += (i + 1) + ') ' + error.toString() + '\n';
}
var error = new Error(message);
error.errors = errors;
throw error;
}
// Return the generated code as the compiled output:
var compiledSrc = codeGenerator.getCode();
return compiledSrc;
return new CompiledTemplate(finalAST, context);
}
}

View File

@ -0,0 +1,84 @@
'use strict';
let CodeWriter = require('./CodeWriter');
function fixIndentation(lines) {
let length = lines.length;
let startLine = 0;
let endLine = length;
for (; startLine<length; startLine++) {
let line = lines[startLine];
if (line.trim() !== '') {
break;
}
}
for (; endLine>startLine; endLine--) {
let line = lines[endLine-1];
if (line.trim() !== '') {
break;
}
}
if (endLine === startLine) {
return '';
}
if (startLine !== 0 || endLine !== length) {
lines = lines.slice(startLine, endLine);
}
let firstLine = lines[0];
let indentToRemove = /^\s*/.exec(firstLine)[0];
if (indentToRemove) {
for (let i=0; i<lines.length; i++) {
let line = lines[i];
if (line.startsWith(indentToRemove)) {
lines[i] = line.substring(indentToRemove.length);
}
}
}
return lines.join('\n');
}
function normalizeTemplateSrc(src) {
let lines = src.split(/\r\n|\n\r|\n/);
if (lines.length) {
if (lines[0].trim() === '') {
return fixIndentation(lines);
}
}
return src.trim();
}
class InlineCompiler {
constructor(context, compiler) {
this.context = context;
this.compiler = compiler;
context.setInline(true);
}
compile(src) {
src = normalizeTemplateSrc(src);
// console.log('TEMPLATE SRC:>\n' + src + '\n<');
return this.compiler.compile(src, this.context);
}
get staticCode() {
let staticNodes = this.context.getStaticNodes();
if (!staticNodes || staticNodes.length === 0) {
return null;
}
let codeWriter = new CodeWriter(this.context.options);
codeWriter.write(staticNodes);
return codeWriter.getCode();
}
}
module.exports = InlineCompiler;

View File

@ -63,8 +63,8 @@ function mergeShorthandClassNames(el, shorthandClassNames, context) {
if (finalClassNames.length === 1) {
el.setAttributeValue('class', finalClassNames[0]);
} else {
var classListVar = context.addStaticVar('__classList', '__helpers.cl');
el.setAttributeValue('class', builder.functionCall(classListVar, finalClassNames));
el.setAttributeValue('class', builder.functionCall(context.helper('classList'), finalClassNames));
}
}
@ -141,6 +141,8 @@ class Parser {
if (tagNameExpression) {
tagName = builder.parseExpression(tagNameExpression);
} else if (tagName === 'marko-compiler-options') {
this.parentNode.setTrimStartEnd(true);
attributes.forEach(function (attr) {
let attrName = attr.name;
let handler = COMPILER_ATTRIBUTE_HANDLERS[attrName];
@ -279,6 +281,8 @@ class Parser {
this.prevTextNode = null;
this.stack.pop();
}
handleComment(comment) {

View File

@ -9,32 +9,37 @@ class ArrayExpression extends Node {
}
generateCode(codegen) {
this.elements = codegen.generateCode(this.elements);
return this;
}
writeCode(writer) {
var elements = this.elements;
if (!elements || !elements.length) {
codegen.write('[]');
writer.write('[]');
return;
}
codegen.incIndent();
codegen.write('[\n');
codegen.incIndent();
writer.incIndent();
writer.write('[\n');
writer.incIndent();
elements.forEach((element, i) => {
codegen.writeLineIndent();
codegen.generateCode(element);
writer.writeLineIndent();
writer.write(element);
if (i < elements.length - 1) {
codegen.write(',\n');
writer.write(',\n');
} else {
codegen.write('\n');
writer.write('\n');
}
});
codegen.decIndent();
codegen.writeLineIndent();
codegen.write(']');
codegen.decIndent();
writer.decIndent();
writer.writeLineIndent();
writer.write(']');
writer.decIndent();
}
walk(walker) {

View File

@ -11,23 +11,29 @@ class Assignment extends Node {
}
generateCode(codegen) {
this.left = codegen.generateCode(this.left);
this.right = codegen.generateCode(this.right);
return this;
}
writeCode(writer) {
var left = this.left;
var right = this.right;
var operator = this.operator;
codegen.generateCode(left);
codegen.write(' ' + (operator || '=') + ' ');
writer.write(left);
writer.write(' ' + (operator || '=') + ' ');
var wrap = right instanceof Assignment;
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(right);
writer.write(right);
if (wrap) {
codegen.write(')');
writer.write(')');
}
}

View File

@ -10,7 +10,12 @@ class AttributePlaceholder extends Node {
}
generateCode(codegen) {
codegen.generateCode(this.value);
this.value = codegen.generateCode(this.value);
return this;
}
writeCode(writer) {
writer.write(this.value);
}
walk(walker) {

View File

@ -3,17 +3,17 @@
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
function generateCodeForOperand(node, codegen) {
function writeCodeForOperand(node, writer) {
var wrap = isCompoundExpression(node);
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(node);
writer.write(node);
if (wrap) {
codegen.write(')');
writer.write(')');
}
}
@ -44,6 +44,35 @@ class BinaryExpression extends Node {
}
generateCode(codegen) {
this.left = codegen.generateCode(this.left);
this.right = codegen.generateCode(this.right);
var left = this.left;
var right = this.right;
var operator = this.operator;
if (!left || !right) {
throw new Error('Invalid BinaryExpression: ' + this);
}
var builder = codegen.builder;
if (left.type === 'Literal' && right.type === 'Literal') {
if (operator === '+') {
return builder.literal(left.value + right.value);
} else if (operator === '-') {
return builder.literal(left.value - right.value);
} else if (operator === '*') {
return builder.literal(left.value * right.value);
} else if (operator === '/') {
return builder.literal(left.value / right.value);
}
}
return this;
}
writeCode(writer) {
var left = this.left;
var operator = this.operator;
var right = this.right;
@ -52,23 +81,11 @@ class BinaryExpression extends Node {
throw new Error('Invalid BinaryExpression: ' + this);
}
if (left.type === 'Literal' && right.type === 'Literal') {
if (operator === '+') {
return codegen.generateCode(codegen.builder.literal(left.value + right.value));
} else if (operator === '-') {
return codegen.generateCode(codegen.builder.literal(left.value - right.value));
} else if (operator === '*') {
return codegen.generateCode(codegen.builder.literal(left.value * right.value));
} else if (operator === '/') {
return codegen.generateCode(codegen.builder.literal(left.value / right.value));
}
}
generateCodeForOperand(left, codegen);
codegen.write(' ');
codegen.generateCode(operator);
codegen.write(' ');
generateCodeForOperand(right, codegen);
writeCodeForOperand(left, writer);
writer.write(' ');
writer.write(operator);
writer.write(' ');
writeCodeForOperand(right, writer);
}
isCompoundExpression() {

View File

@ -10,15 +10,19 @@ class Code extends Node {
}
generateCode(codegen) {
return this;
}
writeCode(writer) {
var code = this.value;
if (!code) {
return;
}
code = adjustIndent(code, codegen.currentIndent);
code = adjustIndent(code, writer.currentIndent);
codegen.write(code);
writer.write(code);
}
}

View File

@ -11,16 +11,22 @@ class ConditionalExpression extends Node {
}
generateCode(codegen) {
this.test = codegen.generateCode(this.test);
this.consequent = codegen.generateCode(this.consequent);
this.alternate = codegen.generateCode(this.alternate);
return this;
}
writeCode(writer) {
var test = this.test;
var consequent = this.consequent;
var alternate = this.alternate;
codegen.generateCode(test);
codegen.write(' ? ');
codegen.generateCode(consequent);
codegen.write(' : ');
codegen.generateCode(alternate);
writer.write(test);
writer.write(' ? ');
writer.write(consequent);
writer.write(' : ');
writer.write(alternate);
}
isCompoundExpression() {

View File

@ -8,6 +8,10 @@ class ContainerNode extends Node {
this.body = this.makeContainer(def.body);
}
generateCode(codegen) {
return codegen.genereateCode(this.body);
}
walk(walker) {
this.body = walker.walk(this.body);
}

View File

@ -230,7 +230,7 @@ class CustomTag extends HtmlElement {
let parentTagNode = getNestedTagParentNode(this, parentTagName);
if (!parentTagNode) {
codegen.addError('Invalid usage of the <' + this.tagName + '> nested tag. Tag not nested within a <' + parentTagName + '> tag.');
return;
return null;
}
parentTagVar = parentTagNode.data.nestedTagVar;
}
@ -239,9 +239,9 @@ class CustomTag extends HtmlElement {
var inputProps = buildInputProps(this, context);
var renderBodyFunction;
var body = codegen.generateCode(this.body);
if (this.body && this.body.length) {
if (body && body.length) {
if (tagDef.bodyFunction) {
let bodyFunction = tagDef.bodyFunction;
let bodyFunctionName = bodyFunction.name;
@ -249,9 +249,9 @@ class CustomTag extends HtmlElement {
return builder.identifier(param);
});
inputProps[bodyFunctionName] = builder.functionDeclaration(bodyFunctionName, bodyFunctionParams, this.body);
inputProps[bodyFunctionName] = builder.functionDeclaration(bodyFunctionName, bodyFunctionParams, body);
} else {
renderBodyFunction = context.builder.renderBodyFunction(this.body);
renderBodyFunction = context.builder.renderBodyFunction(body);
if (nestedTagVar) {
renderBodyFunction.params.push(nestedTagVar);
} else {
@ -289,7 +289,7 @@ class CustomTag extends HtmlElement {
if (Object.keys(inputProps.value).length === 0) {
inputProps = argument;
} else {
var mergeVar = codegen.addStaticVar('__merge', '__helpers.m');
var mergeVar = context.helper('merge');
inputProps = builder.functionCall(mergeVar, [
inputProps, // Input props from the attributes take precedence
argument
@ -320,8 +320,6 @@ class CustomTag extends HtmlElement {
let renderFunctionCall = builder.functionCall(renderMethod, renderArgs);
finalNode = renderFunctionCall;
} else {
var loadTagVar = codegen.addStaticVar('__loadTag', '__helpers.t');
var loadTagArgs = [
requireRendererFunctionCall // The first param is the renderer
];
@ -340,7 +338,7 @@ class CustomTag extends HtmlElement {
}
}
var loadTag = builder.functionCall(loadTagVar, loadTagArgs);
var loadTag = builder.functionCall(context.helper('loadTag'), loadTagArgs);
var tagVar = tagDef.name;
if (context.util.isJavaScriptReservedWord(tagVar)) {

View File

@ -8,12 +8,13 @@ class Declaration extends Node {
}
generateHtmlCode(codegen) {
var builder = codegen.builder;
codegen.addWrite(builder.literal('<?'));
codegen.addWrite(this.declaration);
codegen.addWrite(builder.literal('?>'));
return [
builder.htmlLiteral('<?'),
codegen.generateCode(this.declaration),
builder.htmlLiteral('?>')
];
}
toJSON() {

View File

@ -8,12 +8,13 @@ class DocumentType extends Node {
}
generateHtmlCode(codegen) {
var builder = codegen.builder;
codegen.addWrite(builder.literal('<!'));
codegen.addWrite(this.documentType);
codegen.addWrite(builder.literal('>'));
return [
builder.htmlLiteral('<!'),
builder.html(codegen.generateCode(this.documentType)),
builder.htmlLiteral('>')
];
}
toJSON() {

View File

@ -14,11 +14,15 @@ class Else extends Node {
codegen.addError('Unmatched else statement');
return;
}
var body = this.body;
codegen.write('else ');
codegen.generateBlock(body);
codegen.write('\n');
this.body = codegen.generateCode(this.body);
return this;
}
writeCode(writer) {
var body = this.body;
writer.writeBlock(body);
writer.write('\n');
}
walk(walker) {

View File

@ -17,9 +17,7 @@ class ElseIf extends Node {
return;
}
var ifStatement = codegen.builder.ifStatement(this.test, this.body, this.else);
codegen.write('else ');
codegen.generateCode(ifStatement);
return codegen.builder.ifStatement(this.test, this.body, this.else);
}
walk(walker) {

View File

@ -11,7 +11,11 @@ class Expression extends Node {
}
generateCode(codegen) {
codegen.generateCode(this.value);
return this;
}
writeCode(writer) {
writer.write(this.value);
}
isCompoundExpression() {

View File

@ -22,7 +22,7 @@ class ForEach extends Node {
var separator = this.separator;
var statusVarName = this.statusVarName;
var iterator = this.iterator;
var context = codegen.context;
var builder = codegen.builder;
if (separator && !statusVarName) {
@ -41,7 +41,7 @@ class ForEach extends Node {
builder.functionDeclaration(null, params, this.body)
]);
} else if (statusVarName) {
let forEachVarName = codegen.addStaticVar('forEachWithStatusVar', '__helpers.fv');
let body = this.body;
if (separator) {
@ -58,14 +58,12 @@ class ForEach extends Node {
]);
}
return builder.functionCall(forEachVarName, [
return builder.functionCall(context.helper('forEachWithStatusVar'), [
inExpression,
builder.functionDeclaration(null, [varName, statusVarName], body)
]);
} else {
let forEachVarName = codegen.addStaticVar('forEach', '__helpers.f');
return builder.functionCall(forEachVarName, [
return builder.functionCall(context.helper('forEach'), [
inExpression,
builder.functionDeclaration(null, [varName], this.body)
]);

View File

@ -18,6 +18,7 @@ class ForEachProp extends Node {
}
generateCode(codegen) {
var context = codegen.context;
var nameVarName = this.nameVarName;
var valueVarName = this.valueVarName;
var inExpression = this.in;
@ -55,8 +56,9 @@ class ForEachProp extends Node {
builder.functionDeclaration(null, [nameVarName, valueVarName, statusVarName], body)
]);
} else {
let forEachVarName = codegen.addStaticVar('forEachProp', '__helpers.fp');
return builder.functionCall(forEachVarName, [
return builder.functionCall(
context.helper('forEachProp'),
[
inExpression,
builder.functionDeclaration(null, [nameVarName, valueVarName], body)
]);

View File

@ -12,34 +12,42 @@ class ForStatement extends Node {
}
generateCode(codegen) {
this.init = codegen.generateCode(this.init);
this.test = codegen.generateCode(this.test);
this.update = codegen.generateCode(this.update);
this.body = codegen.generateCode(this.body);
return this;
}
writeCode(writer) {
var init = this.init;
var test = this.test;
var update = this.update;
var body = this.body;
codegen.write('for (');
writer.write('for (');
if (init) {
codegen.generateCode(init);
writer.write(init);
}
codegen.write('; ');
writer.write('; ');
if (test) {
codegen.generateCode(test);
writer.write(test);
}
codegen.write('; ');
writer.write('; ');
if (update) {
codegen.generateCode(update);
writer.write(update);
}
codegen.write(') ');
writer.write(') ');
codegen.generateBlock(body);
writer.writeBlock(body);
codegen.write('\n');
writer.write('\n');
}
walk(walker) {

View File

@ -2,6 +2,7 @@
var ok = require('assert').ok;
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
class FunctionCall extends Node {
constructor(def) {
@ -27,28 +28,47 @@ class FunctionCall extends Node {
}
generateCode(codegen) {
this.callee = codegen.generateCode(this.callee);
this.args = codegen.generateCode(this.args);
return this;
}
writeCode(writer) {
var callee = this.callee;
var args = this.args;
codegen.generateCode(callee);
var wrapWithParens = isCompoundExpression(callee);
codegen.write('(');
if (wrapWithParens) {
writer.write('(');
}
writer.write(callee);
if (wrapWithParens) {
writer.write(')');
}
writer.write('(');
if (args && args.length) {
for (let i=0, argsLen = args.length; i<argsLen; i++) {
if (i !== 0) {
codegen.write(', ');
writer.write(', ');
}
let arg = args[i];
if (!arg) {
throw new Error('Arg ' + i + ' is not valid for function call: ' + JSON.stringify(this.toJSON()));
}
codegen.generateCode(arg);
writer.write(arg);
}
}
codegen.write(')');
writer.write(')');
}
walk(walker) {

View File

@ -12,6 +12,14 @@ class FunctionDeclaration extends Node {
}
generateCode(codegen) {
var oldInFunction = codegen.inFunction;
codegen.inFunction = true;
this.body = codegen.generateCode(this.body);
codegen.inFunction = oldInFunction;
return this;
}
writeCode(writer) {
var name = this.name;
var params = this.params;
var body = this.body;
@ -22,39 +30,37 @@ class FunctionDeclaration extends Node {
}
if (name) {
codegen.write('function ');
codegen.generateCode(name);
codegen.write('(');
writer.write('function ');
writer.write(name);
writer.write('(');
} else {
codegen.write('function(');
writer.write('function(');
}
if (params && params.length) {
for (let i=0, paramsLen = params.length; i<paramsLen; i++) {
if (i !== 0) {
codegen.write(', ');
writer.write(', ');
}
var param = params[i];
if (typeof param === 'string') {
codegen.write(param);
writer.write(param);
} else {
if (param.type !== 'Identifier') {
throw new Error('Illegal param ' + JSON.stringify(param) + ' for FunctionDeclaration: ' + JSON.stringify(this));
}
codegen.generateCode(param);
writer.write(param);
}
}
}
codegen.write(') ');
var oldInFunction = codegen.inFunction;
codegen.inFunction = true;
codegen.generateBlock(body);
codegen.inFunction = oldInFunction;
writer.write(') ');
writer.writeBlock(body);
if (statement) {
codegen.write('\n');
writer.write('\n');
}
}

View File

@ -1,6 +1,7 @@
'use strict';
var Node = require('./Node');
var Literal = require('./Literal');
class Html extends Node {
constructor(def) {
@ -8,13 +9,70 @@ class Html extends Node {
this.argument = def.argument;
}
isLiteral() {
return this.argument instanceof Node && this.argument.type === 'Literal';
_append(appendArgument) {
var argument = this.argument;
if (Array.isArray(argument)) {
var len = argument.length;
var last = argument[len-1];
if (last instanceof Literal && appendArgument instanceof Literal) {
last.value += appendArgument.value;
} else {
this.argument.push(appendArgument);
}
} else {
if (argument instanceof Literal && appendArgument instanceof Literal) {
argument.value += appendArgument.value;
} else {
this.argument = [ this.argument, appendArgument ];
}
}
}
generateHtmlCode(codegen) {
let argument = this.argument;
codegen.addWrite(argument);
append(html) {
var appendArgument = html.argument;
if (!appendArgument) {
return;
}
if (Array.isArray(appendArgument)) {
appendArgument.forEach(this._append, this);
} else {
this._append(appendArgument);
}
}
generateCode() {
return this;
}
writeCode(writer) {
var argument = this.argument;
if (Array.isArray(argument)) {
let args = argument;
for (let i=0, len=args.length; i<len; i++) {
let arg = args[i];
if (i === 0) {
writer.write('out.w(');
} else {
writer.write(' +\n');
writer.writeLineIndent();
writer.writeIndent();
}
writer.write(arg);
}
writer.write(')');
} else {
writer.write('out.w(');
writer.write(argument);
writer.write(')');
}
}
walk(walker) {

View File

@ -52,6 +52,18 @@ function flattenAttrConcats(node) {
function generateCodeForExpressionAttr(name, value, escape, codegen) {
var flattenedConcats = flattenAttrConcats(value);
var hasLiteral = false;
var builder = codegen.builder;
var finalNodes = [];
var context = codegen.context;
function addHtml(argument) {
finalNodes.push(builder.html(argument));
}
function addHtmlLiteral(value) {
finalNodes.push(builder.htmlLiteral(value));
}
for (let i=0; i<flattenedConcats.length; i++) {
if (flattenedConcats[i].type === 'Literal') {
@ -61,7 +73,7 @@ function generateCodeForExpressionAttr(name, value, escape, codegen) {
}
if (hasLiteral) {
codegen.addWriteLiteral(' ' + name + '="');
addHtmlLiteral(' ' + name + '="');
for (let i=0; i<flattenedConcats.length; i++) {
var part = flattenedConcats[i];
if (isStringLiteral(part)) {
@ -69,32 +81,21 @@ function generateCodeForExpressionAttr(name, value, escape, codegen) {
} else if (part.type === 'Literal') {
} else if (isNoEscapeXml(part)) {
part = codegen.builder.functionCall(codegen.builder.identifier('str'), [part]);
part = codegen.builder.functionCall(context.helper('str'), [part]);
} else {
if (escape !== false) {
var escapeXmlAttrVar = codegen.getEscapeXmlAttrVar();
part = codegen.builder.functionCall(escapeXmlAttrVar, [part]);
part = codegen.builder.functionCall(context.helper('escapeXmlAttr'), [part]);
}
}
codegen.addWrite(part);
addHtml(part);
}
codegen.addWriteLiteral('"');
addHtmlLiteral('"');
} else {
if (name === 'class') {
// let builder = codegen.builder;
// let valueWithEscaping = handleEscaping(value);
let classAttrVar = codegen.addStaticVar('classAttr', '__helpers.ca');
codegen.addWrite(codegen.builder.functionCall(classAttrVar, [value]));
addHtml(codegen.builder.functionCall(context.helper('classAttr'), [value]));
} else if (name === 'style') {
// let builder = codegen.builder;
// let valueWithEscaping = handleEscaping(value);
let styleAttrVar = codegen.addStaticVar('styleAttr', '__helpers.sa');
codegen.addWrite(codegen.builder.functionCall(styleAttrVar, [value]));
addHtml(codegen.builder.functionCall(context.helper('styleAttr'), [value]));
} else {
// let builder = codegen.builder;
// let valueWithEscaping = handleEscaping(value);
let attrVar = codegen.addStaticVar('attr', '__helpers.a');
if (escape === false || isNoEscapeXml(value)) {
escape = false;
}
@ -104,12 +105,22 @@ function generateCodeForExpressionAttr(name, value, escape, codegen) {
if (escape === false) {
attrArgs.push(codegen.builder.literal(false));
}
codegen.addWrite(codegen.builder.functionCall(attrVar, attrArgs));
addHtml(codegen.builder.functionCall(context.helper('attr'), attrArgs));
}
}
return finalNodes;
}
function beforeGenerateCode(event) {
event.codegen.isInAttribute = true;
}
function afterGenerateCode(event) {
event.codegen.isInAttribute = false;
}
class HtmlAttribute extends Node {
constructor(def) {
super('HtmlAttribute');
@ -132,6 +143,9 @@ class HtmlAttribute extends Node {
this.argument = def.argument;
this.def = def.def; // The attribute definition loaded from the taglib (if any)
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
}
isLiteralValue() {
@ -153,24 +167,25 @@ class HtmlAttribute extends Node {
let value = this.value;
let argument = this.argument;
let escape = this.escape !== false;
var builder = codegen.builder;
if (!name) {
return;
return null;
}
if (this.isLiteralValue()) {
codegen.addWriteLiteral(attr(name, value.value));
return builder.htmlLiteral(attr(name, value.value));
} else if (value != null) {
codegen.isInAttribute = true;
generateCodeForExpressionAttr(name, value, escape, codegen);
codegen.isInAttribute = false;
return generateCodeForExpressionAttr(name, value, escape, codegen);
} else if (argument) {
codegen.addWriteLiteral(' ' + name + '(');
codegen.addWriteLiteral(argument);
codegen.addWriteLiteral(')');
return [
builder.htmlLiteral(' ' + name + '('),
builder.htmlLiteral(argument),
builder.htmlLiteral(')')
];
} else {
// Attribute with no value is a boolean attribute
codegen.addWriteLiteral(' ' + name);
return builder.htmlLiteral(' ' + name);
}
}

View File

@ -10,11 +10,13 @@ class HtmlComment extends Node {
generateHtmlCode(codegen) {
var comment = this.comment;
var literal = codegen.builder.literal;
var builder = codegen.builder;
codegen.addWrite(literal('<!--'));
codegen.addWrite(comment);
codegen.addWrite(literal('-->'));
return [
builder.htmlLiteral('<!--'),
builder.html(comment),
builder.htmlLiteral('-->')
];
}
walk(walker) {

View File

@ -21,34 +21,36 @@ class StartTag extends Node {
var tagName = this.tagName;
var selfClosed = this.selfClosed;
var dynamicAttributes = this.dynamicAttributes;
var context = codegen.context;
// Starting tag
codegen.addWriteLiteral('<');
codegen.addWrite(tagName);
var nodes = [
builder.htmlLiteral('<'),
builder.html(tagName),
];
var attributes = this.attributes;
if (attributes) {
for (let i=0; i<attributes.length; i++) {
let attr = attributes[i];
codegen.generateCode(attr);
nodes.push(codegen.generateCode(attr));
}
}
if (dynamicAttributes) {
dynamicAttributes.forEach(function(attrsExpression) {
codegen.addStaticVar('attrs', '__helpers.as');
let attrsFunctionCall = builder.functionCall('attrs', [attrsExpression]);
codegen.addWrite(attrsFunctionCall);
let attrsFunctionCall = builder.functionCall(context.helper('attrs'), [attrsExpression]);
nodes.push(builder.html(attrsFunctionCall));
});
}
if (selfClosed) {
codegen.addWriteLiteral('/>');
nodes.push(builder.htmlLiteral('/>'));
} else {
codegen.addWriteLiteral('>');
nodes.push(builder.htmlLiteral('>'));
}
return nodes;
}
}
@ -60,9 +62,13 @@ class EndTag extends Node {
generateCode(codegen) {
var tagName = this.tagName;
codegen.addWriteLiteral('</');
codegen.addWrite(tagName);
codegen.addWriteLiteral('>');
var builder = codegen.builder;
return [
builder.htmlLiteral('</'),
builder.html(tagName),
builder.htmlLiteral('>')
];
}
}
@ -131,6 +137,10 @@ class HtmlElement extends Node {
var builder = codegen.builder;
if (hasBody) {
body = codegen.generateCode(body);
}
if (hasBody || bodyOnlyIf) {
openTagOnly = false;
selfClosed = false;
@ -170,7 +180,7 @@ class HtmlElement extends Node {
];
} else {
if (openTagOnly) {
codegen.generateCode(startTag);
return codegen.generateCode(startTag);
} else {
return [
startTag,

View File

@ -9,8 +9,12 @@ class Identifier extends Node {
}
generateCode(codegen) {
return this;
}
writeCode(writer) {
var name = this.name;
codegen.write(name);
writer.write(name);
}
toString() {

View File

@ -18,7 +18,6 @@ class If extends Node {
}
generateCode(codegen) {
if (this.else) {
this.else.matched = true;
} else {
@ -59,18 +58,26 @@ class If extends Node {
});
}
this.test = codegen.generateCode(this.test);
this.body = codegen.generateCode(this.body);
this.else = codegen.generateCode(this.else);
return this;
}
writeCode(writer) {
var test = this.test;
var body = this.body;
codegen.write('if (');
codegen.generateCode(test);
codegen.write(') ');
codegen.generateBlock(body);
writer.write('if (');
writer.write(test);
writer.write(') ');
writer.writeBlock(body);
if (this.else) {
codegen.write(' ');
codegen.generateCode(this.else);
writer.write(' else ');
writer.write(this.else);
} else {
codegen.write('\n');
writer.write('\n');
}
}

View File

@ -11,8 +11,26 @@ class Literal extends Node {
}
generateCode(codegen) {
if (this.value != null) {
if (isArray(this.value)) {
this.value = codegen.generateCode(this.value);
} else if (typeof this.value === 'object') {
var newObject = {};
for (var k in this.value) {
if (this.value.hasOwnProperty(k)) {
newObject[k] = codegen.generateCode(this.value[k]);
}
}
this.value = newObject;
}
}
return this;
}
writeCode(writer) {
var value = this.value;
codegen.writeLiteral(value);
writer.writeLiteral(value);
}
toString() {

View File

@ -3,21 +3,21 @@
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
function generateCodeForOperand(node, codegen) {
function generateCodeForOperand(node, writer) {
var wrap = isCompoundExpression(node);
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(node);
writer.write(node);
if (wrap) {
codegen.write(')');
writer.write(')');
}
}
function operandToString(node, codegen) {
function operandToString(node) {
var wrap = isCompoundExpression(node);
var result = '';
@ -44,6 +44,12 @@ class LogicalExpression extends Node {
}
generateCode(codegen) {
this.left = codegen.generateCode(this.left);
this.right = codegen.generateCode(this.right);
return this;
}
writeCode(writer) {
var left = this.left;
var operator = this.operator;
var right = this.right;
@ -52,11 +58,11 @@ class LogicalExpression extends Node {
throw new Error('Invalid LogicalExpression: ' + this);
}
generateCodeForOperand(left, codegen);
codegen.write(' ');
codegen.generateCode(operator);
codegen.write(' ');
generateCodeForOperand(right, codegen);
generateCodeForOperand(left, writer);
writer.write(' ');
writer.write(operator);
writer.write(' ');
generateCodeForOperand(right, writer);
}
isCompoundExpression() {

View File

@ -20,13 +20,13 @@ class Macro extends Node {
generateCode(codegen) {
var name = this.name;
var params = this.params || [];
var body = this.body;
var builder = codegen.builder;
var macroDef = codegen.context.registerMacro(name, params);
var functionName = macroDef.functionName;
// Walk the body after registering the macro
var body = codegen.generateCode(this.body);
return builder.functionDeclaration(functionName, macroDef.params, body);
}

View File

@ -1,6 +1,8 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
var ok = require('assert').ok;
class MemberExpression extends Node {
constructor(def) {
@ -8,22 +10,41 @@ class MemberExpression extends Node {
this.object = def.object;
this.property = def.property;
this.computed = def.computed;
ok(this.object, '"object" is required');
ok(this.property, '"property" is required');
}
generateCode(codegen) {
this.object = codegen.generateCode(this.object);
this.property = codegen.generateCode(this.property);
return this;
}
writeCode(writer) {
var object = this.object;
var property = this.property;
var computed = this.computed;
codegen.generateCode(object);
var wrapWithParens = isCompoundExpression(object);
if (wrapWithParens) {
writer.write('(');
}
writer.write(object);
if (wrapWithParens) {
writer.write(')');
}
if (computed) {
codegen.write('[');
codegen.generateCode(property);
codegen.write(']');
writer.write('[');
writer.write(property);
writer.write(']');
} else {
codegen.write('.');
codegen.generateCode(property);
writer.write('.');
writer.write(property);
}
}

View File

@ -11,40 +11,46 @@ class NewExpression extends Node {
}
generateCode(codegen) {
this.callee = codegen.generateCode(this.callee);
this.args = codegen.generateCode(this.args);
return this;
}
writeCode(writer) {
var callee = this.callee;
var args = this.args;
codegen.write('new ');
writer.write('new ');
var wrap = isCompoundExpression(callee);
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(callee);
writer.write(callee);
if (wrap) {
codegen.write(')');
writer.write(')');
}
codegen.write('(');
writer.write('(');
if (args && args.length) {
for (let i=0, argsLen = args.length; i<argsLen; i++) {
if (i !== 0) {
codegen.write(', ');
writer.write(', ');
}
let arg = args[i];
if (!arg) {
throw new Error('Arg ' + i + ' is not valid for new expression: ' + JSON.stringify(this.toJSON()));
}
codegen.generateCode(arg);
writer.write(arg);
}
}
codegen.write(')');
writer.write(')');
}
isCompoundExpression() {

View File

@ -45,6 +45,8 @@ class Node {
this._events = null;
this._childTextNormalized = undefined;
this.data = {};
this._finalNode = false;
this._trimStartEnd = false;
}
on(event, listener) {
@ -189,6 +191,8 @@ class Node {
delete result.tagDef;
delete result._preserveWhitespace;
delete result._events;
delete result._finalNode;
delete result._trimStartEnd;
return result;
}
@ -290,16 +294,26 @@ class Node {
return preserveWhitespace === true;
}
_normalizeChildTextNodes(codegen, trimStartEnd, force) {
if (this._childTextNormalized && force !== true) {
setFinalNode(isFinal) {
this._finalNode = true;
}
setTrimStartEnd(trimStartEnd) {
this._trimStartEnd = trimStartEnd;
}
_normalizeChildTextNodes(context) {
if (this._childTextNormalized) {
return;
}
this._childTextNormalized = true;
var trimStartEnd = this._trimStartEnd === true;
var isPreserveWhitespace = false;
if (codegen.context.isPreserveWhitespace() || this.preserveWhitespace === true || this.isPreserveWhitespace()) {
if (context.isPreserveWhitespace() || this.preserveWhitespace === true || this.isPreserveWhitespace()) {
isPreserveWhitespace = true;
}

View File

@ -9,32 +9,38 @@ class ObjectExpression extends Node {
}
generateCode(codegen) {
this.properties = codegen.generateCode(this.properties);
return this;
}
writeCode(writer) {
var properties = this.properties;
if (!properties || !properties.length) {
codegen.write('{}');
writer.write('{}');
return;
}
codegen.incIndent();
codegen.write('{\n');
codegen.incIndent();
writer.incIndent();
writer.write('{\n');
writer.incIndent();
properties.forEach((prop, i) => {
codegen.writeLineIndent();
codegen.generateCode(prop);
writer.writeLineIndent();
writer.write(prop);
if (i < properties.length - 1) {
codegen.write(',\n');
writer.write(',\n');
} else {
codegen.write('\n');
writer.write('\n');
}
});
codegen.decIndent();
codegen.writeLineIndent();
codegen.write('}');
codegen.decIndent();
writer.decIndent();
writer.writeLineIndent();
writer.write('}');
writer.decIndent();
}
toJSON() {

View File

@ -8,12 +8,12 @@ class Program extends Node {
}
generateCode(codegen) {
var body = this.body;
codegen.generateStatements(body);
if (codegen._bufferedWrites) {
codegen._write('\n');
codegen._flushBufferedWrites();
this.body = codegen.generateCode(this.body);
return this;
}
writeCode(writer) {
writer.writeStatements(this.body);
}
walk(walker) {

View File

@ -20,9 +20,18 @@ class Property extends Node {
}
}
codegen.generateCode(key);
codegen.write(': ');
codegen.generateCode(value);
this.key = codegen.generateCode(key);
this.value = codegen.generateCode(value);
return this;
}
writeCode(writer) {
var key = this.key;
var value = this.value;
writer.write(key);
writer.write(': ');
writer.write(value);
}
toJSON() {

View File

@ -13,13 +13,18 @@ class Return extends Node {
throw new Error('"return" not allowed outside a function body');
}
this.argument = codegen.generateCode(this.argument);
return this;
}
writeCode(writer) {
var argument = this.argument;
if (argument) {
codegen.write('return ');
codegen.generateCode(argument);
writer.write('return ');
writer.write(argument);
} else {
codegen.write('return');
writer.write('return');
}
}

View File

@ -10,16 +10,20 @@ class Scriptlet extends Node {
}
generateCode(codegen) {
return this;
}
writeCode(writer) {
var code = this.code;
if (!code) {
return;
}
code = adjustIndent(code, codegen.currentIndent);
code = adjustIndent(code, writer.currentIndent);
codegen.write(code);
codegen.write('\n');
writer.write(code);
writer.write('\n');
}
}

View File

@ -13,14 +13,12 @@ class SelfInvokingFunction extends Node {
generateCode(codegen) {
var params = this.params || [];
var args = this.args || [];
var body = this.body;
var body = codegen.generateCode(this.body);
codegen.write('(');
var functionDeclaration = codegen.builder.functionDeclaration(null, params, body);
var functionCall = codegen.builder.functionCall(functionDeclaration, args);
codegen.generateCode(functionCall);
codegen.write(')');
return functionCall;
}
walk(walker) {

View File

@ -1,36 +0,0 @@
'use strict';
var Node = require('./Node');
class Slot extends Node {
constructor(def) {
super('Slot');
this.onDone = def.onDone;
this.codegenSlot = null;
}
generateCode(codegen) {
if (this.onDone) {
codegen.onDone((codegen) => {
this.onDone(this, codegen);
});
}
// At the time the code for this node is to be generated we instead
// create a slot. A slot is just a marker in the output code stream
// that we can later inject code into. The injection happens after
// the entire tree has been walked.
this.codegenSlot = codegen.beginSlot(this);
}
setContent(content) {
this.codegenSlot.setContent(content);
}
toJSON() {
return {
type: this.type
};
}
}
module.exports = Slot;

View File

@ -11,6 +11,8 @@ function createVarsArray(vars) {
});
}
var templateExports = null;
class TemplateRoot extends Node {
constructor(def) {
super('TemplateRoot');
@ -20,51 +22,63 @@ class TemplateRoot extends Node {
generateCode(codegen) {
var context = codegen.context;
var body = this.body;
codegen.addStaticVar('str', '__helpers.s');
codegen.addStaticVar('empty', '__helpers.e');
codegen.addStaticVar('notEmpty', '__helpers.ne');
codegen.addStaticVar('escapeXml', '__helpers.x');
var body = codegen.generateCode(this.body);
var builder = codegen.builder;
var program = builder.program;
var functionDeclaration = builder.functionDeclaration;
var returnStatement = builder.returnStatement;
var slot = builder.slot;
var staticsSlot = slot();
var varsSlot = slot();
varsSlot.noOutput = true;
body = [ varsSlot ].concat(body.items);
var outputNode = program([
functionDeclaration('create', ['__helpers'], [
staticsSlot,
returnStatement(
functionDeclaration('render', ['data', 'out'], body))
]),
'(module.exports = require("marko").c(__filename)).c(create)'
]);
codegen.generateCode(outputNode);
var staticVars = context.getStaticVars();
var staticCodeArray = context.getStaticCode();
var staticContent = [builder.vars(createVarsArray(staticVars))];
if (staticCodeArray) {
staticCodeArray.forEach((code) => {
staticContent.push(code);
});
let renderStatements = [];
var vars = createVarsArray(context.getVars());
if (vars.length) {
renderStatements.push(builder.vars(vars));
}
staticsSlot.setContent(staticContent);
renderStatements = renderStatements.concat(body);
var vars = context.getVars();
varsSlot.setContent(builder.vars(createVarsArray(vars)));
if (context.inline) {
var createInlineMarkoTemplateVar = context.importModule('marko_createInlineTemplate', 'marko/runtime/inline');
return builder.functionCall(
createInlineMarkoTemplateVar,
[
builder.identifier('__filename'),
builder.functionDeclaration(
null,
[
builder.identifier('data'),
builder.identifierOut()
],
renderStatements)
]);
} else {
let createStatements = [];
let staticNodes = context.getStaticNodes();
if (staticNodes.length) {
createStatements = createStatements.concat(staticNodes);
}
let renderFunction = builder.functionDeclaration(
'render',
['data', builder.identifierOut()],
renderStatements);
createStatements.push(builder.returnStatement(renderFunction));
if (!templateExports) {
templateExports = builder.parseStatement('(module.exports = require("marko").c(__filename)).c(create)');
}
return builder.program([
builder.functionDeclaration(
'create',
[
context.helpersIdentifier
],
createStatements),
templateExports
]);
}
}
toJSON(prettyPrinter) {

View File

@ -23,20 +23,16 @@ class Text extends Node {
}
generateHtmlCode(codegen) {
var parentNode = this.parentNode;
if (parentNode) {
parentNode._normalizeChildTextNodes(codegen);
}
var context = codegen.context;
var argument = this.argument;
var escape = this.escape !== false;
if (argument instanceof Literal) {
if (!argument.value) {
return;
return null;
}
if (codegen.context.isFlagSet('SCRIPT_BODY')) {
if (context.isFlagSet('SCRIPT_BODY')) {
escape = false;
}
@ -47,23 +43,23 @@ class Text extends Node {
let builder = codegen.builder;
if (escape) {
let escapeFuncVar = 'escapeXml';
let escapeIdentifier = context.helper('escapeXml');
if (codegen.context.isFlagSet('SCRIPT_BODY')) {
escapeFuncVar = codegen.addStaticVar('escapeScript', '__helpers.xs');
if (context.isFlagSet('SCRIPT_BODY')) {
escapeIdentifier = context.helper('escapeScript');
}
// TODO Only escape the parts that need to be escaped if it is a compound expression with static
// text parts
argument = builder.functionCall(
escapeFuncVar,
escapeIdentifier,
[argument]);
} else {
argument = builder.functionCall(builder.identifier('str'), [ argument ]);
argument = builder.functionCall(context.helper('str'), [ argument ]);
}
}
codegen.addWrite(argument);
return codegen.builder.html(argument);
}
isWhitespace() {

View File

@ -8,7 +8,11 @@ class ThisExpression extends Node {
}
generateCode(codegen) {
codegen.write('this');
return this;
}
writeCode(writer) {
writer.write('this');
}
toString() {

View File

@ -12,32 +12,37 @@ class UnaryExpression extends Node {
}
generateCode(codegen) {
this.argument = codegen.generateCode(this.argument);
return this;
}
writeCode(writer) {
var argument = this.argument;
var operator = this.operator;
var prefix = this.prefix;
if (prefix) {
codegen.write(operator);
writer.write(operator);
if (operator === 'typeof' || operator === 'delete') {
codegen.write(' ');
writer.write(' ');
}
}
var wrap = isCompoundExpression(argument);
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(argument);
writer.write(argument);
if (wrap) {
codegen.write(')');
writer.write(')');
}
if (!prefix) {
codegen.write(operator);
writer.write(operator);
}
}

View File

@ -12,28 +12,33 @@ class UpdateExpression extends Node {
}
generateCode(codegen) {
this.argument = codegen.generateCode(this.argument);
return this;
}
writeCode(writer) {
var argument = this.argument;
var operator = this.operator;
var prefix = this.prefix;
if (prefix) {
codegen.generateCode(operator);
writer.write(operator);
}
var wrap = isCompoundExpression(argument);
if (wrap) {
codegen.write('(');
writer.write('(');
}
codegen.generateCode(argument);
writer.write(argument);
if (wrap) {
codegen.write(')');
writer.write(')');
}
if (!prefix) {
codegen.generateCode(operator);
writer.write(operator);
}
}

View File

@ -23,6 +23,12 @@ class VariableDeclarator extends Node {
}
generateCode(codegen) {
this.id = codegen.generateCode(this.id);
this.init = codegen.generateCode(this.init);
return this;
}
writeCode(writer) {
var id = this.id;
var init = this.init;
@ -30,11 +36,11 @@ class VariableDeclarator extends Node {
throw new Error('Invalid variable name: ' + id);
}
codegen.generateCode(id);
writer.write(id);
if (init != null) {
codegen.write(' = ');
codegen.generateCode(init);
writer.write(' = ');
writer.write(init);
}
}

View File

@ -12,51 +12,53 @@ class Vars extends Node {
generateCode(codegen) {
var declarations = this.declarations;
var kind = this.kind;
var isStatement = this.statement;
var body = this.body;
var hasBody = this.body && this.body.length;
if (!declarations || !declarations.length) {
return null;
}
if(hasBody) {
var scopedBody = [this].concat(this.body.items);
if (this.body && this.body.length) {
var scopedBody = [this].concat(this.body);
this.body = null;
return codegen.builder.selfInvokingFunction(scopedBody);
}
return this;
}
writeCode(writer) {
var declarations = this.declarations;
var kind = this.kind;
var isStatement = this.statement;
if (!declarations || !declarations.length) {
return;
}
codegen.incIndent(4);
writer.incIndent(4);
for (let i=0; i<declarations.length; i++) {
var declarator = declarations[i];
if (i === 0) {
codegen.write(kind + ' ');
writer.write(kind + ' ');
} else {
codegen.writeLineIndent();
writer.writeLineIndent();
}
codegen.generateCode(declarator);
writer.write(declarator);
if (i < declarations.length - 1) {
codegen.write(',\n');
writer.write(',\n');
} else {
if (isStatement) {
codegen.write(';\n');
writer.write(';\n');
}
}
}
codegen.decIndent(4);
if (hasBody) {
codegen.generateCode(body);
}
writer.decIndent(4);
}
walk(walker) {

View File

@ -10,16 +10,22 @@ class WhileStatement extends Node {
}
generateCode(codegen) {
this.test = codegen.generateCode(this.test);
this.body = codegen.generateCode(this.body);
return this;
}
writeCode(writer) {
var test = this.test;
var body = this.body;
codegen.write('while (');
codegen.generateCode(test);
codegen.write(') ');
writer.write('while (');
writer.write(test);
writer.write(') ');
codegen.generateBlock(body);
writer.write(body);
codegen.write('\n');
writer.write('\n');
}
walk(walker) {

View File

@ -8,6 +8,10 @@ var Builder = require('./Builder');
var extend = require('raptor-util/extend');
var CompileContext = require('./CompileContext');
var globalConfig = require('./config');
var CompileContext = require('./CompileContext');
var InlineCompiler = require('./InlineCompiler');
var ok = require('assert').ok;
var defaults = extend({}, globalConfig);
Object.defineProperty(exports, 'defaultOptions', {
@ -55,21 +59,53 @@ function createWalker(options) {
return new Walker(options);
}
function compileFile(filename, options, callback) {
var fs = req('fs');
var compiler;
function _compile(src, filename, userOptions, callback) {
ok(filename, '"filename" argument is required');
ok(typeof filename === 'string', '"filename" argument should be a string');
var options = {};
extend(options, globalConfig);
if (userOptions) {
extend(options, userOptions);
}
var compiler = defaultCompiler;
var context = new CompileContext(src, filename, compiler.builder, options);
if (callback) {
let compiled;
try {
compiled = compiler.compile(src, context);
} catch(e) {
return callback(e);
}
callback(null, compiled.code);
} else {
let compiled = compiler.compile(src, context);
return compiled.code;
}
}
function compile(src, filename, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (options) {
compiler = options.compiler;
}
return _compile(src, filename, options, callback);
}
if (!compiler) {
compiler = defaultCompiler;
function compileFile(filename, options, callback) {
var fs = req('fs');
if (typeof options === 'function') {
callback = options;
options = null;
}
if (callback) {
@ -78,43 +114,27 @@ function compileFile(filename, options, callback) {
return callback(err);
}
try {
callback(null, compiler.compile(templateSrc, filename, options));
} catch(e) {
callback(e);
}
_compile(templateSrc, filename, options, callback);
});
} else {
let templateSrc = fs.readFileSync(filename, {encoding: 'utf8'});
return compiler.compile(templateSrc, filename, options);
return _compile(templateSrc, filename, options, callback);
}
}
function compile(src, filename, options, callback) {
var compiler;
function createInlineCompiler(filename, userOptions) {
var options = {};
if (typeof options === 'function') {
callback = options;
options = null;
extend(options, globalConfig);
if (userOptions) {
extend(options, userOptions);
}
if (options) {
compiler = options.compiler;
}
var compiler = defaultCompiler;
var context = new CompileContext('', filename, compiler.builder, options);
if (!compiler) {
compiler = defaultCompiler;
}
if (callback) {
try {
callback(null, compiler.compile(src, filename, options));
} catch(e) {
callback(e);
}
} else {
return compiler.compile(src, filename, options);
}
return new InlineCompiler(context, compiler);
}
function checkUpToDate(templateFile, templateJsFile) {
@ -160,6 +180,7 @@ exports.createBuilder = createBuilder;
exports.compileFile = compileFile;
exports.compile = compile;
exports.parseRaw = parseRaw;
exports.createInlineCompiler = createInlineCompiler;
exports.checkUpToDate = checkUpToDate;
exports.getLastModified = getLastModified;
@ -189,16 +210,3 @@ exports.registerTaglib = function(path) {
taglibLookup.registerTaglib(path);
clearCaches();
};
/*
exports.Taglib = require('./Taglib');
exports.lookup = require('./taglib-lookup');
exports.buildLookup = exports.lookup.buildLookup;
exports.registerTaglib = exports.lookup.registerTaglib;
exports.excludeDir = exports.lookup.excludeDir;
exports.clearCaches = function() {
exports.lookup.clearCaches();
require('./taglib-finder').clearCaches();
};
*/

View File

@ -5,11 +5,12 @@ The Marko compiler is responsible for taking an input Marko template and produci
# Compiler stages
The three primary stages of the Marko compiler are parse, transform and generate:
The four primary stages of the Marko compiler are parse, transform, generate and write:
- __parse__ - Parse the template source to produce an [Abstract Syntax Tree (AST)](https//en.wikipedia.org/wiki/Abstract_syntax_tree).
- __transform__ - Transform the AST (add/remove/modify/rearrange nodes)
- __generate__ - Generate compiled JavaScript code based on the final AST
- __transform__ - Transform the AST using custom transforms (add/remove/modify/rearrange nodes)
- __generate__ - Generate the final AST
- __write__ - Write the JavaScript code based on the final AST
Each of these stages is described in more detail in the sections below.
@ -143,35 +144,79 @@ Continuing with the previous example, after the transformation stage, the AST wi
}
```
You'll notice in the transformed AST that the `HtmlElement` associated with the `<div>` tag was wrapped with a new `If` node. After the AST has been transformed it is now time to generate the compiled JavaScript code.
You'll notice in the transformed AST that the `HtmlElement` associated with the `<div>` tag was wrapped with a new `If` node.
During the transform stage, the entire AST might be walked multiple times. Not until there are no more nodes transformed does the transform stage complete.
After the AST has been transformed it is now time to generate the final AST as part of the _generate_ stage.
## Generate stage
The generate stage is the final stage of the Marko compiler. During the generate stage the Marko compiler will walk the tree to produce the final JavaScript code. Each node in the tree will have an opportunity to generate JavaScript code. The Marko compiler provides a [`CodeGenerator`](../compiler/CodeGenerator.js) class and an API for generating fragments of JavaScript code that makes it easy to produce well-formed and readable JavaScript code as output.
After the AST has been transformed, the final AST consisting of only "final" AST nodes that are capable of writing out JavaScript code must be generated. A final AST node is a node that is capable of writing out JavaScript code and it must have a `writeCode(writer)` method. During this stage, `generateCode` is called on the root node and each node is responsible for recursively calling `generateCode` on children AST nodes to produce the final AST nodes.
Every node in the tree must implement one of the following methods:
- `generateCode(generator)`
- `generate<OUTPUT_TYPE>Code(generator)` (e.g. `generateHtmlCode(generator)`)
- `generateCode(codegen) : Node`
- `generate<OUTPUT_TYPE>Code(codegen) : Node` (e.g. `generateHtmlCode(codegen)`)
The `generator` argument will be an instance of [`CodeGenerator`](../compiler/CodeGenerator.js).
The `codegen` argument will be an instance of [`CodeGenerator`](../compiler/CodeGenerator.js).
The Marko compiler supports compiling templates differently based on an "output type". Currently, the only supported output type is "Html". With the "Html" output type, the compiled template will be a program that, when executed, will produce an HTML string as output. In the future we may support other output types such as DOM, Virtual DOM, incremental DOM, etc. For example, with the "DOM" output type, the compiled program could use the web browser's DOM API to produce a DOM tree as output (instead of an HTML string).
Below is the fragment of code used by the `ForStatement` node to generate the final AST node:
```javascript
generateCode(codegen) {
this.init = codegen.generateCode(this.init);
this.test = codegen.generateCode(this.test);
this.update = codegen.generateCode(this.update);
this.body = codegen.generateCode(this.body);
return this;
}
```
In the above example code, the node is returning itself as the final code. Alternatively, a node could return a completely different node that will automatically be made final:
```javascript
generateCode(codegen) {
let builder = codegen.builder;
return builder.functionCall(
builder.identifier('console'),
[
builder.literal('Hello World!')
]);
}
```
## Write stage
The write stage is the final stage of the Marko compiler. During the write state, each node will have an opportunity to write out JavaScript code to the final output buffer for the compiled template. The Marko compiler provides a [`CodeWriter`](../compiler/CodeWriter.js) class for writing out JavaScript primitives such as blocks and statements and it also provides support controlling indentation.
Every node in the tree must implement one of the following methods:
- `writeCode(writer)`
- `write<OUTPUT_TYPE>Code(writer)` (e.g. `writerHtmlCode(writer)`)
The `writer` argument will be an instance of [`CodeWriter`](../compiler/CodeWriter.js).
Below is the fragment of code used by the `If` node to generate the output JavaScript code:
```javascript
generator.write('if (');
generator.generateCode(test);
generator.write(') ');
generator.generateBlock(body);
if (elseStatement) {
generator.write(' ');
generator.generateCode(elseStatement);
} else {
generator.write('\n');
writeCode(writer) {
var test = this.test;
var body = this.body;
writer.write('if (');
writer.write(test);
writer.write(') ');
writer.writeBlock(body);
if (this.else) {
writer.write(' else ');
writer.write(this.else);
} else {
writer.write('\n');
}
}
```

1
runtime/inline.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./')._inline;

View File

@ -310,6 +310,10 @@ function load(templatePath, templateSrc, options) {
return template;
}
function createInlineMarkoTemplate(filename, renderFunc) {
return new Template(filename, renderFunc);
}
exports.load = load;
exports.createWriter = function(writer) {
@ -320,6 +324,8 @@ exports.helpers = helpers;
exports.Template = Template;
exports._inline = createInlineMarkoTemplate;
// The loader is used to load templates that have not already been
// loaded and cached. On the server, the loader will use
// the compiler to compile the template and then load the generated

View File

@ -52,7 +52,7 @@ module.exports = function transform(el, context) {
if (isObjectEmpty(arg)) {
arg = el.getAttributeValue('arg');
} else {
let mergeVar = context.addStaticVar('__merge', '__helpers.m');
let mergeVar = context.helper('merge');
arg = builder.functionCall(mergeVar, [
builder.literal(arg), // Input props from the attributes take precedence
el.getAttributeValue('arg')

View File

@ -58,7 +58,7 @@ module.exports = function codeGenerator(el, codegen) {
if (Object.keys(templateData).length === 0) {
templateData = args[1];
} else {
let mergeVar = codegen.addStaticVar('__merge', '__helpers.m');
let mergeVar = codegen.context.helper('merge');
templateData = builder.functionCall(mergeVar, [
builder.literal(templateData), // Input props from the attributes take precedence
args[1] // The template data object is passed as the second argument: <include("./foo.marko", { ... })/>

1
test/.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules
/scratch.js
*.generated.js

View File

@ -1,3 +1,3 @@
out.w("<div class=\"foo" +
className +
"\">Hello World</div>");
"\">Hello World</div>")

View File

@ -1,7 +1,7 @@
if (true) {
if (!(!data.url)) {
out.w("<a" +
attr("href", data.url) +
marko_attr("href", data.url) +
">");
}

View File

@ -1,5 +1,6 @@
if (a > b) {
before();
out.w("<div class=\"greeting\">Hello World</div>");
after();

View File

@ -2,7 +2,9 @@ if (a > b) {
var before;
before();
var foo;
after();
var after;

View File

@ -1,5 +1,7 @@
if (a > b) {
before();
var foo;
after();
}

View File

@ -1,4 +1,5 @@
before();
out.w("<div class=\"greeting\">Hello World</div>");
after();

View File

@ -21,5 +21,7 @@ module.exports = function(builder) {
event.insertCode(builder.functionCall('after', []));
});
return htmlElement;
return builder.program([
htmlElement
]);
};

View File

@ -1,9 +1,5 @@
function create(__helpers) {
var foo = "Hello World",
str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
var foo = "Hello World";
return function render(data, out) {
out.w("<div></div>");

View File

@ -1,9 +1,4 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
return function render(data, out) {
var foo = "Hello World";

View File

@ -1,8 +1,5 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
var marko_escapeXml = __markoHelpers.x;
return function render(data, out) {
out.w("<ul>");
@ -11,7 +8,7 @@ function create(__helpers) {
foo();
out.w("<li>" +
escapeXml(color) +
marko_escapeXml(color) +
"</li>");
bar();

View File

@ -0,0 +1,9 @@
function create(__markoHelpers) {
var foo = "Hello World";
return function render(data, out) {
out.w("<div></div>");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,15 @@
'use strict';
module.exports = function(builder, codegen) {
var templateRoot = builder.templateRoot([
builder.htmlElement(
'div',
[])
]);
codegen.context.addStaticVar('foo', builder.literal('Hello World'));
codegen.context.addStaticVar('foo', builder.literal('Hello World'));
return templateRoot;
};

View File

@ -1,9 +1,5 @@
function create(__helpers) {
var foo = "Hello World",
str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
var foo = "Hello World";
return function render(data, out) {
out.w("<div></div>");

View File

@ -0,0 +1,13 @@
function create(__markoHelpers) {
var fooStatic = "Hello Foo",
barStatic = "Hello Bar";
return function render(data, out) {
var foo = "Hello Foo",
bar = "Hello Bar";
out.w("<div></div>");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,18 @@
'use strict';
module.exports = function(builder, codegen) {
var templateRoot = builder.templateRoot([
builder.htmlElement(
'div',
[])
]);
codegen.context.addVar('foo', builder.literal('Hello Foo'));
codegen.context.addVar('bar', builder.literal('Hello Bar'));
codegen.context.addStaticVar('fooStatic', builder.literal('Hello Foo'));
codegen.context.addStaticVar('barStatic', builder.literal('Hello Bar'));
return templateRoot;
};

View File

@ -1,9 +1,4 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
return function render(data, out) {
var foo = "Hello World";

View File

@ -1,13 +1,10 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x,
forEach = __helpers.f;
function create(__markoHelpers) {
var marko_forEach = __markoHelpers.f,
marko_escapeXml = __markoHelpers.x;
return function render(data, out) {
forEach(data.colors, function(color) {
out.w(escapeXml(color));
marko_forEach(data.colors, function(color) {
out.w(marko_escapeXml(color));
});
};
}

View File

@ -1,12 +1,8 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x,
forEachProp = __helpers.fp;
function create(__markoHelpers) {
var marko_forEachProp = __markoHelpers.fp;
return function render(data, out) {
forEachProp(myObject, function(k, v) {
marko_forEachProp(myObject, function(k, v) {
console.log("k:", k, "v:", v);
});
};

View File

@ -1,15 +1,10 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
return function render(data, out) {
(function() {
for (var i = 0; i <= myArray.length; i += 2) {
console.log(i);
}
}());
})();
};
}

View File

@ -1 +1 @@
out.w("<div>Hello World</div>");
out.w("<div>Hello World</div>")

View File

@ -1 +1 @@
out.w("<!--This is an HTML comment-->");
out.w("<!--This is an HTML comment-->")

View File

@ -1,3 +1,3 @@
out.w("<div class=\"foo" +
className +
"\">Hello World</div>");
"\">Hello World</div>")

View File

@ -1,3 +1,3 @@
out.w("<div class=\"greeting\"" +
attr("foo", bar) +
">Hello World</div>");
marko_attr("foo", bar) +
">Hello World</div>")

View File

@ -2,4 +2,4 @@ out.w("<" +
data.tagName +
" class=\"greeting\">Hello World</" +
data.tagName +
">");
">")

View File

@ -1 +1 @@
out.w("<div class=\"greeting\">Hello World</div>");
out.w("<div class=\"greeting\">Hello World</div>")

View File

@ -1,9 +1,4 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
return function render(data, out) {
if (true) {
out.w("A");

View File

@ -1,6 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
marko_escapeXml(name));
}
macro_greeting("Frank", 10, out, function renderBody(out) {

View File

@ -1,6 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
marko_escapeXml(name));
}
macro_greeting("Frank", 10, out);

View File

@ -1,6 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
marko_escapeXml(name));
}
macro_greeting("Frank", 10, out, function renderBody(out) {

View File

@ -1,6 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
marko_escapeXml(name));
}
macro_greeting("Frank", 10, out);

View File

@ -1,4 +1,4 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
marko_escapeXml(name));
}

View File

@ -1,8 +1,5 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x;
function create(__markoHelpers) {
var marko_escapeXml = __markoHelpers.x;
return function render(data, out) {
out.w("Hello" +
@ -14,7 +11,7 @@ function create(__helpers) {
forEach(data.colors, function(color) {
out.w("<li class=\"color\">" +
escapeXml(color) +
marko_escapeXml(color) +
"</li>");
});

View File

@ -17,8 +17,8 @@ module.exports = function(builder) {
div.moveChildrenTo(span);
return [
return builder.program([
div,
span
];
]);
};

View File

@ -1,8 +1,10 @@
'use strict';
module.exports = function(builder) {
return builder.node(function(node, codegen) {
return builder.program([
builder.node(function(node, codegen) {
var builder = codegen.builder;
return builder.text(builder.literal('Hello World!'));
});
})
]);
};

View File

@ -12,5 +12,5 @@ module.exports = function(builder) {
div.removeAllAttributes();
return div;
return builder.program([div]);
};

View File

@ -2,4 +2,4 @@
var foo;
foo = "bar";
}())
})()

View File

@ -2,4 +2,4 @@
var foo;
foo = "bar";
}())
})()

View File

@ -1,3 +1,3 @@
(function(win) {
win.foo = "bar";
}(window))
})(window)

View File

@ -1,6 +0,0 @@
a = "abc";
var foo = "abc",
bar = 123;
b = "def";

View File

@ -1,25 +0,0 @@
'use strict';
module.exports = function(builder) {
var vars = [];
return builder.program([
builder.assignment('a', builder.literal('abc')),
builder.slot((slot, codegen) => {
slot.setContent(codegen.builder.vars(vars));
}),
builder.node(function(node, codegen) {
vars.push({
id: 'foo',
init: codegen.builder.literal('abc')
});
}),
builder.node(function(node, codegen) {
vars.push({
id: 'bar',
init: codegen.builder.literal(123)
});
}),
builder.assignment('b', builder.literal('def'))
]);
};

View File

@ -1,8 +0,0 @@
if (true) {
out.w("BEFORE - Hello World");
var foo = "abc",
bar = 123;
out.w("AFTER - Hello World");
}

Some files were not shown because too many files have changed in this diff Show More