UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,393 lines (1,389 loc) 75.4 kB
import templateOnly from '../../component/template-only.js'; import { isPresentArray, getLast, mapPresentArray, exhausted, assert as debugAssert, assertPresentArray } from '../../../@glimmer/util/index.js'; import { WellKnownTagNames, WellKnownAttrNames, SexpOpcodes as opcodes } from '../../../@glimmer/wire-format/index.js'; import { n as node, a as api$2, g as generateSyntaxError, b as api$1, m as maybeLoc, K as KEYWORDS_TYPES, i as isKeyword, S as SourceSlice, c as normalize, l as loc } from '../../../shared-chunks/transform-resolutions-O6uYv8DS.js'; import { CurriedType as CurriedTypes } from '../../../@glimmer/vm/index.js'; import { setComponentTemplate } from '../../../@glimmer/manager/index.js'; import { templateFactory } from '../../../@glimmer/opcode-compiler/index.js'; import compileOptions from './compile-options.js'; (function (HeadKind) { return HeadKind.Block = "Block", HeadKind.Call = "Call", HeadKind.Element = "Element", HeadKind.AppendPath = "AppendPath", HeadKind.AppendExpr = "AppendExpr", HeadKind.Literal = "Literal", HeadKind.Modifier = "Modifier", HeadKind.DynamicComponent = "DynamicComponent", HeadKind.Comment = "Comment", HeadKind.Splat = "Splat", HeadKind.Keyword = "Keyword", HeadKind; })({}); (function (VariableKind) { return VariableKind.Local = "Local", VariableKind.Free = "Free", VariableKind.Arg = "Arg", VariableKind.Block = "Block", VariableKind.This = "This", VariableKind; })({}); (function (Builder) { return Builder[Builder.Literal = 0] = "Literal", Builder[Builder.Comment = 1] = "Comment", Builder[Builder.Append = 2] = "Append", Builder[Builder.Modifier = 3] = "Modifier", Builder[Builder.DynamicComponent = 4] = "DynamicComponent", Builder[Builder.Get = 5] = "Get", Builder[Builder.Concat = 6] = "Concat", Builder[Builder.HasBlock = 7] = "HasBlock", Builder[Builder.HasBlockParams = 8] = "HasBlockParams", Builder; })({}); (function (ExpressionKind) { return ExpressionKind.Literal = "Literal", ExpressionKind.Call = "Call", ExpressionKind.GetPath = "GetPath", ExpressionKind.GetVar = "GetVar", ExpressionKind.Concat = "Concat", ExpressionKind.HasBlock = "HasBlock", ExpressionKind.HasBlockParams = "HasBlockParams", ExpressionKind; })({}); class Template extends node("Template").fields() {} class InElement extends node("InElement").fields() {} class Not extends node("Not").fields() {} class If extends node("If").fields() {} class IfInline extends node("IfInline").fields() {} class Each extends node("Each").fields() {} class Let extends node("Let").fields() {} class WithDynamicVars extends node("WithDynamicVars").fields() {} class GetDynamicVar extends node("GetDynamicVar").fields() {} class Log extends node("Log").fields() {} class InvokeComponent extends node("InvokeComponent").fields() {} class NamedBlocks extends node("NamedBlocks").fields() {} class NamedBlock extends node("NamedBlock").fields() {} class AppendTrustedHTML extends node("AppendTrustedHTML").fields() {} class AppendTextNode extends node("AppendTextNode").fields() {} class AppendComment extends node("AppendComment").fields() {} class Component extends node("Component").fields() {} class StaticAttr extends node("StaticAttr").fields() {} class DynamicAttr extends node("DynamicAttr").fields() {} class SimpleElement extends node("SimpleElement").fields() {} class ElementParameters extends node("ElementParameters").fields() {} class Yield extends node("Yield").fields() {} class Debugger extends node("Debugger").fields() {} class CallExpression extends node("CallExpression").fields() {} class Modifier extends node("Modifier").fields() {} class InvokeBlock extends node("InvokeBlock").fields() {} class SplatAttr extends node("SplatAttr").fields() {} class PathExpression extends node("PathExpression").fields() {} class Missing extends node("Missing").fields() {} class InterpolateExpression extends node("InterpolateExpression").fields() {} class HasBlock extends node("HasBlock").fields() {} class HasBlockParams extends node("HasBlockParams").fields() {} class Curry extends node("Curry").fields() {} class Positional extends node("Positional").fields() {} class NamedArguments extends node("NamedArguments").fields() {} class NamedArgument extends node("NamedArgument").fields() {} class Args extends node("Args").fields() {} class Tail extends node("Tail").fields() {} class PresentList { constructor(list) { this.list = list; } toArray() { return this.list; } map(callback) { let result = mapPresentArray(this.list, callback); return new PresentList(result); } filter(predicate) { let out = []; for (let item of this.list) predicate(item) && out.push(item); return OptionalList(out); } toPresentArray() { return this.list; } into({ ifPresent: ifPresent }) { return ifPresent(this); } } class EmptyList { list = []; map(_callback) { return new EmptyList(); } filter(_predicate) { return new EmptyList(); } toArray() { return this.list; } toPresentArray() { return null; } into({ ifEmpty: ifEmpty }) { return ifEmpty(); } } // export type OptionalList<T> = PresentList<T> | EmptyList<T>; function OptionalList(value) { return isPresentArray(value) ? new PresentList(value) : new EmptyList(); } class ResultImpl { static all(...results) { let out = []; for (let result of results) { if (result.isErr) return result.cast(); out.push(result.value); } return Ok(out); } } const Result = ResultImpl; class OkImpl extends ResultImpl { isOk = !0; isErr = !1; constructor(value) { super(), this.value = value; } expect(_message) { return this.value; } ifOk(callback) { return callback(this.value), this; } andThen(callback) { return callback(this.value); } mapOk(callback) { return Ok(callback(this.value)); } ifErr(_callback) { return this; } mapErr(_callback) { return this; } } class ErrImpl extends ResultImpl { isOk = !1; isErr = !0; constructor(reason) { super(), this.reason = reason; } expect(message) { throw new Error(message || "expected an Ok, got Err"); } andThen(_callback) { return this.cast(); } mapOk(_callback) { return this.cast(); } ifOk(_callback) { return this; } mapErr(callback) { return Err(callback(this.reason)); } ifErr(callback) { return callback(this.reason), this; } cast() { return this; } } function Ok(value) { return new OkImpl(value); } function Err(reason) { return new ErrImpl(reason); } class ResultArray { constructor(items = []) { this.items = items; } add(item) { this.items.push(item); } toArray() { let err = this.items.filter(item => item instanceof ErrImpl)[0]; return void 0 !== err ? err.cast() : Ok(this.items.map(item => item.value)); } toOptionalList() { return this.toArray().mapOk(arr => OptionalList(arr)); } } function convertPathToCallIfKeyword(path) { return "Path" === path.type && "Free" === path.ref.type && path.ref.name in KEYWORDS_TYPES ? new api$1.CallExpression({ callee: path, args: api$1.Args.empty(path.loc), loc: path.loc }) : path; } const VISIT_EXPRS = new class { visit(node, state) { switch (node.type) { case "Literal": return Ok(this.Literal(node)); case "Keyword": return Ok(this.Keyword(node)); case "Interpolate": return this.Interpolate(node, state); case "Path": return this.PathExpression(node); case "Call": { let translated = CALL_KEYWORDS.translate(node, state); return null !== translated ? translated : this.CallExpression(node, state); } } } visitList(nodes, state) { return new ResultArray(nodes.map(e => VISIT_EXPRS.visit(e, state))).toOptionalList(); } /** * Normalize paths into `hir.Path` or a `hir.Expr` that corresponds to the ref. * * TODO since keywords don't support tails anyway, distinguish PathExpression from * VariableReference in ASTv2. */ PathExpression(path) { let ref = this.VariableReference(path.ref), { tail: tail } = path; if (isPresentArray(tail)) { let tailLoc = tail[0].loc.extend(getLast(tail).loc); return Ok(new PathExpression({ loc: path.loc, head: ref, tail: new Tail({ loc: tailLoc, members: tail }) })); } return Ok(ref); } VariableReference(ref) { return ref; } Literal(literal) { return literal; } Keyword(keyword) { return keyword; } Interpolate(expr, state) { let parts = expr.parts.map(convertPathToCallIfKeyword); return VISIT_EXPRS.visitList(parts, state).mapOk(parts => new InterpolateExpression({ loc: expr.loc, parts: parts })); } CallExpression(expr, state) { if ("Call" === expr.callee.type) throw new Error("unimplemented: subexpression at the head of a subexpression"); return Result.all(VISIT_EXPRS.visit(expr.callee, state), VISIT_EXPRS.Args(expr.args, state)).mapOk(([callee, args]) => new CallExpression({ loc: expr.loc, callee: callee, args: args })); } Args({ positional: positional, named: named, loc: loc }, state) { return Result.all(this.Positional(positional, state), this.NamedArguments(named, state)).mapOk(([positional, named]) => new Args({ loc: loc, positional: positional, named: named })); } Positional(positional, state) { return VISIT_EXPRS.visitList(positional.exprs, state).mapOk(list => new Positional({ loc: positional.loc, list: list })); } NamedArguments(named, state) { let pairs = named.entries.map(arg => { let value = convertPathToCallIfKeyword(arg.value); return VISIT_EXPRS.visit(value, state).mapOk(value => new NamedArgument({ loc: arg.loc, key: arg.name, value: value })); }); return new ResultArray(pairs).toOptionalList().mapOk(pairs => new NamedArguments({ loc: named.loc, entries: pairs })); } }(); class KeywordImpl { types; constructor(keyword, type, delegate) { this.keyword = keyword, this.delegate = delegate; let nodes = new Set(); for (let nodeType of KEYWORD_NODES[type]) nodes.add(nodeType); this.types = nodes; } match(node) { if (!this.types.has(node.type)) return !1; let path = getCalleeExpression(node); return null !== path && "Path" === path.type && "Free" === path.ref.type && path.ref.name === this.keyword; } translate(node, state) { if (this.match(node)) { let path = getCalleeExpression(node); return null !== path && "Path" === path.type && path.tail.length > 0 ? Err(generateSyntaxError(`The \`${this.keyword}\` keyword was used incorrectly. It was used as \`${path.loc.asString()}\`, but it cannot be used with additional path segments. \n\nError caused by`, node.loc)) : this.delegate.assert(node, state).andThen(param => this.delegate.translate({ node: node, state: state }, param)); } return null; } } const KEYWORD_NODES = { Call: ["Call"], Block: ["InvokeBlock"], Append: ["AppendContent"], Modifier: ["ElementModifier"] }; /** * A "generic" keyword is something like `has-block`, which makes sense in the context * of sub-expression or append */ function getCalleeExpression(node) { switch (node.type) { // This covers the inside of attributes and expressions, as well as the callee // of call nodes case "Path": return node; case "AppendContent": return getCalleeExpression(node.value); case "Call": case "InvokeBlock": case "ElementModifier": return node.callee; default: return null; } } class Keywords { _keywords = []; _type; constructor(type) { this._type = type; } kw(name, delegate) { return this._keywords.push(function (keyword, type, delegate) { return new KeywordImpl(keyword, type, delegate); }(name, this._type, delegate)), this; } translate(node, state) { for (let keyword of this._keywords) { let result = keyword.translate(node, state); if (null !== result) return result; } let path = getCalleeExpression(node); if (path && "Path" === path.type && "Free" === path.ref.type && isKeyword(path.ref.name)) { let { name: name } = path.ref, usedType = this._type, validTypes = KEYWORDS_TYPES[name]; if (!validTypes.includes(usedType)) return Err(generateSyntaxError(`The \`${name}\` keyword was used incorrectly. It was used as ${typesToReadableName[usedType]}, but its valid usages are:\n\n${function (name, types) { return types.map(type => { switch (type) { case "Append": return `- As an append statement, as in: {{${name}}}`; case "Block": return `- As a block statement, as in: {{#${name}}}{{/${name}}}`; case "Call": return `- As an expression, as in: (${name})`; case "Modifier": return `- As a modifier, as in: <div {{${name}}}></div>`; default: return exhausted(type); } }).join("\n\n"); } /** * This function builds keyword definitions for a particular type of AST node (`KeywordType`). * * You can build keyword definitions for: * * - `Expr`: A `SubExpression` or `PathExpression` * - `Block`: A `BlockStatement` * - A `BlockStatement` is a keyword candidate if its head is a * `PathExpression` * - `Append`: An `AppendStatement` * * A node is a keyword candidate if: * * - A `PathExpression` is a keyword candidate if it has no tail, and its * head expression is a `LocalVarHead` or `FreeVarHead` whose name is * the keyword's name. * - A `SubExpression`, `AppendStatement`, or `BlockStatement` is a keyword * candidate if its head is a keyword candidate. * * The keyword infrastructure guarantees that: * * - If a node is not a keyword candidate, it is never passed to any keyword's * `assert` method. * - If a node is not the `KeywordType` for a particular keyword, it will not * be passed to the keyword's `assert` method. * * `Expr` keywords are used in expression positions and should return HIR * expressions. `Block` and `Append` keywords are used in statement * positions and should return HIR statements. * * A keyword definition has two parts: * * - `match`, which determines whether an AST node matches the keyword, and can * optionally return some information extracted from the AST node. * - `translate`, which takes a matching AST node as well as the extracted * information and returns an appropriate HIR instruction. * * # Example * * This keyword: * * - turns `(hello)` into `"hello"` * - as long as `hello` is not in scope * - makes it an error to pass any arguments (such as `(hello world)`) * * ```ts * keywords('SubExpr').kw('hello', { * assert(node: ExprKeywordNode): Result<void> | false { * // we don't want to transform `hello` as a `PathExpression` * if (node.type !== 'SubExpression') { * return false; * } * * // node.head would be `LocalVarHead` if `hello` was in scope * if (node.head.type !== 'FreeVarHead') { * return false; * } * * if (node.params.length || node.hash) { * return Err(generateSyntaxError(`(hello) does not take any arguments`), node.loc); * } else { * return Ok(); * } * }, * * translate(node: ASTv2.SubExpression): hir.Expression { * return ASTv2.builders.literal("hello", node.loc) * } * }) * ``` * * The keyword infrastructure checks to make sure that the node is the right * type before calling `assert`, so you only need to consider `SubExpression` * and `PathExpression` here. It also checks to make sure that the node passed * to `assert` has the keyword name in the right place. * * Note the important difference between returning `false` from `assert`, * which just means that the node didn't match, and returning `Err`, which * means that the node matched, but there was a keyword-specific syntax * error. */(name, validTypes)}\n\nError caused by`, node.loc)); } return null; } } const typesToReadableName = { Append: "an append statement", Block: "a block statement", Call: "a call expression", Modifier: "a modifier" }; function keywords(type) { return new Keywords(type); } function toAppend({ assert: assert, translate: translate }) { return { assert: assert, translate: ({ node: node, state: state }, value) => translate({ node: node, state: state }, value).mapOk(text => new AppendTextNode({ text: text, loc: node.loc })) }; } const CurriedTypeToReadableType = { [CurriedTypes.Component]: "component", [CurriedTypes.Helper]: "helper", [CurriedTypes.Modifier]: "modifier" }; function assertCurryKeyword(curriedType) { return (node, state) => { let readableType = CurriedTypeToReadableType[curriedType], stringsAllowed = curriedType === CurriedTypes.Component, { args: args } = node, definition = args.nth(0); if (null === definition) return Err(generateSyntaxError(`(${readableType}) requires a ${readableType} definition or identifier as its first positional parameter, did not receive any parameters.`, args.loc)); if ("Literal" === definition.type) { if (stringsAllowed && state.isStrict) return Err(generateSyntaxError(`(${readableType}) cannot resolve string values in strict mode templates`, node.loc)); if (!stringsAllowed) return Err(generateSyntaxError(`(${readableType}) cannot resolve string values, you must pass a ${readableType} definition directly`, node.loc)); } return args = new api$1.Args({ positional: new api$1.PositionalArguments({ exprs: args.positional.exprs.slice(1), loc: args.positional.loc }), named: args.named, loc: args.loc }), Ok({ definition: definition, args: args }); }; } function translateCurryKeyword(curriedType) { return ({ node: node, state: state }, { definition: definition, args: args }) => { let definitionResult = VISIT_EXPRS.visit(definition, state), argsResult = VISIT_EXPRS.Args(args, state); return Result.all(definitionResult, argsResult).mapOk(([definition, args]) => new Curry({ loc: node.loc, curriedType: curriedType, definition: definition, args: args })); }; } function curryKeyword(curriedType) { return { assert: assertCurryKeyword(curriedType), translate: translateCurryKeyword(curriedType) }; } const getDynamicVarKeyword = { assert: function (node) { let call = "AppendContent" === node.type ? node.value : node, named = "Call" === call.type ? call.args.named : null, positionals = "Call" === call.type ? call.args.positional : null; if (named && !named.isEmpty()) return Err(generateSyntaxError("(-get-dynamic-vars) does not take any named arguments", node.loc)); let varName = positionals?.nth(0); return varName ? positionals && positionals.size > 1 ? Err(generateSyntaxError("(-get-dynamic-vars) only receives one positional arg", node.loc)) : Ok(varName) : Err(generateSyntaxError("(-get-dynamic-vars) requires a var name to get", node.loc)); }, translate: function ({ node: node, state: state }, name) { return VISIT_EXPRS.visit(name, state).mapOk(name => new GetDynamicVar({ name: name, loc: node.loc })); } }; function assertHasBlockKeyword(type) { return node => { let call = "AppendContent" === node.type ? node.value : node, named = "Call" === call.type ? call.args.named : null, positionals = "Call" === call.type ? call.args.positional : null; if (named && !named.isEmpty()) return Err(generateSyntaxError(`(${type}) does not take any named arguments`, call.loc)); if (!positionals || positionals.isEmpty()) return Ok(SourceSlice.synthetic("default")); if (1 === positionals.exprs.length) { let positional = positionals.exprs[0]; return api$1.isLiteral(positional, "string") ? Ok(positional.toSlice()) : Err(generateSyntaxError(`(${type}) can only receive a string literal as its first argument`, call.loc)); } return Err(generateSyntaxError(`(${type}) only takes a single positional argument`, call.loc)); }; } function translateHasBlockKeyword(type) { return ({ node: node, state: { scope: scope } }, target) => Ok("has-block" === type ? new HasBlock({ loc: node.loc, target: target, symbol: scope.allocateBlock(target.chars) }) : new HasBlockParams({ loc: node.loc, target: target, symbol: scope.allocateBlock(target.chars) })); } function hasBlockKeyword(type) { return { assert: assertHasBlockKeyword(type), translate: translateHasBlockKeyword(type) }; } function assertIfUnlessInlineKeyword(type) { return originalNode => { let inverted = "unless" === type, node = "AppendContent" === originalNode.type ? originalNode.value : originalNode, named = "Call" === node.type ? node.args.named : null, positional = "Call" === node.type ? node.args.positional : null; if (named && !named.isEmpty()) return Err(generateSyntaxError(`(${type}) cannot receive named parameters, received ${named.entries.map(e => e.name.chars).join(", ")}`, originalNode.loc)); let condition = positional?.nth(0); if (!positional || !condition) return Err(generateSyntaxError(`When used inline, (${type}) requires at least two parameters 1. the condition that determines the state of the (${type}), and 2. the value to return if the condition is ${inverted ? "false" : "true"}. Did not receive any parameters`, originalNode.loc)); let truthy = positional.nth(1), falsy = positional.nth(2); return null === truthy ? Err(generateSyntaxError(`When used inline, (${type}) requires at least two parameters 1. the condition that determines the state of the (${type}), and 2. the value to return if the condition is ${inverted ? "false" : "true"}. Received only one parameter, the condition`, originalNode.loc)) : positional.size > 3 ? Err(generateSyntaxError(`When used inline, (${type}) can receive a maximum of three positional parameters 1. the condition that determines the state of the (${type}), 2. the value to return if the condition is ${inverted ? "false" : "true"}, and 3. the value to return if the condition is ${inverted ? "true" : "false"}. Received ${positional?.size ?? 0} parameters`, originalNode.loc)) : Ok({ condition: condition, truthy: truthy, falsy: falsy }); }; } function translateIfUnlessInlineKeyword(type) { let inverted = "unless" === type; return ({ node: node, state: state }, { condition: condition, truthy: truthy, falsy: falsy }) => { let conditionResult = VISIT_EXPRS.visit(condition, state), truthyResult = VISIT_EXPRS.visit(truthy, state), falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null); return Result.all(conditionResult, truthyResult, falsyResult).mapOk(([condition, truthy, falsy]) => (inverted && (condition = new Not({ value: condition, loc: node.loc })), new IfInline({ loc: node.loc, condition: condition, truthy: truthy, falsy: falsy }))); }; } function ifUnlessInlineKeyword(type) { return { assert: assertIfUnlessInlineKeyword(type), translate: translateIfUnlessInlineKeyword(type) }; } const logKeyword = { assert: function (node) { let { args: { named: named, positional: positional } } = node; return named && !named.isEmpty() ? Err(generateSyntaxError("(log) does not take any named arguments", node.loc)) : Ok(positional); }, translate: function ({ node: node, state: state }, positional) { return VISIT_EXPRS.Positional(positional, state).mapOk(positional => new Log({ positional: positional, loc: node.loc })); } }, APPEND_KEYWORDS = keywords("Append").kw("has-block", toAppend(hasBlockKeyword("has-block"))).kw("has-block-params", toAppend(hasBlockKeyword("has-block-params"))).kw("-get-dynamic-var", toAppend(getDynamicVarKeyword)).kw("log", toAppend(logKeyword)).kw("if", toAppend(ifUnlessInlineKeyword("if"))).kw("unless", toAppend(ifUnlessInlineKeyword("unless"))).kw("yield", { assert(node) { let { args: args } = node; if (args.named.isEmpty()) return Ok({ target: api$2.SourceSpan.synthetic("default").toSlice(), positional: args.positional }); { let target = args.named.get("to"); return args.named.size > 1 || null === target ? Err(generateSyntaxError("yield only takes a single named argument: 'to'", args.named.loc)) : api$1.isLiteral(target, "string") ? Ok({ target: target.toSlice(), positional: args.positional }) : Err(generateSyntaxError("you can only yield to a literal string value", target.loc)); } }, translate: ({ node: node, state: state }, { target: target, positional: positional }) => VISIT_EXPRS.Positional(positional, state).mapOk(positional => new Yield({ loc: node.loc, target: target, to: state.scope.allocateBlock(target.chars), positional: positional })) }).kw("debugger", { assert(node) { let { args: args } = node, { positional: positional } = args; return args.isEmpty() ? Ok(void 0) : positional.isEmpty() ? Err(generateSyntaxError("debugger does not take any named arguments", node.loc)) : Err(generateSyntaxError("debugger does not take any positional arguments", node.loc)); }, translate: ({ node: node, state: { scope: scope } }) => (scope.setHasDebugger(), Ok(new Debugger({ loc: node.loc, scope: scope }))) }).kw("component", { assert: assertCurryKeyword(CurriedTypes.Component), translate({ node: node, state: state }, { definition: definition, args: args }) { let definitionResult = VISIT_EXPRS.visit(definition, state), argsResult = VISIT_EXPRS.Args(args, state); return Result.all(definitionResult, argsResult).mapOk(([definition, args]) => new InvokeComponent({ loc: node.loc, definition: definition, args: args, blocks: null })); } }).kw("helper", { assert: assertCurryKeyword(CurriedTypes.Helper), translate({ node: node, state: state }, { definition: definition, args: args }) { let definitionResult = VISIT_EXPRS.visit(definition, state), argsResult = VISIT_EXPRS.Args(args, state); return Result.all(definitionResult, argsResult).mapOk(([definition, args]) => { let text = new CallExpression({ callee: definition, args: args, loc: node.loc }); return new AppendTextNode({ loc: node.loc, text: text }); }); } }), BLOCK_KEYWORDS = keywords("Block").kw("in-element", { assert(node) { let { args: args } = node, guid = args.get("guid"); if (guid) return Err(generateSyntaxError("Cannot pass `guid` to `{{#in-element}}`", guid.loc)); let insertBefore = args.get("insertBefore"), destination = args.nth(0); return null === destination ? Err(generateSyntaxError("{{#in-element}} requires a target element as its first positional parameter", args.loc)) : Ok({ insertBefore: insertBefore, destination: destination }); // TODO Better syntax checks }, translate({ node: node, state: state }, { insertBefore: insertBefore, destination: destination }) { let named = node.blocks.get("default"), body = VISIT_STMTS.NamedBlock(named, state), destinationResult = VISIT_EXPRS.visit(destination, state); return Result.all(body, destinationResult).andThen(([body, destination]) => insertBefore ? VISIT_EXPRS.visit(insertBefore, state).mapOk(insertBefore => ({ body: body, destination: destination, insertBefore: insertBefore })) : Ok({ body: body, destination: destination, insertBefore: new Missing({ loc: node.callee.loc.collapse("end") }) })).mapOk(({ body: body, destination: destination, insertBefore: insertBefore }) => new InElement({ loc: node.loc, block: body, insertBefore: insertBefore, guid: state.generateUniqueCursor(), destination: destination })); } }).kw("if", { assert(node) { let { args: args } = node; if (!args.named.isEmpty()) return Err(generateSyntaxError(`{{#if}} cannot receive named parameters, received ${args.named.entries.map(e => e.name.chars).join(", ")}`, node.loc)); if (args.positional.size > 1) return Err(generateSyntaxError(`{{#if}} can only receive one positional parameter in block form, the conditional value. Received ${args.positional.size} parameters`, node.loc)); let condition = args.nth(0); return null === condition ? Err(generateSyntaxError("{{#if}} requires a condition as its first positional parameter, did not receive any parameters", node.loc)) : Ok({ condition: condition }); }, translate({ node: node, state: state }, { condition: condition }) { let block = node.blocks.get("default"), inverse = node.blocks.get("else"), conditionResult = VISIT_EXPRS.visit(condition, state), blockResult = VISIT_STMTS.NamedBlock(block, state), inverseResult = inverse ? VISIT_STMTS.NamedBlock(inverse, state) : Ok(null); return Result.all(conditionResult, blockResult, inverseResult).mapOk(([condition, block, inverse]) => new If({ loc: node.loc, condition: condition, block: block, inverse: inverse })); } }).kw("unless", { assert(node) { let { args: args } = node; if (!args.named.isEmpty()) return Err(generateSyntaxError(`{{#unless}} cannot receive named parameters, received ${args.named.entries.map(e => e.name.chars).join(", ")}`, node.loc)); if (args.positional.size > 1) return Err(generateSyntaxError(`{{#unless}} can only receive one positional parameter in block form, the conditional value. Received ${args.positional.size} parameters`, node.loc)); let condition = args.nth(0); return null === condition ? Err(generateSyntaxError("{{#unless}} requires a condition as its first positional parameter, did not receive any parameters", node.loc)) : Ok({ condition: condition }); }, translate({ node: node, state: state }, { condition: condition }) { let block = node.blocks.get("default"), inverse = node.blocks.get("else"), conditionResult = VISIT_EXPRS.visit(condition, state), blockResult = VISIT_STMTS.NamedBlock(block, state), inverseResult = inverse ? VISIT_STMTS.NamedBlock(inverse, state) : Ok(null); return Result.all(conditionResult, blockResult, inverseResult).mapOk(([condition, block, inverse]) => new If({ loc: node.loc, condition: new Not({ value: condition, loc: node.loc }), block: block, inverse: inverse })); } }).kw("each", { assert(node) { let { args: args } = node; if (!args.named.entries.every(e => "key" === e.name.chars)) return Err(generateSyntaxError(`{{#each}} can only receive the 'key' named parameter, received ${args.named.entries.filter(e => "key" !== e.name.chars).map(e => e.name.chars).join(", ")}`, args.named.loc)); if (args.positional.size > 1) return Err(generateSyntaxError(`{{#each}} can only receive one positional parameter, the collection being iterated. Received ${args.positional.size} parameters`, args.positional.loc)); let value = args.nth(0), key = args.get("key"); return null === value ? Err(generateSyntaxError("{{#each}} requires an iterable value to be passed as its first positional parameter, did not receive any parameters", args.loc)) : Ok({ value: value, key: key }); }, translate({ node: node, state: state }, { value: value, key: key }) { let block = node.blocks.get("default"), inverse = node.blocks.get("else"), valueResult = VISIT_EXPRS.visit(value, state), keyResult = key ? VISIT_EXPRS.visit(key, state) : Ok(null), blockResult = VISIT_STMTS.NamedBlock(block, state), inverseResult = inverse ? VISIT_STMTS.NamedBlock(inverse, state) : Ok(null); return Result.all(valueResult, keyResult, blockResult, inverseResult).mapOk(([value, key, block, inverse]) => new Each({ loc: node.loc, value: value, key: key, block: block, inverse: inverse })); } }).kw("let", { assert(node) { let { args: args } = node; return args.named.isEmpty() ? 0 === args.positional.size ? Err(generateSyntaxError("{{#let}} requires at least one value as its first positional parameter, did not receive any parameters", args.positional.loc)) : node.blocks.get("else") ? Err(generateSyntaxError("{{#let}} cannot receive an {{else}} block", args.positional.loc)) : Ok({ positional: args.positional }) : Err(generateSyntaxError(`{{#let}} cannot receive named parameters, received ${args.named.entries.map(e => e.name.chars).join(", ")}`, args.named.loc)); }, translate({ node: node, state: state }, { positional: positional }) { let block = node.blocks.get("default"), positionalResult = VISIT_EXPRS.Positional(positional, state), blockResult = VISIT_STMTS.NamedBlock(block, state); return Result.all(positionalResult, blockResult).mapOk(([positional, block]) => new Let({ loc: node.loc, positional: positional, block: block })); } }).kw("-with-dynamic-vars", { assert: node => Ok({ named: node.args.named }), translate({ node: node, state: state }, { named: named }) { let block = node.blocks.get("default"), namedResult = VISIT_EXPRS.NamedArguments(named, state), blockResult = VISIT_STMTS.NamedBlock(block, state); return Result.all(namedResult, blockResult).mapOk(([named, block]) => new WithDynamicVars({ loc: node.loc, named: named, block: block })); } }).kw("component", { assert: assertCurryKeyword(CurriedTypes.Component), translate({ node: node, state: state }, { definition: definition, args: args }) { let definitionResult = VISIT_EXPRS.visit(definition, state), argsResult = VISIT_EXPRS.Args(args, state), blocksResult = VISIT_STMTS.NamedBlocks(node.blocks, state); return Result.all(definitionResult, argsResult, blocksResult).mapOk(([definition, args, blocks]) => new InvokeComponent({ loc: node.loc, definition: definition, args: args, blocks: blocks })); } }), CALL_KEYWORDS = keywords("Call").kw("has-block", hasBlockKeyword("has-block")).kw("has-block-params", hasBlockKeyword("has-block-params")).kw("-get-dynamic-var", getDynamicVarKeyword).kw("log", logKeyword).kw("if", ifUnlessInlineKeyword("if")).kw("unless", ifUnlessInlineKeyword("unless")).kw("component", curryKeyword(CurriedTypes.Component)).kw("helper", curryKeyword(CurriedTypes.Helper)).kw("modifier", curryKeyword(CurriedTypes.Modifier)), MODIFIER_KEYWORDS = keywords("Modifier"), XLINK = "http://www.w3.org/1999/xlink", XML = "http://www.w3.org/XML/1998/namespace", XMLNS = "http://www.w3.org/2000/xmlns/", WHITELIST = { "xlink:actuate": XLINK, "xlink:arcrole": XLINK, "xlink:href": XLINK, "xlink:role": XLINK, "xlink:show": XLINK, "xlink:title": XLINK, "xlink:type": XLINK, "xml:base": XML, "xml:lang": XML, "xml:space": XML, xmlns: XMLNS, "xmlns:xlink": XMLNS }, DEFLATE_TAG_TABLE = { div: WellKnownTagNames.div, span: WellKnownTagNames.span, p: WellKnownTagNames.p, a: WellKnownTagNames.a }; const DEFLATE_ATTR_TABLE = { class: WellKnownAttrNames.class, id: WellKnownAttrNames.id, value: WellKnownAttrNames.value, name: WellKnownAttrNames.name, type: WellKnownAttrNames.type, style: WellKnownAttrNames.style, href: WellKnownAttrNames.href }; function deflateAttrName(attrName) { return DEFLATE_ATTR_TABLE[attrName] ?? attrName; } class ClassifiedElement { delegate; constructor(element, delegate, state) { this.element = element, this.state = state, this.delegate = delegate; } toStatement() { return this.prepare().andThen(prepared => this.delegate.toStatement(this, prepared)); } attr(attr) { let name = attr.name, rawValue = attr.value, namespace = (attrName = name.chars, WHITELIST[attrName] || void 0); var attrName; return api$1.isLiteral(rawValue, "string") ? Ok(new StaticAttr({ loc: attr.loc, name: name, value: rawValue.toSlice(), namespace: namespace, kind: { component: this.delegate.dynamicFeatures } })) : VISIT_EXPRS.visit(convertPathToCallIfKeyword(rawValue), this.state).mapOk(value => { let isTrusting = attr.trusting; return new DynamicAttr({ loc: attr.loc, name: name, value: value, namespace: namespace, kind: { trusting: isTrusting, component: this.delegate.dynamicFeatures } }); }); } modifier(modifier) { let translated = MODIFIER_KEYWORDS.translate(modifier, this.state); if (null !== translated) return translated; let head = VISIT_EXPRS.visit(modifier.callee, this.state), args = VISIT_EXPRS.Args(modifier.args, this.state); return Result.all(head, args).mapOk(([head, args]) => new Modifier({ loc: modifier.loc, callee: head, args: args })); } attrs() { let attrs = new ResultArray(), args = new ResultArray(), typeAttr = null, simple = 0 === this.element.attrs.filter(attr => "SplatAttr" === attr.type).length; for (let attr of this.element.attrs) "SplatAttr" === attr.type ? attrs.add(Ok(new SplatAttr({ loc: attr.loc, symbol: this.state.scope.allocateBlock("attrs") }))) : "type" === attr.name.chars && simple ? typeAttr = attr : attrs.add(this.attr(attr)); for (let arg of this.element.componentArgs) args.add(this.delegate.arg(arg, this)); return typeAttr && attrs.add(this.attr(typeAttr)), Result.all(args.toArray(), attrs.toArray()).mapOk(([args, attrs]) => ({ attrs: attrs, args: new NamedArguments({ loc: maybeLoc(args, api$2.SourceSpan.NON_EXISTENT), entries: OptionalList(args) }) })); } prepare() { let attrs = this.attrs(), modifiers = new ResultArray(this.element.modifiers.map(m => this.modifier(m))).toArray(); return Result.all(attrs, modifiers).mapOk(([result, modifiers]) => { let { attrs: attrs, args: args } = result, elementParams = [...attrs, ...modifiers]; return { args: args, params: new ElementParameters({ loc: maybeLoc(elementParams, api$2.SourceSpan.NON_EXISTENT), body: OptionalList(elementParams) }) }; }); } } class ClassifiedComponent { dynamicFeatures = !0; constructor(tag, element) { this.tag = tag, this.element = element; } arg(attr, { state: state }) { let name = attr.name; return VISIT_EXPRS.visit(convertPathToCallIfKeyword(attr.value), state).mapOk(value => new NamedArgument({ loc: attr.loc, key: name, value: value })); } toStatement(component, { args: args, params: params }) { let { element: element, state: state } = component; return this.blocks(state).mapOk(blocks => new Component({ loc: element.loc, tag: this.tag, params: params, args: args, blocks: blocks })); } blocks(state) { return VISIT_STMTS.NamedBlocks(this.element.blocks, state); } } class ClassifiedSimpleElement { constructor(tag, element, dynamicFeatures) { this.tag = tag, this.element = element, this.dynamicFeatures = dynamicFeatures; } isComponent = !1; arg(attr) { return Err(generateSyntaxError(`${attr.name.chars} is not a valid attribute name. @arguments are only allowed on components, but the tag for this element (\`${this.tag.chars}\`) is a regular, non-component HTML element.`, attr.loc)); } toStatement(classified, { params: params }) { let { state: state, element: element } = classified; return VISIT_STMTS.visitList(this.element.body, state).mapOk(body => new SimpleElement({ loc: element.loc, tag: this.tag, params: params, body: body.toArray(), dynamicFeatures: this.dynamicFeatures })); } } const VISIT_STMTS = new class { visitList(nodes, state) { return new ResultArray(nodes.map(e => VISIT_STMTS.visit(e, state))).toOptionalList().mapOk(list => list.filter(s => null !== s)); } visit(node, state) { switch (node.type) { case "GlimmerComment": return Ok(null); case "AppendContent": return this.AppendContent(node, state); case "HtmlText": return Ok(this.TextNode(node)); case "HtmlComment": return Ok(this.HtmlComment(node)); case "InvokeBlock": return this.InvokeBlock(node, state); case "InvokeComponent": return this.Component(node, state); case "SimpleElement": return this.SimpleElement(node, state); } } InvokeBlock(node, state) { let translated = BLOCK_KEYWORDS.translate(node, state); if (null !== translated) return translated; let head = VISIT_EXPRS.visit(node.callee, state), args = VISIT_EXPRS.Args(node.args, state); return Result.all(head, args).andThen(([head, args]) => this.NamedBlocks(node.blocks, state).mapOk(blocks => new InvokeBlock({ loc: node.loc, head: head, args: args, blocks: blocks }))); } NamedBlocks(blocks, state) { return new ResultArray(blocks.blocks.map(b => this.NamedBlock(b, state))).toArray().mapOk(list => new NamedBlocks({ loc: blocks.loc, blocks: OptionalList(list) })); } NamedBlock(named, state) { return state.visitBlock(named.block).mapOk(body => new NamedBlock({ loc: named.loc, name: named.name, body: body.toArray(), scope: named.block.scope })); } SimpleElement(element, state) { return new ClassifiedElement(element, new ClassifiedSimpleElement(element.tag, element, function ({ attrs: attrs, modifiers: modifiers }) { // ElementModifier needs the special ComponentOperations return modifiers.length > 0 || !!attrs.filter(attr => "SplatAttr" === attr.type)[0]; // Splattributes need the special ComponentOperations to merge into }(element)), state).toStatement(); } Component(component, state) { return VISIT_EXPRS.visit(component.callee, state).andThen(callee => new ClassifiedElement(component, new ClassifiedComponent(callee, component), state).toStatement()); } AppendContent(append, state) { let translated = APPEND_KEYWORDS.translate(append, state); return null !== translated ? translated : VISIT_EXPRS.visit(append.value, state).mapOk(value => append.trusting ? new AppendTrustedHTML({ loc: append.loc, html: value }) : new AppendTextNode({ loc: append.loc, text: value })); } TextNode(text) { return new AppendTextNode({ loc: text.loc, text: new api$1.LiteralExpression({ loc: text.loc, value: text.chars }) }); } HtmlComment(comment) { return new AppendComment({ loc: comment.loc, value: comment.text }); } }(); /** * This is the mutable state for this compiler pass. */ class NormalizationState { _currentScope; _cursorCount = 0; constructor(block, isStrict) { this.isStrict = isStrict, this._currentScope = block; } generateUniqueCursor() { return `%cursor:${this._cursorCount++}%`; } get scope() { return this._currentScope; } visitBlock(block) { let oldBlock = this._currentScope; this._currentScope = block.scope; try { return VISIT_STMTS.visitList(block.body, this); } finally { this._currentScope = oldBlock; } } } var ResolutionType = function (ResolutionType) { return ResolutionType.Value = "value", ResolutionType.Component = "component", ResolutionType.Helper = "helper", ResolutionType.Modifier = "modifier", ResolutionType.ComponentOrHelper = "component or helper", ResolutionType; }(ResolutionType || {}); class StrictModeValidationPass { // This is done at the end of all the keyword normalizations // At this point any free variables that isn't a valid keyword // in its context should be considered a syntax error. We // probably had various opportunities to do this inline in the // earlier passes, but this aims to produce a better syntax // error as we don't always have the right loc-context to do // so in the other spots. static validate(template) { return new this(template).validate(); } constructor(template) { this.template = template; } validate() { return this.Statements(this.template.body).mapOk(() => this.template); } Statements(statements) { let result = Ok(null); for (let statement of statements) result = result.andThen(() => this.Statement(statement)); return result; } NamedBlocks({ blocks: blocks }) { let result = Ok(null); for (let block of blocks.toArray()) result = result.andThen(() => this.NamedBlock(block)); return result; } NamedBlock(block) { return this.Statements(block.body); } Statement(statement) { switch (statement.type) { case "InElement": return this.InElement(statement); case "Debugger": case "AppendComment": return Ok(null); case "Yield": return this.Yield(statement); case "AppendTrustedHTML": return this.AppendTrustedHTML(statement); case "AppendTextNode": return this.AppendTextNode(statement); case "Component": return this.Component(statement); case "SimpleElement": return this.SimpleElement(statement); case "InvokeBlock": return this.InvokeBlock(statement); case "If": return this.If(statement); case "Each": return this.Each(statement); case "Let": return this.Let(statement); case "WithDynamicVars": return this.WithDynamicVars(statement); case "InvokeComponent": return this.InvokeComponent(statement); } } Expressions(expressions) { let result = Ok(null); for (let expression of expressions) result = result.andThen(() => this.Expression(expression)); return result; } Expression(expression, span = expression, resolution) { switch (expression.type) { case "Literal": case "Keyword": case "Missing": case "This": case "Arg": case "Local": case "HasBlock": case "HasBlockParams": case "GetDynamicVar": return Ok(null); case "PathExpression": return this.Expression(expression.head, span, resolution); case "Free": return this.errorFor(expression.name, span, resolution); case "InterpolateExpression": return this.InterpolateExpression(expression, span, resolution); case "CallExpression": return this.CallExpression(expression, span, resolution ?? ResolutionType.Helper); case "Not": return this.Expression(expression.value, span, resolution); case "IfInline": return this.IfInline(expression); case "Curry": return this.Curry(expression); case "Log": return this.Log(expression); } } Args(args) { return this.Positional(args.positional).andThen(() => this.NamedArguments(args.named)); } Positional(positional, span) { let result = Ok(null), expressions = positional.list.toArray(); // For cases like {{yield foo}}, when there is only a single argument, it // makes for a slightly better error to report that entire span. However, // when there are more than one, we need to be specific. return result = 1 === expressions.length ? this.Expression(expressions[0], span) : this.Expressions(expressions), result; } NamedArguments({ entries: entries }) { let result = Ok(null); for (let arg of entries.toArray()) result = result.andThen(() => this.NamedArgument(arg)); return result; } NamedArgument(arg) { return "CallExpression" === arg.value.type ? this.Expression(ar