mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
54ed62d240
Add the runtime primitives `ɵɵboundaryCreate` and `ɵɵboundaryUpdate` to the core instructions, which handle synchronous view destruction and provide the `ON_ERROR` interceptor hooks. Also include the initial compiler AST representations for the new syntax including the Lexer tokenization and HTML Parser integration. This lays the foundational structure for `@boundary` prior to code generation. Co-authored-by: Matthieu Riegler <kyro38@gmail.com> PR Close #70463
745 lines
23 KiB
TypeScript
745 lines
23 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {AST, ASTWithSource, EmptyExpr, RecursiveAstVisitor} from '../expression_parser/ast';
|
|
import * as html from '../ml_parser/ast';
|
|
import {ParseError, ParseSourceSpan} from '../parse_util';
|
|
import {BindingParser} from '../template_parser/binding_parser';
|
|
|
|
import * as t from './r3_ast';
|
|
import {IDENTIFIER_PATTERN, LET_PATTERN, parseLetParameters} from './util';
|
|
|
|
/** Pattern for the expression in a for loop block. */
|
|
const FOR_LOOP_EXPRESSION_PATTERN = /^\s*([0-9A-Za-z_$]*)\s+of\s+([\S\s]*)/;
|
|
|
|
/** Pattern for the tracking expression in a for loop block. */
|
|
const FOR_LOOP_TRACK_PATTERN = /^track\s+([\S\s]*)/;
|
|
|
|
/** Pattern for the `as` expression in a conditional block. */
|
|
const CONDITIONAL_ALIAS_PATTERN = /^(as\s+)(.*)/;
|
|
|
|
/** Names of variables that are allowed to be used in the `let` expression of a `for` loop. */
|
|
const ALLOWED_FOR_LOOP_LET_VARIABLES = new Set([
|
|
'$index',
|
|
'$first',
|
|
'$last',
|
|
'$even',
|
|
'$odd',
|
|
'$count',
|
|
]);
|
|
|
|
/**
|
|
* Predicate function that determines if a block with
|
|
* a specific name cam be connected to a `for` block.
|
|
*/
|
|
export function isConnectedForLoopBlock(name: string): boolean {
|
|
return name === 'empty';
|
|
}
|
|
|
|
/**
|
|
* Predicate function that determines if a block with
|
|
* a specific name cam be connected to an `if` block.
|
|
*/
|
|
export function isConnectedIfLoopBlock(name: string): boolean {
|
|
return name === 'else' || name === 'else if';
|
|
}
|
|
|
|
/** Creates an `if` loop block from an HTML AST node. */
|
|
export function createIfBlock(
|
|
ast: html.Block,
|
|
connectedBlocks: html.Block[],
|
|
visitor: html.Visitor,
|
|
bindingParser: BindingParser,
|
|
): {node: t.IfBlock | null; errors: ParseError[]} {
|
|
const errors: ParseError[] = validateIfConnectedBlocks(connectedBlocks);
|
|
const branches: t.IfBlockBranch[] = [];
|
|
const mainBlockParams = parseConditionalBlockParameters(ast, errors, bindingParser);
|
|
|
|
if (mainBlockParams !== null) {
|
|
branches.push(
|
|
new t.IfBlockBranch(
|
|
mainBlockParams.expression,
|
|
html.visitAll(visitor, ast.children, ast.children),
|
|
mainBlockParams.expressionAlias,
|
|
ast.sourceSpan,
|
|
ast.startSourceSpan,
|
|
ast.endSourceSpan,
|
|
ast.nameSpan,
|
|
ast.i18n,
|
|
),
|
|
);
|
|
}
|
|
|
|
for (const block of connectedBlocks) {
|
|
if (block.name === 'else if') {
|
|
const params = parseConditionalBlockParameters(block, errors, bindingParser);
|
|
|
|
if (params !== null) {
|
|
const children = html.visitAll(visitor, block.children, block.children);
|
|
branches.push(
|
|
new t.IfBlockBranch(
|
|
params.expression,
|
|
children,
|
|
params.expressionAlias,
|
|
block.sourceSpan,
|
|
block.startSourceSpan,
|
|
block.endSourceSpan,
|
|
block.nameSpan,
|
|
block.i18n,
|
|
),
|
|
);
|
|
}
|
|
} else if (block.name === 'else') {
|
|
const children = html.visitAll(visitor, block.children, block.children);
|
|
branches.push(
|
|
new t.IfBlockBranch(
|
|
null,
|
|
children,
|
|
null,
|
|
block.sourceSpan,
|
|
block.startSourceSpan,
|
|
block.endSourceSpan,
|
|
block.nameSpan,
|
|
block.i18n,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The outer IfBlock should have a span that encapsulates all branches.
|
|
const ifBlockStartSourceSpan =
|
|
branches.length > 0 ? branches[0].startSourceSpan : ast.startSourceSpan;
|
|
const ifBlockEndSourceSpan =
|
|
branches.length > 0 ? branches[branches.length - 1].endSourceSpan : ast.endSourceSpan;
|
|
|
|
let wholeSourceSpan = ast.sourceSpan;
|
|
const lastBranch = branches[branches.length - 1];
|
|
if (lastBranch !== undefined) {
|
|
wholeSourceSpan = new ParseSourceSpan(ifBlockStartSourceSpan.start, lastBranch.sourceSpan.end);
|
|
}
|
|
|
|
return {
|
|
node: new t.IfBlock(
|
|
branches,
|
|
wholeSourceSpan,
|
|
ast.startSourceSpan,
|
|
ifBlockEndSourceSpan,
|
|
ast.nameSpan,
|
|
),
|
|
errors,
|
|
};
|
|
}
|
|
|
|
/** Creates a `for` loop block from an HTML AST node. */
|
|
export function createForLoop(
|
|
ast: html.Block,
|
|
connectedBlocks: html.Block[],
|
|
visitor: html.Visitor,
|
|
bindingParser: BindingParser,
|
|
): {node: t.ForLoopBlock | null; errors: ParseError[]} {
|
|
const errors: ParseError[] = [];
|
|
const params = parseForLoopParameters(ast, errors, bindingParser);
|
|
let node: t.ForLoopBlock | null = null;
|
|
let empty: t.ForLoopBlockEmpty | null = null;
|
|
|
|
for (const block of connectedBlocks) {
|
|
if (block.name === 'empty') {
|
|
if (empty !== null) {
|
|
errors.push(new ParseError(block.sourceSpan, '@for loop can only have one @empty block'));
|
|
} else if (block.parameters.length > 0) {
|
|
errors.push(new ParseError(block.sourceSpan, '@empty block cannot have parameters'));
|
|
} else {
|
|
empty = new t.ForLoopBlockEmpty(
|
|
html.visitAll(visitor, block.children, block.children),
|
|
block.sourceSpan,
|
|
block.startSourceSpan,
|
|
block.endSourceSpan,
|
|
block.nameSpan,
|
|
block.i18n,
|
|
);
|
|
}
|
|
} else {
|
|
errors.push(new ParseError(block.sourceSpan, `Unrecognized @for loop block "${block.name}"`));
|
|
}
|
|
}
|
|
|
|
if (params !== null) {
|
|
// The `for` block has a main span that includes the `empty` branch. For only the span of the
|
|
// main `for` body, use `mainSourceSpan`.
|
|
const endSpan = empty?.endSourceSpan ?? ast.endSourceSpan;
|
|
const sourceSpan = new ParseSourceSpan(
|
|
ast.sourceSpan.start,
|
|
endSpan?.end ?? ast.sourceSpan.end,
|
|
);
|
|
let trackExpression: ASTWithSource | null;
|
|
let trackKeywordSpan: ParseSourceSpan | null;
|
|
|
|
if (params.trackBy === null) {
|
|
trackExpression = trackKeywordSpan = null;
|
|
errors.push(new ParseError(ast.startSourceSpan, '@for loop must have a "track" expression'));
|
|
} else {
|
|
trackExpression = params.trackBy.expression;
|
|
trackKeywordSpan = params.trackBy.keywordSpan;
|
|
validateTrackByExpression(params.trackBy.expression, params.trackBy.keywordSpan, errors);
|
|
}
|
|
|
|
node = new t.ForLoopBlock(
|
|
params.itemName,
|
|
params.expression,
|
|
trackExpression,
|
|
trackKeywordSpan,
|
|
params.context,
|
|
html.visitAll(visitor, ast.children, ast.children),
|
|
empty,
|
|
sourceSpan,
|
|
ast.sourceSpan,
|
|
ast.startSourceSpan,
|
|
endSpan,
|
|
ast.nameSpan,
|
|
ast.i18n,
|
|
);
|
|
}
|
|
|
|
return {node, errors};
|
|
}
|
|
|
|
/** Creates a switch block from an HTML AST node. */
|
|
export function createSwitchBlock(
|
|
ast: html.Block,
|
|
visitor: html.Visitor,
|
|
bindingParser: BindingParser,
|
|
): {node: t.SwitchBlock | null; errors: ParseError[]} {
|
|
const errors = validateSwitchBlock(ast);
|
|
const primaryExpression =
|
|
ast.parameters.length > 0
|
|
? parseBlockParameterToBinding(ast.parameters[0], bindingParser)
|
|
: bindingParser.parseBinding('', false, ast.sourceSpan, 0);
|
|
const groups: t.SwitchBlockCaseGroup[] = [];
|
|
const unknownBlocks: t.UnknownBlock[] = [];
|
|
let collectedCases: t.SwitchBlockCase[] = [];
|
|
let firstCaseStart: ParseSourceSpan | null = null;
|
|
let exhaustiveCheck: t.SwitchExhaustiveCheck | null = null;
|
|
|
|
// Here we assume that all the blocks are valid given that we validated them above.
|
|
for (const node of ast.children) {
|
|
if (!(node instanceof html.Block)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
(node.name !== 'case' || node.parameters.length === 0) &&
|
|
node.name !== 'default' &&
|
|
node.name !== 'default never'
|
|
) {
|
|
unknownBlocks.push(new t.UnknownBlock(node.name, node.sourceSpan, node.nameSpan));
|
|
continue;
|
|
}
|
|
|
|
if (exhaustiveCheck !== null) {
|
|
errors.push(
|
|
new ParseError(
|
|
node.sourceSpan,
|
|
'@default block with "never" parameter must be the last case in a switch',
|
|
),
|
|
);
|
|
}
|
|
|
|
const isCase = node.name === 'case';
|
|
let expression: AST | null = null;
|
|
|
|
if (isCase) {
|
|
expression = parseBlockParameterToBinding(node.parameters[0], bindingParser);
|
|
} else if (node.name === 'default never') {
|
|
if (node.parameters.length > 0) {
|
|
expression = parseBlockParameterToBinding(node.parameters[0], bindingParser);
|
|
}
|
|
|
|
if (
|
|
node.children.length > 0 ||
|
|
(node.endSourceSpan !== null &&
|
|
node.endSourceSpan.start.offset !== node.endSourceSpan.end.offset)
|
|
) {
|
|
errors.push(
|
|
new ParseError(
|
|
node.sourceSpan,
|
|
'@default block with "never" parameter cannot have a body',
|
|
),
|
|
);
|
|
}
|
|
|
|
if (collectedCases.length > 0) {
|
|
errors.push(
|
|
new ParseError(
|
|
node.sourceSpan,
|
|
'A @case block with no body cannot be followed by a @default block with "never" parameter',
|
|
),
|
|
);
|
|
}
|
|
|
|
exhaustiveCheck = new t.SwitchExhaustiveCheck(
|
|
expression,
|
|
node.sourceSpan,
|
|
node.startSourceSpan,
|
|
node.endSourceSpan,
|
|
node.nameSpan,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const switchCase = new t.SwitchBlockCase(
|
|
expression,
|
|
node.sourceSpan,
|
|
node.startSourceSpan,
|
|
node.endSourceSpan,
|
|
node.nameSpan,
|
|
);
|
|
collectedCases.push(switchCase);
|
|
|
|
// We need to take into account that some cases might have an empty body. ({})
|
|
const caseWithoutBody =
|
|
node.children.length === 0 &&
|
|
node.endSourceSpan !== null &&
|
|
node.endSourceSpan.start.offset === node.endSourceSpan.end.offset;
|
|
|
|
if (caseWithoutBody) {
|
|
if (firstCaseStart === null) {
|
|
firstCaseStart = node.sourceSpan;
|
|
}
|
|
// we'll collect the cases until we find a body.
|
|
continue;
|
|
}
|
|
|
|
let sourceSpan = node.sourceSpan;
|
|
let startSourceSpan = node.startSourceSpan;
|
|
if (firstCaseStart !== null) {
|
|
// We need to create a new sourceSpan that represents all the cases that fallthrough to a single block body
|
|
sourceSpan = new ParseSourceSpan(firstCaseStart.start, node.sourceSpan.end);
|
|
startSourceSpan = new ParseSourceSpan(firstCaseStart.start, node.startSourceSpan.end);
|
|
firstCaseStart = null;
|
|
}
|
|
|
|
const group = new t.SwitchBlockCaseGroup(
|
|
collectedCases,
|
|
html.visitAll(visitor, node.children, node.children),
|
|
sourceSpan,
|
|
startSourceSpan,
|
|
node.endSourceSpan,
|
|
node.nameSpan,
|
|
node.i18n,
|
|
);
|
|
groups.push(group);
|
|
collectedCases = [];
|
|
}
|
|
|
|
const node = new t.SwitchBlock(
|
|
primaryExpression,
|
|
groups,
|
|
unknownBlocks,
|
|
exhaustiveCheck,
|
|
ast.sourceSpan,
|
|
ast.startSourceSpan,
|
|
ast.endSourceSpan,
|
|
ast.nameSpan,
|
|
);
|
|
|
|
return {node, errors};
|
|
}
|
|
|
|
/** Parses the parameters of a `for` loop block. */
|
|
function parseForLoopParameters(
|
|
block: html.Block,
|
|
errors: ParseError[],
|
|
bindingParser: BindingParser,
|
|
) {
|
|
if (block.parameters.length === 0) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@for loop does not have an expression'));
|
|
return null;
|
|
}
|
|
|
|
const [expressionParam, ...secondaryParams] = block.parameters;
|
|
const match = stripOptionalParentheses(expressionParam, errors)?.match(
|
|
FOR_LOOP_EXPRESSION_PATTERN,
|
|
);
|
|
|
|
if (!match || match[2].trim().length === 0) {
|
|
errors.push(
|
|
new ParseError(
|
|
expressionParam.sourceSpan,
|
|
'Cannot parse expression. @for loop expression must match the pattern "<identifier> of <expression>"',
|
|
),
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const [, itemName, rawExpression] = match;
|
|
if (ALLOWED_FOR_LOOP_LET_VARIABLES.has(itemName)) {
|
|
errors.push(
|
|
new ParseError(
|
|
expressionParam.sourceSpan,
|
|
`@for loop item name cannot be one of ${Array.from(ALLOWED_FOR_LOOP_LET_VARIABLES).join(
|
|
', ',
|
|
)}.`,
|
|
),
|
|
);
|
|
}
|
|
|
|
// `expressionParam.expression` contains the variable declaration and the expression of the
|
|
// for...of statement, i.e. 'user of users' The variable of a ForOfStatement is _only_ the "const
|
|
// user" part and does not include "of x".
|
|
const variableName = expressionParam.expression.split(' ')[0];
|
|
const variableSpan = new ParseSourceSpan(
|
|
expressionParam.sourceSpan.start,
|
|
expressionParam.sourceSpan.start.moveBy(variableName.length),
|
|
);
|
|
const result = {
|
|
itemName: new t.Variable(itemName, '$implicit', variableSpan, variableSpan),
|
|
trackBy: null as {expression: ASTWithSource; keywordSpan: ParseSourceSpan} | null,
|
|
expression: parseBlockParameterToBinding(expressionParam, bindingParser, rawExpression),
|
|
context: Array.from(ALLOWED_FOR_LOOP_LET_VARIABLES, (variableName) => {
|
|
// Give ambiently-available context variables empty spans at the end of
|
|
// the start of the `for` block, since they are not explicitly defined.
|
|
const emptySpanAfterForBlockStart = new ParseSourceSpan(
|
|
block.startSourceSpan.end,
|
|
block.startSourceSpan.end,
|
|
);
|
|
return new t.Variable(
|
|
variableName,
|
|
variableName,
|
|
emptySpanAfterForBlockStart,
|
|
emptySpanAfterForBlockStart,
|
|
);
|
|
}),
|
|
};
|
|
|
|
for (const param of secondaryParams) {
|
|
const letMatch = param.expression.match(LET_PATTERN);
|
|
|
|
if (letMatch !== null) {
|
|
const variablesSpan = new ParseSourceSpan(
|
|
param.sourceSpan.start.moveBy(letMatch[0].length - letMatch[1].length),
|
|
param.sourceSpan.end,
|
|
);
|
|
parseLetParameters(
|
|
param.sourceSpan,
|
|
letMatch[1],
|
|
variablesSpan,
|
|
result.context,
|
|
errors,
|
|
(name, variableName, sourceSpan) => {
|
|
if (!ALLOWED_FOR_LOOP_LET_VARIABLES.has(variableName)) {
|
|
errors.push(
|
|
new ParseError(
|
|
sourceSpan,
|
|
`Unknown "let" parameter variable "${variableName}". The allowed variables are: ${Array.from(
|
|
ALLOWED_FOR_LOOP_LET_VARIABLES,
|
|
).join(', ')}`,
|
|
),
|
|
);
|
|
} else if (name === itemName) {
|
|
errors.push(
|
|
new ParseError(
|
|
sourceSpan,
|
|
`Invalid @for loop "let" parameter. Variable cannot be called "${itemName}"`,
|
|
),
|
|
);
|
|
} else if (result.context.some((v) => v.name === name)) {
|
|
errors.push(
|
|
new ParseError(sourceSpan, `Duplicate "let" parameter variable "${variableName}"`),
|
|
);
|
|
}
|
|
},
|
|
'@for loop',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const trackMatch = param.expression.match(FOR_LOOP_TRACK_PATTERN);
|
|
|
|
if (trackMatch !== null) {
|
|
if (result.trackBy !== null) {
|
|
errors.push(
|
|
new ParseError(param.sourceSpan, '@for loop can only have one "track" expression'),
|
|
);
|
|
} else {
|
|
const expression = parseBlockParameterToBinding(param, bindingParser, trackMatch[1]);
|
|
if (expression.ast instanceof EmptyExpr) {
|
|
errors.push(
|
|
new ParseError(block.startSourceSpan, '@for loop must have a "track" expression'),
|
|
);
|
|
}
|
|
const keywordSpan = new ParseSourceSpan(
|
|
param.sourceSpan.start,
|
|
param.sourceSpan.start.moveBy('track'.length),
|
|
);
|
|
result.trackBy = {expression, keywordSpan};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
errors.push(
|
|
new ParseError(param.sourceSpan, `Unrecognized @for loop parameter "${param.expression}"`),
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function validateTrackByExpression(
|
|
expression: ASTWithSource<AST>,
|
|
parseSourceSpan: ParseSourceSpan,
|
|
errors: ParseError[],
|
|
): void {
|
|
const visitor = new PipeVisitor();
|
|
expression.ast.visit(visitor);
|
|
if (visitor.hasPipe) {
|
|
errors.push(new ParseError(parseSourceSpan, 'Cannot use pipes in track expressions'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks that the shape of the blocks connected to an
|
|
* `@if` block is correct. Returns an array of errors.
|
|
*/
|
|
function validateIfConnectedBlocks(connectedBlocks: html.Block[]): ParseError[] {
|
|
const errors: ParseError[] = [];
|
|
let hasElse = false;
|
|
|
|
for (let i = 0; i < connectedBlocks.length; i++) {
|
|
const block = connectedBlocks[i];
|
|
|
|
if (block.name === 'else') {
|
|
if (hasElse) {
|
|
errors.push(
|
|
new ParseError(block.startSourceSpan, 'Conditional can only have one @else block'),
|
|
);
|
|
} else if (connectedBlocks.length > 1 && i < connectedBlocks.length - 1) {
|
|
errors.push(
|
|
new ParseError(block.startSourceSpan, '@else block must be last inside the conditional'),
|
|
);
|
|
} else if (block.parameters.length > 0) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@else block cannot have parameters'));
|
|
}
|
|
hasElse = true;
|
|
} else if (block.name !== 'else if') {
|
|
errors.push(
|
|
new ParseError(block.startSourceSpan, `Unrecognized conditional block @${block.name}`),
|
|
);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/** Checks that the shape of a `switch` block is valid. Returns an array of errors. */
|
|
function validateSwitchBlock(ast: html.Block): ParseError[] {
|
|
const errors: ParseError[] = [];
|
|
let hasDefault = false;
|
|
|
|
if (ast.parameters.length !== 1) {
|
|
errors.push(
|
|
new ParseError(ast.startSourceSpan, '@switch block must have exactly one parameter'),
|
|
);
|
|
return errors;
|
|
}
|
|
|
|
for (const node of ast.children) {
|
|
// Skip over comments and empty text nodes inside the switch block.
|
|
// Empty text nodes can be used for formatting while comments don't affect the runtime.
|
|
if (
|
|
node instanceof html.Comment ||
|
|
(node instanceof html.Text && node.value.trim().length === 0)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
!(node instanceof html.Block) ||
|
|
(node.name !== 'case' && node.name !== 'default' && node.name !== 'default never')
|
|
) {
|
|
errors.push(
|
|
new ParseError(node.sourceSpan, '@switch block can only contain @case and @default blocks'),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (node.name === 'default never') {
|
|
if (hasDefault) {
|
|
errors.push(
|
|
new ParseError(node.startSourceSpan, '@switch block can only have one @default block'),
|
|
);
|
|
}
|
|
hasDefault = true;
|
|
} else if (node.name === 'default') {
|
|
if (hasDefault) {
|
|
errors.push(
|
|
new ParseError(node.startSourceSpan, '@switch block can only have one @default block'),
|
|
);
|
|
} else if (node.parameters.length > 0) {
|
|
errors.push(new ParseError(node.startSourceSpan, '@default block cannot have parameters'));
|
|
}
|
|
hasDefault = true;
|
|
} else if (node.name === 'case' && node.parameters.length !== 1) {
|
|
errors.push(
|
|
new ParseError(node.startSourceSpan, '@case block must have exactly one parameter'),
|
|
);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Parses a block parameter into a binding AST.
|
|
* @param ast Block parameter that should be parsed.
|
|
* @param bindingParser Parser that the expression should be parsed with.
|
|
* @param part Specific part of the expression that should be parsed.
|
|
*/
|
|
function parseBlockParameterToBinding(
|
|
ast: html.BlockParameter,
|
|
bindingParser: BindingParser,
|
|
part?: string,
|
|
): ASTWithSource {
|
|
let start: number;
|
|
let end: number;
|
|
|
|
if (typeof part === 'string') {
|
|
// Note: `lastIndexOf` here should be enough to know the start index of the expression,
|
|
// because we know that it'll be at the end of the param. Ideally we could use the `d`
|
|
// flag when matching via regex and get the index from `match.indices`, but it's unclear
|
|
// if we can use it yet since it's a relatively new feature. See:
|
|
// https://github.com/tc39/proposal-regexp-match-indices
|
|
start = Math.max(0, ast.expression.lastIndexOf(part));
|
|
end = start + part.length;
|
|
} else {
|
|
start = 0;
|
|
end = ast.expression.length;
|
|
}
|
|
|
|
return bindingParser.parseBinding(
|
|
ast.expression.slice(start, end),
|
|
false,
|
|
ast.sourceSpan,
|
|
ast.sourceSpan.start.offset + start,
|
|
);
|
|
}
|
|
|
|
/** Parses the parameter of a conditional block (`if` or `else if`). */
|
|
function parseConditionalBlockParameters(
|
|
block: html.Block,
|
|
errors: ParseError[],
|
|
bindingParser: BindingParser,
|
|
) {
|
|
if (block.parameters.length === 0) {
|
|
errors.push(
|
|
new ParseError(block.startSourceSpan, 'Conditional block does not have an expression'),
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const expression = parseBlockParameterToBinding(block.parameters[0], bindingParser);
|
|
let expressionAlias: t.Variable | null = null;
|
|
|
|
// Start from 1 since we processed the first parameter already.
|
|
for (let i = 1; i < block.parameters.length; i++) {
|
|
const param = block.parameters[i];
|
|
const aliasMatch = param.expression.match(CONDITIONAL_ALIAS_PATTERN);
|
|
|
|
// For now conditionals can only have an `as` parameter.
|
|
// We may want to rework this later if we add more.
|
|
if (aliasMatch === null) {
|
|
errors.push(
|
|
new ParseError(
|
|
param.sourceSpan,
|
|
`Unrecognized conditional parameter "${param.expression}"`,
|
|
),
|
|
);
|
|
} else if (block.name !== 'if' && block.name !== 'else if') {
|
|
errors.push(
|
|
new ParseError(
|
|
param.sourceSpan,
|
|
'"as" expression is only allowed on `@if` and `@else if` blocks',
|
|
),
|
|
);
|
|
} else if (expressionAlias !== null) {
|
|
errors.push(
|
|
new ParseError(param.sourceSpan, 'Conditional can only have one "as" expression'),
|
|
);
|
|
} else {
|
|
const name = aliasMatch[2].trim();
|
|
|
|
if (IDENTIFIER_PATTERN.test(name)) {
|
|
const variableStart = param.sourceSpan.start.moveBy(aliasMatch[1].length);
|
|
const variableSpan = new ParseSourceSpan(variableStart, variableStart.moveBy(name.length));
|
|
expressionAlias = new t.Variable(name, name, variableSpan, variableSpan);
|
|
} else {
|
|
errors.push(
|
|
new ParseError(param.sourceSpan, '"as" expression must be a valid JavaScript identifier'),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {expression, expressionAlias};
|
|
}
|
|
|
|
/** Strips optional parentheses around from a control from expression parameter. */
|
|
function stripOptionalParentheses(param: html.BlockParameter, errors: ParseError[]): string | null {
|
|
const expression = param.expression;
|
|
const spaceRegex = /^\s$/;
|
|
let openParens = 0;
|
|
let start = 0;
|
|
let end = expression.length - 1;
|
|
|
|
for (let i = 0; i < expression.length; i++) {
|
|
const char = expression[i];
|
|
|
|
if (char === '(') {
|
|
start = i + 1;
|
|
openParens++;
|
|
} else if (spaceRegex.test(char)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (openParens === 0) {
|
|
return expression;
|
|
}
|
|
|
|
for (let i = expression.length - 1; i > -1; i--) {
|
|
const char = expression[i];
|
|
|
|
if (char === ')') {
|
|
end = i;
|
|
openParens--;
|
|
if (openParens === 0) {
|
|
break;
|
|
}
|
|
} else if (spaceRegex.test(char)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (openParens !== 0) {
|
|
errors.push(new ParseError(param.sourceSpan, 'Unclosed parentheses in expression'));
|
|
return null;
|
|
}
|
|
|
|
return expression.slice(start, end);
|
|
}
|
|
|
|
class PipeVisitor extends RecursiveAstVisitor {
|
|
hasPipe = false;
|
|
override visitPipe(): any {
|
|
this.hasPipe = true;
|
|
}
|
|
}
|