diff --git a/.changeset/tame-geese-drop.md b/.changeset/tame-geese-drop.md new file mode 100644 index 00000000..3e4967be --- /dev/null +++ b/.changeset/tame-geese-drop.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": patch +--- + +Switch from regexp based parsing for the expression continuations. This slightly improves performance and more importantly fixes usage of the parser in safari. diff --git a/src/__tests__/fixtures/attr-complex-functions/__snapshots__/attr-complex-functions.expected.txt b/src/__tests__/fixtures/attr-complex-functions/__snapshots__/attr-complex-functions.expected.txt index 8a4dd549..4452261c 100644 --- a/src/__tests__/fixtures/attr-complex-functions/__snapshots__/attr-complex-functions.expected.txt +++ b/src/__tests__/fixtures/attr-complex-functions/__snapshots__/attr-complex-functions.expected.txt @@ -20,7 +20,31 @@ │ ├─ closeTagEnd(tag) │ ├─ openTagEnd ╰─ ╰─ tagName "tag" -4╭─ tag a = async x => { console.log("y") } b +4╭─ tag a = x => y b + │ │ │ │ │ ╰─ attrName + │ │ │ │ ╰─ attrValue.value "x => y" + │ │ │ ╰─ attrValue "= x => y" + │ │ ╰─ attrName + │ ├─ closeTagEnd(tag) + │ ├─ openTagEnd + ╰─ ╰─ tagName "tag" +5╭─ tag a = x => y + 1 b + │ │ │ │ │ ╰─ attrName + │ │ │ │ ╰─ attrValue.value "x => y + 1" + │ │ │ ╰─ attrValue "= x => y + 1" + │ │ ╰─ attrName + │ ├─ closeTagEnd(tag) + │ ├─ openTagEnd + ╰─ ╰─ tagName "tag" +6╭─ tag a = x => y = 1 b + │ │ │ │ │ ╰─ attrName + │ │ │ │ ╰─ attrValue.value "x => y = 1" + │ │ │ ╰─ attrValue "= x => y = 1" + │ │ ╰─ attrName + │ ├─ closeTagEnd(tag) + │ ├─ openTagEnd + ╰─ ╰─ tagName "tag" +7╭─ tag a = async x => { console.log("y") } b │ │ │ │ │ ╰─ attrName │ │ │ │ ╰─ attrValue.value "async x => { console.log(\"y\") }" │ │ │ ╰─ attrValue "= async x => { console.log(\"y\") }" @@ -28,7 +52,7 @@ │ ├─ closeTagEnd(tag) │ ├─ openTagEnd ╰─ ╰─ tagName "tag" -5╭─ tag a = async function (x) { console.log("y") } b +8╭─ tag a = async function (x) { console.log("y") } b │ │ │ │ │ │├─ closeTagEnd(tag) │ │ │ │ │ │╰─ openTagEnd │ │ │ │ │ ╰─ attrName diff --git a/src/__tests__/fixtures/attr-complex-functions/input.marko b/src/__tests__/fixtures/attr-complex-functions/input.marko index 148f6ccd..2d061747 100644 --- a/src/__tests__/fixtures/attr-complex-functions/input.marko +++ b/src/__tests__/fixtures/attr-complex-functions/input.marko @@ -1,5 +1,8 @@ tag a = function (x) { console.log("y") } b tag a = (x) => { console.log("y") } b tag a = x => { console.log("y") } b +tag a = x => y b +tag a = x => y + 1 b +tag a = x => y = 1 b tag a = async x => { console.log("y") } b tag a = async function (x) { console.log("y") } b \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-complex-instanceof/__snapshots__/attr-complex-instanceof.expected.txt b/src/__tests__/fixtures/attr-complex-instanceof/__snapshots__/attr-complex-instanceof.expected.txt index af8f1d09..dc74e8d0 100644 --- a/src/__tests__/fixtures/attr-complex-instanceof/__snapshots__/attr-complex-instanceof.expected.txt +++ b/src/__tests__/fixtures/attr-complex-instanceof/__snapshots__/attr-complex-instanceof.expected.txt @@ -73,4 +73,23 @@ │ ││ │ ╰─ attrValue "= 'foo'" │ ││ ╰─ attrName │ │╰─ tagName "tag" - ╰─ ╰─ openTagStart \ No newline at end of file + ╰─ ╰─ openTagStart +12├─ +13╭─ tag a = 'foo' instanceofthing String + │ │ │ │ │ │ ╰─ attrName "String" + │ │ │ │ │ ╰─ attrName "instanceofthing" + │ │ │ │ ╰─ attrValue.value "'foo'" + │ │ │ ╰─ attrValue "= 'foo'" + │ │ ╰─ attrName + ╰─ ╰─ tagName "tag" +14╭─ + ╰─ ╰─ openTagEnd +15╭─ tag a = 'foo' instanceof + │ │ │ │ │ │ ├─ closeTagEnd(tag) + │ │ │ │ │ │ ╰─ openTagEnd + │ │ │ │ │ ╰─ attrName "instanceof" + │ │ │ │ ╰─ attrValue.value "'foo'" + │ │ │ ╰─ attrValue "= 'foo'" + │ │ ╰─ attrName + │ ├─ closeTagEnd(tag) + ╰─ ╰─ tagName "tag" \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-complex-instanceof/input.marko b/src/__tests__/fixtures/attr-complex-instanceof/input.marko index 5bb5fcf5..12dc9920 100644 --- a/src/__tests__/fixtures/attr-complex-instanceof/input.marko +++ b/src/__tests__/fixtures/attr-complex-instanceof/input.marko @@ -8,4 +8,8 @@ tag a = 'foo' instanceof; tag a = 'foo' instanceof, b - \ No newline at end of file + + +tag a = 'foo' instanceofthing String + +tag a = 'foo' instanceof \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-concise-hyphens/__snapshots__/attr-concise-hyphens.expected.txt b/src/__tests__/fixtures/attr-concise-hyphens/__snapshots__/attr-concise-hyphens.expected.txt new file mode 100644 index 00000000..5795dd7a --- /dev/null +++ b/src/__tests__/fixtures/attr-concise-hyphens/__snapshots__/attr-concise-hyphens.expected.txt @@ -0,0 +1,18 @@ +1╭─ a b=c -- y + │ │ │││ │ ╰─ text + │ │ │││ ╰─ openTagEnd + │ │ ││╰─ attrValue.value + │ │ │╰─ attrValue "=c" + │ │ ╰─ attrName + ╰─ ╰─ tagName +2├─ +3╭─ a [b=c -- y] + │ │ │││ ╰─ attrName + │ │ ││╰─ attrValue.value "c --" + │ │ │╰─ attrValue "=c --" + │ │ ╰─ attrName + │ ├─ closeTagEnd(a) + ╰─ ╰─ tagName +4╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(a) \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-concise-hyphens/input.marko b/src/__tests__/fixtures/attr-concise-hyphens/input.marko new file mode 100644 index 00000000..04201acf --- /dev/null +++ b/src/__tests__/fixtures/attr-concise-hyphens/input.marko @@ -0,0 +1,3 @@ +a b=c -- y + +a [b=c -- y] diff --git a/src/__tests__/fixtures/attr-operator-whitespace-eof/__snapshots__/attr-operator-whitespace-eof.expected.txt b/src/__tests__/fixtures/attr-operator-whitespace-eof/__snapshots__/attr-operator-whitespace-eof.expected.txt new file mode 100644 index 00000000..5f832363 --- /dev/null +++ b/src/__tests__/fixtures/attr-operator-whitespace-eof/__snapshots__/attr-operator-whitespace-eof.expected.txt @@ -0,0 +1,9 @@ +1╭─ tag a = 'foo' instanceof + │ │ │ │ │ ╰─ attrName "instanceof" + │ │ │ │ ╰─ attrValue.value "'foo'" + │ │ │ ╰─ attrValue "= 'foo'" + │ │ ╰─ attrName + ╰─ ╰─ tagName "tag" +2╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(tag) \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-operator-whitespace-eof/input.marko b/src/__tests__/fixtures/attr-operator-whitespace-eof/input.marko new file mode 100644 index 00000000..8bb913cc --- /dev/null +++ b/src/__tests__/fixtures/attr-operator-whitespace-eof/input.marko @@ -0,0 +1 @@ +tag a = 'foo' instanceof diff --git a/src/__tests__/fixtures/attr-operators-newline-after/__snapshots__/attr-operators-newline-after.expected.txt b/src/__tests__/fixtures/attr-operators-newline-after/__snapshots__/attr-operators-newline-after.expected.txt index 3b7daf30..7558659a 100644 --- a/src/__tests__/fixtures/attr-operators-newline-after/__snapshots__/attr-operators-newline-after.expected.txt +++ b/src/__tests__/fixtures/attr-operators-newline-after/__snapshots__/attr-operators-newline-after.expected.txt @@ -683,14 +683,17 @@ │ │╰─ openTagEnd:selfClosed "/>" ╰─ ╰─ attrName 159╭─ = - │ │││╰─ attrValue.value "x >=\ny" - │ ││├─ attrValue "=x >=\ny" + │ ││││ │╰─ text "=\ny " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -160╭─ y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +160╭─ y + │ │ │╰─ closeTagEnd(a) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "" ╰─ ╰─ attrName 173╭─ >= - │ │││╰─ attrValue.value "x >>=\ny" - │ ││├─ attrValue "=x >>=\ny" + │ ││││ │╰─ text ">=\ny " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -174╭─ y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +174╭─ y + │ │ │╰─ closeTagEnd(a) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart ">>= - │ │││╰─ attrValue.value "x >>>=\ny" - │ ││├─ attrValue "=x >>>=\ny" + │ ││││ │╰─ text ">>=\ny " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -176╭─ y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +176╭─ y + │ │ │╰─ closeTagEnd(a) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "" ╰─ ╰─ attrName 193╭─ > - │ │││╰─ attrValue.value "x >>\ny" - │ ││├─ attrValue "=x >>\ny" + │ ││││ │╰─ text ">\ny " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -194╭─ y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +194╭─ y + │ │ │╰─ closeTagEnd(a) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart ">> - │ │││╰─ attrValue.value "x >>>\ny" - │ ││├─ attrValue "=x >>>\ny" + │ ││││ │╰─ text ">>\ny " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -196╭─ y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +196╭─ y + │ │ │╰─ closeTagEnd(a) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " = -y a/> +y >= -y a/> +y >>= -y a/> +y > -y a/> +y >> -y a/> +y " ╰─ ╰─ attrName 45╭─ = y" - │ ││├─ attrValue "=x\n>= y" + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -46╭─ >= y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +46╭─ >= y + │ ││ │ │╰─ closeTagEnd(a) + │ ││ │ ╰─ closeTagName + │ ││ ╰─ closeTagStart "" ╰─ ╰─ attrName 59╭─ >= y" - │ ││├─ attrValue "=x\n>>= y" + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -60╭─ >>= y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +60╭─ >>= y + │ ││ │ │╰─ closeTagEnd(a) + │ ││ │ ╰─ closeTagName + │ ││ ╰─ closeTagStart "= y " + ╰─ ╰─ openTagEnd 61╭─ >>= y" - │ ││├─ attrValue "=x\n>>>= y" + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -62╭─ >>>= y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +62╭─ >>>= y + │ ││ │ │╰─ closeTagEnd(a) + │ ││ │ ╰─ closeTagName + │ ││ ╰─ closeTagStart ">= y " + ╰─ ╰─ openTagEnd 63╭─ " ╰─ ╰─ attrName 79╭─ >y" - │ ││├─ attrValue "=x\n>>y" + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -80╭─ >>y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +80╭─ >>y + │ ││ │ │╰─ closeTagEnd(a) + │ ││ │ ╰─ closeTagName + │ ││ ╰─ closeTagStart "y " + ╰─ ╰─ openTagEnd 81╭─ >> y" - │ ││├─ attrValue "=x\n>>> y" + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -82╭─ >>> y a/> - │ │╰─ openTagEnd:selfClosed "/>" - ╰─ ╰─ attrName +82╭─ >>> y + │ ││ │ │╰─ closeTagEnd(a) + │ ││ │ ╰─ closeTagName + │ ││ ╰─ closeTagStart "> y " + ╰─ ╰─ openTagEnd 83╭─ = y a/> +>= y >= y a/> +>>= y >>= y a/> +>>>= y >y a/> +>>y >> y a/> +>>> y = y a/> - │ │││││╰─ text "= y a/>\n" +66╭─ = y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart " │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -543,7 +545,6 @@ │ ││├─ attrValue "=x|= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 70╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -552,7 +553,6 @@ │ ││├─ attrValue "=x||= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 71╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -561,7 +561,6 @@ │ ││├─ attrValue "=x^= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 72╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -570,19 +569,23 @@ │ ││├─ attrValue "=x~= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -73╭─ >= y a/> - │ │││││╰─ text ">= y a/>\n" +73╭─ >= y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart "= y " │ ││││╰─ openTagEnd │ │││╰─ attrValue.value │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -74╭─ >>= y a/> - │ │││││╰─ text ">>= y a/>\n" +74╭─ >>= y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart ">= y " │ ││││╰─ openTagEnd │ │││╰─ attrValue.value │ ││├─ attrValue "=x" @@ -604,7 +607,6 @@ │ ││├─ attrValue "=x/= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 77╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -613,7 +615,6 @@ │ ││├─ attrValue "=x*= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 78╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -622,7 +623,6 @@ │ ││├─ attrValue "=x**= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 79╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -631,7 +631,6 @@ │ ││├─ attrValue "=x%= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 80╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -640,7 +639,6 @@ │ ││├─ attrValue "=x+= y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 81╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -649,7 +647,6 @@ │ ││├─ attrValue "=x++ + y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 82╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -658,31 +655,34 @@ │ ││├─ attrValue "=x+ ++ y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -83╭─ y - │ ││││││ ││╰─ openTagEnd:selfClosed "/>" - │ ││││││ │╰─ tagName - │ ││││││ ╰─ openTagStart +83╭─ y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart "> y a/> - │ │││││╰─ text "> y a/>\n" +84╭─ > y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart " y " │ ││││╰─ openTagEnd │ │││╰─ attrValue.value │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -85╭─ >> y a/> - │ │││││╰─ text ">> y a/>\n" +85╭─ >> y + │ ││││││ │ │╰─ closeTagEnd(a) + │ ││││││ │ ╰─ closeTagName + │ ││││││ ╰─ closeTagStart "> y " │ ││││╰─ openTagEnd │ │││╰─ attrValue.value │ ││├─ attrValue "=x" @@ -704,33 +704,30 @@ │ ││├─ attrValue "=z {y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 88╭─ y a/> - │ ││││ │╰─ text " y a/>\n" - │ ││││ ╰─ openTagEnd - │ │││╰─ attrValue.value "x=" - │ ││├─ attrValue "=x=" + │ ││││ │╰─ openTagEnd:selfClosed "/>" + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "x=> y" + │ ││├─ attrValue "=x=> y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 89╭─ (y ) a/> - │ ││││ │╰─ text " (y ) a/>\n" - │ ││││ ╰─ openTagEnd - │ │││╰─ attrValue.value "x=" - │ ││├─ attrValue "=x=" + │ ││││ │╰─ openTagEnd:selfClosed "/>" + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "x=> (y )" + │ ││├─ attrValue "=x=> (y )" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart 90╭─ {y } a/> - │ ││││ │╰─ text " {y } a/>\n" - │ ││││ ╰─ openTagEnd - │ │││╰─ attrValue.value "x=" - │ ││├─ attrValue "=x=" + │ ││││ │╰─ openTagEnd:selfClosed "/>" + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "x=> {y }" + │ ││├─ attrValue "=x=> {y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ error(MISSING_END_TAG:Missing ending "a" tag) "" ╰─ ╰─ openTagStart 91╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -740,5 +737,4 @@ │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -92╭─ - ╰─ ╰─ text "\n" \ No newline at end of file +92╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-operators-space-after/input.marko b/src/__tests__/fixtures/attr-operators-space-after/input.marko index 70a18e02..4accea87 100644 --- a/src/__tests__/fixtures/attr-operators-space-after/input.marko +++ b/src/__tests__/fixtures/attr-operators-space-after/input.marko @@ -63,15 +63,15 @@ a=(x ) {y } a -= y a/> += y ->= y a/> ->>= y a/> +>= y +>>= y @@ -80,9 +80,9 @@ a=(x ) {y } a - y -> y a/> ->> y a/> + y +> y +>> y y a/> diff --git a/src/__tests__/fixtures/attr-operators-space-before/__snapshots__/attr-operators-space-before.expected.txt b/src/__tests__/fixtures/attr-operators-space-before/__snapshots__/attr-operators-space-before.expected.txt index 9290fab3..72b9be8c 100644 --- a/src/__tests__/fixtures/attr-operators-space-before/__snapshots__/attr-operators-space-before.expected.txt +++ b/src/__tests__/fixtures/attr-operators-space-before/__snapshots__/attr-operators-space-before.expected.txt @@ -511,11 +511,14 @@ │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -66╭─ =y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >=y" - │ ││├─ attrValue "=x >=y" +66╭─ =y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart ">=y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>=y" - │ ││├─ attrValue "=x >>=y" +73╭─ >=y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "=y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -74╭─ >>=y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>>=y" - │ ││├─ attrValue "=x >>>=y" +74╭─ >>=y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart ">=y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart @@ -647,35 +656,38 @@ │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -83╭─ y - │ ││││ ││ ││╰─ openTagEnd:selfClosed "/>" - │ ││││ ││ │╰─ tagName - │ ││││ ││ ╰─ openTagStart +83╭─ y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "" ╰─ ╰─ openTagStart -84╭─ >y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>y" - │ ││├─ attrValue "=x >>y" +84╭─ >y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -85╭─ >>y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>>y" - │ ││├─ attrValue "=x >>>y" +85╭─ >>y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart ">y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 86╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -684,7 +696,6 @@ │ ││├─ attrValue "=x (y )" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 87╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -693,7 +704,6 @@ │ ││├─ attrValue "=x {y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 88╭─ y a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -702,7 +712,6 @@ │ ││├─ attrValue "=x =>y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 89╭─ (y ) a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -711,7 +720,6 @@ │ ││├─ attrValue "=x => (y )" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 90╭─ {y } a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -720,7 +728,6 @@ │ ││├─ attrValue "=x => {y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart 91╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" @@ -729,7 +736,5 @@ │ ││├─ attrValue "=( x ) {y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -92╭─ - ╰─ ╰─ text "\n" \ No newline at end of file +92╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-operators-space-before/input.marko b/src/__tests__/fixtures/attr-operators-space-before/input.marko index f87ef5aa..7c1d7a8d 100644 --- a/src/__tests__/fixtures/attr-operators-space-before/input.marko +++ b/src/__tests__/fixtures/attr-operators-space-before/input.marko @@ -63,15 +63,15 @@ a=( x ) {y } a -=y a/> +=y ->=y a/> ->>=y a/> +>=y +>>=y @@ -80,9 +80,9 @@ a=( x ) {y } a -y ->y a/> ->>y a/> +y +>y +>>y y a/> diff --git a/src/__tests__/fixtures/attr-operators-space-between/__snapshots__/attr-operators-space-between.expected.txt b/src/__tests__/fixtures/attr-operators-space-between/__snapshots__/attr-operators-space-between.expected.txt index 65d484a0..3c041919 100644 --- a/src/__tests__/fixtures/attr-operators-space-between/__snapshots__/attr-operators-space-between.expected.txt +++ b/src/__tests__/fixtures/attr-operators-space-between/__snapshots__/attr-operators-space-between.expected.txt @@ -384,11 +384,10 @@ │ ├─ openTagEnd ╰─ ╰─ tagName 49╭─ a=x ++ y a - │ │││ │ │ ╰─ attrName - │ │││ │ ╰─ attrName - │ │││ ╰─ attrName "++" - │ ││╰─ attrValue.value - │ │├─ attrValue "=x" + │ │││ │ ╰─ attrName + │ │││ ╰─ attrName + │ ││╰─ attrValue.value "x ++" + │ │├─ attrValue "=x ++" │ │╰─ attrName │ ├─ closeTagEnd(a) │ ├─ openTagEnd @@ -700,11 +699,14 @@ │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -89╭─ = y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >= y" - │ ││├─ attrValue "=x >= y" +89╭─ = y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart ">= y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>= y" - │ ││├─ attrValue "=x >>= y" +96╭─ >= y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "= y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -97╭─ >>= y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>>= y" - │ ││├─ attrValue "=x >>>= y" +97╭─ >>= y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart ">= y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart @@ -879,142 +887,149 @@ │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -111╭─ - │ ││││ │ │ │╰─ openTagEnd:selfClosed "/>" - │ ││││ │ │ ╰─ attrName - │ ││││ │ ╰─ attrName - │ ││││ ╰─ attrName "++" - │ │││╰─ attrValue.value - │ ││├─ attrValue "=x" +111╭─ + │ ││││ │╰─ openTagEnd:selfClosed "/>" + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "typeof ++ y" + │ ││├─ attrValue "=typeof ++ y" + │ ││╰─ attrName + │ │╰─ tagName + ╰─ ╰─ openTagStart +112╭─ + │ ││││ │╰─ openTagEnd:selfClosed "/>" + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "typeof y ++" + │ ││├─ attrValue "=typeof y ++" + │ ││╰─ attrName + │ │╰─ tagName + ╰─ ╰─ openTagStart +113╭─ + │ ││││ │ │╰─ openTagEnd:selfClosed "/>" + │ ││││ │ ╰─ attrName + │ ││││ ╰─ attrName + │ │││╰─ attrValue.value "x ++" + │ ││├─ attrValue "=x ++" │ ││╰─ attrName │ │╰─ tagName ╰─ ╰─ openTagStart -112╭─ y - │ ││││ ││ ││╰─ openTagEnd:selfClosed "/>" - │ ││││ ││ │╰─ tagName - │ ││││ ││ ╰─ openTagStart +114╭─ y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "" ╰─ ╰─ openTagStart -113╭─ > y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >> y" - │ ││├─ attrValue "=x >> y" +115╭─ > y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart " y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -114╭─ >> y a/> - │ ││││ │╰─ openTagEnd:selfClosed "/>" - │ ││││ ╰─ attrName - │ │││╰─ attrValue.value "x >>> y" - │ ││├─ attrValue "=x >>> y" +116╭─ >> y + │ ││││ ││ │ │╰─ closeTagEnd(a) + │ ││││ ││ │ ╰─ closeTagName + │ ││││ ││ ╰─ closeTagStart "> y " + │ ││││ ╰─ openTagEnd + │ │││╰─ attrValue.value + │ ││├─ attrValue "=x" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -115╭─ +117╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "x ( y )" │ ││├─ attrValue "=x ( y )" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -116╭─ +118╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "x { y }" │ ││├─ attrValue "=x { y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -117╭─ y a/> +119╭─ y a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "x => y" │ ││├─ attrValue "=x => y" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -118╭─ ( y ) a/> +120╭─ ( y ) a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "x => ( y )" │ ││├─ attrValue "=x => ( y )" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -119╭─ { y } a/> +121╭─ { y } a/> │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "x => { y }" │ ││├─ attrValue "=x => { y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -120╭─ +122╭─ │ ││││ │╰─ openTagEnd:selfClosed "/>" │ ││││ ╰─ attrName │ │││╰─ attrValue.value "( x ) { y }" │ ││├─ attrValue "=( x ) { y }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -121╭─ { console.log("y") } a/> +123╭─ { console.log("y") } a/> │ │││ │ │╰─ openTagEnd:selfClosed "/>" │ │││ │ ╰─ attrName │ │││ ╰─ attrValue.value "(x) => { console.log(\"y\") }" │ ││├─ attrValue "= (x) => { console.log(\"y\") }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -122╭─ { console.log("y") } a/> +124╭─ { console.log("y") } a/> │ │││ │ │╰─ openTagEnd:selfClosed "/>" │ │││ │ ╰─ attrName │ │││ ╰─ attrValue.value "async x => { console.log(\"y\") }" │ ││├─ attrValue "= async x => { console.log(\"y\") }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -123╭─ +125╭─ │ │││ │ │╰─ openTagEnd:selfClosed "/>" │ │││ │ ╰─ attrName │ │││ ╰─ attrValue.value "function (x) { console.log(\"y\") }" │ ││├─ attrValue "= function (x) { console.log(\"y\") }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -124╭─ { console.log("y") } a/> +126╭─ { console.log("y") } a/> │ │││ │ │╰─ openTagEnd:selfClosed "/>" │ │││ │ ╰─ attrName │ │││ ╰─ attrValue.value "x => { console.log(\"y\") }" │ ││├─ attrValue "= x => { console.log(\"y\") }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart -125╭─ +127╭─ │ │││ │ │╰─ openTagEnd:selfClosed "/>" │ │││ │ ╰─ attrName │ │││ ╰─ attrValue.value "async function (x) { console.log(\"y\") }" │ ││├─ attrValue "= async function (x) { console.log(\"y\") }" │ ││╰─ attrName │ │╰─ tagName - │ ├─ text "\n" ╰─ ╰─ openTagStart \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-operators-space-between/input.marko b/src/__tests__/fixtures/attr-operators-space-between/input.marko index f960d5ca..af2fb653 100644 --- a/src/__tests__/fixtures/attr-operators-space-between/input.marko +++ b/src/__tests__/fixtures/attr-operators-space-between/input.marko @@ -86,15 +86,15 @@ a = async function (x) { console.log("y") } a -= y a/> += y ->= y a/> ->>= y a/> +>= y +>>= y @@ -108,10 +108,12 @@ a = async function (x) { console.log("y") } a + + - y -> y a/> ->> y a/> + y +> y +>> y y a/> diff --git a/src/states/ATTRIBUTE.ts b/src/states/ATTRIBUTE.ts index 869f6ee5..a5adfab6 100644 --- a/src/states/ATTRIBUTE.ts +++ b/src/states/ATTRIBUTE.ts @@ -8,7 +8,10 @@ import { Ranges, Meta, ErrorCode, + matchesCloseCurlyBrace, + matchesCloseParen, } from "../internal"; +import { TAG_STAGE } from "./OPEN_TAG"; const enum ATTR_STAGE { UNKNOWN, @@ -27,36 +30,6 @@ export interface AttrMeta extends Meta { bound: boolean; } -const HTML_VALUE_TERMINATORS = [ - CODE.CLOSE_ANGLE_BRACKET, - CODE.COMMA, - [CODE.FORWARD_SLASH, CODE.CLOSE_ANGLE_BRACKET], -]; - -const CONCISE_VALUE_TERMINATORS = [ - CODE.CLOSE_SQUARE_BRACKET, - CODE.SEMICOLON, - CODE.COMMA, -]; - -const HTML_NAME_TERMINATORS = [ - CODE.CLOSE_ANGLE_BRACKET, - CODE.COMMA, - CODE.OPEN_PAREN, - CODE.EQUAL, - [CODE.COLON, CODE.EQUAL], - [CODE.FORWARD_SLASH, CODE.CLOSE_ANGLE_BRACKET], -]; - -const CONCISE_NAME_TERMINATORS = [ - CODE.CLOSE_SQUARE_BRACKET, - CODE.SEMICOLON, - CODE.EQUAL, - CODE.COMMA, - CODE.OPEN_PAREN, - [CODE.COLON, CODE.EQUAL], -]; - // We enter STATE.ATTRIBUTE when we see a non-whitespace // character after reading the tag name export const ATTRIBUTE: StateDefinition = { @@ -108,33 +81,36 @@ export const ATTRIBUTE: StateDefinition = { attr.stage = ATTR_STAGE.VALUE; const expr = this.enterState(STATE.EXPRESSION); + expr.operators = true; expr.terminatedByWhitespace = true; - expr.terminator = this.isConcise - ? CONCISE_VALUE_TERMINATORS - : HTML_VALUE_TERMINATORS; + expr.shouldTerminate = this.isConcise + ? this.activeTag!.stage === TAG_STAGE.ATTR_GROUP + ? shouldTerminateConciseGroupedAttrValue + : shouldTerminateConciseAttrValue + : shouldTerminateHtmlAttrValue; } else if (code === CODE.OPEN_PAREN) { ensureAttrName(this, attr); attr.stage = ATTR_STAGE.ARGUMENT; this.pos++; // skip ( this.forward = 0; - this.enterState(STATE.EXPRESSION).terminator = CODE.CLOSE_PAREN; + this.enterState(STATE.EXPRESSION).shouldTerminate = matchesCloseParen; } else if (code === CODE.OPEN_CURLY_BRACE && attr.args) { ensureAttrName(this, attr); attr.stage = ATTR_STAGE.BLOCK; this.pos++; // skip { this.forward = 0; - const expr = this.enterState(STATE.EXPRESSION); - expr.terminatedByWhitespace = false; - expr.terminator = CODE.CLOSE_CURLY_BRACE; + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseCurlyBrace; } else if (attr.stage === ATTR_STAGE.UNKNOWN) { attr.stage = ATTR_STAGE.NAME; this.forward = 0; const expr = this.enterState(STATE.EXPRESSION); expr.terminatedByWhitespace = true; - expr.skipOperators = true; - expr.terminator = this.isConcise - ? CONCISE_NAME_TERMINATORS - : HTML_NAME_TERMINATORS; + expr.shouldTerminate = this.isConcise + ? this.activeTag!.stage === TAG_STAGE.ATTR_GROUP + ? shouldTerminateConciseGroupedAttrName + : shouldTerminateConciseAttrName + : shouldTerminateHtmlAttrName; } else { this.exitState(); } @@ -273,3 +249,103 @@ function ensureAttrName(parser: Parser, attr: AttrMeta) { }); } } + +function shouldTerminateHtmlAttrName(code: number, data: string, pos: number) { + switch (code) { + case CODE.COMMA: + case CODE.EQUAL: + case CODE.OPEN_PAREN: + case CODE.CLOSE_ANGLE_BRACKET: + return true; + case CODE.COLON: + return data.charCodeAt(pos + 1) === CODE.EQUAL; + case CODE.FORWARD_SLASH: + return data.charCodeAt(pos + 1) === CODE.CLOSE_ANGLE_BRACKET; + default: + return false; + } +} + +function shouldTerminateHtmlAttrValue(code: number, data: string, pos: number) { + switch (code) { + case CODE.COMMA: + return true; + case CODE.FORWARD_SLASH: + return data.charCodeAt(pos + 1) === CODE.CLOSE_ANGLE_BRACKET; + // Add special case for => + case CODE.CLOSE_ANGLE_BRACKET: + return data.charCodeAt(pos - 1) !== CODE.EQUAL; + default: + return false; + } +} + +function shouldTerminateConciseAttrName( + code: number, + data: string, + pos: number +) { + switch (code) { + case CODE.COMMA: + case CODE.EQUAL: + case CODE.OPEN_PAREN: + case CODE.SEMICOLON: + return true; + case CODE.COLON: + return data.charCodeAt(pos + 1) === CODE.EQUAL; + case CODE.HYPHEN: + return ( + data.charCodeAt(pos + 1) === CODE.HYPHEN && + isWhitespaceCode(data.charCodeAt(pos - 1)) + ); + default: + return false; + } +} + +function shouldTerminateConciseAttrValue( + code: number, + data: string, + pos: number +) { + switch (code) { + case CODE.COMMA: + case CODE.SEMICOLON: + return true; + case CODE.HYPHEN: + return ( + data.charCodeAt(pos + 1) === CODE.HYPHEN && + isWhitespaceCode(data.charCodeAt(pos - 1)) + ); + default: + return false; + } +} + +function shouldTerminateConciseGroupedAttrName( + code: number, + data: string, + pos: number +) { + switch (code) { + case CODE.COMMA: + case CODE.EQUAL: + case CODE.OPEN_PAREN: + case CODE.CLOSE_SQUARE_BRACKET: + return true; + case CODE.COLON: + return data.charCodeAt(pos + 1) === CODE.EQUAL; + default: + return false; + } +} + +function shouldTerminateConciseGroupedAttrValue(code: number) { + switch (code) { + case CODE.COMMA: + case CODE.CLOSE_SQUARE_BRACKET: + return true; + default: + return false; + } +} diff --git a/src/states/EXPRESSION.ts b/src/states/EXPRESSION.ts index 5fa20467..3c8a8dce 100644 --- a/src/states/EXPRESSION.ts +++ b/src/states/EXPRESSION.ts @@ -8,23 +8,29 @@ import { ErrorCode, } from "../internal"; -const enum PatternType { - HTML_ATTRS, - CONCISE_ATTRS, - CONCISE_ATTRS_GROUP, -} - export interface ExpressionMeta extends Meta { groupStack: number[]; - terminator: number | (number | number[])[]; - skipOperators: boolean; + operators: boolean; terminatedByEOL: boolean; terminatedByWhitespace: boolean; + shouldTerminate(code: number, data: string, pos: number): boolean; } -const htmlAttrsPattern = buildPattern(PatternType.HTML_ATTRS); -const conciseAttrsPattern = buildPattern(PatternType.CONCISE_ATTRS); -const conciseAttrsGroupPattern = buildPattern(PatternType.CONCISE_ATTRS_GROUP); +// Never terminate early by default. +const shouldTerminate = () => false; + +const unaryKeywords = [ + "async", + "await", + "keyof", + "class", + "function", + "new", + "typeof", + "void", +] as const; + +const binaryKeywords = ["instanceof", "in", "as", "extends"] as const; export const EXPRESSION: StateDefinition = { name: "EXPRESSION", @@ -36,8 +42,8 @@ export const EXPRESSION: StateDefinition = { start, end: start, groupStack: [], - terminator: -1, - skipOperators: false, + shouldTerminate, + operators: false, terminatedByEOL: false, terminatedByWhitespace: false, }; @@ -48,17 +54,13 @@ export const EXPRESSION: StateDefinition = { char(code, expression) { if (!expression.groupStack.length) { if (expression.terminatedByWhitespace && isWhitespaceCode(code)) { - if (!checkForOperators(this, expression)) { + if (!checkForOperators(this, expression, false)) { this.exitState(); } return; } - if ( - typeof expression.terminator === "number" - ? expression.terminator === code - : checkForTerminators(this, code, expression.terminator) - ) { + if (expression.shouldTerminate(code, this.data, this.pos)) { this.exitState(); return; } @@ -86,11 +88,7 @@ export const EXPRESSION: StateDefinition = { this.pos++; break; default: { - if ( - canCharCodeBeFollowedByDivision( - this.getPreviousNonWhitespaceCharCode() - ) - ) { + if (canFollowDivision(this.getPreviousNonWhitespaceCharCode())) { this.pos++; this.consumeWhitespace(); } else { @@ -145,7 +143,7 @@ export const EXPRESSION: StateDefinition = { if ( !expression.groupStack.length && (expression.terminatedByEOL || expression.terminatedByWhitespace) && - !checkForOperators(this, expression) + !checkForOperators(this, expression, true) ) { this.exitState(); } @@ -212,95 +210,164 @@ export const EXPRESSION: StateDefinition = { return() {}, }; -function buildPattern(type: PatternType) { - const space = type === PatternType.CONCISE_ATTRS ? "[ \\t]" : "\\s"; - const binary = - "(?:[!~*%&^|?<]+=*)+" + // Any of these characters can always continue an expression - "|:+(?!=)" + // Match a colon without matching a bound attribute - "|[>/+=-]+=|=>" + // Match equality and multi char assignment operators w/o matching equals by itself - `|(?${type === PatternType.HTML_ATTRS ? "{2,}" : "+"}` + // in html mode only consume closing angle brackets if it is >> - "|[ \\t]+(?:in(?:stanceof)?|as|extends)(?=[ \\t]+[^=/,;:>])"; // We only continue after word operators (instanceof/in) when they are not followed by a terminator - const unary = - "\\b(?])`; // if we have spaces followed by an opening bracket, we'll consume the spaces and let the expression state handle the brackets - const lookBehindPattern = `(?<=${unary}|${binary})`; - return new RegExp(`${lookAheadPattern}|${lookBehindPattern}`, "ym"); -} +function checkForOperators( + parser: Parser, + expression: ExpressionMeta, + eol: boolean +) { + if (!expression.operators) return false; -function checkForOperators(parser: Parser, expression: ExpressionMeta) { - if (expression.skipOperators) { - return false; + const { pos, data } = parser; + if (lookBehindForOperator(data, pos) !== -1) { + parser.consumeWhitespace(); + parser.forward = 0; + return true; } - const pattern = parser.isConcise - ? parser.activeTag?.stage === STATE.TAG_STAGE.ATTR_GROUP - ? conciseAttrsGroupPattern - : conciseAttrsPattern - : expression.terminatedByEOL - ? conciseAttrsPattern - : htmlAttrsPattern; - pattern.lastIndex = parser.pos; - const matches = pattern.exec(parser.data); - - if (matches) { - const [match] = matches; - if (match.length === 0) { - // We matched a look behind. - parser.consumeWhitespace(); - } else { - // We matched a look ahead. - parser.pos += match.length; - } + const terminatedByEOL = expression.terminatedByEOL || parser.isConcise; + if (!(terminatedByEOL && eol)) { + const nextNonSpace = lookAheadWhile( + terminatedByEOL ? isIndentCode : isWhitespaceCode, + data, + pos + 1 + ); - // After this point we should be on the character we want to process next - // so we don't want to move forward and miss that character. - parser.forward = 0; - } else { - return false; + if ( + !expression.shouldTerminate( + data.charCodeAt(nextNonSpace), + data, + nextNonSpace + ) + ) { + const lookAheadPos = lookAheadForOperator(data, nextNonSpace); + if (lookAheadPos !== -1) { + parser.pos = lookAheadPos; + parser.forward = 0; + return true; + } + } } - return true; + return false; } -function checkForTerminators( - parser: Parser, - code: number, - terminators: (number | number[])[] -) { - outer: for (const terminator of terminators) { - if (typeof terminator === "number") { - if (code === terminator) return true; - } else { - if (terminator[0] === code) { - for (let i = terminator.length; i-- > 1; ) { - if (parser.data.charCodeAt(parser.pos + i) !== terminator[i]) - continue outer; +function lookBehindForOperator(data: string, pos: number): number { + const curPos = pos - 1; + const code = data.charCodeAt(curPos); + + switch (code) { + case CODE.AMPERSAND: + case CODE.ASTERISK: + case CODE.CARET: + case CODE.COLON: + case CODE.EQUAL: + case CODE.EXCLAMATION: + case CODE.OPEN_ANGLE_BRACKET: + case CODE.CLOSE_ANGLE_BRACKET: + case CODE.PERCENT: + case CODE.PERIOD: + case CODE.PIPE: + case CODE.QUESTION: + case CODE.TILDE: + return curPos; + + // special case -- and ++ + case CODE.PLUS: + case CODE.HYPHEN: { + if (data.charCodeAt(curPos - 1) === code) { + // Check if we should continue for another reason. + // eg "typeof++ x" + return lookBehindForOperator( + data, + lookBehindWhile(isWhitespaceCode, data, curPos - 2) + ); + } + + return curPos; + } + + default: { + for (const keyword of unaryKeywords) { + const keywordPos = lookBehindFor(data, curPos, keyword); + if (keywordPos !== -1) { + return data.charCodeAt(keywordPos - 1) === CODE.PERIOD + ? -1 + : keywordPos; } + } + return -1; + } + } +} - return true; +function lookAheadForOperator(data: string, pos: number): number { + switch (data.charCodeAt(pos)) { + case CODE.AMPERSAND: + case CODE.ASTERISK: + case CODE.CARET: + case CODE.EXCLAMATION: + case CODE.OPEN_ANGLE_BRACKET: + case CODE.PERCENT: + case CODE.PIPE: + case CODE.QUESTION: + case CODE.TILDE: + case CODE.PLUS: + case CODE.HYPHEN: + case CODE.COLON: + case CODE.CLOSE_ANGLE_BRACKET: + case CODE.EQUAL: + return pos + 1; + + case CODE.FORWARD_SLASH: + case CODE.OPEN_CURLY_BRACE: + case CODE.OPEN_PAREN: + return pos; // defers to base expression state to track block groups. + + case CODE.PERIOD: + // Only match a dot if its not ... + return data.charCodeAt(pos + 1) === CODE.PERIOD ? -1 : pos + 1; + + default: { + for (const keyword of binaryKeywords) { + let nextPos = lookAheadFor(data, pos, keyword); + if (nextPos === -1) continue; + + const max = data.length - 1; + if (nextPos === max) return -1; + + let nextCode = data.charCodeAt(nextPos + 1); + if (isWhitespaceCode(nextCode)) { + // skip any whitespace after the operator + nextPos = lookAheadWhile(isWhitespaceCode, data, nextPos + 2); + if (nextPos === max) return -1; + nextCode = data.charCodeAt(nextPos); + } else if (isWordCode(nextCode)) { + // bail if we are continuing a word, eg "**in**teger" + return -1; + } + + // finally check that this is not followed by a terminator. + switch (nextCode) { + case CODE.COLON: + case CODE.COMMA: + case CODE.EQUAL: + case CODE.FORWARD_SLASH: + case CODE.CLOSE_ANGLE_BRACKET: + case CODE.SEMICOLON: + return -1; + default: + return nextPos; + } } + + return -1; } } } -function canCharCodeBeFollowedByDivision(code: number) { +function canFollowDivision(code: number) { return ( - (code >= CODE.NUMBER_0 && code <= CODE.NUMBER_9) || - (code >= CODE.UPPER_A && code <= CODE.UPPER_Z) || - (code >= CODE.LOWER_A && code <= CODE.LOWER_Z) || + isWordCode(code) || code === CODE.PERCENT || code === CODE.CLOSE_PAREN || code === CODE.PERIOD || @@ -309,3 +376,73 @@ function canCharCodeBeFollowedByDivision(code: number) { code === CODE.CLOSE_CURLY_BRACE ); } + +function isWordCode(code: number) { + return ( + (code >= CODE.UPPER_A && code <= CODE.UPPER_Z) || + (code >= CODE.LOWER_A && code <= CODE.LOWER_Z) || + (code >= CODE.NUMBER_0 && code <= CODE.NUMBER_9) || + code === CODE.UNDERSCORE + ); +} + +function isIndentCode(code: number) { + return code === CODE.TAB || code === CODE.SPACE; +} + +function lookAheadWhile( + match: (code: number) => boolean, + data: string, + pos: number +) { + const max = data.length; + for (let i = pos; i < max; i++) { + if (!match(data.charCodeAt(i))) return i; + } + + return max - 1; +} + +function lookBehindWhile( + match: (code: number) => boolean, + data: string, + pos: number +) { + let i = pos; + + do { + if (!match(data.charCodeAt(i))) { + return i + 1; + } + } while (i--); + + return 0; +} + +function lookBehindFor(data: string, pos: number, str: string) { + let i = str.length; + const endPos = pos - i + 1; + if (endPos < 0) return -1; + + while (i--) { + if (data.charCodeAt(endPos + i) !== str.charCodeAt(i)) { + return -1; + } + } + + return endPos; +} + +function lookAheadFor(data: string, pos: number, str: string) { + let i = str.length; + const endPos = pos + i; + if (endPos > data.length) return -1; + + while (i--) { + if (data.charCodeAt(pos + i) !== str.charCodeAt(i)) { + return -1; + } + } + + return endPos - 1; +} diff --git a/src/states/INLINE_SCRIPT.ts b/src/states/INLINE_SCRIPT.ts index 2b8c462d..83e2d3cb 100644 --- a/src/states/INLINE_SCRIPT.ts +++ b/src/states/INLINE_SCRIPT.ts @@ -1,4 +1,11 @@ -import { CODE, Range, STATE, StateDefinition, Meta } from "../internal"; +import { + CODE, + Range, + STATE, + StateDefinition, + Meta, + matchesCloseCurlyBrace, +} from "../internal"; interface ScriptletMeta extends Meta { block: boolean; @@ -43,11 +50,11 @@ export const INLINE_SCRIPT: StateDefinition = { if (code === CODE.OPEN_CURLY_BRACE) { inlineScript.block = true; this.pos++; // skip { - const expr = this.enterState(STATE.EXPRESSION); - expr.terminator = CODE.CLOSE_CURLY_BRACE; - expr.skipOperators = true; + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseCurlyBrace; } else { const expr = this.enterState(STATE.EXPRESSION); + expr.operators = true; expr.terminatedByEOL = true; } }, diff --git a/src/states/OPEN_TAG.ts b/src/states/OPEN_TAG.ts index e8ae3d0d..ca844836 100644 --- a/src/states/OPEN_TAG.ts +++ b/src/states/OPEN_TAG.ts @@ -7,9 +7,11 @@ import { Meta, TagType, ErrorCode, + matchesPipe, + matchesCloseParen, } from "../internal"; -export const enum TAG_STAGE { +export enum TAG_STAGE { UNKNOWN, VAR, ARGUMENT, @@ -32,24 +34,6 @@ export interface OpenTagMeta extends Meta { nestedIndent: string | undefined; parentTag: OpenTagMeta | undefined; } -const CONCISE_TAG_VAR_TERMINATORS = [ - CODE.SEMICOLON, - CODE.OPEN_PAREN, - CODE.PIPE, - CODE.EQUAL, - CODE.COMMA, - [CODE.COLON, CODE.EQUAL], -]; - -const HTML_TAG_VAR_TERMINATORS = [ - CODE.CLOSE_ANGLE_BRACKET, - CODE.OPEN_PAREN, - CODE.PIPE, - CODE.EQUAL, - CODE.COMMA, - [CODE.COLON, CODE.EQUAL], - [CODE.FORWARD_SLASH, CODE.CLOSE_ANGLE_BRACKET], -]; export const OPEN_TAG: StateDefinition = { name: "OPEN_TAG", @@ -309,10 +293,11 @@ export const OPEN_TAG: StateDefinition = { } const expr = this.enterState(STATE.EXPRESSION); + expr.operators = true; expr.terminatedByWhitespace = true; - expr.terminator = this.isConcise - ? CONCISE_TAG_VAR_TERMINATORS - : HTML_TAG_VAR_TERMINATORS; + expr.shouldTerminate = this.isConcise + ? shouldTerminateConciseTagVar + : shouldTerminateHtmlTagVar; } else if (code === CODE.OPEN_PAREN && !tag.hasAttrs) { if (tag.hasArgs) { this.emitError( @@ -325,16 +310,12 @@ export const OPEN_TAG: StateDefinition = { tag.stage = TAG_STAGE.ARGUMENT; this.pos++; // skip ( this.forward = 0; - const expr = this.enterState(STATE.EXPRESSION); - expr.skipOperators = true; - expr.terminator = CODE.CLOSE_PAREN; + this.enterState(STATE.EXPRESSION).shouldTerminate = matchesCloseParen; } else if (code === CODE.PIPE && !tag.hasAttrs) { tag.stage = TAG_STAGE.PARAMS; this.pos++; // skip | this.forward = 0; - const expr = this.enterState(STATE.EXPRESSION); - expr.skipOperators = true; - expr.terminator = CODE.PIPE; + this.enterState(STATE.EXPRESSION).shouldTerminate = matchesPipe; } else { this.forward = 0; @@ -416,3 +397,35 @@ export const OPEN_TAG: StateDefinition = { } }, }; + +function shouldTerminateConciseTagVar(code: number, data: string, pos: number) { + switch (code) { + case CODE.COMMA: + case CODE.EQUAL: + case CODE.PIPE: + case CODE.OPEN_PAREN: + case CODE.SEMICOLON: + return true; + case CODE.COLON: + return data.charCodeAt(pos + 1) === CODE.EQUAL; + default: + return false; + } +} + +function shouldTerminateHtmlTagVar(code: number, data: string, pos: number) { + switch (code) { + case CODE.PIPE: + case CODE.COMMA: + case CODE.EQUAL: + case CODE.OPEN_PAREN: + case CODE.CLOSE_ANGLE_BRACKET: + return true; + case CODE.COLON: + return data.charCodeAt(pos + 1) === CODE.EQUAL; + case CODE.FORWARD_SLASH: + return data.charCodeAt(pos + 1) === CODE.CLOSE_ANGLE_BRACKET; + default: + return false; + } +} diff --git a/src/states/PLACEHOLDER.ts b/src/states/PLACEHOLDER.ts index c3a887db..3e0929ae 100644 --- a/src/states/PLACEHOLDER.ts +++ b/src/states/PLACEHOLDER.ts @@ -5,6 +5,7 @@ import { StateDefinition, Meta, ErrorCode, + matchesCloseCurlyBrace, } from "../internal"; interface PlaceholderMeta extends Meta { @@ -94,7 +95,8 @@ export function checkForPlaceholder(parser: Parser, code: number) { parser.enterState(PLACEHOLDER).escape = escape; parser.pos += escape ? 2 : 3; // skip ${ or $!{ parser.forward = 0; - parser.enterState(STATE.EXPRESSION).terminator = CODE.CLOSE_CURLY_BRACE; + parser.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseCurlyBrace; return true; } } diff --git a/src/states/TAG_NAME.ts b/src/states/TAG_NAME.ts index e45ff421..183e1746 100644 --- a/src/states/TAG_NAME.ts +++ b/src/states/TAG_NAME.ts @@ -7,6 +7,7 @@ import { Meta, TagType, ErrorCode, + matchesCloseCurlyBrace, } from "../internal"; export interface TagNameMeta extends Meta, Ranges.Template { @@ -94,7 +95,9 @@ export const TAG_NAME: StateDefinition = { ); } - this.enterState(STATE.EXPRESSION).terminatedByEOL = true; + const expr = this.enterState(STATE.EXPRESSION); + expr.operators = true; + expr.terminatedByEOL = true; } } @@ -110,7 +113,8 @@ export const TAG_NAME: StateDefinition = { ) { this.pos += 2; // skip ${ this.forward = 0; - this.enterState(STATE.EXPRESSION).terminator = CODE.CLOSE_CURLY_BRACE; + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseCurlyBrace; } else if ( isWhitespaceCode(code) || code === CODE.EQUAL || diff --git a/src/states/TEMPLATE_STRING.ts b/src/states/TEMPLATE_STRING.ts index 95893a3a..7e4bf7f3 100644 --- a/src/states/TEMPLATE_STRING.ts +++ b/src/states/TEMPLATE_STRING.ts @@ -1,4 +1,10 @@ -import { CODE, ErrorCode, STATE, StateDefinition } from "../internal"; +import { + CODE, + ErrorCode, + STATE, + StateDefinition, + matchesCloseCurlyBrace, +} from "../internal"; export const TEMPLATE_STRING: StateDefinition = { name: "TEMPLATE_STRING", @@ -15,21 +21,21 @@ export const TEMPLATE_STRING: StateDefinition = { exit() {}, char(code) { - if ( - code === CODE.DOLLAR && - this.lookAtCharCodeAhead(1) === CODE.OPEN_CURLY_BRACE - ) { - this.pos++; // skip { - const expr = this.enterState(STATE.EXPRESSION); - expr.skipOperators = true; - expr.terminator = CODE.CLOSE_CURLY_BRACE; - } else { - if (code === CODE.BACK_SLASH) { + switch (code) { + case CODE.DOLLAR: + if (this.lookAtCharCodeAhead(1) === CODE.OPEN_CURLY_BRACE) { + this.pos++; // skip { + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseCurlyBrace; + } + break; + case CODE.BACK_SLASH: this.pos++; // skip \ - } else if (code === CODE.BACKTICK) { + break; + case CODE.BACKTICK: this.pos++; // skip ` this.exitState(); - } + break; } }, diff --git a/src/util/constants.ts b/src/util/constants.ts index 3ed47da0..9ac8a85d 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -27,6 +27,7 @@ export const enum CODE { DOLLAR = 36, PERCENT = 37, PERIOD = 46, + PLUS = 43, COMMA = 44, COLON = 58, SEMICOLON = 59, @@ -36,6 +37,10 @@ export const enum CODE { CARRIAGE_RETURN = 13, SPACE = 32, TAB = 9, + AMPERSAND = 38, + CARET = 94, + TILDE = 126, + UNDERSCORE = 95, } // Same format as https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position diff --git a/src/util/util.ts b/src/util/util.ts index 65a96bcf..090d568f 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -68,6 +68,18 @@ export function htmlEOF(this: Parser) { } } +export function matchesCloseParen(code: number) { + return code === CODE.CLOSE_PAREN; +} + +export function matchesCloseCurlyBrace(code: number) { + return code === CODE.CLOSE_CURLY_BRACE; +} + +export function matchesPipe(code: number) { + return code === CODE.PIPE; +} + function getPosAfterLine( lines: number[], startLine: number,