UNPKG

ember-legacy-class-transform

Version:
239 lines 9.14 kB
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; }