import { EventedTokenizer, EntityParser, HTML5NamedCharRefs } from 'simple-html-tokenizer'; import { assign } from '@glimmer/util'; import { parse } from 'handlebars'; function isCall(node) { return node.type === 'SubExpression' || node.type === 'MustacheStatement' && node.path.type === 'PathExpression'; } function isLiteral(input) { return !!(typeof input === 'object' && input.type.match(/Literal$/)); } var nodes = /*#__PURE__*/Object.freeze({ isCall: isCall, isLiteral: isLiteral }); function buildMustache(path, params, hash, raw, loc) { if (!isLiteral(path)) { path = buildPath(path); } return { type: 'MustacheStatement', path, params: params || [], hash: hash || buildHash([]), escaped: !raw, loc: buildLoc(loc || null) }; } function buildBlock(path, params, hash, program, inverse, loc) { return { type: 'BlockStatement', path: buildPath(path), params: params || [], hash: hash || buildHash([]), program: program || null, inverse: inverse || null, loc: buildLoc(loc || null) }; } function buildElementModifier(path, params, hash, loc) { return { type: 'ElementModifierStatement', path: buildPath(path), params: params || [], hash: hash || buildHash([]), loc: buildLoc(loc || null) }; } function buildPartial(name, params, hash, indent, loc) { return { type: 'PartialStatement', name: name, params: params || [], hash: hash || buildHash([]), indent: indent || '', strip: { open: false, close: false }, loc: buildLoc(loc || null) }; } function buildComment(value, loc) { return { type: 'CommentStatement', value: value, loc: buildLoc(loc || null) }; } function buildMustacheComment(value, loc) { return { type: 'MustacheCommentStatement', value: value, loc: buildLoc(loc || null) }; } function buildConcat(parts, loc) { return { type: 'ConcatStatement', parts: parts || [], loc: buildLoc(loc || null) }; } function buildElement(tag, attributes, modifiers, children, comments, blockParams, loc) { // this is used for backwards compat prior to `blockParams` being added to the AST if (Array.isArray(comments)) { if (isBlockParms(comments)) { blockParams = comments; comments = []; } else if (isLoc(blockParams)) { loc = blockParams; blockParams = []; } } else if (isLoc(comments)) { // this is used for backwards compat prior to `comments` being added to the AST loc = comments; comments = []; } else if (isLoc(blockParams)) { loc = blockParams; blockParams = []; } // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST let selfClosing = false; if (typeof tag === 'object') { selfClosing = tag.selfClosing; tag = tag.name; } return { type: 'ElementNode', tag: tag || '', selfClosing: selfClosing, attributes: attributes || [], blockParams: blockParams || [], modifiers: modifiers || [], comments: comments || [], children: children || [], loc: buildLoc(loc || null) }; } function buildAttr(name, value, loc) { return { type: 'AttrNode', name: name, value: value, loc: buildLoc(loc || null) }; } function buildText(chars, loc) { return { type: 'TextNode', chars: chars || '', loc: buildLoc(loc || null) }; } // Expressions function buildSexpr(path, params, hash, loc) { return { type: 'SubExpression', path: buildPath(path), params: params || [], hash: hash || buildHash([]), loc: buildLoc(loc || null) }; } function buildPath(original, loc) { if (typeof original !== 'string') return original; let parts = original.split('.'); let thisHead = false; if (parts[0] === 'this') { thisHead = true; parts = parts.slice(1); } return { type: 'PathExpression', original, this: thisHead, parts, data: false, loc: buildLoc(loc || null) }; } function buildLiteral(type, value, loc) { return { type, value, original: value, loc: buildLoc(loc || null) }; } // Miscellaneous function buildHash(pairs, loc) { return { type: 'Hash', pairs: pairs || [], loc: buildLoc(loc || null) }; } function buildPair(key, value, loc) { return { type: 'HashPair', key: key, value, loc: buildLoc(loc || null) }; } function buildProgram(body, blockParams, loc) { return { type: 'Program', body: body || [], blockParams: blockParams || [], loc: buildLoc(loc || null) }; } function buildSource(source) { return source || null; } function buildPosition(line, column) { return { line, column }; } const SYNTHETIC = { source: '(synthetic)', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }; function buildLoc(...args) { if (args.length === 1) { let loc = args[0]; if (loc && typeof loc === 'object') { return { source: buildSource(loc.source), start: buildPosition(loc.start.line, loc.start.column), end: buildPosition(loc.end.line, loc.end.column) }; } else { return SYNTHETIC; } } else { let [startLine, startColumn, endLine, endColumn, source] = args; return { source: buildSource(source), start: buildPosition(startLine, startColumn), end: buildPosition(endLine, endColumn) }; } } function isBlockParms(arr) { return arr[0] === 'string'; } function isLoc(item) { return !Array.isArray(item); } var b = { mustache: buildMustache, block: buildBlock, partial: buildPartial, comment: buildComment, mustacheComment: buildMustacheComment, element: buildElement, elementModifier: buildElementModifier, attr: buildAttr, text: buildText, sexpr: buildSexpr, path: buildPath, concat: buildConcat, hash: buildHash, pair: buildPair, literal: buildLiteral, program: buildProgram, loc: buildLoc, pos: buildPosition, string: literal('StringLiteral'), boolean: literal('BooleanLiteral'), number: literal('NumberLiteral'), undefined() { return buildLiteral('UndefinedLiteral', undefined); }, null() { return buildLiteral('NullLiteral', null); } }; function literal(type) { return function (value) { return buildLiteral(type, value); }; } /** * Subclass of `Error` with additional information * about location of incorrect markup. */ const SyntaxError = function () { SyntaxError.prototype = Object.create(Error.prototype); SyntaxError.prototype.constructor = SyntaxError; function SyntaxError(message, location) { let error = Error.call(this, message); this.message = message; this.stack = error.stack; this.location = location; } return SyntaxError; }(); // Regex to validate the identifier for block parameters. // Based on the ID validation regex in Handlebars. let ID_INVERSE_PATTERN = /[!"#%-,\.\/;->@\[-\^`\{-~]/; // Checks the element's attributes to see if it uses block params. // If it does, registers the block params with the program and // removes the corresponding attributes from the element. function parseElementBlockParams(element) { let params = parseBlockParams(element); if (params) element.blockParams = params; } function parseBlockParams(element) { let l = element.attributes.length; let attrNames = []; for (let i = 0; i < l; i++) { attrNames.push(element.attributes[i].name); } let asIndex = attrNames.indexOf('as'); if (asIndex !== -1 && l > asIndex && attrNames[asIndex + 1].charAt(0) === '|') { // Some basic validation, since we're doing the parsing ourselves let paramsString = attrNames.slice(asIndex).join(' '); if (paramsString.charAt(paramsString.length - 1) !== '|' || paramsString.match(/\|/g).length !== 2) { throw new SyntaxError("Invalid block parameters syntax: '" + paramsString + "'", element.loc); } let params = []; for (let i = asIndex + 1; i < l; i++) { let param = attrNames[i].replace(/\|/g, ''); if (param !== '') { if (ID_INVERSE_PATTERN.test(param)) { throw new SyntaxError("Invalid identifier for block parameters: '" + param + "' in '" + paramsString + "'", element.loc); } params.push(param); } } if (params.length === 0) { throw new SyntaxError("Cannot use zero block parameters: '" + paramsString + "'", element.loc); } element.attributes = element.attributes.slice(0, asIndex); return params; } return null; } function childrenFor(node) { switch (node.type) { case 'Program': return node.body; case 'ElementNode': return node.children; } } function appendChild(parent, node) { childrenFor(parent).push(node); } function isLiteral$1(path) { return path.type === 'StringLiteral' || path.type === 'BooleanLiteral' || path.type === 'NumberLiteral' || path.type === 'NullLiteral' || path.type === 'UndefinedLiteral'; } function printLiteral(literal) { if (literal.type === 'UndefinedLiteral') { return 'undefined'; } else { return JSON.stringify(literal.value); } } const entityParser = new EntityParser(HTML5NamedCharRefs); class Parser { constructor(source) { this.elementStack = []; this.currentAttribute = null; this.currentNode = null; this.tokenizer = new EventedTokenizer(this, entityParser); this.source = source.split(/(?:\r\n?|\n)/g); } get currentAttr() { return this.currentAttribute; } get currentTag() { let node = this.currentNode; return node; } get currentStartTag() { let node = this.currentNode; return node; } get currentEndTag() { let node = this.currentNode; return node; } get currentComment() { let node = this.currentNode; return node; } get currentData() { let node = this.currentNode; return node; } acceptNode(node) { return this[node.type](node); } currentElement() { return this.elementStack[this.elementStack.length - 1]; } sourceForNode(node, endNode) { let firstLine = node.loc.start.line - 1; let currentLine = firstLine - 1; let firstColumn = node.loc.start.column; let string = []; let line; let lastLine; let lastColumn; if (endNode) { lastLine = endNode.loc.end.line - 1; lastColumn = endNode.loc.end.column; } else { lastLine = node.loc.end.line - 1; lastColumn = node.loc.end.column; } while (currentLine < lastLine) { currentLine++; line = this.source[currentLine]; if (currentLine === firstLine) { if (firstLine === lastLine) { string.push(line.slice(firstColumn, lastColumn)); } else { string.push(line.slice(firstColumn)); } } else if (currentLine === lastLine) { string.push(line.slice(0, lastColumn)); } else { string.push(line); } } return string.join('\n'); } } class HandlebarsNodeVisitors extends Parser { constructor() { super(...arguments); this.cursorCount = 0; } cursor() { return `%cursor:${this.cursorCount++}%`; } Program(program) { let body = []; this.cursorCount = 0; let node = b.program(body, program.blockParams, program.loc); let i, l = program.body.length; this.elementStack.push(node); if (l === 0) { return this.elementStack.pop(); } for (i = 0; i < l; i++) { this.acceptNode(program.body[i]); } // Ensure that that the element stack is balanced properly. let poppedNode = this.elementStack.pop(); if (poppedNode !== node) { let elementNode = poppedNode; throw new SyntaxError('Unclosed element `' + elementNode.tag + '` (on line ' + elementNode.loc.start.line + ').', elementNode.loc); } return node; } BlockStatement(block) { if (this.tokenizer['state'] === 'comment') { this.appendToCommentData(this.sourceForNode(block)); return; } if (this.tokenizer['state'] !== 'comment' && this.tokenizer['state'] !== 'data' && this.tokenizer['state'] !== 'beforeData') { throw new SyntaxError('A block may only be used inside an HTML element or another block.', block.loc); } let { path, params, hash } = acceptCallNodes(this, block); let program = this.Program(block.program); let inverse = block.inverse ? this.Program(block.inverse) : null; if (path.original === 'in-element') { hash = addInElementHash(this.cursor(), hash, block.loc); } let node = b.block(path, params, hash, program, inverse, block.loc); let parentProgram = this.currentElement(); appendChild(parentProgram, node); } MustacheStatement(rawMustache) { let { tokenizer } = this; if (tokenizer.state === 'comment') { this.appendToCommentData(this.sourceForNode(rawMustache)); return; } let mustache; let { escaped, loc } = rawMustache; if (rawMustache.path.type.match(/Literal$/)) { mustache = { type: 'MustacheStatement', path: this.acceptNode(rawMustache.path), params: [], hash: b.hash(), escaped, loc }; } else { let { path, params, hash } = acceptCallNodes(this, rawMustache); mustache = b.mustache(path, params, hash, !escaped, loc); } switch (tokenizer.state) { // Tag helpers case "tagOpen" /* tagOpen */: case "tagName" /* tagName */: throw new SyntaxError(`Cannot use mustaches in an elements tagname: \`${this.sourceForNode(rawMustache, rawMustache.path)}\` at L${loc.start.line}:C${loc.start.column}`, mustache.loc); case "beforeAttributeName" /* beforeAttributeName */: addElementModifier(this.currentStartTag, mustache); break; case "attributeName" /* attributeName */: case "afterAttributeName" /* afterAttributeName */: this.beginAttributeValue(false); this.finishAttributeValue(); addElementModifier(this.currentStartTag, mustache); tokenizer.transitionTo("beforeAttributeName" /* beforeAttributeName */); break; case "afterAttributeValueQuoted" /* afterAttributeValueQuoted */: addElementModifier(this.currentStartTag, mustache); tokenizer.transitionTo("beforeAttributeName" /* beforeAttributeName */); break; // Attribute values case "beforeAttributeValue" /* beforeAttributeValue */: this.beginAttributeValue(false); appendDynamicAttributeValuePart(this.currentAttribute, mustache); tokenizer.transitionTo("attributeValueUnquoted" /* attributeValueUnquoted */); break; case "attributeValueDoubleQuoted" /* attributeValueDoubleQuoted */: case "attributeValueSingleQuoted" /* attributeValueSingleQuoted */: case "attributeValueUnquoted" /* attributeValueUnquoted */: appendDynamicAttributeValuePart(this.currentAttribute, mustache); break; // TODO: Only append child when the tokenizer state makes // sense to do so, otherwise throw an error. default: appendChild(this.currentElement(), mustache); } return mustache; } ContentStatement(content) { updateTokenizerLocation(this.tokenizer, content); this.tokenizer.tokenizePart(content.value); this.tokenizer.flushData(); } CommentStatement(rawComment) { let { tokenizer } = this; if (tokenizer.state === "comment" /* comment */) { this.appendToCommentData(this.sourceForNode(rawComment)); return null; } let { value, loc } = rawComment; let comment = b.mustacheComment(value, loc); switch (tokenizer.state) { case "beforeAttributeName" /* beforeAttributeName */: this.currentStartTag.comments.push(comment); break; case "beforeData" /* beforeData */: case "data" /* data */: appendChild(this.currentElement(), comment); break; default: throw new SyntaxError(`Using a Handlebars comment when in the \`${tokenizer['state']}\` state is not supported: "${comment.value}" on line ${loc.start.line}:${loc.start.column}`, rawComment.loc); } return comment; } PartialStatement(partial) { let { loc } = partial; throw new SyntaxError(`Handlebars partials are not supported: "${this.sourceForNode(partial, partial.name)}" at L${loc.start.line}:C${loc.start.column}`, partial.loc); } PartialBlockStatement(partialBlock) { let { loc } = partialBlock; throw new SyntaxError(`Handlebars partial blocks are not supported: "${this.sourceForNode(partialBlock, partialBlock.name)}" at L${loc.start.line}:C${loc.start.column}`, partialBlock.loc); } Decorator(decorator) { let { loc } = decorator; throw new SyntaxError(`Handlebars decorators are not supported: "${this.sourceForNode(decorator, decorator.path)}" at L${loc.start.line}:C${loc.start.column}`, decorator.loc); } DecoratorBlock(decoratorBlock) { let { loc } = decoratorBlock; throw new SyntaxError(`Handlebars decorator blocks are not supported: "${this.sourceForNode(decoratorBlock, decoratorBlock.path)}" at L${loc.start.line}:C${loc.start.column}`, decoratorBlock.loc); } SubExpression(sexpr) { let { path, params, hash } = acceptCallNodes(this, sexpr); return b.sexpr(path, params, hash, sexpr.loc); } PathExpression(path) { let { original, loc } = path; let parts; if (original.indexOf('/') !== -1) { if (original.slice(0, 2) === './') { throw new SyntaxError(`Using "./" is not supported in Glimmer and unnecessary: "${path.original}" on line ${loc.start.line}.`, path.loc); } if (original.slice(0, 3) === '../') { throw new SyntaxError(`Changing context using "../" is not supported in Glimmer: "${path.original}" on line ${loc.start.line}.`, path.loc); } if (original.indexOf('.') !== -1) { throw new SyntaxError(`Mixing '.' and '/' in paths is not supported in Glimmer; use only '.' to separate property paths: "${path.original}" on line ${loc.start.line}.`, path.loc); } parts = [path.parts.join('/')]; } else if (original === '.') { let locationInfo = `L${loc.start.line}:C${loc.start.column}`; throw new SyntaxError(`'.' is not a supported path in Glimmer; check for a path with a trailing '.' at ${locationInfo}.`, path.loc); } else { parts = path.parts; } let thisHead = false; // This is to fix a bug in the Handlebars AST where the path expressions in // `{{this.foo}}` (and similarly `{{foo-bar this.foo named=this.foo}}` etc) // are simply turned into `{{foo}}`. The fix is to push it back onto the // parts array and let the runtime see the difference. However, we cannot // simply use the string `this` as it means literally the property called // "this" in the current context (it can be expressed in the syntax as // `{{[this]}}`, where the square bracket are generally for this kind of // escaping – such as `{{foo.["bar.baz"]}}` would mean lookup a property // named literally "bar.baz" on `this.foo`). By convention, we use `null` // for this purpose. if (original.match(/^this(\..+)?$/)) { thisHead = true; } return { type: 'PathExpression', original: path.original, this: thisHead, parts, data: path.data, loc: path.loc }; } Hash(hash) { let pairs = []; for (let i = 0; i < hash.pairs.length; i++) { let pair = hash.pairs[i]; pairs.push(b.pair(pair.key, this.acceptNode(pair.value), pair.loc)); } return b.hash(pairs, hash.loc); } StringLiteral(string) { return b.literal('StringLiteral', string.value, string.loc); } BooleanLiteral(boolean) { return b.literal('BooleanLiteral', boolean.value, boolean.loc); } NumberLiteral(number) { return b.literal('NumberLiteral', number.value, number.loc); } UndefinedLiteral(undef) { return b.literal('UndefinedLiteral', undefined, undef.loc); } NullLiteral(nul) { return b.literal('NullLiteral', null, nul.loc); } } function calculateRightStrippedOffsets(original, value) { if (value === '') { // if it is empty, just return the count of newlines // in original return { lines: original.split('\n').length - 1, columns: 0 }; } // otherwise, return the number of newlines prior to // `value` let difference = original.split(value)[0]; let lines = difference.split(/\n/); let lineCount = lines.length - 1; return { lines: lineCount, columns: lines[lineCount].length }; } function updateTokenizerLocation(tokenizer, content) { let line = content.loc.start.line; let column = content.loc.start.column; let offsets = calculateRightStrippedOffsets(content.original, content.value); line = line + offsets.lines; if (offsets.lines) { column = offsets.columns; } else { column = column + offsets.columns; } tokenizer.line = line; tokenizer.column = column; } function acceptCallNodes(compiler, node) { let path = compiler.PathExpression(node.path); let params = node.params ? node.params.map(e => compiler.acceptNode(e)) : []; let hash = node.hash ? compiler.Hash(node.hash) : b.hash(); return { path, params, hash }; } function addElementModifier(element, mustache) { let { path, params, hash, loc } = mustache; if (isLiteral$1(path)) { let modifier = `{{${printLiteral(path)}}}`; let tag = `<${element.name} ... ${modifier} ...`; throw new SyntaxError(`In ${tag}, ${modifier} is not a valid modifier: "${path.original}" on line ${loc && loc.start.line}.`, mustache.loc); } let modifier = b.elementModifier(path, params, hash, loc); element.modifiers.push(modifier); } function addInElementHash(cursor, hash, loc) { let hasNextSibling = false; hash.pairs.forEach(pair => { if (pair.key === 'guid') { throw new SyntaxError('Cannot pass `guid` from user space', loc); } if (pair.key === 'nextSibling') { hasNextSibling = true; } }); let guid = b.literal('StringLiteral', cursor); let guidPair = b.pair('guid', guid); hash.pairs.unshift(guidPair); if (!hasNextSibling) { let nullLiteral = b.literal('NullLiteral', null); let nextSibling = b.pair('nextSibling', nullLiteral); hash.pairs.push(nextSibling); } return hash; } function appendDynamicAttributeValuePart(attribute, part) { attribute.isDynamic = true; attribute.parts.push(part); } var visitorKeys = { Program: ['body'], MustacheStatement: ['path', 'params', 'hash'], BlockStatement: ['path', 'params', 'hash', 'program', 'inverse'], ElementModifierStatement: ['path', 'params', 'hash'], PartialStatement: ['name', 'params', 'hash'], CommentStatement: [], MustacheCommentStatement: [], ElementNode: ['attributes', 'modifiers', 'children', 'comments'], AttrNode: ['value'], TextNode: [], ConcatStatement: ['parts'], SubExpression: ['path', 'params', 'hash'], PathExpression: [], StringLiteral: [], BooleanLiteral: [], NumberLiteral: [], NullLiteral: [], UndefinedLiteral: [], Hash: ['pairs'], HashPair: ['value'] }; const TraversalError = function () { TraversalError.prototype = Object.create(Error.prototype); TraversalError.prototype.constructor = TraversalError; function TraversalError(message, node, parent, key) { let error = Error.call(this, message); this.key = key; this.message = message; this.node = node; this.parent = parent; this.stack = error.stack; } return TraversalError; }(); function cannotRemoveNode(node, parent, key) { return new TraversalError('Cannot remove a node unless it is part of an array', node, parent, key); } function cannotReplaceNode(node, parent, key) { return new TraversalError('Cannot replace a node with multiple nodes unless it is part of an array', node, parent, key); } function cannotReplaceOrRemoveInKeyHandlerYet(node, key) { return new TraversalError('Replacing and removing in key handlers is not yet supported.', node, null, key); } function visitNode(visitor, node) { let handler = visitor[node.type] || visitor.All || null; let result; if (handler && handler['enter']) { result = handler['enter'].call(null, node); } if (result !== undefined && result !== null) { if (JSON.stringify(node) === JSON.stringify(result)) { result = undefined; } else if (Array.isArray(result)) { return visitArray(visitor, result) || result; } else { return visitNode(visitor, result) || result; } } if (result === undefined) { let keys = visitorKeys[node.type]; for (let i = 0; i < keys.length; i++) { visitKey(visitor, handler, node, keys[i]); } if (handler && handler['exit']) { result = handler['exit'].call(null, node); } } return result; } function visitKey(visitor, handler, node, key) { let value = node[key]; if (!value) { return; } let keyHandler = handler && (handler.keys[key] || handler.keys.All); let result; if (keyHandler && keyHandler.enter) { result = keyHandler.enter.call(null, node, key); if (result !== undefined) { throw cannotReplaceOrRemoveInKeyHandlerYet(node, key); } } if (Array.isArray(value)) { visitArray(visitor, value); } else { let result = visitNode(visitor, value); if (result !== undefined) { assignKey(node, key, result); } } if (keyHandler && keyHandler.exit) { result = keyHandler.exit.call(null, node, key); if (result !== undefined) { throw cannotReplaceOrRemoveInKeyHandlerYet(node, key); } } } function visitArray(visitor, array) { for (let i = 0; i < array.length; i++) { let result = visitNode(visitor, array[i]); if (result !== undefined) { i += spliceArray(array, i, result) - 1; } } } function assignKey(node, key, result) { if (result === null) { throw cannotRemoveNode(node[key], node, key); } else if (Array.isArray(result)) { if (result.length === 1) { node[key] = result[0]; } else { if (result.length === 0) { throw cannotRemoveNode(node[key], node, key); } else { throw cannotReplaceNode(node[key], node, key); } } } else { node[key] = result; } } function spliceArray(array, index, result) { if (result === null) { array.splice(index, 1); return 0; } else if (Array.isArray(result)) { array.splice(index, 1, ...result); return result.length; } else { array.splice(index, 1, result); return 1; } } function traverse(node, visitor) { visitNode(normalizeVisitor(visitor), node); } function normalizeVisitor(visitor) { let normalizedVisitor = {}; for (let type in visitor) { let handler = visitor[type] || visitor.All; let normalizedKeys = {}; if (typeof handler === 'object') { let keys = handler.keys; if (keys) { for (let key in keys) { let keyHandler = keys[key]; if (typeof keyHandler === 'object') { normalizedKeys[key] = { enter: typeof keyHandler.enter === 'function' ? keyHandler.enter : null, exit: typeof keyHandler.exit === 'function' ? keyHandler.exit : null }; } else if (typeof keyHandler === 'function') { normalizedKeys[key] = { enter: keyHandler, exit: null }; } } } normalizedVisitor[type] = { enter: typeof handler.enter === 'function' ? handler.enter : null, exit: typeof handler.exit === 'function' ? handler.exit : null, keys: normalizedKeys }; } else if (typeof handler === 'function') { normalizedVisitor[type] = { enter: handler, exit: null, keys: normalizedKeys }; } } return normalizedVisitor; } const ATTR_VALUE_REGEX_TEST = /[\xA0"&]/; const ATTR_VALUE_REGEX_REPLACE = new RegExp(ATTR_VALUE_REGEX_TEST.source, 'g'); const TEXT_REGEX_TEST = /[\xA0&<>]/; const TEXT_REGEX_REPLACE = new RegExp(TEXT_REGEX_TEST.source, 'g'); function attrValueReplacer(char) { switch (char.charCodeAt(0)) { case 160 /* NBSP */: return ' '; case 34 /* QUOT */: return '"'; case 38 /* AMP */: return '&'; default: return char; } } function textReplacer(char) { switch (char.charCodeAt(0)) { case 160 /* NBSP */: return ' '; case 38 /* AMP */: return '&'; case 60 /* LT */: return '<'; case 62 /* GT */: return '>'; default: return char; } } function escapeAttrValue(attrValue) { if (ATTR_VALUE_REGEX_TEST.test(attrValue)) { return attrValue.replace(ATTR_VALUE_REGEX_REPLACE, attrValueReplacer); } return attrValue; } function escapeText(text) { if (TEXT_REGEX_TEST.test(text)) { return text.replace(TEXT_REGEX_REPLACE, textReplacer); } return text; } function unreachable() { throw new Error('unreachable'); } function build(ast) { if (!ast) { return ''; } const output = []; switch (ast.type) { case 'Program': { const chainBlock = ast['chained'] && ast.body[0]; if (chainBlock) { chainBlock['chained'] = true; } const body = buildEach(ast.body).join(''); output.push(body); } break; case 'ElementNode': output.push('<', ast.tag); if (ast.attributes.length) { output.push(' ', buildEach(ast.attributes).join(' ')); } if (ast.modifiers.length) { output.push(' ', buildEach(ast.modifiers).join(' ')); } if (ast.comments.length) { output.push(' ', buildEach(ast.comments).join(' ')); } if (ast.blockParams.length) { output.push(' ', 'as', ' ', `|${ast.blockParams.join(' ')}|`); } if (voidMap[ast.tag]) { if (ast.selfClosing) { output.push(' /'); } output.push('>'); } else { output.push('>'); output.push.apply(output, buildEach(ast.children)); output.push(''); } break; case 'AttrNode': if (ast.value.type === 'TextNode') { if (ast.value.chars !== '') { output.push(ast.name, '='); output.push('"', escapeAttrValue(ast.value.chars), '"'); } else { output.push(ast.name); } } else { output.push(ast.name, '='); // ast.value is mustache or concat output.push(build(ast.value)); } break; case 'ConcatStatement': output.push('"'); ast.parts.forEach(node => { if (node.type === 'TextNode') { output.push(escapeAttrValue(node.chars)); } else { output.push(build(node)); } }); output.push('"'); break; case 'TextNode': output.push(escapeText(ast.chars)); break; case 'MustacheStatement': { output.push(compactJoin(['{{', pathParams(ast), '}}'])); } break; case 'MustacheCommentStatement': { output.push(compactJoin(['{{!--', ast.value, '--}}'])); } break; case 'ElementModifierStatement': { output.push(compactJoin(['{{', pathParams(ast), '}}'])); } break; case 'PathExpression': output.push(ast.original); break; case 'SubExpression': { output.push('(', pathParams(ast), ')'); } break; case 'BooleanLiteral': output.push(ast.value ? 'true' : 'false'); break; case 'BlockStatement': { const lines = []; if (ast['chained']) { lines.push(['{{else ', pathParams(ast), '}}'].join('')); } else { lines.push(openBlock(ast)); } lines.push(build(ast.program)); if (ast.inverse) { if (!ast.inverse['chained']) { lines.push('{{else}}'); } lines.push(build(ast.inverse)); } if (!ast['chained']) { lines.push(closeBlock(ast)); } output.push(lines.join('')); } break; case 'PartialStatement': { output.push(compactJoin(['{{>', pathParams(ast), '}}'])); } break; case 'CommentStatement': { output.push(compactJoin([''])); } break; case 'StringLiteral': { output.push(`"${ast.value}"`); } break; case 'NumberLiteral': { output.push(String(ast.value)); } break; case 'UndefinedLiteral': { output.push('undefined'); } break; case 'NullLiteral': { output.push('null'); } break; case 'Hash': { output.push(ast.pairs.map(pair => { return build(pair); }).join(' ')); } break; case 'HashPair': { output.push(`${ast.key}=${build(ast.value)}`); } break; } return output.join(''); } function compact(array) { const newArray = []; array.forEach(a => { if (typeof a !== 'undefined' && a !== null && a !== '') { newArray.push(a); } }); return newArray; } function buildEach(asts) { return asts.map(build); } function pathParams(ast) { let path; switch (ast.type) { case 'MustacheStatement': case 'SubExpression': case 'ElementModifierStatement': case 'BlockStatement': if (isLiteral(ast.path)) { return String(ast.path.value); } path = build(ast.path); break; case 'PartialStatement': path = build(ast.name); break; default: return unreachable(); } return compactJoin([path, buildEach(ast.params).join(' '), build(ast.hash)], ' '); } function compactJoin(array, delimiter) { return compact(array).join(delimiter || ''); } function blockParams(block) { const params = block.program.blockParams; if (params.length) { return ` as |${params.join(' ')}|`; } return null; } function openBlock(block) { return ['{{#', pathParams(block), blockParams(block), '}}'].join(''); } function closeBlock(block) { return ['{{/', build(block.path), '}}'].join(''); } class Walker { constructor(order) { this.order = order; this.stack = []; } visit(node, callback) { if (!node) { return; } this.stack.push(node); if (this.order === 'post') { this.children(node, callback); callback(node, this); } else { callback(node, this); this.children(node, callback); } this.stack.pop(); } children(node, callback) { let visitor = visitors[node.type]; if (visitor) { visitor(this, node, callback); } } } let visitors = { Program(walker, node, callback) { for (let i = 0; i < node.body.length; i++) { walker.visit(node.body[i], callback); } }, ElementNode(walker, node, callback) { for (let i = 0; i < node.children.length; i++) { walker.visit(node.children[i], callback); } }, BlockStatement(walker, node, callback) { walker.visit(node.program, callback); walker.visit(node.inverse || null, callback); } }; 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; }); 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, selfClosing } = this.currentStartTag; let loc = b.loc(this.tagOpenLine, this.tagOpenColumn); let element = b.element({ name, selfClosing }, 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. // or
, 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 + ')'; } const syntax = { parse: preprocess, builders: b, print: build, traverse, Walker }; function preprocess(html, options) { let ast = typeof html === 'object' ? html : parse(html); let program = new TokenizerEventHandlers(html).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.visitor); } } return program; } // used by ember-compiler export { nodes as AST, preprocess, b as builders, TraversalError, cannotRemoveNode, cannotReplaceNode, cannotReplaceOrRemoveInKeyHandlerYet, traverse, Walker, build as print, SyntaxError, isLiteral$1 as isLiteral, printLiteral };