marko/compiler/CodeGenerator.js
Patrick Steele-Idem c386da875e Fixes #349 - Inline Marko template compilation support
Also changed how JavaScript code is generated
2016-08-19 10:50:28 -06:00

302 lines
7.8 KiB
JavaScript

'use strict';
const isArray = Array.isArray;
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 createError = require('raptor-util/createError');
class GeneratorEvent {
constructor(node, codegen) {
this.node = node;
this.codegen = codegen;
this.isBefore = true;
this.builder = codegen.builder;
this.context = codegen.context;
this.insertedNodes = null;
}
insertCode(newCode) {
this.insertedNodes = newCode;
}
}
class FinalNodes {
constructor() {
this.nodes = [];
this.nodes._finalNode = true; // Mark the array as a collection of final nodes
this.lastNode = null;
}
push(node) {
if (!node) {
return;
}
if (node instanceof Html && this.lastNode instanceof Html) {
this.lastNode.append(node);
return;
}
if (node.setFinalNode) {
node.setFinalNode(true);
}
this.lastNode = node;
this.nodes.push(node);
}
}
class CodeGenerator {
constructor(context, options) {
options = options || {};
this.root = null;
this._code = '';
this.currentIndent = '';
this.inFunction = false;
this._doneListeners = [];
this.builder = context.builder;
this.outputType = options.output || 'html';
this.context = context;
ok(this.builder, '"this.builder" is required');
this._codegenCodeMethodName = 'generate' +
this.outputType.charAt(0).toUpperCase() +
this.outputType.substring(1) +
'Code';
}
addVar(name, value) {
return this.context.addVar(name, value);
}
addStaticVar(name, value) {
return this.context.addStaticVar(name, value);
}
addStaticCode(code) {
this.context.addStaticCode(code);
}
getEscapeXmlAttrVar() {
return this.context.getEscapeXmlAttrVar();
}
importModule(varName, path) {
return this.context.importModule(varName, path);
}
_invokeCodeGenerator(func, node, isMethod) {
try {
if (isMethod) {
return func.call(node, this);
} else {
return func.call(node, node, this);
}
} catch(err) {
var errorMessage = 'Generating code for ';
if (node instanceof HtmlElement) {
errorMessage += '<'+node.tagName+'> tag';
} else {
errorMessage += node.type + ' node';
}
if (node.pos) {
errorMessage += ' ('+this.context.getPosInfo(node.pos)+')';
}
errorMessage += ' failed. Error: ' + err;
throw createError(errorMessage, err /* cause */);
}
}
_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 (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(generatedCode, finalNodes);
}
} else {
finalNodes.push(node);
}
}
if (beforeAfterEvent) {
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;
}
generateCode(node) {
if (!node) {
return null;
}
if (node._finalNode) {
return node;
}
let finalNodes = new FinalNodes();
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];
}
}
return finalNodes;
}
isLiteralNode(node) {
return node instanceof Literal;
}
isIdentifierNode(node) {
return node instanceof Identifier;
}
isPreserveWhitespaceEnabled() {
return false;
}
addError(message, code) {
ok('"message" is required');
let node = this._currentNode;
if (typeof message === 'object') {
let errorInfo = message;
errorInfo.node = node;
this.context.addError(errorInfo);
} else {
this.context.addError({node, message, code});
}
}
onDone(listenerFunc) {
this._doneListeners.push(listenerFunc);
}
getRequirePath(targetFilename) {
return this.context.getRequirePath(targetFilename);
}
resolvePath(pathExpression) {
return this.context.resolvePath(pathExpression);
}
}
module.exports = CodeGenerator;