UNPKG

ember-legacy-class-transform

Version:
260 lines 11.1 kB
import b from "../builders"; import { appendChild, isLiteral, printLiteral } from "../utils"; import { Parser } from '../parser'; import SyntaxError from '../errors/syntax-error'; export class HandlebarsNodeVisitors extends Parser { Program(program) { let body = []; 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; 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 "tagName": addElementModifier(this.currentStartTag, mustache); tokenizer.state = "beforeAttributeName"; break; case "beforeAttributeName": addElementModifier(this.currentStartTag, mustache); break; case "attributeName": case "afterAttributeName": this.beginAttributeValue(false); this.finishAttributeValue(); addElementModifier(this.currentStartTag, mustache); tokenizer.state = "beforeAttributeName"; break; case "afterAttributeValueQuoted": addElementModifier(this.currentStartTag, mustache); tokenizer.state = "beforeAttributeName"; break; // Attribute values case "beforeAttributeValue": appendDynamicAttributeValuePart(this.currentAttribute, mustache); tokenizer.state = 'attributeValueUnquoted'; break; case "attributeValueDoubleQuoted": case "attributeValueSingleQuoted": case "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') { this.appendToCommentData(this.sourceForNode(rawComment)); return null; } let { value, loc } = rawComment; let comment = b.mustacheComment(value, loc); switch (tokenizer.state) { case "beforeAttributeName": this.currentStartTag.comments.push(comment); break; case 'beforeData': case '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 { 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(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 appendDynamicAttributeValuePart(attribute, part) { attribute.isDynamic = true; attribute.parts.push(part); }