@lwc/template-compiler 8.21.6 → 8.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -8511,7 +8511,7 @@ new Set([
8511
8511
  TAG_NAMES.WBR,
8512
8512
  ]);
8513
8513
 
8514
- function parseFragment$1(fragmentContext, html, options) {
8514
+ function parseFragment(fragmentContext, html, options) {
8515
8515
  if (typeof fragmentContext === 'string') {
8516
8516
  options = html;
8517
8517
  html = fragmentContext;
@@ -8542,7 +8542,7 @@ function isTemplateNode(node) {
8542
8542
  const isElementNode = defaultTreeAdapter.isElementNode;
8543
8543
  const isCommentNode = defaultTreeAdapter.isCommentNode;
8544
8544
  defaultTreeAdapter.isDocumentTypeNode;
8545
- const isTextNode$1 = defaultTreeAdapter.isTextNode;
8545
+ const isTextNode = defaultTreeAdapter.isTextNode;
8546
8546
 
8547
8547
  defaultTreeAdapter.appendChild;
8548
8548
 
@@ -9652,10 +9652,6 @@ class ParserCtx {
9652
9652
  this.config = config;
9653
9653
  this.renderMode = exports.LWCDirectiveRenderMode.shadow;
9654
9654
  this.preserveComments = config.preserveHtmlComments;
9655
- // TODO [#3370]: remove experimental template expression flag
9656
- if (config.experimentalComplexExpressions) {
9657
- this.preparsedJsExpressions = new Map();
9658
- }
9659
9655
  this.ecmaVersion = config.experimentalComplexExpressions
9660
9656
  ? TMPL_EXPR_ECMASCRIPT_EDITION
9661
9657
  : 2020;
@@ -9995,6 +9991,200 @@ const errorCodesToWarnOnInOlderAPIVersions = new Set([
9995
9991
  'eof-in-element-that-can-contain-only-text',
9996
9992
  ]);
9997
9993
 
9994
+ /*
9995
+ * Copyright (c) 2018, salesforce.com, inc.
9996
+ * All rights reserved.
9997
+ * SPDX-License-Identifier: MIT
9998
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
9999
+ */
10000
+ function getLwcErrorFromParse5Error(ctx, code) {
10001
+ /* istanbul ignore else */
10002
+ if (errorCodesToErrorOn.has(code)) {
10003
+ return errors.ParserDiagnostics.INVALID_HTML_SYNTAX;
10004
+ }
10005
+ else if (errorCodesToWarnOnInOlderAPIVersions.has(code)) {
10006
+ // In newer API versions, all parse 5 errors are errors, not warnings
10007
+ if (shared.isAPIFeatureEnabled(1 /* APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS */, ctx.apiVersion)) {
10008
+ return errors.ParserDiagnostics.INVALID_HTML_SYNTAX;
10009
+ }
10010
+ else {
10011
+ return errors.ParserDiagnostics.INVALID_HTML_SYNTAX_WARNING;
10012
+ }
10013
+ }
10014
+ else {
10015
+ // It should be impossible to reach here; we have a test in parser.spec.ts to ensure
10016
+ // all error codes are accounted for. But just to be safe, make it a warning.
10017
+ // TODO [#2650]: better system for handling unexpected parse5 errors
10018
+ // eslint-disable-next-line no-console
10019
+ console.warn('Found a Parse5 error that we do not know how to handle:', code);
10020
+ return errors.ParserDiagnostics.INVALID_HTML_SYNTAX_WARNING;
10021
+ }
10022
+ }
10023
+ function parseHTML(ctx, source) {
10024
+ const onParseError = (err) => {
10025
+ const { code, ...location } = err;
10026
+ const lwcError = getLwcErrorFromParse5Error(ctx, code);
10027
+ ctx.warnAtLocation(lwcError, sourceLocation(location), [code]);
10028
+ };
10029
+ return parseFragment(source, {
10030
+ sourceCodeLocationInfo: true,
10031
+ onParseError,
10032
+ });
10033
+ }
10034
+ // https://github.com/babel/babel/blob/d33d02359474296402b1577ef53f20d94e9085c4/packages/babel-types/src/react.js#L9-L55
10035
+ function cleanTextNode(value) {
10036
+ const lines = value.split(/\r\n|\n|\r/);
10037
+ let lastNonEmptyLine = 0;
10038
+ for (let i = 0; i < lines.length; i++) {
10039
+ if (lines[i].match(/[^ \t]/)) {
10040
+ lastNonEmptyLine = i;
10041
+ }
10042
+ }
10043
+ let str = '';
10044
+ for (let i = 0; i < lines.length; i++) {
10045
+ const line = lines[i];
10046
+ const isFirstLine = i === 0;
10047
+ const isLastLine = i === lines.length - 1;
10048
+ const isLastNonEmptyLine = i === lastNonEmptyLine;
10049
+ let trimmedLine = line.replace(/\t/g, ' ');
10050
+ if (!isFirstLine) {
10051
+ trimmedLine = trimmedLine.replace(/^[ ]+/, '');
10052
+ }
10053
+ if (!isLastLine) {
10054
+ trimmedLine = trimmedLine.replace(/[ ]+$/, '');
10055
+ }
10056
+ if (trimmedLine) {
10057
+ if (!isLastNonEmptyLine) {
10058
+ trimmedLine += ' ';
10059
+ }
10060
+ str += trimmedLine;
10061
+ }
10062
+ }
10063
+ return str;
10064
+ }
10065
+ function decodeTextContent(source) {
10066
+ return he__namespace.decode(source);
10067
+ }
10068
+
10069
+ /*
10070
+ * Copyright (c) 2018, salesforce.com, inc.
10071
+ * All rights reserved.
10072
+ * SPDX-License-Identifier: MIT
10073
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10074
+ */
10075
+ // https://262.ecma-international.org/12.0/#sec-keywords-and-reserved-words
10076
+ // prettier-ignore
10077
+ const REVERSED_KEYWORDS = new Set([
10078
+ // Reserved keywords
10079
+ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete',
10080
+ 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
10081
+ 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
10082
+ 'typeof', 'var', 'void', 'while', 'with', 'yield',
10083
+ // Strict mode only reserved keywords
10084
+ 'let', 'static', 'implements', 'interface', 'package', 'private', 'protected', 'public'
10085
+ ]);
10086
+ function isReservedES6Keyword(str) {
10087
+ return REVERSED_KEYWORDS.has(str);
10088
+ }
10089
+
10090
+ /*
10091
+ * Copyright (c) 2018, salesforce.com, inc.
10092
+ * All rights reserved.
10093
+ * SPDX-License-Identifier: MIT
10094
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10095
+ */
10096
+ const EXPRESSION_SYMBOL_START = '{';
10097
+ const EXPRESSION_SYMBOL_END = '}';
10098
+ const POTENTIAL_EXPRESSION_RE = /^.?{.+}.*$/;
10099
+ const WHITESPACES_RE = /\s/;
10100
+ function isExpression(source) {
10101
+ // Issue #3418: Legacy behavior, previous regex treated "{}" attribute value as non expression
10102
+ return source[0] === '{' && source.slice(-1) === '}' && source.length > 2;
10103
+ }
10104
+ function isPotentialExpression(source) {
10105
+ return !!source.match(POTENTIAL_EXPRESSION_RE);
10106
+ }
10107
+ function validateExpression(source, node, config) {
10108
+ const isValidNode = isIdentifier(node) || isMemberExpression(node);
10109
+ // INVALID_XYZ_COMPLEX provides additional context to the user if CTE is enabled.
10110
+ // The author may not have delimited the CTE with quotes, resulting in it being parsed
10111
+ // as a legacy expression.
10112
+ errors.invariant(isValidNode, config.experimentalComplexExpressions
10113
+ ? errors.ParserDiagnostics.INVALID_NODE_COMPLEX
10114
+ : errors.ParserDiagnostics.INVALID_NODE, [node.type, source]);
10115
+ if (isMemberExpression(node)) {
10116
+ errors.invariant(config.experimentalComputedMemberExpression || !node.computed, config.experimentalComplexExpressions
10117
+ ? errors.ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_COMPLEX
10118
+ : errors.ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED, [source]);
10119
+ const { object, property } = node;
10120
+ if (!isIdentifier(object)) {
10121
+ validateExpression(source, object, config);
10122
+ }
10123
+ if (!isIdentifier(property)) {
10124
+ validateExpression(source, property, config);
10125
+ }
10126
+ }
10127
+ }
10128
+ function validateSourceIsParsedExpression(source, parsedExpression) {
10129
+ if (parsedExpression.end === source.length - 1) {
10130
+ return;
10131
+ }
10132
+ let unclosedParenthesisCount = 0;
10133
+ for (let i = 0, n = parsedExpression.start; i < n; i++) {
10134
+ if (source[i] === '(') {
10135
+ unclosedParenthesisCount++;
10136
+ }
10137
+ }
10138
+ // source[source.length - 1] === '}', n = source.length - 1 is to avoid processing '}'.
10139
+ for (let i = parsedExpression.end, n = source.length - 1; i < n; i++) {
10140
+ const character = source[i];
10141
+ if (character === ')') {
10142
+ unclosedParenthesisCount--;
10143
+ }
10144
+ else if (character === ';') {
10145
+ // acorn parseExpressionAt will stop at the first ";", it may be that the expression is not
10146
+ // a multiple expression ({foo;}), but this is a case that we explicitly do not want to support.
10147
+ // in such case, let's fail with the same error as if it were a multiple expression.
10148
+ errors.invariant(false, errors.ParserDiagnostics.MULTIPLE_EXPRESSIONS);
10149
+ }
10150
+ else {
10151
+ errors.invariant(WHITESPACES_RE.test(character), errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['Unexpected end of expression']);
10152
+ }
10153
+ }
10154
+ errors.invariant(unclosedParenthesisCount === 0, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, [
10155
+ 'Unexpected end of expression',
10156
+ ]);
10157
+ }
10158
+ function parseExpression(ctx, source, location) {
10159
+ const { ecmaVersion } = ctx;
10160
+ return ctx.withErrorWrapping(() => {
10161
+ const parsed = acorn.parseExpressionAt(source, 1, {
10162
+ ecmaVersion,
10163
+ allowAwaitOutsideFunction: false,
10164
+ onComment: () => errors.invariant(false, errors.ParserDiagnostics.INVALID_EXPR_COMMENTS_DISALLOWED),
10165
+ });
10166
+ validateSourceIsParsedExpression(source, parsed);
10167
+ validateExpression(source, parsed, ctx.config);
10168
+ return { ...parsed, location };
10169
+ }, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, location, (err) => `Invalid expression ${source} - ${err.message}`);
10170
+ }
10171
+ function parseIdentifier(ctx, source, location) {
10172
+ let isValid = true;
10173
+ isValid = acorn.isIdentifierStart(source.charCodeAt(0));
10174
+ for (let i = 1; i < source.length && isValid; i++) {
10175
+ isValid = acorn.isIdentifierChar(source.charCodeAt(i));
10176
+ }
10177
+ if (isValid && !isReservedES6Keyword(source)) {
10178
+ return {
10179
+ ...identifier(source),
10180
+ location,
10181
+ };
10182
+ }
10183
+ else {
10184
+ ctx.throwAtLocation(errors.ParserDiagnostics.INVALID_IDENTIFIER, location, [source]);
10185
+ }
10186
+ }
10187
+
9998
10188
  /**
9999
10189
  * @typedef { import('estree').Node} Node
10000
10190
  * @typedef {{
@@ -10233,6 +10423,13 @@ function walk(ast, { enter, leave }) {
10233
10423
  * SPDX-License-Identifier: MIT
10234
10424
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10235
10425
  */
10426
+ const OPENING_CURLY_LEN = 1;
10427
+ const CLOSING_CURLY_LEN = 1;
10428
+ const CLOSING_CURLY_BRACKET = 0x7d;
10429
+ const TRAILING_SPACES_AND_PARENS = /[\s)]*/;
10430
+ function getTrailingChars(str) {
10431
+ return TRAILING_SPACES_AND_PARENS.exec(str)[0];
10432
+ }
10236
10433
  const ALWAYS_INVALID_TYPES = new Map(Object.entries({
10237
10434
  AwaitExpression: 'await expressions',
10238
10435
  ClassExpression: 'classes',
@@ -10340,157 +10537,6 @@ function validateExpressionAst(rootNode) {
10340
10537
  },
10341
10538
  });
10342
10539
  }
10343
-
10344
- /*
10345
- * Copyright (c) 2018, salesforce.com, inc.
10346
- * All rights reserved.
10347
- * SPDX-License-Identifier: MIT
10348
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10349
- */
10350
- // https://262.ecma-international.org/12.0/#sec-keywords-and-reserved-words
10351
- // prettier-ignore
10352
- const REVERSED_KEYWORDS = new Set([
10353
- // Reserved keywords
10354
- 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete',
10355
- 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
10356
- 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
10357
- 'typeof', 'var', 'void', 'while', 'with', 'yield',
10358
- // Strict mode only reserved keywords
10359
- 'let', 'static', 'implements', 'interface', 'package', 'private', 'protected', 'public'
10360
- ]);
10361
- function isReservedES6Keyword(str) {
10362
- return REVERSED_KEYWORDS.has(str);
10363
- }
10364
-
10365
- /*
10366
- * Copyright (c) 2018, salesforce.com, inc.
10367
- * All rights reserved.
10368
- * SPDX-License-Identifier: MIT
10369
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10370
- */
10371
- const EXPRESSION_SYMBOL_START = '{';
10372
- const EXPRESSION_SYMBOL_END = '}';
10373
- const POTENTIAL_EXPRESSION_RE = /^.?{.+}.*$/;
10374
- const WHITESPACES_RE = /\s/;
10375
- function isExpression(source) {
10376
- // Issue #3418: Legacy behavior, previous regex treated "{}" attribute value as non expression
10377
- return source[0] === '{' && source.slice(-1) === '}' && source.length > 2;
10378
- }
10379
- function isPotentialExpression(source) {
10380
- return !!source.match(POTENTIAL_EXPRESSION_RE);
10381
- }
10382
- function validateExpression(node, config) {
10383
- // TODO [#3370]: remove experimental template expression flag
10384
- if (config.experimentalComplexExpressions) {
10385
- return validateExpressionAst(node);
10386
- }
10387
- const isValidNode = isIdentifier(node) || isMemberExpression(node);
10388
- errors.invariant(isValidNode, errors.ParserDiagnostics.INVALID_NODE, [node.type]);
10389
- if (isMemberExpression(node)) {
10390
- errors.invariant(config.experimentalComputedMemberExpression || !node.computed, errors.ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED);
10391
- const { object, property } = node;
10392
- if (!isIdentifier(object)) {
10393
- validateExpression(object, config);
10394
- }
10395
- if (!isIdentifier(property)) {
10396
- validateExpression(property, config);
10397
- }
10398
- }
10399
- }
10400
- function validateSourceIsParsedExpression(source, parsedExpression) {
10401
- if (parsedExpression.end === source.length - 1) {
10402
- return;
10403
- }
10404
- let unclosedParenthesisCount = 0;
10405
- for (let i = 0, n = parsedExpression.start; i < n; i++) {
10406
- if (source[i] === '(') {
10407
- unclosedParenthesisCount++;
10408
- }
10409
- }
10410
- // source[source.length - 1] === '}', n = source.length - 1 is to avoid processing '}'.
10411
- for (let i = parsedExpression.end, n = source.length - 1; i < n; i++) {
10412
- const character = source[i];
10413
- if (character === ')') {
10414
- unclosedParenthesisCount--;
10415
- }
10416
- else if (character === ';') {
10417
- // acorn parseExpressionAt will stop at the first ";", it may be that the expression is not
10418
- // a multiple expression ({foo;}), but this is a case that we explicitly do not want to support.
10419
- // in such case, let's fail with the same error as if it were a multiple expression.
10420
- errors.invariant(false, errors.ParserDiagnostics.MULTIPLE_EXPRESSIONS);
10421
- }
10422
- else {
10423
- errors.invariant(WHITESPACES_RE.test(character), errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['Unexpected end of expression']);
10424
- }
10425
- }
10426
- errors.invariant(unclosedParenthesisCount === 0, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, [
10427
- 'Unexpected end of expression',
10428
- ]);
10429
- }
10430
- function validatePreparsedJsExpressions(ctx) {
10431
- ctx.preparsedJsExpressions?.forEach(({ parsedExpression, rawText }) => {
10432
- const acornLoc = parsedExpression.loc;
10433
- const parse5Loc = {
10434
- startLine: acornLoc.start.line,
10435
- startCol: acornLoc.start.column,
10436
- startOffset: parsedExpression.start,
10437
- endLine: acornLoc.end.line,
10438
- endCol: acornLoc.end.column,
10439
- endOffset: parsedExpression.end,
10440
- };
10441
- ctx.withErrorWrapping(() => {
10442
- validateExpressionAst(parsedExpression);
10443
- }, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, sourceLocation(parse5Loc), (err) => `Invalid expression ${rawText} - ${err.message}`);
10444
- });
10445
- }
10446
- function parseExpression(ctx, source, location) {
10447
- const { ecmaVersion } = ctx;
10448
- return ctx.withErrorWrapping(() => {
10449
- const parsed = acorn.parseExpressionAt(source, 1, {
10450
- ecmaVersion,
10451
- // TODO [#3370]: remove experimental template expression flag
10452
- allowAwaitOutsideFunction: ctx.config.experimentalComplexExpressions,
10453
- });
10454
- validateSourceIsParsedExpression(source, parsed);
10455
- validateExpression(parsed, ctx.config);
10456
- return { ...parsed, location };
10457
- }, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, location, (err) => `Invalid expression ${source} - ${err.message}`);
10458
- }
10459
- function parseIdentifier(ctx, source, location) {
10460
- let isValid = true;
10461
- isValid = acorn.isIdentifierStart(source.charCodeAt(0));
10462
- for (let i = 1; i < source.length && isValid; i++) {
10463
- isValid = acorn.isIdentifierChar(source.charCodeAt(i));
10464
- }
10465
- if (isValid && !isReservedES6Keyword(source)) {
10466
- return {
10467
- ...identifier(source),
10468
- location,
10469
- };
10470
- }
10471
- else {
10472
- ctx.throwAtLocation(errors.ParserDiagnostics.INVALID_IDENTIFIER, location, [source]);
10473
- }
10474
- }
10475
-
10476
- /*
10477
- * Copyright (c) 2023, salesforce.com, inc.
10478
- * All rights reserved.
10479
- * SPDX-License-Identifier: MIT
10480
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10481
- */
10482
- const OPENING_CURLY_LEN = 1;
10483
- const CLOSING_CURLY_LEN = 1;
10484
- const OPENING_CURLY_BRACKET = 0x7b;
10485
- const CLOSING_CURLY_BRACKET = 0x7d;
10486
- const WHITESPACE = /\s*/;
10487
- const TRAILING_SPACES_AND_PARENS = /[\s)]*/;
10488
- function getWhitespaceLen(str) {
10489
- return WHITESPACE.exec(str)[0].length;
10490
- }
10491
- function getTrailingChars(str) {
10492
- return TRAILING_SPACES_AND_PARENS.exec(str)[0];
10493
- }
10494
10540
  /**
10495
10541
  * This function checks for "unbalanced" extraneous parentheses surrounding the expression.
10496
10542
  *
@@ -10519,189 +10565,32 @@ function validateMatchingExtraParens(leadingChars, trailingChars) {
10519
10565
  const numTrailingParens = trailingChars.split(')').length - 1;
10520
10566
  errors.invariant(numLeadingParens === numTrailingParens, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['expression must have balanced parentheses.']);
10521
10567
  }
10522
- /**
10523
- * This class extends `parse5`'s internal tokenizer.
10524
- *
10525
- * Its behavior diverges from that specified in the WHATWG HTML spec
10526
- * in two places:
10527
- * - 13.2.5.38 - unquoted attribute values
10528
- * - 13.2.5.1 - the "data" state, which corresponds to parsing outside of tags
10529
- *
10530
- * Specifically, this tokenizer defers to Acorn's JavaScript parser when
10531
- * encountering a `{` character for an attribute value or within a text
10532
- * node. Acorn parses the expression, and the tokenizer continues its work
10533
- * following the closing `}`.
10534
- *
10535
- * The tokenizer itself is a massive state machine - code points are consumed one at
10536
- * a time and, when certain conditions are met, sequences of those code points are
10537
- * emitted as tokens. The tokenizer will also transition to new states, under conditions
10538
- * specified by the HTML spec.
10539
- */
10540
- class TemplateHtmlTokenizer extends Tokenizer {
10541
- constructor(opts, parser, handler) {
10542
- super(opts, handler);
10543
- // We track which attribute values are in-progess so that we can defer
10544
- // to the default tokenizer's behavior after the first character of
10545
- // an unquoted attr value has been checked for an opening curly brace.
10546
- this.checkedAttrs = new WeakSet();
10547
- this.parser = parser;
10548
- }
10549
- parseTemplateExpression() {
10550
- const expressionStart = this.preprocessor.pos;
10551
- const html = this.preprocessor.html;
10552
- const leadingWhitespaceLen = getWhitespaceLen(html.slice(expressionStart + 1));
10553
- const javascriptExprStart = expressionStart + leadingWhitespaceLen + OPENING_CURLY_LEN;
10554
- // Start parsing after the opening curly brace and any leading whitespace.
10555
- const estreeNode = acorn.parseExpressionAt(html, javascriptExprStart, {
10556
- ecmaVersion: TMPL_EXPR_ECMASCRIPT_EDITION,
10557
- allowAwaitOutsideFunction: true,
10558
- locations: true,
10559
- ranges: true,
10560
- onComment: () => errors.invariant(false, errors.ParserDiagnostics.INVALID_EXPR_COMMENTS_DISALLOWED),
10561
- });
10562
- const leadingChars = html.slice(expressionStart + 1, estreeNode.start);
10563
- const trailingChars = getTrailingChars(html.slice(estreeNode.end));
10564
- validateMatchingExtraParens(leadingChars, trailingChars);
10565
- const idxOfClosingBracket = estreeNode.end + trailingChars.length;
10566
- // Capture text content between the outer curly braces, inclusive.
10567
- const expressionTextNodeValue = html.slice(expressionStart, idxOfClosingBracket + CLOSING_CURLY_LEN);
10568
- errors.invariant(html.codePointAt(idxOfClosingBracket) === CLOSING_CURLY_BRACKET, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['expression must end with curly brace.']);
10569
- // Parsed expressions that are cached here will be later retrieved when the
10570
- // LWC template AST is being constructed.
10571
- this.parser.preparsedJsExpressions.set(expressionStart, {
10572
- parsedExpression: estreeNode,
10573
- rawText: expressionTextNodeValue,
10574
- });
10575
- return expressionTextNodeValue;
10576
- }
10577
- // ATTRIBUTE_VALUE_UNQUOTED_STATE is entered when an opening tag is being parsed,
10578
- // after an attribute name is parsed, and after the `=` character is parsed. The
10579
- // next character determines whether the lexer enters the ATTRIBUTE_VALUE_QUOTED_STATE
10580
- // or ATTRIBUTE_VALUE_UNQUOTED_STATE. Customizations required to support template
10581
- // expressions are only in effect when parsing an unquoted attribute value.
10582
- _stateAttributeValueUnquoted(codePoint) {
10583
- if (codePoint === OPENING_CURLY_BRACKET && !this.checkedAttrs.has(this.currentAttr)) {
10584
- this.checkedAttrs.add(this.currentAttr);
10585
- this.currentAttr.value = this.parseTemplateExpression();
10586
- this._advanceBy(this.currentAttr.value.length - 1);
10587
- this.consumedAfterSnapshot = this.currentAttr.value.length;
10588
- }
10589
- else {
10590
- // If the first character in an unquoted-attr-value is not an opening
10591
- // curly brace, it isn't a template expression. Opening curly braces
10592
- // coming later in an unquoted attr value should not be considered
10593
- // the beginning of a template expression.
10594
- this.checkedAttrs.add(this.currentAttr);
10595
- super._stateAttributeValueUnquoted(codePoint);
10596
- }
10597
- }
10598
- // DATA_STATE is the initial & default state of the lexer. It can be thought of as the
10599
- // state when the cursor is outside of an (opening or closing) tag, and outside of
10600
- // special parts of an HTML document like the contents of a <style> or <script> tag.
10601
- // In other words, we're parsing a text node when in DATA_STATE.
10602
- _stateData(codePoint) {
10603
- if (codePoint === OPENING_CURLY_BRACKET) {
10604
- // An opening curly brace may be the first character in a text node.
10605
- // If that is not the case, we need to emit the text node characters
10606
- // that come before the curly brace.
10607
- if (this.currentCharacterToken) {
10608
- this.currentLocation = this.getCurrentLocation(0);
10609
- this.consumedAfterSnapshot = 0;
10610
- // Emit the text segment preceding the curly brace.
10611
- this._emitCurrentCharacterToken(this.currentLocation);
10612
- }
10613
- const expressionTextNodeValue = this.parseTemplateExpression();
10614
- // Create a new text-node token to contain our `{expression}`
10615
- this._createCharacterToken(TokenType.CHARACTER, expressionTextNodeValue);
10616
- this._advanceBy(expressionTextNodeValue.length);
10617
- this.currentLocation = this.getCurrentLocation(0);
10618
- // Emit the text node token containing the `{expression}`
10619
- this._emitCurrentCharacterToken(this.currentLocation);
10620
- // Moving the cursor back by one allows the state machine to correctly detect
10621
- // the state into which it should next transition.
10622
- this.preprocessor.retreat(1);
10623
- this.currentToken = null;
10624
- this.currentCharacterToken = null;
10625
- }
10626
- else {
10627
- super._stateData(codePoint);
10628
- }
10629
- }
10630
- }
10631
- function isTemplateElement(node) {
10632
- return node.nodeName === 'template';
10633
- }
10634
- function isTextNode(node) {
10635
- return node?.nodeName === '#text';
10636
- }
10637
- function isTemplateExpressionTextNode(node, html) {
10638
- return (isTextNode(node) &&
10639
- isTemplateExpressionTextNodeValue(node.value, node.sourceCodeLocation.startOffset, html));
10640
- }
10641
- function isTemplateExpressionTextNodeValue(value, startOffset, html) {
10642
- return (value.startsWith(EXPRESSION_SYMBOL_START) &&
10643
- html.startsWith(EXPRESSION_SYMBOL_START, startOffset));
10644
- }
10645
- /**
10646
- * This class extends `parse5`'s internal parser. The heavy lifting is
10647
- * done in the tokenizer. This class is only present to facilitate use
10648
- * of that tokenizer when parsing expressions.
10649
- */
10650
- class TemplateHtmlParser extends Parser {
10651
- constructor(extendedOpts, document, fragmentCxt) {
10652
- const { preparsedJsExpressions, ...options } = extendedOpts;
10653
- super(options, document, fragmentCxt);
10654
- this.preparsedJsExpressions = preparsedJsExpressions;
10655
- this.tokenizer = new TemplateHtmlTokenizer(this.options, this, this);
10656
- }
10657
- // The parser will try to concatenate adjacent text tokens into a single
10658
- // text node. Template expressions should be encapsulated in their own
10659
- // text node, and not combined with adjacent text or whitespace. To avoid
10660
- // that, we create a new text node for the template expression rather than
10661
- // allowing the concatenation to proceed.
10662
- _insertCharacters(token) {
10663
- const parentNode = this.openElements.current;
10664
- const previousPeer = parentNode.childNodes.at(-1);
10665
- const html = this.tokenizer.preprocessor.html;
10666
- if (
10667
- // If we're not dealing with a template expression...
10668
- !isTemplateExpressionTextNodeValue(token.chars, token.location.startOffset, html) &&
10669
- // ... and the previous node wasn't a text-node-template-expression...
10670
- !isTemplateExpressionTextNode(previousPeer, html)) {
10671
- // ... concatenate the provided characters with the previous token's characters.
10672
- return super._insertCharacters(token);
10673
- }
10674
- const textNode = {
10675
- nodeName: '#text',
10676
- value: token.chars,
10677
- sourceCodeLocation: token.location ? { ...token.location } : null,
10678
- parentNode,
10679
- };
10680
- if (isTemplateElement(parentNode)) {
10681
- parentNode.content.childNodes.push(textNode);
10682
- }
10683
- else {
10684
- parentNode.childNodes.push(textNode);
10685
- }
10686
- }
10687
- }
10688
- /**
10689
- * Parse the LWC template using a customized parser & lexer that allow
10690
- * for template expressions to be parsed correctly.
10691
- * @param source raw template markup
10692
- * @param config
10693
- * @returns the parsed document
10694
- */
10695
- function parseFragment(source, config) {
10696
- const { ctx, sourceCodeLocationInfo = true, onParseError } = config;
10697
- const opts = {
10698
- sourceCodeLocationInfo,
10699
- onParseError,
10700
- preparsedJsExpressions: ctx.preparsedJsExpressions,
10568
+ function validateComplexExpression(expression, source, templateSource, expressionStart, options, location) {
10569
+ const leadingChars = source.slice(expressionStart + OPENING_CURLY_LEN, expression.start);
10570
+ const trailingChars = getTrailingChars(source.slice(expression.end));
10571
+ const idxOfClosingBracket = expression.end + trailingChars.length;
10572
+ // Capture text content between the outer curly braces, inclusive.
10573
+ const expressionTextNodeValue = source.slice(expressionStart, idxOfClosingBracket + CLOSING_CURLY_LEN);
10574
+ validateExpressionAst(expression);
10575
+ validateMatchingExtraParens(leadingChars, trailingChars);
10576
+ errors.invariant(source.codePointAt(idxOfClosingBracket) === CLOSING_CURLY_BRACKET, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['expression must end with curly brace.']);
10577
+ /*
10578
+ This second parsing step should never be needed, but accounts for cases
10579
+ where a portion of the expression has been incorrectly parsed by the html parser.
10580
+ - E.g. the expression {call("}<c-status></c-status>")} would be parsed by parse5 like this:
10581
+ 1. {call("} (will next be evaluated as an expression)
10582
+ 2. <c-status></c-status> (will be evaluated as an element)
10583
+ 3. ")} (text node)
10584
+ - The expression: call(" is invalid so the parser would have already failed to validate. But if, somehow a valid expression was produced, this step
10585
+ would compare the length of that expression to the length of the expression parsed from the raw template source.
10586
+ If the two expressions don't match in length, that indicates parse5 interpreted a portion of the expression as HTML and we throw.
10587
+ */
10588
+ const templateExpression = acorn.parseExpressionAt(templateSource, expressionStart + OPENING_CURLY_LEN, options);
10589
+ errors.invariant(expression.end === templateExpression.end, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, ['expression incorrectly formed']);
10590
+ return {
10591
+ expression: { ...expression, location },
10592
+ raw: expressionTextNodeValue,
10701
10593
  };
10702
- const parser = TemplateHtmlParser.getFragmentParser(null, opts);
10703
- parser.tokenizer.write(source, true);
10704
- return parser.getFragment();
10705
10594
  }
10706
10595
 
10707
10596
  /*
@@ -10714,88 +10603,17 @@ function isComplexTemplateExpressionEnabled(ctx) {
10714
10603
  return (ctx.config.experimentalComplexExpressions &&
10715
10604
  shared.isAPIFeatureEnabled(11 /* APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS */, ctx.apiVersion));
10716
10605
  }
10717
-
10718
- /*
10719
- * Copyright (c) 2018, salesforce.com, inc.
10720
- * All rights reserved.
10721
- * SPDX-License-Identifier: MIT
10722
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
10723
- */
10724
- function getLwcErrorFromParse5Error(ctx, code) {
10725
- /* istanbul ignore else */
10726
- if (errorCodesToErrorOn.has(code)) {
10727
- return errors.ParserDiagnostics.INVALID_HTML_SYNTAX;
10728
- }
10729
- else if (errorCodesToWarnOnInOlderAPIVersions.has(code)) {
10730
- // In newer API versions, all parse 5 errors are errors, not warnings
10731
- if (shared.isAPIFeatureEnabled(1 /* APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS */, ctx.apiVersion)) {
10732
- return errors.ParserDiagnostics.INVALID_HTML_SYNTAX;
10733
- }
10734
- else {
10735
- return errors.ParserDiagnostics.INVALID_HTML_SYNTAX_WARNING;
10736
- }
10737
- }
10738
- else {
10739
- // It should be impossible to reach here; we have a test in parser.spec.ts to ensure
10740
- // all error codes are accounted for. But just to be safe, make it a warning.
10741
- // TODO [#2650]: better system for handling unexpected parse5 errors
10742
- // eslint-disable-next-line no-console
10743
- console.warn('Found a Parse5 error that we do not know how to handle:', code);
10744
- return errors.ParserDiagnostics.INVALID_HTML_SYNTAX_WARNING;
10745
- }
10746
- }
10747
- function parseHTML(ctx, source) {
10748
- const onParseError = (err) => {
10749
- const { code, ...location } = err;
10750
- const lwcError = getLwcErrorFromParse5Error(ctx, code);
10751
- ctx.warnAtLocation(lwcError, sourceLocation(location), [code]);
10752
- };
10753
- // TODO [#3370]: remove experimental template expression flag
10754
- if (ctx.config.experimentalComplexExpressions) {
10755
- return parseFragment(source, {
10756
- ctx,
10757
- sourceCodeLocationInfo: true,
10758
- onParseError,
10759
- });
10760
- }
10761
- return parseFragment$1(source, {
10762
- sourceCodeLocationInfo: true,
10763
- onParseError,
10764
- });
10765
- }
10766
- // https://github.com/babel/babel/blob/d33d02359474296402b1577ef53f20d94e9085c4/packages/babel-types/src/react.js#L9-L55
10767
- function cleanTextNode(value) {
10768
- const lines = value.split(/\r\n|\n|\r/);
10769
- let lastNonEmptyLine = 0;
10770
- for (let i = 0; i < lines.length; i++) {
10771
- if (lines[i].match(/[^ \t]/)) {
10772
- lastNonEmptyLine = i;
10773
- }
10774
- }
10775
- let str = '';
10776
- for (let i = 0; i < lines.length; i++) {
10777
- const line = lines[i];
10778
- const isFirstLine = i === 0;
10779
- const isLastLine = i === lines.length - 1;
10780
- const isLastNonEmptyLine = i === lastNonEmptyLine;
10781
- let trimmedLine = line.replace(/\t/g, ' ');
10782
- if (!isFirstLine) {
10783
- trimmedLine = trimmedLine.replace(/^[ ]+/, '');
10784
- }
10785
- if (!isLastLine) {
10786
- trimmedLine = trimmedLine.replace(/[ ]+$/, '');
10787
- }
10788
- if (trimmedLine) {
10789
- if (!isLastNonEmptyLine) {
10790
- trimmedLine += ' ';
10791
- }
10792
- str += trimmedLine;
10793
- }
10794
- }
10795
- return str;
10796
- }
10797
- function decodeTextContent(source) {
10798
- return he__namespace.decode(source);
10606
+ function parseComplexExpression(ctx, source, templateSource, location, expressionStart = 0) {
10607
+ const { ecmaVersion } = ctx;
10608
+ return ctx.withErrorWrapping(() => {
10609
+ const options = {
10610
+ ecmaVersion,
10611
+ onComment: () => errors.invariant(false, errors.ParserDiagnostics.INVALID_EXPR_COMMENTS_DISALLOWED),
10612
+ allowAwaitOutsideFunction: true,
10613
+ };
10614
+ const estreeNode = acorn.parseExpressionAt(source, expressionStart + OPENING_CURLY_LEN, options);
10615
+ return validateComplexExpression(estreeNode, source, templateSource, expressionStart, options, location);
10616
+ }, errors.ParserDiagnostics.TEMPLATE_EXPRESSION_PARSING_ERROR, location, (err) => `Invalid expression ${source} - ${err.message}`);
10799
10617
  }
10800
10618
 
10801
10619
  /*
@@ -11276,7 +11094,7 @@ function normalizeAttributeValue(ctx, raw, tag, attr, location) {
11276
11094
  }
11277
11095
  // <input value={myValue} />
11278
11096
  // -> Valid identifier.
11279
- return { value, escapedExpression: false };
11097
+ return { value, escapedExpression: false, quotedExpression: !!isQuoted };
11280
11098
  }
11281
11099
  else if (!isEscaped && isPotentialExpression(value)) {
11282
11100
  const isExpressionEscaped = value.startsWith(`\\${EXPRESSION_SYMBOL_START}`);
@@ -11287,12 +11105,16 @@ function normalizeAttributeValue(ctx, raw, tag, attr, location) {
11287
11105
  // <input value={myValue}/>
11288
11106
  // -> By design the html parser consider the / as the last character of the attribute value.
11289
11107
  // Make sure to remove strip the trailing / for self closing elements.
11290
- return { value: value.slice(0, -1), escapedExpression: false };
11108
+ return {
11109
+ value: value.slice(0, -1),
11110
+ escapedExpression: false,
11111
+ quotedExpression: !!isQuoted,
11112
+ };
11291
11113
  }
11292
11114
  else if (isExpressionEscaped) {
11293
11115
  // <input value="\{myValue}"/>
11294
11116
  // -> Valid escaped string literal
11295
- return { value: value.slice(1), escapedExpression: true };
11117
+ return { value: value.slice(1), escapedExpression: true, quotedExpression: !!isQuoted };
11296
11118
  }
11297
11119
  let escaped = raw.replace(/="?/, '="\\');
11298
11120
  escaped += escaped.endsWith('"') ? '' : '"';
@@ -11304,7 +11126,7 @@ function normalizeAttributeValue(ctx, raw, tag, attr, location) {
11304
11126
  }
11305
11127
  // <input value="myValue"/>
11306
11128
  // -> Valid string literal.
11307
- return { value, escapedExpression: false };
11129
+ return { value, escapedExpression: false, quotedExpression: !!isQuoted };
11308
11130
  }
11309
11131
  function attributeName(attr) {
11310
11132
  const { prefix, name } = attr;
@@ -11467,7 +11289,6 @@ function parse$1(source, state) {
11467
11289
  return { warnings: ctx.warnings };
11468
11290
  }
11469
11291
  const root = ctx.withErrorRecovery(() => {
11470
- validatePreparsedJsExpressions(ctx);
11471
11292
  const templateRoot = getTemplateRoot(ctx, fragment);
11472
11293
  return parseRoot(ctx, templateRoot);
11473
11294
  });
@@ -11674,8 +11495,8 @@ function parseChildren(ctx, parse5Parent, parent, parse5ParentLocation) {
11674
11495
  ctx.endIfChain();
11675
11496
  }
11676
11497
  }
11677
- else if (isTextNode$1(child)) {
11678
- const textNodes = parseText(ctx, child);
11498
+ else if (isTextNode(child)) {
11499
+ const textNodes = parseTextNode(ctx, child);
11679
11500
  parent.children.push(...textNodes);
11680
11501
  // Non whitespace text nodes end any if chain we may be parsing
11681
11502
  if (ctx.isParsingSiblingIfBlock() && textNodes.length > 0) {
@@ -11695,53 +11516,9 @@ function parseChildren(ctx, parse5Parent, parent, parse5ParentLocation) {
11695
11516
  }
11696
11517
  ctx.endSiblingScope();
11697
11518
  }
11698
- function parseText(ctx, parse5Text) {
11519
+ function parseText(ctx, rawText, sourceLocation, location) {
11699
11520
  const parsedTextNodes = [];
11700
- const location = parse5Text.sourceCodeLocation;
11701
- /* istanbul ignore if */
11702
- if (!location) {
11703
- // Parse5 will recover from invalid HTML. When this happens the node's sourceCodeLocation will be undefined.
11704
- // https://github.com/inikulin/parse5/blob/master/packages/parse5/docs/options/parser-options.md#sourcecodelocationinfo
11705
- // This is a defensive check as this should never happen for TextNode.
11706
- throw new Error('An internal parsing error occurred during node creation; a text node was found without a sourceCodeLocation.');
11707
- }
11708
- // Extract the raw source to avoid HTML entity decoding done by parse5
11709
- const rawText = cleanTextNode(ctx.getSource(location.startOffset, location.endOffset));
11710
- /*
11711
- The original job of this if-block was to discard the whitespace between HTML tags, HTML
11712
- comments, and HTML tags and HTML comments. The whitespace inside the text content of HTML tags
11713
- would never be considered here because they would not be parsed into individual text nodes until
11714
- later (several lines below).
11715
-
11716
- ["Hello {first} {last}!"] => ["Hello ", "{first}", " ", "{last}", "!"]
11717
-
11718
- With the implementation of complex template expressions, whitespace that shouldn't be discarded
11719
- has already been parsed into individual text nodes at this point so we only discard when
11720
- experimentalComplexExpressions is disabled.
11721
-
11722
- When removing the experimentalComplexExpressions flag, we need to figure out how to best discard
11723
- the HTML whitespace while preserving text content whitespace, while also taking into account how
11724
- comments are sometimes preserved (in which case we need to keep the HTML whitespace).
11725
- */
11726
- if (!rawText.trim().length && !ctx.config.experimentalComplexExpressions) {
11727
- return parsedTextNodes;
11728
- }
11729
- // TODO [#3370]: remove experimental template expression flag
11730
- if (ctx.config.experimentalComplexExpressions && isExpression(rawText)) {
11731
- // Implementation of the lexer ensures that each text-node template expression
11732
- // will be contained in its own text node. Adjacent static text will be in
11733
- // separate text nodes.
11734
- const entry = ctx.preparsedJsExpressions.get(location.startOffset);
11735
- if (!entry?.parsedExpression) {
11736
- throw new Error('Implementation error: cannot find preparsed template expression');
11737
- }
11738
- const value = {
11739
- ...entry.parsedExpression,
11740
- location: sourceLocation(location),
11741
- };
11742
- return [text(rawText, value, location)];
11743
- }
11744
- // Split the text node content arround expression and create node for each
11521
+ // Split the text node content around expression and create node for each
11745
11522
  const tokenizedContent = rawText.split(EXPRESSION_RE);
11746
11523
  for (const token of tokenizedContent) {
11747
11524
  // Don't create nodes for emtpy strings
@@ -11750,7 +11527,7 @@ function parseText(ctx, parse5Text) {
11750
11527
  }
11751
11528
  let value;
11752
11529
  if (isExpression(token)) {
11753
- value = parseExpression(ctx, token, sourceLocation(location));
11530
+ value = parseExpression(ctx, token, sourceLocation);
11754
11531
  }
11755
11532
  else {
11756
11533
  value = literal(decodeTextContent(token));
@@ -11759,6 +11536,53 @@ function parseText(ctx, parse5Text) {
11759
11536
  }
11760
11537
  return parsedTextNodes;
11761
11538
  }
11539
+ function parseTextComplex(ctx, rawText, sourceLocation, location) {
11540
+ const parsedTextNodes = [];
11541
+ let start = 0;
11542
+ let index = 0;
11543
+ const templateSource = cleanTextNode(ctx.getSource(location.startOffset));
11544
+ while (index < rawText.length) {
11545
+ if (rawText[index] === EXPRESSION_SYMBOL_START) {
11546
+ // Parse any literal that preceeded the expression
11547
+ if (start < index) {
11548
+ const literalToken = rawText.slice(start, index);
11549
+ parsedTextNodes.push(text(literalToken, literal(decodeTextContent(literalToken)), location));
11550
+ }
11551
+ const parsed = parseComplexExpression(ctx, rawText, templateSource, sourceLocation, index);
11552
+ parsedTextNodes.push(text(parsed.raw, parsed.expression, location));
11553
+ // Parse the remainder of the text node for expressions
11554
+ index += parsed.raw.length;
11555
+ start = index;
11556
+ continue;
11557
+ }
11558
+ index++;
11559
+ }
11560
+ // Parse any remaining literal
11561
+ if (start < rawText.length) {
11562
+ const literalToken = rawText.slice(start, index);
11563
+ parsedTextNodes.push(text(literalToken, literal(decodeTextContent(literalToken)), location));
11564
+ }
11565
+ return parsedTextNodes;
11566
+ }
11567
+ function parseTextNode(ctx, parse5Text) {
11568
+ const location = parse5Text.sourceCodeLocation;
11569
+ /* istanbul ignore if */
11570
+ if (!location) {
11571
+ // Parse5 will recover from invalid HTML. When this happens the node's sourceCodeLocation will be undefined.
11572
+ // https://github.com/inikulin/parse5/blob/master/packages/parse5/docs/options/parser-options.md#sourcecodelocationinfo
11573
+ // This is a defensive check as this should never happen for TextNode.
11574
+ throw new Error('An internal parsing error occurred during node creation; a text node was found without a sourceCodeLocation.');
11575
+ }
11576
+ // Extract the raw source to avoid HTML entity decoding done by parse5
11577
+ const rawText = cleanTextNode(ctx.getSource(location.startOffset, location.endOffset));
11578
+ if (!rawText.trim().length) {
11579
+ return [];
11580
+ }
11581
+ const sourceLocation$1 = sourceLocation(location);
11582
+ return ctx.config.experimentalComplexExpressions
11583
+ ? parseTextComplex(ctx, rawText, sourceLocation$1, location)
11584
+ : parseText(ctx, rawText, sourceLocation$1, location);
11585
+ }
11762
11586
  function parseComment(parse5Comment) {
11763
11587
  const location = parse5Comment.sourceCodeLocation;
11764
11588
  /* istanbul ignore if */
@@ -11773,7 +11597,7 @@ function parseComment(parse5Comment) {
11773
11597
  function getTemplateRoot(ctx, documentFragment) {
11774
11598
  // Filter all the empty text nodes
11775
11599
  const validRoots = documentFragment.childNodes.filter((child) => isElementNode(child) ||
11776
- (isTextNode$1(child) && child.value.trim().length));
11600
+ (isTextNode(child) && child.value.trim().length));
11777
11601
  if (validRoots.length > 1) {
11778
11602
  const duplicateRoot = validRoots[1].sourceCodeLocation ?? undefined;
11779
11603
  ctx.throw(errors.ParserDiagnostics.MULTIPLE_ROOTS_FOUND, [], duplicateRoot ? sourceLocation(duplicateRoot) : (duplicateRoot ?? undefined));
@@ -12628,13 +12452,22 @@ function getTemplateAttribute(ctx, tag, attribute$1, attributeLocation) {
12628
12452
  ]);
12629
12453
  }
12630
12454
  const isBooleanAttribute = !rawAttribute.includes('=');
12631
- const { value, escapedExpression } = normalizeAttributeValue(ctx, rawAttribute, tag, attribute$1, location);
12455
+ const { value, escapedExpression, quotedExpression } = normalizeAttributeValue(ctx, rawAttribute, tag, attribute$1, location);
12632
12456
  let attrValue;
12633
- // TODO [#3370]: If complex template expressions are adopted, `preparsedJsExpressions`
12634
- // should be checked. However, to avoid significant complications in the internal types,
12635
- // arising from supporting both implementations simultaneously, we will re-parse the
12636
- // expression here when `ctx.config.experimentalComplexExpressions` is true.
12637
- if (isExpression(value) && !escapedExpression) {
12457
+ /*
12458
+ A complex attribute expression should only be parsed as a complex expression if it has been quoted.
12459
+ Quoting complex expressions ensures that the expression is valid HTML. If the complex expression
12460
+ has not been quoted, then it is parsed as a legacy expression and it will fail with an appropriate explanation.
12461
+ This ensures backward compatibility with legacy expressions which do not require, or currently permit quotes
12462
+ to be used.
12463
+ */
12464
+ const isPotentialComplexExpression = quotedExpression && !escapedExpression && value.startsWith(EXPRESSION_SYMBOL_START);
12465
+ if (ctx.config.experimentalComplexExpressions && isPotentialComplexExpression) {
12466
+ const attributeNameOffset = attribute$1.name.length + 2; // The +2 accounts for the '="' in the attribute: attr="...
12467
+ const templateSource = ctx.getSource(attributeLocation.startOffset + attributeNameOffset);
12468
+ attrValue = parseComplexExpression(ctx, value, templateSource, location).expression;
12469
+ }
12470
+ else if (isExpression(value) && !escapedExpression) {
12638
12471
  attrValue = parseExpression(ctx, value, location);
12639
12472
  }
12640
12473
  else if (isBooleanAttribute) {
@@ -14716,5 +14549,5 @@ exports.generateScopeTokens = generateScopeTokens;
14716
14549
  exports.kebabcaseToCamelcase = kebabcaseToCamelcase;
14717
14550
  exports.parse = parse;
14718
14551
  exports.toPropertyName = toPropertyName;
14719
- /** version: 8.21.6 */
14552
+ /** version: 8.22.3 */
14720
14553
  //# sourceMappingURL=index.cjs.js.map