@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 +316 -483
- package/dist/index.js +316 -483
- package/dist/parser/attribute.d.ts +1 -0
- package/dist/parser/expression-complex/index.d.ts +5 -1
- package/dist/parser/expression-complex/validate.d.ts +10 -2
- package/dist/parser/expression.d.ts +2 -1
- package/dist/parser/parser.d.ts +1 -8
- package/dist/shared/ast.d.ts +1 -1
- package/package.json +3 -3
- package/dist/parser/expression-complex/html.d.ts +0 -18
package/dist/index.cjs.js
CHANGED
|
@@ -8511,7 +8511,7 @@ new Set([
|
|
|
8511
8511
|
TAG_NAMES.WBR,
|
|
8512
8512
|
]);
|
|
8513
8513
|
|
|
8514
|
-
function parseFragment
|
|
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
|
|
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
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
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
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
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 {
|
|
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
|
|
11678
|
-
const textNodes =
|
|
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,
|
|
11519
|
+
function parseText(ctx, rawText, sourceLocation, location) {
|
|
11699
11520
|
const parsedTextNodes = [];
|
|
11700
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
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.
|
|
14552
|
+
/** version: 8.22.3 */
|
|
14720
14553
|
//# sourceMappingURL=index.cjs.js.map
|