@adobe/htlengine
Version:
Javascript Based HTL (Sightly) parser
314 lines (285 loc) • 9.19 kB
JavaScript
/*
* Copyright 2018 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
const OutText = require('../parser/commands/OutText');
const VariableBinding = require('../parser/commands/VariableBinding');
const FileReference = require('../parser/commands/FileReference');
const FunctionBlock = require('../parser/commands/FunctionBlock');
const ExternalCode = require('../parser/commands/ExternalCode');
const FunctionCall = require('../parser/commands/FunctionCall');
const Conditional = require('../parser/commands/Conditional');
const Loop = require('../parser/commands/Loop');
const OutputVariable = require('../parser/commands/OutputVariable');
const CreateElement = require('../parser/commands/CreateElement');
const PushElement = require('../parser/commands/PushElement');
const PopElement = require('../parser/commands/PopElement');
const Doctype = require('../parser/commands/Doctype');
const Comment = require('../parser/commands/Comment');
const AddAttribute = require('../parser/commands/AddAttribute');
const DomHandler = require('./DomHandler');
module.exports = class JSCodeGenVisitor {
constructor() {
this._main = {
line: 1,
code: '',
map: [],
};
this._blocks = [];
this._blk = this._main;
this._templateStack = [];
this._sourceFile = null;
this._indentLevel = 0;
this._templateIndentLevel = 0;
this._indents = [];
this._dom = new DomHandler(this);
this._enableSourceMaps = false;
this._scriptId = 'global';
this._runtimeGlobals = [];
}
withIndent(delim) {
this._indents = [delim];
this._indent = delim;
for (let i = 0; i < 50; i += 1) {
this._indents[i + 1] = this._indents[i] + delim;
}
return this;
}
withSourceMap(enabled) {
this._enableSourceMaps = enabled;
return this;
}
/**
* Sets the name of the source file used when generating the source map.
* @param {string} value the source file name.
* @returns {Compiler} this
*/
withSourceFile(value) {
this._sourceFile = value;
return this;
}
/**
* Sets the offset of the code in the source file when generating the source map.
* @param {number} value the offset.
* @returns {Compiler} this
*/
withSourceOffset(value) {
this._sourceOffset = value;
return this;
}
withScriptId(id) {
this._scriptId = id;
return this;
}
withGlobals(value) {
this._runtimeGlobals = value;
return this;
}
get scriptId() {
return this._scriptId;
}
indent() {
this._indent = this._indents[++this._indentLevel] || ''; // eslint-disable-line no-plusplus
return this;
}
outdent() {
this._indent = this._indents[--this._indentLevel] || ''; // eslint-disable-line no-plusplus
return this;
}
setIndent(n) {
this._indentLevel = n;
this._indent = this._indents[n] || '';
return this;
}
pushBlock(name) {
const block = {
name,
line: 1,
code: '',
lastIndentLevel: this._indentLevel,
map: [],
};
this._blocks.push(block);
this._templateStack.push(block);
this._blk = block;
this.setIndent(this._templateIndentLevel);
return block;
}
popBlock() {
const blk = this._templateStack.pop();
this.setIndent(blk.lastIndentLevel);
if (this._templateStack.length > 0) {
this._blk = this._templateStack[this._templateStack.length - 1];
} else {
this._blk = this._main;
}
}
process({ commands, templates }) {
this._dom.beginDocument();
// create a global block for global templates
const globalBlock = this.pushBlock('');
this.popBlock();
// first process the main commands
commands.forEach((c) => {
c.accept(this);
});
// add global vars for templates
this.outdent();
// eslint-disable-next-line no-underscore-dangle
Object.keys(this._dom._globalTemplates).forEach((name) => {
if (this._runtimeGlobals.indexOf(name) < 0) {
globalBlock.code += `${this._indent}let ${name};\n`;
globalBlock.line += 1;
}
});
this.indent();
// eslint-disable-next-line no-underscore-dangle
this._dom._globalTemplates = [];
// then process the templates
this._sourceOffset = 0;
this._templateIndentLevel = 1;
Object.values(templates).forEach((t) => {
// this is kind of a hack to push code to the template stack
const groupBlock = this.pushBlock('');
this.outdent();
this.out(`(function _template_${t.id.replace(/[^\w]/g, '_')}(){ `);
this.popBlock();
this._sourceFile = t.file;
t.commands.forEach((c) => {
c.accept(this);
});
this.pushBlock('');
this.outdent();
this.out('})();');
this.popBlock();
// add variable initializers for the template names
// eslint-disable-next-line no-underscore-dangle
Object.keys(this._dom._globalTemplates).forEach((name) => {
groupBlock.code += `${this._indent}let ${name};\n`;
groupBlock.line += 1;
});
// eslint-disable-next-line no-underscore-dangle
this._dom._globalTemplates = [];
});
this._dom.endDocument();
let templateCode = '';
// create the template code mappings
const templateMappings = [];
let offset = 0;
this._blocks.forEach((blk) => {
templateCode += blk.code;
blk.map.forEach((m) => {
// eslint-disable-next-line no-param-reassign
m.generated.line += offset;
templateMappings.push(m);
});
// shift the offset by the length of the block
offset += blk.line - 1;
});
return {
code: this._main.code,
mappings: this._main.map,
templateCode,
templateMappings,
};
}
out(msg) {
if (this._indent) {
this._blk.code += `${this._indent}${msg}\n`;
} else {
this._blk.code += msg;
}
this._blk.line += 1;
}
_addMapping(location) {
if (!location) {
return;
}
const line = location.line + this._sourceOffset + 1;
const { column } = location;
// skip multiple mappings for the same original line and column, as
// IDEs wouldn't probably know how to distinguish them
const lastmapping = this._blk.map[this._blk.map.length - 1];
if (lastmapping
&& lastmapping.original.line === line
&& lastmapping.original.column === column) {
return;
}
this._blk.map.push({
original: {
line,
column,
},
generated: {
line: this._blk.line,
column: 0,
},
source: this._sourceFile || '<internal>',
});
}
visit(cmd) {
if (this._enableSourceMaps) {
this._addMapping(cmd.location);
}
if (cmd instanceof OutText) {
this._dom.outText(cmd);
} else if (cmd instanceof VariableBinding.Start) {
this._dom.setVariable(cmd);
} else if (cmd instanceof VariableBinding.Global) {
this._dom.setVariable(cmd);
} else if (cmd instanceof VariableBinding.End) {
// nop
} else if (cmd instanceof FileReference) {
this._dom.bindFunction(cmd);
} else if (cmd instanceof FunctionBlock.Start) {
this.pushBlock(cmd.expression);
this._dom.functionStart(cmd);
} else if (cmd instanceof FunctionBlock.End) {
this._dom.functionEnd(cmd);
this.popBlock();
} else if (cmd instanceof FunctionCall) {
this._dom.functionCall(cmd);
} else if (cmd instanceof Conditional.Start) {
this._dom.conditionStart(cmd);
this.indent();
} else if (cmd instanceof Conditional.End) {
this.outdent();
this._dom.conditionEnd();
} else if (cmd instanceof OutputVariable) {
this._dom.outVariable(cmd.variableName);
} else if (cmd instanceof Loop.Init) {
this._dom.loopInit(cmd);
} else if (cmd instanceof Loop.Start) {
this._dom.loopStart(cmd);
this.indent();
} else if (cmd instanceof Loop.End) {
this.outdent();
this._dom.loopEnd(cmd);
} else if (cmd instanceof CreateElement) {
this._dom.createElement(cmd);
} else if (cmd instanceof PushElement) {
this._dom.pushElement(cmd);
} else if (cmd instanceof PopElement) {
this._dom.popElement(cmd);
} else if (cmd instanceof AddAttribute) {
this._dom.addAttribute(cmd);
} else if (cmd instanceof Doctype) {
this._dom.doctype(cmd);
} else if (cmd instanceof Comment) {
this._dom.comment(cmd);
} else if (cmd instanceof ExternalCode) {
this.pushBlock('');
this.out(cmd.code);
this.popBlock();
} else {
throw new Error(`unknown command: ${cmd}`);
}
}
};