diff --git a/.changeset/spread-keyword-and-bang-runs.md b/.changeset/spread-keyword-and-bang-runs.md new file mode 100644 index 0000000..4431901 --- /dev/null +++ b/.changeset/spread-keyword-and-bang-runs.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +A keyword operator directly after a spread's `...` is an operator, so `
` is one spread rather than `...new` plus an attribute `Attrs()`. A run of non-null assertions (`x!!`) and one after a string or template literal (`"s"!`, `` `s`! ``) now ends an unenclosed value like `x!` does. diff --git a/src/__tests__/fixtures/attr-spread-keyword-operator/__snapshots__/attr-spread-keyword-operator.expected.txt b/src/__tests__/fixtures/attr-spread-keyword-operator/__snapshots__/attr-spread-keyword-operator.expected.txt new file mode 100644 index 0000000..0c305c9 --- /dev/null +++ b/src/__tests__/fixtures/attr-spread-keyword-operator/__snapshots__/attr-spread-keyword-operator.expected.txt @@ -0,0 +1,22 @@ +1╭─
+ │ ││ │ │ │╰─ openTagEnd:selfClosed "/>" + │ ││ │ │ ╰─ attrName + │ ││ │ ╰─ attrSpread.value "new Attrs()" + │ ││ ╰─ attrSpread "...new Attrs()" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
+ │ ││ │ │ │╰─ openTagEnd:selfClosed "/>" + │ ││ │ │ ╰─ attrName + │ ││ │ ╰─ attrSpread.value "await load()" + │ ││ ╰─ attrSpread "...await load()" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +3╭─
+ │ ││ │ │ │╰─ openTagEnd:selfClosed "/>" + │ ││ │ │ ╰─ attrName + │ ││ │ ╰─ attrSpread.value "input.new" + │ ││ ╰─ attrSpread "...input.new" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +4╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-spread-keyword-operator/input.marko b/src/__tests__/fixtures/attr-spread-keyword-operator/input.marko new file mode 100644 index 0000000..908010e --- /dev/null +++ b/src/__tests__/fixtures/attr-spread-keyword-operator/input.marko @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/__snapshots__/non-null-assertion-repeated-or-after-literal.expected.txt b/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/__snapshots__/non-null-assertion-repeated-or-after-literal.expected.txt new file mode 100644 index 0000000..3c7a058 --- /dev/null +++ b/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/__snapshots__/non-null-assertion-repeated-or-after-literal.expected.txt @@ -0,0 +1,35 @@ +1╭─
+ │ ││ │││ │││ │││ │││ │││ │╰─ openTagEnd:selfClosed "/>" + │ ││ │││ │││ │││ │││ │││ ╰─ attrName + │ ││ │││ │││ │││ │││ ││╰─ attrValue.value "!!x" + │ ││ │││ │││ │││ │││ │╰─ attrValue "=!!x" + │ ││ │││ │││ │││ │││ ╰─ attrName + │ ││ │││ │││ │││ ││╰─ attrValue.value "f()!!" + │ ││ │││ │││ │││ │╰─ attrValue "=f()!!" + │ ││ │││ │││ │││ ╰─ attrName + │ ││ │││ │││ ││╰─ attrValue.value "`t`!" + │ ││ │││ │││ │╰─ attrValue "=`t`!" + │ ││ │││ │││ ╰─ attrName + │ ││ │││ ││╰─ attrValue.value "\"s\"!" + │ ││ │││ │╰─ attrValue "=\"s\"!" + │ ││ │││ ╰─ attrName + │ ││ ││╰─ attrValue.value "x!!" + │ ││ │╰─ attrValue "=x!!" + │ ││ ╰─ attrName + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─ static const g = h!! + ╰─ ╰─ tagName "static" +3╭─ static const i = "j"! + │ ├─ openTagEnd + ╰─ ╰─ tagName "static" +4╭─
+ │ ││ │││ │╰─ openTagEnd:selfClosed "/>" + │ ││ │││ ╰─ attrName + │ ││ ││╰─ attrValue.value "typeof!! l" + │ ││ │╰─ attrValue "=typeof!! l" + │ ││ ╰─ attrName + │ │╰─ tagName "div" + │ ├─ openTagEnd + ╰─ ╰─ openTagStart +5╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/input.marko b/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/input.marko new file mode 100644 index 0000000..b85198a --- /dev/null +++ b/src/__tests__/fixtures/non-null-assertion-repeated-or-after-literal/input.marko @@ -0,0 +1,4 @@ +
+static const g = h!! +static const i = "j"! +
diff --git a/src/states/EXPRESSION.ts b/src/states/EXPRESSION.ts index 52ef489..19a443a 100644 --- a/src/states/EXPRESSION.ts +++ b/src/states/EXPRESSION.ts @@ -483,17 +483,28 @@ function lookBehindForOperator( return curPos; case CODE.EXCLAMATION: { - // After an operand, `!` is a TypeScript non-null assertion (postfix); - // after a keyword operator (`typeof!a`, `a in!b`) it is the prefix `!`. - const prevCode = data.charCodeAt(curPos - 1); - switch (prevCode) { + // After an operand, `!` is a TypeScript non-null assertion (postfix), + // as is each `!` of a run after one (`x!!`); after a keyword operator + // (`typeof!a`, `a in!b`) it is the prefix `!`. + let operandEnd = curPos - 1; + while (data.charCodeAt(operandEnd) === CODE.EXCLAMATION) operandEnd--; + const operandCode = data.charCodeAt(operandEnd); + switch (operandCode) { case CODE.CLOSE_PAREN: case CODE.CLOSE_SQUARE_BRACKET: + case CODE.DOUBLE_QUOTE: + case CODE.SINGLE_QUOTE: + case CODE.BACKTICK: return -1; default: - return isWordCode(prevCode) && - lookBehindForOperator(expression, data, curPos) === -1 && - lookBehindForKeyword(data, curPos - 1, relationalKeywords) === -1 + return isWordCode(operandCode) && + lookBehindForOperator(expression, data, operandEnd + 1) === -1 && + lookBehindForKeyword( + expression, + data, + operandEnd, + relationalKeywords, + ) === -1 ? -1 : curPos; } @@ -534,6 +545,7 @@ function lookBehindForOperator( if (code < CODE.LOWER_A || code > CODE.LOWER_Z) return -1; return lookBehindForKeyword( + expression, data, curPos, expression.inType ? tsUnaryKeywords : jsUnaryKeywords, @@ -670,8 +682,10 @@ function lookBehindWhile( return 0; } -// Returns where a whole keyword ending at `pos` starts, or -1. +// Returns where a whole keyword ending at `pos` starts, or -1. A keyword that +// starts the expression is whole even after a `.` (a spread's `...new x`). function lookBehindForKeyword( + expression: ExpressionMeta, data: string, pos: number, keywords: readonly string[], @@ -679,7 +693,9 @@ function lookBehindForKeyword( for (const keyword of keywords) { const keywordPos = lookBehindFor(data, pos, keyword); if (keywordPos !== -1) { - return isWordOrPeriodCode(data.charCodeAt(keywordPos - 1)) + return keywordPos < expression.start || + (keywordPos > expression.start && + isWordOrPeriodCode(data.charCodeAt(keywordPos - 1))) ? -1 : keywordPos; }