'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 util = require('util'); 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; } setContent(content) { this._content = content; } 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); } slotCode = capture.end(); if (isStatement && slotCode.startsWith(codegen.currentIndent)) { slotCode = slotCode.substring(codegen.currentIndent.length); } } let oldCode = codegen._code; let beforeCode = oldCode.substring(0, this._start); let afterCode = oldCode.substring(this._end); codegen._code = beforeCode + (slotCode || '') + afterCode; } } class Generator { constructor(context, options) { options = options || {}; this.root = null; this._indentStr = options.indent != null ? options.indent : ' '; this._indentSize = this._indentStr.length; this._code = ''; this.currentIndent = ''; this.inFunction = false; this._doneListeners = []; this._bufferedWrites = null; this.builder = context.builder; this.walker = context.walker; 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'; this._slots = []; } beginSlot(slotNode) { var addSeparator = slotNode.statement; this._flushBufferedWrites(addSeparator); let slot = new Slot(this, slotNode); this._slots.push(slot); return slot; } addStaticVar(name, value) { return this.context.addStaticVar(name, value); } addStaticCode(code) { this.context.addStaticCode(code); } getStaticVars() { return this.context.getStaticVars(); } getStaticCode() { return this.context.getStaticCode(); } 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 instanceof Container)) { node.forEach(this.generateCode, this); return; } let oldCurrentNode = this._currentNode; this._currentNode = node; let finalNode; let generateCodeFunc; var isStatement = node.statement; if (node.getCodeGenerator) { generateCodeFunc = node.getCodeGenerator(this.outputType); if (generateCodeFunc) { finalNode = generateCodeFunc(node, this); 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 node = null; } } } if (finalNode) { if (isStatement) { this.generateStatements(finalNode); } else { this.generateCode(finalNode); } } 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); } } } this._currentNode = oldCurrentNode; } getCode() { this._flushBufferedWrites(); while(this._doneListeners.length || this._slots.length) { let doneListeners = this._doneListeners; if (doneListeners.length) { this._doneListeners = []; for (let i=0; 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 { 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