UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

646 lines (619 loc) 17.2 kB
import '../@ember/debug/index.js'; import calculateLocationDisplay from '../@ember/template-compiler/lib/system/calculate-location-display.js'; import { isPath, trackLocals, isStringLiteral } from '../@ember/template-compiler/lib/plugins/utils.js'; import { assert } from '../@ember/debug/lib/assert.js'; const Char = { NBSP: 0xa0, QUOT: 0x22, LT: 0x3c, GT: 0x3e, AMP: 0x26 }; // \x26 is ampersand, \xa0 is non-breaking space const ATTR_VALUE_REGEX_TEST = /["\x26\xa0]/u; const ATTR_VALUE_REGEX_REPLACE = new RegExp(ATTR_VALUE_REGEX_TEST.source, 'gu'); const TEXT_REGEX_TEST = /[&<>\xa0]/u; const TEXT_REGEX_REPLACE = new RegExp(TEXT_REGEX_TEST.source, 'gu'); function attrValueReplacer(char) { switch (char.charCodeAt(0)) { case Char.NBSP: return '&nbsp;'; case Char.QUOT: return '&quot;'; case Char.AMP: return '&amp;'; default: return char; } } function textReplacer(char) { switch (char.charCodeAt(0)) { case Char.NBSP: return '&nbsp;'; case Char.AMP: return '&amp;'; case Char.LT: return '&lt;'; case Char.GT: return '&gt;'; 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 sortByLoc(a, b) { // If either is invisible, don't try to order them if (a.loc.isInvisible || b.loc.isInvisible) { return 0; } if (a.loc.startPosition.line < b.loc.startPosition.line) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column < b.loc.startPosition.column) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column === b.loc.startPosition.column) { return 0; } return 1; } const voidMap = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']); const NON_WHITESPACE = /^\S/u; /** * Examples when true: * - link * - liNK * * Examples when false: * - Link (component) */ function isVoidTag(tag) { return voidMap.has(tag.toLowerCase()) && tag[0]?.toLowerCase() === tag[0]; } class Printer { buffer = ''; options; constructor(options) { this.options = options; } /* This is used by _all_ methods on this Printer class that add to `this.buffer`, it allows consumers of the printer to use alternate string representations for a given node. The primary use case for this are things like source -> source codemod utilities. For example, ember-template-recast attempts to always preserve the original string formatting in each AST node if no modifications are made to it. */ handledByOverride(node, ensureLeadingWhitespace = false) { if (this.options.override !== undefined) { let result = this.options.override(node, this.options); if (typeof result === 'string') { if (ensureLeadingWhitespace && NON_WHITESPACE.test(result)) { result = ` ${result}`; } this.buffer += result; return true; } } return false; } Node(node) { switch (node.type) { case 'MustacheStatement': case 'BlockStatement': case 'MustacheCommentStatement': case 'CommentStatement': case 'TextNode': case 'ElementNode': case 'AttrNode': case 'Block': case 'Template': return this.TopLevelStatement(node); case 'StringLiteral': case 'BooleanLiteral': case 'NumberLiteral': case 'UndefinedLiteral': case 'NullLiteral': case 'PathExpression': case 'SubExpression': return this.Expression(node); case 'ConcatStatement': // should have an AttrNode parent return this.ConcatStatement(node); case 'Hash': return this.Hash(node); case 'HashPair': return this.HashPair(node); case 'ElementModifierStatement': return this.ElementModifierStatement(node); } } Expression(expression) { switch (expression.type) { case 'StringLiteral': case 'BooleanLiteral': case 'NumberLiteral': case 'UndefinedLiteral': case 'NullLiteral': return this.Literal(expression); case 'PathExpression': return this.PathExpression(expression); case 'SubExpression': return this.SubExpression(expression); } } Literal(literal) { switch (literal.type) { case 'StringLiteral': return this.StringLiteral(literal); case 'BooleanLiteral': return this.BooleanLiteral(literal); case 'NumberLiteral': return this.NumberLiteral(literal); case 'UndefinedLiteral': return this.UndefinedLiteral(literal); case 'NullLiteral': return this.NullLiteral(literal); } } TopLevelStatement(statement) { switch (statement.type) { case 'MustacheStatement': return this.MustacheStatement(statement); case 'BlockStatement': return this.BlockStatement(statement); case 'MustacheCommentStatement': return this.MustacheCommentStatement(statement); case 'CommentStatement': return this.CommentStatement(statement); case 'TextNode': return this.TextNode(statement); case 'ElementNode': return this.ElementNode(statement); case 'Block': return this.Block(statement); case 'Template': return this.Template(statement); case 'AttrNode': // should have element return this.AttrNode(statement); } } Template(template) { this.TopLevelStatements(template.body); } Block(block) { /* When processing a template like: ```hbs {{#if whatever}} whatever {{else if somethingElse}} something else {{else}} fallback {{/if}} ``` The AST still _effectively_ looks like: ```hbs {{#if whatever}} whatever {{else}}{{#if somethingElse}} something else {{else}} fallback {{/if}}{{/if}} ``` The only way we can tell if that is the case is by checking for `block.chained`, but unfortunately when the actual statements are processed the `block.body[0]` node (which will always be a `BlockStatement`) has no clue that its ancestor `Block` node was chained. This "forwards" the `chained` setting so that we can check it later when processing the `BlockStatement`. */ if (block.chained) { let firstChild = block.body[0]; firstChild.chained = true; } if (this.handledByOverride(block)) { return; } this.TopLevelStatements(block.body); } TopLevelStatements(statements) { statements.forEach(statement => this.TopLevelStatement(statement)); } ElementNode(el) { if (this.handledByOverride(el)) { return; } this.OpenElementNode(el); this.TopLevelStatements(el.children); this.CloseElementNode(el); } OpenElementNode(el) { this.buffer += `<${el.tag}`; const parts = [...el.attributes, ...el.modifiers, ...el.comments].sort(sortByLoc); for (const part of parts) { this.buffer += ' '; switch (part.type) { case 'AttrNode': this.AttrNode(part); break; case 'ElementModifierStatement': this.ElementModifierStatement(part); break; case 'MustacheCommentStatement': this.MustacheCommentStatement(part); break; } } if (el.blockParams.length) { this.BlockParams(el.blockParams); } if (el.selfClosing) { this.buffer += ' /'; } this.buffer += '>'; } CloseElementNode(el) { if (el.selfClosing || isVoidTag(el.tag)) { return; } this.buffer += `</${el.tag}>`; } AttrNode(attr) { if (this.handledByOverride(attr)) { return; } let { name, value } = attr; this.buffer += name; const isAttribute = !name.startsWith('@'); const shouldElideValue = isAttribute && value.type == 'TextNode' && value.chars.length === 0; if (!shouldElideValue) { this.buffer += '='; this.AttrNodeValue(value); } } AttrNodeValue(value) { if (value.type === 'TextNode') { let quote = '"'; if (this.options.entityEncoding === 'raw') { if (value.chars.includes('"') && !value.chars.includes("'")) { quote = "'"; } } this.buffer += quote; this.TextNode(value, quote); this.buffer += quote; } else { this.Node(value); } } TextNode(text, isInAttr) { if (this.handledByOverride(text)) { return; } if (this.options.entityEncoding === 'raw') { if (isInAttr && text.chars.includes(isInAttr)) { this.buffer += escapeAttrValue(text.chars); } else { this.buffer += text.chars; } } else if (isInAttr) { this.buffer += escapeAttrValue(text.chars); } else { this.buffer += escapeText(text.chars); } } MustacheStatement(mustache) { if (this.handledByOverride(mustache)) { return; } this.buffer += mustache.trusting ? '{{{' : '{{'; if (mustache.strip.open) { this.buffer += '~'; } this.Expression(mustache.path); this.Params(mustache.params); this.Hash(mustache.hash); if (mustache.strip.close) { this.buffer += '~'; } this.buffer += mustache.trusting ? '}}}' : '}}'; } BlockStatement(block) { if (this.handledByOverride(block)) { return; } if (block.chained) { this.buffer += block.inverseStrip.open ? '{{~' : '{{'; this.buffer += 'else '; } else { this.buffer += block.openStrip.open ? '{{~#' : '{{#'; } this.Expression(block.path); this.Params(block.params); this.Hash(block.hash); if (block.program.blockParams.length) { this.BlockParams(block.program.blockParams); } if (block.chained) { this.buffer += block.inverseStrip.close ? '~}}' : '}}'; } else { this.buffer += block.openStrip.close ? '~}}' : '}}'; } this.Block(block.program); if (block.inverse) { if (!block.inverse.chained) { this.buffer += block.inverseStrip.open ? '{{~' : '{{'; this.buffer += 'else'; this.buffer += block.inverseStrip.close ? '~}}' : '}}'; } this.Block(block.inverse); } if (!block.chained) { this.buffer += block.closeStrip.open ? '{{~/' : '{{/'; this.Expression(block.path); this.buffer += block.closeStrip.close ? '~}}' : '}}'; } } BlockParams(blockParams) { this.buffer += ` as |${blockParams.join(' ')}|`; } ConcatStatement(concat) { if (this.handledByOverride(concat)) { return; } this.buffer += '"'; concat.parts.forEach(part => { if (part.type === 'TextNode') { this.TextNode(part, '"'); } else { this.Node(part); } }); this.buffer += '"'; } MustacheCommentStatement(comment) { if (this.handledByOverride(comment)) { return; } this.buffer += `{{!--${comment.value}--}}`; } ElementModifierStatement(mod) { if (this.handledByOverride(mod)) { return; } this.buffer += '{{'; this.Expression(mod.path); this.Params(mod.params); this.Hash(mod.hash); this.buffer += '}}'; } CommentStatement(comment) { if (this.handledByOverride(comment)) { return; } this.buffer += `<!--${comment.value}-->`; } PathExpression(path) { if (this.handledByOverride(path)) { return; } this.buffer += path.original; } SubExpression(sexp) { if (this.handledByOverride(sexp)) { return; } this.buffer += '('; this.Expression(sexp.path); this.Params(sexp.params); this.Hash(sexp.hash); this.buffer += ')'; } Params(params) { // TODO: implement a top level Params AST node (just like the Hash object) // so that this can also be overridden if (params.length) { params.forEach(param => { this.buffer += ' '; this.Expression(param); }); } } Hash(hash) { if (this.handledByOverride(hash, true)) { return; } hash.pairs.forEach(pair => { this.buffer += ' '; this.HashPair(pair); }); } HashPair(pair) { if (this.handledByOverride(pair)) { return; } this.buffer += pair.key; this.buffer += '='; this.Node(pair.value); } StringLiteral(str) { if (this.handledByOverride(str)) { return; } this.buffer += JSON.stringify(str.value); } BooleanLiteral(bool) { if (this.handledByOverride(bool)) { return; } this.buffer += String(bool.value); } NumberLiteral(number) { if (this.handledByOverride(number)) { return; } this.buffer += String(number.value); } UndefinedLiteral(node) { if (this.handledByOverride(node)) { return; } this.buffer += 'undefined'; } NullLiteral(node) { if (this.handledByOverride(node)) { return; } this.buffer += 'null'; } print(node) { let { options } = this; if (options.override) { let result = options.override(node, options); if (result !== undefined) { return result; } } this.buffer = ''; this.Node(node); return this.buffer; } } function build(ast, options = { entityEncoding: 'transformed' }) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS users if (!ast) { return ''; } let printer = new Printer(options); return printer.print(ast); } /** @module ember */ /** A Glimmer2 AST transformation that replaces all instances of ```handlebars {{helper "..." ...}} ``` with ```handlebars {{helper (-resolve "helper:...") ...}} ``` and ```handlebars {{helper ... ...}} ``` with ```handlebars {{helper (-disallow-dynamic-resolution ...) ...}} ``` and ```handlebars {{modifier "..." ...}} ``` with ```handlebars {{modifier (-resolve "modifier:...") ...}} ``` and ```handlebars {{modifier ... ...}} ``` with ```handlebars {{modifier (-disallow-dynamic-resolution ...) ...}} ``` @private @class TransformResolutions */ const TARGETS = Object.freeze(['helper', 'modifier']); function transformResolutions(env) { let { builders: b } = env.syntax; let moduleName = env.meta?.moduleName; let { hasLocal, node: tracker } = trackLocals(env); let seen; return { name: 'transform-resolutions', visitor: { Template: { enter() { seen = new Set(); }, exit() { seen = undefined; } }, Block: tracker, ElementNode: { keys: { children: tracker } }, MustacheStatement(node) { (!(seen) && assert('[BUG] seen set should be available', seen)); if (seen.has(node)) { return; } if (isPath(node.path) && !isLocalVariable(node.path, hasLocal) && TARGETS.indexOf(node.path.original) !== -1) { let result = b.mustache(node.path, transformParams(b, node.params, node.path.original, moduleName, node.loc), node.hash, node.trusting, node.loc, node.strip); // Avoid double/infinite-processing seen.add(result); return result; } }, SubExpression(node) { (!(seen) && assert('[BUG] seen set should be available', seen)); if (seen.has(node)) { return; } if (isPath(node.path) && !isLocalVariable(node.path, hasLocal) && TARGETS.indexOf(node.path.original) !== -1) { let result = b.sexpr(node.path, transformParams(b, node.params, node.path.original, moduleName, node.loc), node.hash, node.loc); // Avoid double/infinite-processing seen.add(result); return result; } } } }; } function isLocalVariable(node, hasLocal) { return !(node.head.type === 'ThisHead') && node.tail.length === 1 && hasLocal(node.head.original); } function transformParams(b, params, type, moduleName, loc) { let [first, ...rest] = params; (!(first) && assert(`The ${type} keyword requires at least one positional arguments ${calculateLocationDisplay(moduleName, loc)}`, first)); if (isStringLiteral(first)) { return [b.sexpr(b.path('-resolve', first.loc), [b.string(`${type}:${first.value}`)], undefined, first.loc), ...rest]; } else { return [b.sexpr(b.path('-disallow-dynamic-resolution', first.loc), [first], b.hash([b.pair('type', b.string(type), first.loc), b.pair('loc', b.string(calculateLocationDisplay(moduleName, loc)), first.loc), b.pair('original', b.string(build(first)))]), first.loc), ...rest]; } } export { Printer as P, build as b, isVoidTag as i, transformResolutions as t, voidMap as v };