ember-legacy-class-transform
Version:
The default blueprint for ember-cli addons.
239 lines • 9.14 kB
JavaScript
import b, { SYNTHETIC } from "../builders";
import { appendChild, parseElementBlockParams } from "../utils";
import { HandlebarsNodeVisitors } from './handlebars-node-visitors';
import SyntaxError from '../errors/syntax-error';
import builders from "../builders";
import traverse from "../traversal/traverse";
import print from "../generation/print";
import Walker from "../traversal/walker";
import * as handlebars from "handlebars";
import { assign } from '@glimmer/util';
const voidMap = Object.create(null);
let voidTagNames = "area base br col command embed hr img input keygen link meta param source track wbr";
voidTagNames.split(" ").forEach(tagName => {
voidMap[tagName] = true;
});
export class TokenizerEventHandlers extends HandlebarsNodeVisitors {
constructor() {
super(...arguments);
this.tagOpenLine = 0;
this.tagOpenColumn = 0;
}
reset() {
this.currentNode = null;
}
// Comment
beginComment() {
this.currentNode = b.comment("");
this.currentNode.loc = {
source: null,
start: b.pos(this.tagOpenLine, this.tagOpenColumn),
end: null
};
}
appendToCommentData(char) {
this.currentComment.value += char;
}
finishComment() {
this.currentComment.loc.end = b.pos(this.tokenizer.line, this.tokenizer.column);
appendChild(this.currentElement(), this.currentComment);
}
// Data
beginData() {
this.currentNode = b.text();
this.currentNode.loc = {
source: null,
start: b.pos(this.tokenizer.line, this.tokenizer.column),
end: null
};
}
appendToData(char) {
this.currentData.chars += char;
}
finishData() {
this.currentData.loc.end = b.pos(this.tokenizer.line, this.tokenizer.column);
appendChild(this.currentElement(), this.currentData);
}
// Tags - basic
tagOpen() {
this.tagOpenLine = this.tokenizer.line;
this.tagOpenColumn = this.tokenizer.column;
}
beginStartTag() {
this.currentNode = {
type: 'StartTag',
name: "",
attributes: [],
modifiers: [],
comments: [],
selfClosing: false,
loc: SYNTHETIC
};
}
beginEndTag() {
this.currentNode = {
type: 'EndTag',
name: "",
attributes: [],
modifiers: [],
comments: [],
selfClosing: false,
loc: SYNTHETIC
};
}
finishTag() {
let { line, column } = this.tokenizer;
let tag = this.currentTag;
tag.loc = b.loc(this.tagOpenLine, this.tagOpenColumn, line, column);
if (tag.type === 'StartTag') {
this.finishStartTag();
if (voidMap[tag.name] || tag.selfClosing) {
this.finishEndTag(true);
}
} else if (tag.type === 'EndTag') {
this.finishEndTag(false);
}
}
finishStartTag() {
let { name, attributes, modifiers, comments } = this.currentStartTag;
let loc = b.loc(this.tagOpenLine, this.tagOpenColumn);
let element = b.element(name, attributes, modifiers, [], comments, loc);
this.elementStack.push(element);
}
finishEndTag(isVoid) {
let tag = this.currentTag;
let element = this.elementStack.pop();
let parent = this.currentElement();
validateEndTag(tag, element, isVoid);
element.loc.end.line = this.tokenizer.line;
element.loc.end.column = this.tokenizer.column;
parseElementBlockParams(element);
appendChild(parent, element);
}
markTagAsSelfClosing() {
this.currentTag.selfClosing = true;
}
// Tags - name
appendToTagName(char) {
this.currentTag.name += char;
}
// Tags - attributes
beginAttribute() {
let tag = this.currentTag;
if (tag.type === 'EndTag') {
throw new SyntaxError(`Invalid end tag: closing tag must not have attributes, ` + `in \`${tag.name}\` (on line ${this.tokenizer.line}).`, tag.loc);
}
this.currentAttribute = {
name: "",
parts: [],
isQuoted: false,
isDynamic: false,
start: b.pos(this.tokenizer.line, this.tokenizer.column),
valueStartLine: 0,
valueStartColumn: 0
};
}
appendToAttributeName(char) {
this.currentAttr.name += char;
}
beginAttributeValue(isQuoted) {
this.currentAttr.isQuoted = isQuoted;
this.currentAttr.valueStartLine = this.tokenizer.line;
this.currentAttr.valueStartColumn = this.tokenizer.column;
}
appendToAttributeValue(char) {
let parts = this.currentAttr.parts;
let lastPart = parts[parts.length - 1];
if (lastPart && lastPart.type === 'TextNode') {
lastPart.chars += char;
// update end location for each added char
lastPart.loc.end.line = this.tokenizer.line;
lastPart.loc.end.column = this.tokenizer.column;
} else {
// initially assume the text node is a single char
let loc = b.loc(this.tokenizer.line, this.tokenizer.column, this.tokenizer.line, this.tokenizer.column);
// correct for `\n` as first char
if (char === '\n') {
loc.start.line -= 1;
loc.start.column = lastPart ? lastPart.loc.end.column : this.currentAttr.valueStartColumn;
}
let text = b.text(char, loc);
parts.push(text);
}
}
finishAttributeValue() {
let { name, parts, isQuoted, isDynamic, valueStartLine, valueStartColumn } = this.currentAttr;
let value = assembleAttributeValue(parts, isQuoted, isDynamic, this.tokenizer.line);
value.loc = b.loc(valueStartLine, valueStartColumn, this.tokenizer.line, this.tokenizer.column);
let loc = b.loc(this.currentAttr.start.line, this.currentAttr.start.column, this.tokenizer.line, this.tokenizer.column);
let attribute = b.attr(name, value, loc);
this.currentStartTag.attributes.push(attribute);
}
reportSyntaxError(message) {
throw new SyntaxError(`Syntax error at line ${this.tokenizer.line} col ${this.tokenizer.column}: ${message}`, b.loc(this.tokenizer.line, this.tokenizer.column));
}
}
;
function assembleAttributeValue(parts, isQuoted, isDynamic, line) {
if (isDynamic) {
if (isQuoted) {
return assembleConcatenatedValue(parts);
} else {
if (parts.length === 1 || parts.length === 2 && parts[1].type === 'TextNode' && parts[1].chars === '/') {
return parts[0];
} else {
throw new SyntaxError(`An unquoted attribute value must be a string or a mustache, ` + `preceeded by whitespace or a '=' character, and ` + `followed by whitespace, a '>' character, or '/>' (on line ${line})`, b.loc(line, 0));
}
}
} else {
return parts.length > 0 ? parts[0] : b.text("");
}
}
function assembleConcatenatedValue(parts) {
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (part.type !== 'MustacheStatement' && part.type !== 'TextNode') {
throw new SyntaxError("Unsupported node in quoted attribute value: " + part['type'], part.loc);
}
}
return b.concat(parts);
}
function validateEndTag(tag, element, selfClosing) {
let error;
if (voidMap[tag.name] && !selfClosing) {
// EngTag is also called by StartTag for void and self-closing tags (i.e.
// <input> or <br />, so we need to check for that here. Otherwise, we would
// throw an error for those cases.
error = "Invalid end tag " + formatEndTagInfo(tag) + " (void elements cannot have end tags).";
} else if (element.tag === undefined) {
error = "Closing tag " + formatEndTagInfo(tag) + " without an open tag.";
} else if (element.tag !== tag.name) {
error = "Closing tag " + formatEndTagInfo(tag) + " did not match last open tag `" + element.tag + "` (on line " + element.loc.start.line + ").";
}
if (error) {
throw new SyntaxError(error, element.loc);
}
}
function formatEndTagInfo(tag) {
return "`" + tag.name + "` (on line " + tag.loc.end.line + ")";
}
export const syntax = {
parse: preprocess,
builders,
print,
traverse,
Walker
};
export function preprocess(html, options) {
let ast = typeof html === 'object' ? html : handlebars.parse(html);
let program = new TokenizerEventHandlers(html, options).acceptNode(ast);
if (options && options.plugins && options.plugins.ast) {
for (let i = 0, l = options.plugins.ast.length; i < l; i++) {
let transform = options.plugins.ast[i];
let env = assign({}, options, { syntax }, { plugins: undefined });
let pluginResult = transform(env);
traverse(program, pluginResult.visitors);
}
}
return program;
}