diff --git a/.changeset/concise-backtick-text.md b/.changeset/concise-backtick-text.md new file mode 100644 index 00000000..ec1e2164 --- /dev/null +++ b/.changeset/concise-backtick-text.md @@ -0,0 +1,9 @@ +--- +"htmljs-parser": minor +--- + +Add backtick text to concise mode. A run of backticks after a tag's attributes (or at the start of a line) begins text just like `--`, either on one line or as an indented block closed by a line of the same backticks. The text is exactly what follows the opening backticks, so leading and trailing whitespace is kept, and a closing run of the same length is optional on a single line and may be followed by a comment. A backtick after whitespace where an attribute could start was previously read as an attribute named by the template literal; it is now text. + +`onText` and `onPlaceholder` ranges now have a `delimiter`: the range of the `--` or backtick run that began the concise text block, or `undefined` outside of one. + +A concise line that ends its open tag before any tag name, such as a line that starts with a semicolon, is now reported as a parse error instead of emitting an open tag end with no tag name. diff --git a/README.md b/README.md index b31bdec9..b8bd0e59 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,14 @@ const parser = createParser({ * @example * 1╭─
Hi
* ╰─ ╰─ text "Hi" + * + * Text within a concise mode `--` or backtick block also has the + * `range.delimiter` that began the block. + * + * @example + * 1╭─ div `Hi` + * │ │╰─ text "Hi" + * ╰─ ╰─ text.delimiter "`" */ onText(range) {}, diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts index 8744bb29..2a1a1c06 100644 --- a/src/__tests__/api.test.ts +++ b/src/__tests__/api.test.ts @@ -41,6 +41,48 @@ describe("api", () => { }); }); +describe("text delimiters", () => { + function delimiters(src: string) { + const out: (string | undefined)[] = []; + const read = (range: { start: number; end: number } | undefined) => + range && src.slice(range.start, range.end); + const parser = createParser({ + onText: (range) => out.push(read(range.delimiter)), + onPlaceholder: (range) => out.push(read(range.delimiter)), + }); + parser.parse(src); + return out; + } + + it("is the hyphens or backticks that begin the text", () => { + assert.deepEqual(delimiters("a -- x"), ["--"]); + assert.deepEqual(delimiters("a ---- x"), ["----"]); + assert.deepEqual(delimiters("a `x`"), ["`"]); + assert.deepEqual(delimiters("a ``x``"), ["``"]); + assert.deepEqual(delimiters("-- x\n`y`"), ["--", "`"]); + }); + + it("applies to every line of a block and to placeholders", () => { + assert.deepEqual(delimiters("a `\n x ${y}\n z\n `"), [ + "`", + "`", + "`", + "`", + ]); + assert.deepEqual(delimiters("a --\n x\n z\n --"), ["--", "--"]); + }); + + it("is absent outside of a block", () => { + assert.deepEqual(delimiters("x ${y}"), [undefined, undefined]); + assert.deepEqual(delimiters("a `x`\ny"), ["`", undefined]); + assert.deepEqual(delimiters("a -- x\nb\n y"), ["--", undefined]); + }); + + it("covers text until the end of the input", () => { + assert.deepEqual(delimiters("a `\n x"), ["`"]); + }); +}); + describe("validation internals", () => { it("rejects doubled operators followed by a terminator", () => { assert.equal(isValidAttrValue(" ++ ,", true), 0); diff --git a/src/__tests__/fixtures/concise-backtick-attr-name-in-group/__snapshots__/concise-backtick-attr-name-in-group.expected.txt b/src/__tests__/fixtures/concise-backtick-attr-name-in-group/__snapshots__/concise-backtick-attr-name-in-group.expected.txt new file mode 100644 index 00000000..45eafcd6 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-name-in-group/__snapshots__/concise-backtick-attr-name-in-group.expected.txt @@ -0,0 +1,14 @@ +1╭─ div [a `t`] + │ │ │ ╰─ attrName "`t`" + │ │ ╰─ attrName + ╰─ ╰─ tagName "div" +2╭─ div [a, `t`] `text` + │ │ │ │ │╰─ text "text" + │ │ │ │ ╰─ openTagEnd + │ │ │ ╰─ attrName "`t`" + │ │ ╰─ attrName + │ ├─ closeTagEnd(div) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +3╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-attr-name-in-group/input.marko b/src/__tests__/fixtures/concise-backtick-attr-name-in-group/input.marko new file mode 100644 index 00000000..bd7e26d6 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-name-in-group/input.marko @@ -0,0 +1,2 @@ +div [a `t`] +div [a, `t`] `text` diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/__snapshots__/concise-backtick-attr-value-tagged-template.expected.txt b/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/__snapshots__/concise-backtick-attr-value-tagged-template.expected.txt new file mode 100644 index 00000000..f04ee8f5 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/__snapshots__/concise-backtick-attr-value-tagged-template.expected.txt @@ -0,0 +1,16 @@ +1╭─ div a=b` hi` + │ │ ││╰─ attrValue.value "b` hi`" + │ │ │╰─ attrValue "=b` hi`" + │ │ ╰─ attrName + ╰─ ╰─ tagName "div" +2╭─ div a=b`hi` `text` + │ │ │││ │╰─ text "text" + │ │ │││ ╰─ openTagEnd + │ │ ││╰─ attrValue.value "b`hi`" + │ │ │╰─ attrValue "=b`hi`" + │ │ ╰─ attrName + │ ├─ closeTagEnd(div) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +3╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/input.marko b/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/input.marko new file mode 100644 index 00000000..72ec268a --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-tagged-template/input.marko @@ -0,0 +1,2 @@ +div a=b` hi` +div a=b`hi` `text` diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-template/__snapshots__/concise-backtick-attr-value-template.expected.txt b/src/__tests__/fixtures/concise-backtick-attr-value-template/__snapshots__/concise-backtick-attr-value-template.expected.txt new file mode 100644 index 00000000..9371f0ef --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-template/__snapshots__/concise-backtick-attr-value-template.expected.txt @@ -0,0 +1,19 @@ +1╭─ div a=`x` b=`y` `text` + │ │ │││ │││ │╰─ text "text" + │ │ │││ │││ ╰─ openTagEnd + │ │ │││ ││╰─ attrValue.value "`y`" + │ │ │││ │╰─ attrValue "=`y`" + │ │ │││ ╰─ attrName + │ │ ││╰─ attrValue.value "`x`" + │ │ │╰─ attrValue "=`x`" + │ │ ╰─ attrName + ╰─ ╰─ tagName "div" +2╭─ div a=`x ${y} z` + │ │ ││╰─ attrValue.value "`x ${y} z`" + │ │ │╰─ attrValue "=`x ${y} z`" + │ │ ╰─ attrName + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-template/input.marko b/src/__tests__/fixtures/concise-backtick-attr-value-template/input.marko new file mode 100644 index 00000000..352c32e4 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-template/input.marko @@ -0,0 +1,2 @@ +div a=`x` b=`y` `text` +div a=`x ${y} z` diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-ternary/__snapshots__/concise-backtick-attr-value-ternary.expected.txt b/src/__tests__/fixtures/concise-backtick-attr-value-ternary/__snapshots__/concise-backtick-attr-value-ternary.expected.txt new file mode 100644 index 00000000..7a80a7c9 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-ternary/__snapshots__/concise-backtick-attr-value-ternary.expected.txt @@ -0,0 +1,16 @@ +1╭─ div class=a ? `x` : `y` + │ │ │ │╰─ attrValue.value "a ? `x` : `y`" + │ │ │ ╰─ attrValue "=a ? `x` : `y`" + │ │ ╰─ attrName "class" + ╰─ ╰─ tagName "div" +2╭─ div class=a ? + │ │ │ │╰─ attrValue.value "a ?\n `x` : `y`" + │ │ │ ╰─ attrValue "=a ?\n `x` : `y`" + │ │ ╰─ attrName "class" + │ ├─ closeTagEnd(div) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +3├─ `x` : `y` +4╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-attr-value-ternary/input.marko b/src/__tests__/fixtures/concise-backtick-attr-value-ternary/input.marko new file mode 100644 index 00000000..7a689b4e --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-attr-value-ternary/input.marko @@ -0,0 +1,3 @@ +div class=a ? `x` : `y` +div class=a ? + `x` : `y` diff --git a/src/__tests__/fixtures/concise-backtick-tag-args-template/__snapshots__/concise-backtick-tag-args-template.expected.txt b/src/__tests__/fixtures/concise-backtick-tag-args-template/__snapshots__/concise-backtick-tag-args-template.expected.txt new file mode 100644 index 00000000..88563377 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-tag-args-template/__snapshots__/concise-backtick-tag-args-template.expected.txt @@ -0,0 +1,14 @@ +1╭─ div(`x`) `text` + │ │ ││ │╰─ text "text" + │ │ ││ ╰─ openTagEnd + │ │ │╰─ tagArgs.value "`x`" + │ │ ╰─ tagArgs "(`x`)" + ╰─ ╰─ tagName "div" +2╭─ div(`x`) + │ │ │╰─ tagArgs.value "`x`" + │ │ ╰─ tagArgs "(`x`)" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-tag-args-template/input.marko b/src/__tests__/fixtures/concise-backtick-tag-args-template/input.marko new file mode 100644 index 00000000..ea316253 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-tag-args-template/input.marko @@ -0,0 +1,2 @@ +div(`x`) `text` +div(`x`) diff --git a/src/__tests__/fixtures/concise-backtick-text-after-comma/__snapshots__/concise-backtick-text-after-comma.expected.txt b/src/__tests__/fixtures/concise-backtick-text-after-comma/__snapshots__/concise-backtick-text-after-comma.expected.txt new file mode 100644 index 00000000..69e21572 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-after-comma/__snapshots__/concise-backtick-text-after-comma.expected.txt @@ -0,0 +1,18 @@ +1╭─ div a=1, `t` + │ │ │││ │╰─ text + │ │ │││ ╰─ openTagEnd + │ │ ││╰─ attrValue.value + │ │ │╰─ attrValue "=1" + │ │ ╰─ attrName + ╰─ ╰─ tagName "div" +2╭─ div a=1, + │ │ ││╰─ attrValue.value + │ │ │╰─ attrValue "=1" + │ │ ╰─ attrName + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ `t` + │ │╰─ text + ╰─ ╰─ openTagEnd +4╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-after-comma/input.marko b/src/__tests__/fixtures/concise-backtick-text-after-comma/input.marko new file mode 100644 index 00000000..9d53e916 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-after-comma/input.marko @@ -0,0 +1,3 @@ +div a=1, `t` +div a=1, + `t` diff --git a/src/__tests__/fixtures/concise-backtick-text-after-semicolon/__snapshots__/concise-backtick-text-after-semicolon.expected.txt b/src/__tests__/fixtures/concise-backtick-text-after-semicolon/__snapshots__/concise-backtick-text-after-semicolon.expected.txt new file mode 100644 index 00000000..6b110317 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-after-semicolon/__snapshots__/concise-backtick-text-after-semicolon.expected.txt @@ -0,0 +1,5 @@ +1╭─ div; `x` + │ │ │╰─ error(INVALID_CODE_AFTER_SEMICOLON:A semicolon indicates the end of a line. Only comments may follow it.) + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-after-semicolon/input.marko b/src/__tests__/fixtures/concise-backtick-text-after-semicolon/input.marko new file mode 100644 index 00000000..28429570 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-after-semicolon/input.marko @@ -0,0 +1 @@ +div; `x` diff --git a/src/__tests__/fixtures/concise-backtick-text-bad-indentation/__snapshots__/concise-backtick-text-bad-indentation.expected.txt b/src/__tests__/fixtures/concise-backtick-text-bad-indentation/__snapshots__/concise-backtick-text-bad-indentation.expected.txt new file mode 100644 index 00000000..ffb51912 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-bad-indentation/__snapshots__/concise-backtick-text-bad-indentation.expected.txt @@ -0,0 +1,8 @@ +1╭─ div + ╰─ ╰─ tagName "div" +2╭─ `a` + │ │ ╰─ text + ╰─ ╰─ openTagEnd +3╭─ `b` + ╰─ ╰─ error(INVALID_INDENTATION:Line indentation does match indentation of previous line) +4╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-bad-indentation/input.marko b/src/__tests__/fixtures/concise-backtick-text-bad-indentation/input.marko new file mode 100644 index 00000000..8bd6a80a --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-bad-indentation/input.marko @@ -0,0 +1,3 @@ +div + `a` + `b` diff --git a/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/__snapshots__/concise-backtick-text-block-comment-then-text.expected.txt b/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/__snapshots__/concise-backtick-text-block-comment-then-text.expected.txt new file mode 100644 index 00000000..64847504 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/__snapshots__/concise-backtick-text-block-comment-then-text.expected.txt @@ -0,0 +1,8 @@ +1╭─ div `hello` /* note */ more + │ │ ││ │ │ ╰─ error(INVALID_CHARACTER:In concise mode a javascript comment block can only be followed by whitespace characters and a newline.) + │ │ ││ │ ╰─ comment.value " note " + │ │ ││ ╰─ comment "/* note */" + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/input.marko b/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/input.marko new file mode 100644 index 00000000..5f10d6e3 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-block-comment-then-text/input.marko @@ -0,0 +1 @@ +div `hello` /* note */ more diff --git a/src/__tests__/fixtures/concise-backtick-text-crlf/__snapshots__/concise-backtick-text-crlf.expected.txt b/src/__tests__/fixtures/concise-backtick-text-crlf/__snapshots__/concise-backtick-text-crlf.expected.txt new file mode 100644 index 00000000..70ebe059 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-crlf/__snapshots__/concise-backtick-text-crlf.expected.txt @@ -0,0 +1,23 @@ +1╭─ div `a` + │ │ │╰─ text + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ span ` + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +3╭─ b + ╰─ ╰─ text "b\n" +4╭─ c + ╰─ ╰─ text +5├─ ` +6╭─ div + │ ├─ closeTagEnd(span) + ╰─ ╰─ tagName "div" +7╭─ `` + ╰─ ╰─ openTagEnd +8╭─ d + ╰─ ╰─ text " d" +9├─ `` +10╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-crlf/input.marko b/src/__tests__/fixtures/concise-backtick-text-crlf/input.marko new file mode 100644 index 00000000..6dd5cb8c --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-crlf/input.marko @@ -0,0 +1,9 @@ +div `a` +span ` + b + c + ` +div + `` + d + `` diff --git a/src/__tests__/fixtures/concise-backtick-text-empty/__snapshots__/concise-backtick-text-empty.expected.txt b/src/__tests__/fixtures/concise-backtick-text-empty/__snapshots__/concise-backtick-text-empty.expected.txt new file mode 100644 index 00000000..c88ea847 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-empty/__snapshots__/concise-backtick-text-empty.expected.txt @@ -0,0 +1,22 @@ +1╭─ div `` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ a + ╰─ ╰─ text +3├─ `` +4╭─ div ` ` + │ │ │╰─ text " " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div `` x `` + │ │ │ ╰─ text " x " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +7╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-empty/input.marko b/src/__tests__/fixtures/concise-backtick-text-empty/input.marko new file mode 100644 index 00000000..b0a358a1 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-empty/input.marko @@ -0,0 +1,6 @@ +div `` + a + `` +div ` ` +div `` x `` +span diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/__snapshots__/concise-backtick-text-eof-fence-only.expected.txt b/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/__snapshots__/concise-backtick-text-eof-fence-only.expected.txt new file mode 100644 index 00000000..714e504b --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/__snapshots__/concise-backtick-text-eof-fence-only.expected.txt @@ -0,0 +1,4 @@ +1╭─ div ` + │ │ │╰─ closeTagEnd(div) + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/input.marko b/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/input.marko new file mode 100644 index 00000000..41ddd7aa --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-fence-only/input.marko @@ -0,0 +1 @@ +div ` \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-multiline/__snapshots__/concise-backtick-text-eof-multiline.expected.txt b/src/__tests__/fixtures/concise-backtick-text-eof-multiline/__snapshots__/concise-backtick-text-eof-multiline.expected.txt new file mode 100644 index 00000000..326f76b3 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-multiline/__snapshots__/concise-backtick-text-eof-multiline.expected.txt @@ -0,0 +1,6 @@ +1╭─ div ` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ hello + │ │ ╰─ closeTagEnd(div) + ╰─ ╰─ text "hello" \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-multiline/input.marko b/src/__tests__/fixtures/concise-backtick-text-eof-multiline/input.marko new file mode 100644 index 00000000..d698480f --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-multiline/input.marko @@ -0,0 +1,2 @@ +div ` + hello \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/__snapshots__/concise-backtick-text-eof-placeholder.expected.txt b/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/__snapshots__/concise-backtick-text-eof-placeholder.expected.txt new file mode 100644 index 00000000..f19f38cb --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/__snapshots__/concise-backtick-text-eof-placeholder.expected.txt @@ -0,0 +1,5 @@ +1╭─ div `a ${b + │ │ ││ ╰─ error(MALFORMED_PLACEHOLDER:EOF reached while parsing placeholder) + │ │ │╰─ text "a " + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/input.marko b/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/input.marko new file mode 100644 index 00000000..a1212ea4 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-eof-placeholder/input.marko @@ -0,0 +1 @@ +div `a ${b \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-fence-length/__snapshots__/concise-backtick-text-fence-length.expected.txt b/src/__tests__/fixtures/concise-backtick-text-fence-length/__snapshots__/concise-backtick-text-fence-length.expected.txt new file mode 100644 index 00000000..727d5e6c --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-fence-length/__snapshots__/concise-backtick-text-fence-length.expected.txt @@ -0,0 +1,26 @@ +1╭─ div ``a`b`` + │ │ │ ╰─ text "a`b" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div ``a ` b`` + │ │ │ ╰─ text "a ` b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div ```a``b``` + │ │ │ ╰─ text "a``b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div `a``b` + │ │ │╰─ text "a``b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div ``a` + │ │ │ ╰─ text "a`" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-fence-length/input.marko b/src/__tests__/fixtures/concise-backtick-text-fence-length/input.marko new file mode 100644 index 00000000..fe0f0185 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-fence-length/input.marko @@ -0,0 +1,5 @@ +div ``a`b`` +div ``a ` b`` +div ```a``b``` +div `a``b` +div ``a` diff --git a/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/__snapshots__/concise-backtick-text-matches-hyphen-text.expected.txt b/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/__snapshots__/concise-backtick-text-matches-hyphen-text.expected.txt new file mode 100644 index 00000000..2d3690e9 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/__snapshots__/concise-backtick-text-matches-hyphen-text.expected.txt @@ -0,0 +1,46 @@ +1╭─ div -- hello + │ │ │ ╰─ text "hello" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ -- hello + │ │ ╰─ text "hello" + ╰─ ╰─ openTagEnd +4╭─ div -- + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ hello + ╰─ ╰─ text "hello" +6╭─ div `hello + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +7╭─ div `hello` + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +8╭─ div ` + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +9╭─ hello + ╰─ ╰─ text "hello" +10╭─ div + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +11╭─ `hello + │ │ ╰─ text "hello" + ╰─ ╰─ openTagEnd +12╭─ div + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +13╭─ `hello` + │ │ ╰─ text "hello" + ╰─ ╰─ openTagEnd +14╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/input.marko b/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/input.marko new file mode 100644 index 00000000..62423035 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-matches-hyphen-text/input.marko @@ -0,0 +1,13 @@ +div -- hello +div + -- hello +div -- + hello +div `hello +div `hello` +div ` + hello +div + `hello +div + `hello` diff --git a/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/__snapshots__/concise-backtick-text-mid-line-backtick.expected.txt b/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/__snapshots__/concise-backtick-text-mid-line-backtick.expected.txt new file mode 100644 index 00000000..bd74f1ea --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/__snapshots__/concise-backtick-text-mid-line-backtick.expected.txt @@ -0,0 +1,26 @@ +1╭─ div `say `hi` + │ │ │╰─ text "say `hi" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div `a` b + │ │ │╰─ text "a` b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div `a` `b` + │ │ │╰─ text "a` `b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div `a`, b=1 + │ │ │╰─ text "a`, b=1" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div `a`b` + │ │ │╰─ text "a`b" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/input.marko b/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/input.marko new file mode 100644 index 00000000..7ded1fef --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-mid-line-backtick/input.marko @@ -0,0 +1,5 @@ +div `say `hi` +div `a` b +div `a` `b` +div `a`, b=1 +div `a`b` diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/__snapshots__/concise-backtick-text-multiline-blank-lines.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/__snapshots__/concise-backtick-text-multiline-blank-lines.expected.txt new file mode 100644 index 00000000..d63e11bc --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/__snapshots__/concise-backtick-text-multiline-blank-lines.expected.txt @@ -0,0 +1,16 @@ +1╭─ div ` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ a + ╰─ ╰─ text "a\n\n" +3├─ +4╭─ b + ╰─ ╰─ text +5├─ +6├─ +7╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +8╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/input.marko new file mode 100644 index 00000000..bb9e0447 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-blank-lines/input.marko @@ -0,0 +1,7 @@ +div ` + a + + b + + +span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/__snapshots__/concise-backtick-text-multiline-closing-fence.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/__snapshots__/concise-backtick-text-multiline-closing-fence.expected.txt new file mode 100644 index 00000000..85a4d2ba --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/__snapshots__/concise-backtick-text-multiline-closing-fence.expected.txt @@ -0,0 +1,23 @@ +1╭─ div ` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ hello + ╰─ ╰─ text "hello" +3├─ ` +4╭─ div `` + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ a + ╰─ ╰─ text "a\n" +6╭─ `b` + ╰─ ╰─ text "`b`\n" +7╭─ ``` + ╰─ ╰─ text "```" +8├─ `` +9╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +10╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/input.marko new file mode 100644 index 00000000..55ea11f1 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-closing-fence/input.marko @@ -0,0 +1,9 @@ +div ` + hello + ` +div `` + a + `b` + ``` + `` +span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/__snapshots__/concise-backtick-text-multiline-dedent.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/__snapshots__/concise-backtick-text-multiline-dedent.expected.txt new file mode 100644 index 00000000..5440178e --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/__snapshots__/concise-backtick-text-multiline-dedent.expected.txt @@ -0,0 +1,17 @@ +1╭─ div + ╰─ ╰─ tagName "div" +2╭─ p ` + │ │ │ ╰─ openTagEnd + │ │ ╰─ tagName + ╰─ ╰─ openTagEnd +3╭─ a + ╰─ ╰─ text "a\n" +4╭─ b + ╰─ ╰─ text +5╭─ span + │ │ ╰─ tagName "span" + ╰─ ╰─ closeTagEnd(p) +6╭─ + │ ├─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/input.marko new file mode 100644 index 00000000..dd49de3d --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-dedent/input.marko @@ -0,0 +1,5 @@ +div + p ` + a + b + span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/__snapshots__/concise-backtick-text-multiline-matches-hyphen-text.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/__snapshots__/concise-backtick-text-multiline-matches-hyphen-text.expected.txt new file mode 100644 index 00000000..7ae59877 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/__snapshots__/concise-backtick-text-multiline-matches-hyphen-text.expected.txt @@ -0,0 +1,51 @@ +1╭─ div -- + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ hello + ╰─ ╰─ text "hello\n" +3╭─ world + ╰─ ╰─ text "world" +4├─ -- +5╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +6╭─ div ` + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(span) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +7╭─ hello + ╰─ ╰─ text "hello\n" +8╭─ world + ╰─ ╰─ text "world" +9├─ ` +10╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +11╭─ div + │ ├─ closeTagEnd(span) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +12╭─ -- + ╰─ ╰─ openTagEnd +13╭─ hello + ╰─ ╰─ text " hello" +14├─ -- +15╭─ span + ╰─ ╰─ tagName "span" +16╭─ div + │ ├─ closeTagEnd(div) + │ ├─ closeTagEnd(span) + │ ├─ openTagEnd + ╰─ ╰─ tagName "div" +17╭─ ` + ╰─ ╰─ openTagEnd +18╭─ hello + ╰─ ╰─ text " hello" +19├─ ` +20╭─ span + ╰─ ╰─ tagName "span" +21╭─ + │ ├─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/input.marko new file mode 100644 index 00000000..faaa882d --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-matches-hyphen-text/input.marko @@ -0,0 +1,20 @@ +div -- + hello + world + -- +span +div ` + hello + world + ` +span +div + -- + hello + -- + span +div + ` + hello + ` + span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/__snapshots__/concise-backtick-text-multiline-nested-closing-fence.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/__snapshots__/concise-backtick-text-multiline-nested-closing-fence.expected.txt new file mode 100644 index 00000000..d7205521 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/__snapshots__/concise-backtick-text-multiline-nested-closing-fence.expected.txt @@ -0,0 +1,15 @@ +1╭─ div + ╰─ ╰─ tagName "div" +2╭─ ` + ╰─ ╰─ openTagEnd +3╭─ hello + ╰─ ╰─ text " hello\n" +4╭─ `x` + ╰─ ╰─ text " `x`" +5├─ ` +6╭─ span + ╰─ ╰─ tagName "span" +7╭─ + │ ├─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/input.marko new file mode 100644 index 00000000..d3162550 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-nested-closing-fence/input.marko @@ -0,0 +1,6 @@ +div + ` + hello + `x` + ` + span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/__snapshots__/concise-backtick-text-multiline-tabs.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/__snapshots__/concise-backtick-text-multiline-tabs.expected.txt new file mode 100644 index 00000000..ad785d74 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/__snapshots__/concise-backtick-text-multiline-tabs.expected.txt @@ -0,0 +1,15 @@ +1╭─ div + ╰─ ╰─ tagName "div" +2╭─ ` + ╰─ ╰─ openTagEnd +3╭─ a + ╰─ ╰─ text "\ta\n" +4╭─ b + ╰─ ╰─ text "\tb" +5├─ ` +6╭─ span + ╰─ ╰─ tagName "span" +7╭─ + │ ├─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/input.marko new file mode 100644 index 00000000..8fbab6c2 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline-tabs/input.marko @@ -0,0 +1,6 @@ +div + ` + a + b + ` + span diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline/__snapshots__/concise-backtick-text-multiline.expected.txt b/src/__tests__/fixtures/concise-backtick-text-multiline/__snapshots__/concise-backtick-text-multiline.expected.txt new file mode 100644 index 00000000..8173b89c --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline/__snapshots__/concise-backtick-text-multiline.expected.txt @@ -0,0 +1,13 @@ +1╭─ div ` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ hello + ╰─ ╰─ text "hello\n" +3╭─ world + ╰─ ╰─ text "world" +4╭─ span + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "span" +5╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(span) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-multiline/input.marko b/src/__tests__/fixtures/concise-backtick-text-multiline/input.marko new file mode 100644 index 00000000..e964256b --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-multiline/input.marko @@ -0,0 +1,4 @@ +div ` + hello + world +span diff --git a/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/__snapshots__/concise-backtick-text-open-tag-shapes.expected.txt b/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/__snapshots__/concise-backtick-text-open-tag-shapes.expected.txt new file mode 100644 index 00000000..58607d02 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/__snapshots__/concise-backtick-text-open-tag-shapes.expected.txt @@ -0,0 +1,79 @@ +1╭─ div class=a id=b `hello` + │ │ │ ││ │ ││ │╰─ text "hello" + │ │ │ ││ │ ││ ╰─ openTagEnd + │ │ │ ││ │ │╰─ attrValue.value + │ │ │ ││ │ ╰─ attrValue "=b" + │ │ │ ││ ╰─ attrName "id" + │ │ │ │╰─ attrValue.value + │ │ │ ╰─ attrValue "=a" + │ │ ╰─ attrName "class" + ╰─ ╰─ tagName "div" +2╭─ div class="a" `hello` + │ │ │ ││ │╰─ text "hello" + │ │ │ ││ ╰─ openTagEnd + │ │ │ │╰─ attrValue.value "\"a\"" + │ │ │ ╰─ attrValue "=\"a\"" + │ │ ╰─ attrName "class" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div disabled `hello` + │ │ │ │╰─ text "hello" + │ │ │ ╰─ openTagEnd + │ │ ╰─ attrName "disabled" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div(x) `hello` + │ │ ││ │╰─ text "hello" + │ │ ││ ╰─ openTagEnd + │ │ │╰─ tagArgs.value + │ │ ╰─ tagArgs "(x)" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div/v `hello` + │ │ ││ │╰─ text "hello" + │ │ ││ ╰─ openTagEnd + │ │ │╰─ tagVar.value + │ │ ╰─ tagVar "/v" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ div|p| `hello` + │ │ ││ │╰─ text "hello" + │ │ ││ ╰─ openTagEnd + │ │ │╰─ tagParams.value + │ │ ╰─ tagParams "|p|" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +7╭─ div [a, b] `hello` + │ │ │ │ │╰─ text "hello" + │ │ │ │ ╰─ openTagEnd + │ │ │ ╰─ attrName + │ │ ╰─ attrName + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +8╭─ div.a#b `hello` + │ │ ││││ │╰─ text "hello" + │ │ ││││ ╰─ openTagEnd + │ │ │││╰─ tagShorthandId.quasis[0] + │ │ ││╰─ tagShorthandId "#b" + │ │ │╰─ tagShorthandClass.quasis[0] + │ │ ╰─ tagShorthandClass ".a" + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +9╭─ ${tag} `hello` + │ │ ││╰─ text "hello" + │ │ │╰─ openTagEnd + │ │ ╰─ tagName.quasis[1] + │ ├─ closeTagEnd(div) + │ ├─ tagName.expressions[0] "${tag}" + │ ├─ tagName.quasis[0] + ╰─ ╰─ tagName "${tag}" +10╭─ div a=1 /* c */ `hello` + │ │ │││ │╰─ text "hello" + │ │ │││ ╰─ openTagEnd + │ │ ││╰─ attrValue.value "1 /* c */" + │ │ │╰─ attrValue "=1 /* c */" + │ │ ╰─ attrName + │ ├─ closeTagEnd(${tag}) + ╰─ ╰─ tagName "div" +11╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/input.marko b/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/input.marko new file mode 100644 index 00000000..10b6ab8f --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-open-tag-shapes/input.marko @@ -0,0 +1,10 @@ +div class=a id=b `hello` +div class="a" `hello` +div disabled `hello` +div(x) `hello` +div/v `hello` +div|p| `hello` +div [a, b] `hello` +div.a#b `hello` +${tag} `hello` +div a=1 /* c */ `hello` diff --git a/src/__tests__/fixtures/concise-backtick-text-root-level/__snapshots__/concise-backtick-text-root-level.expected.txt b/src/__tests__/fixtures/concise-backtick-text-root-level/__snapshots__/concise-backtick-text-root-level.expected.txt new file mode 100644 index 00000000..44bbc464 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-root-level/__snapshots__/concise-backtick-text-root-level.expected.txt @@ -0,0 +1,8 @@ +1╭─ `top` + ╰─ ╰─ text "top" +2├─ ` +3╭─ multi + ╰─ ╰─ text " multi\n line" +4├─ line +5├─ ` +6╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-root-level/input.marko b/src/__tests__/fixtures/concise-backtick-text-root-level/input.marko new file mode 100644 index 00000000..634b33e6 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-root-level/input.marko @@ -0,0 +1,5 @@ +`top` +` + multi + line +` diff --git a/src/__tests__/fixtures/concise-backtick-text-script-line-start/__snapshots__/concise-backtick-text-script-line-start.expected.txt b/src/__tests__/fixtures/concise-backtick-text-script-line-start/__snapshots__/concise-backtick-text-script-line-start.expected.txt new file mode 100644 index 00000000..32f9087a --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-line-start/__snapshots__/concise-backtick-text-script-line-start.expected.txt @@ -0,0 +1,11 @@ +1╭─ script + ╰─ ╰─ tagName "script" +2╭─ `x` + │ │ ╰─ text + ╰─ ╰─ openTagEnd +3╭─ `y` + ╰─ ╰─ text +4╭─ -- z + ╰─ ╰─ text +5╭─ + ╰─ ╰─ closeTagEnd(script) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-script-line-start/input.marko b/src/__tests__/fixtures/concise-backtick-text-script-line-start/input.marko new file mode 100644 index 00000000..f9f68c93 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-line-start/input.marko @@ -0,0 +1,4 @@ +script + `x` + `y` + -- z diff --git a/src/__tests__/fixtures/concise-backtick-text-script-multiline/__snapshots__/concise-backtick-text-script-multiline.expected.txt b/src/__tests__/fixtures/concise-backtick-text-script-multiline/__snapshots__/concise-backtick-text-script-multiline.expected.txt new file mode 100644 index 00000000..7432e62f --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-multiline/__snapshots__/concise-backtick-text-script-multiline.expected.txt @@ -0,0 +1,18 @@ +1╭─ script ` + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "script" +2╭─ const a = `x`; + ╰─ ╰─ text "const a = `x`;\n" +3╭─ const b = ` + ╰─ ╰─ text "const b = `\n y`;" +4├─ y`; +5├─ ` +6╭─ style ` + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(script) + ╰─ ╰─ tagName "style" +7╭─ a { b: c } + ╰─ ╰─ text "a { b: c }" +8├─ ` +9╭─ + ╰─ ╰─ closeTagEnd(style) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-script-multiline/input.marko b/src/__tests__/fixtures/concise-backtick-text-script-multiline/input.marko new file mode 100644 index 00000000..7414df36 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-multiline/input.marko @@ -0,0 +1,8 @@ +script ` + const a = `x`; + const b = ` + y`; + ` +style ` + a { b: c } + ` diff --git a/src/__tests__/fixtures/concise-backtick-text-script-style/__snapshots__/concise-backtick-text-script-style.expected.txt b/src/__tests__/fixtures/concise-backtick-text-script-style/__snapshots__/concise-backtick-text-script-style.expected.txt new file mode 100644 index 00000000..00292885 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-style/__snapshots__/concise-backtick-text-script-style.expected.txt @@ -0,0 +1,30 @@ +1╭─ script `a(`x`)` + │ │ │╰─ text "a(`x`)" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "script" +2╭─ script `x = `a`` + │ │ │╰─ text "x = `a`" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(script) + ╰─ ╰─ tagName "script" +3╭─ script ``x = `a` `` + │ │ │ ╰─ text "x = `a` " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(script) + ╰─ ╰─ tagName "script" +4╭─ script `x` // c + │ │ ││ │ ╰─ comment.value " c" + │ │ ││ ╰─ comment "// c" + │ │ │╰─ text + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(script) + ╰─ ╰─ tagName "script" +5╭─ style.less `a { b: c }` + │ │ ││ │╰─ text "a { b: c }" + │ │ ││ ╰─ openTagEnd + │ │ │╰─ tagShorthandClass.quasis[0] "less" + │ │ ╰─ tagShorthandClass ".less" + │ ├─ closeTagEnd(script) + ╰─ ╰─ tagName "style" +6╭─ + ╰─ ╰─ closeTagEnd(style) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-script-style/input.marko b/src/__tests__/fixtures/concise-backtick-text-script-style/input.marko new file mode 100644 index 00000000..dfd80efd --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-script-style/input.marko @@ -0,0 +1,5 @@ +script `a(`x`)` +script `x = `a`` +script ``x = `a` `` +script `x` // c +style.less `a { b: c }` diff --git a/src/__tests__/fixtures/concise-backtick-text-siblings/__snapshots__/concise-backtick-text-siblings.expected.txt b/src/__tests__/fixtures/concise-backtick-text-siblings/__snapshots__/concise-backtick-text-siblings.expected.txt new file mode 100644 index 00000000..e0318e61 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-siblings/__snapshots__/concise-backtick-text-siblings.expected.txt @@ -0,0 +1,26 @@ +1╭─ ul + ╰─ ╰─ tagName "ul" +2╭─ li `a` + │ │ │ │╰─ text + │ │ │ ╰─ openTagEnd + │ │ ╰─ tagName "li" + ╰─ ╰─ openTagEnd +3╭─ li `b` + │ │ │ │╰─ text + │ │ │ ╰─ openTagEnd + │ │ ╰─ tagName "li" + ╰─ ╰─ closeTagEnd(li) +4╭─ span `c` + │ │ │╰─ text + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "span" +5╭─ li + │ │ ╰─ tagName "li" + │ ├─ closeTagEnd(li) + ╰─ ╰─ closeTagEnd(span) +6╭─ `d` + │ │ ╰─ text + ╰─ ╰─ openTagEnd +7╭─ + │ ├─ closeTagEnd(ul) + ╰─ ╰─ closeTagEnd(li) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-siblings/input.marko b/src/__tests__/fixtures/concise-backtick-text-siblings/input.marko new file mode 100644 index 00000000..b1e4b625 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-siblings/input.marko @@ -0,0 +1,6 @@ +ul + li `a` + li `b` + span `c` + li + `d` diff --git a/src/__tests__/fixtures/concise-backtick-text-single-line/__snapshots__/concise-backtick-text-single-line.expected.txt b/src/__tests__/fixtures/concise-backtick-text-single-line/__snapshots__/concise-backtick-text-single-line.expected.txt new file mode 100644 index 00000000..c0cb891a --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-single-line/__snapshots__/concise-backtick-text-single-line.expected.txt @@ -0,0 +1,46 @@ +1╭─ div `hello` + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div `hello + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div ` hello ` + │ │ │╰─ text " hello " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div `a ${b} c` + │ │ ││ │ │ ╰─ text " c" + │ │ ││ │ ╰─ placeholder:escape.value + │ │ ││ ╰─ placeholder:escape "${b}" + │ │ │╰─ text "a " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div `${a}` + │ │ ││ ╰─ placeholder:escape.value + │ │ │╰─ placeholder:escape "${a}" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ div `hi` + │ │ │││││ │ │╰─ closeTagEnd(b) + │ │ │││││ │ ╰─ closeTagName + │ │ │││││ ╰─ closeTagStart "hi` +div `$ not a scriptlet` diff --git a/src/__tests__/fixtures/concise-backtick-text-trailing-comment/__snapshots__/concise-backtick-text-trailing-comment.expected.txt b/src/__tests__/fixtures/concise-backtick-text-trailing-comment/__snapshots__/concise-backtick-text-trailing-comment.expected.txt new file mode 100644 index 00000000..9b453728 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-trailing-comment/__snapshots__/concise-backtick-text-trailing-comment.expected.txt @@ -0,0 +1,29 @@ +1╭─ div `hello` // note + │ │ ││ │ ╰─ comment.value " note" + │ │ ││ ╰─ comment "// note" + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div `hello`// note + │ │ ││ │ ╰─ comment.value " note" + │ │ ││ ╰─ comment "// note" + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div `hello` /* note */ + │ │ ││ │ ╰─ comment.value " note " + │ │ ││ ╰─ comment "/* note */" + │ │ │╰─ text "hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div `hello // note + │ │ ││ │ ╰─ comment.value " note" + │ │ ││ ╰─ comment "// note" + │ │ │╰─ text "hello " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-trailing-comment/input.marko b/src/__tests__/fixtures/concise-backtick-text-trailing-comment/input.marko new file mode 100644 index 00000000..a1cece6e --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-trailing-comment/input.marko @@ -0,0 +1,4 @@ +div `hello` // note +div `hello`// note +div `hello` /* note */ +div `hello // note diff --git a/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/__snapshots__/concise-backtick-text-unclosed-mixed-mode-tag.expected.txt b/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/__snapshots__/concise-backtick-text-unclosed-mixed-mode-tag.expected.txt new file mode 100644 index 00000000..512dd5a5 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/__snapshots__/concise-backtick-text-unclosed-mixed-mode-tag.expected.txt @@ -0,0 +1,9 @@ +1╭─ div `x` + │ │ ││││╰─ text + │ │ │││╰─ openTagEnd + │ │ ││╰─ tagName + │ │ │├─ error(MISSING_END_TAG:Missing ending "b" tag) "" + │ │ │╰─ openTagStart + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/input.marko b/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/input.marko new file mode 100644 index 00000000..0a2a3339 --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-unclosed-mixed-mode-tag/input.marko @@ -0,0 +1 @@ +div `x` diff --git a/src/__tests__/fixtures/concise-backtick-text-whitespace/__snapshots__/concise-backtick-text-whitespace.expected.txt b/src/__tests__/fixtures/concise-backtick-text-whitespace/__snapshots__/concise-backtick-text-whitespace.expected.txt new file mode 100644 index 00000000..74f85b2c --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-whitespace/__snapshots__/concise-backtick-text-whitespace.expected.txt @@ -0,0 +1,41 @@ +1╭─ div ` hello` + │ │ │╰─ text " hello" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div ` hello` + │ │ │╰─ text " hello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ div ` hello` + │ │ │╰─ text "\thello" + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +4╭─ div `hello ` + │ │ │╰─ text "hello " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ div ` hello ` + │ │ │╰─ text " hello " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +6╭─ div ` ` + │ │ │╰─ text " " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +7╭─ div ` ` + │ │ │╰─ text " " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +8╭─ div `` a `` + │ │ │ ╰─ text " a " + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +9╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-backtick-text-whitespace/input.marko b/src/__tests__/fixtures/concise-backtick-text-whitespace/input.marko new file mode 100644 index 00000000..94c6794d --- /dev/null +++ b/src/__tests__/fixtures/concise-backtick-text-whitespace/input.marko @@ -0,0 +1,8 @@ +div ` hello` +div ` hello` +div ` hello` +div `hello ` +div ` hello ` +div ` ` +div ` ` +div `` a `` diff --git a/src/__tests__/fixtures/concise-comma-line-without-tag-name/__snapshots__/concise-comma-line-without-tag-name.expected.txt b/src/__tests__/fixtures/concise-comma-line-without-tag-name/__snapshots__/concise-comma-line-without-tag-name.expected.txt new file mode 100644 index 00000000..60c55b38 --- /dev/null +++ b/src/__tests__/fixtures/concise-comma-line-without-tag-name/__snapshots__/concise-comma-line-without-tag-name.expected.txt @@ -0,0 +1,3 @@ +1╭─ ,-- text + ╰─ ╰─ error(MALFORMED_OPEN_TAG:A concise tag must start with a tag name) +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-comma-line-without-tag-name/input.marko b/src/__tests__/fixtures/concise-comma-line-without-tag-name/input.marko new file mode 100644 index 00000000..a7af584a --- /dev/null +++ b/src/__tests__/fixtures/concise-comma-line-without-tag-name/input.marko @@ -0,0 +1 @@ +,-- text diff --git a/src/__tests__/fixtures/concise-hyphen-text-with-backticks/__snapshots__/concise-hyphen-text-with-backticks.expected.txt b/src/__tests__/fixtures/concise-hyphen-text-with-backticks/__snapshots__/concise-hyphen-text-with-backticks.expected.txt new file mode 100644 index 00000000..f8b819fc --- /dev/null +++ b/src/__tests__/fixtures/concise-hyphen-text-with-backticks/__snapshots__/concise-hyphen-text-with-backticks.expected.txt @@ -0,0 +1,18 @@ +1╭─ div -- `t` + │ │ │ ╰─ text "`t`" + │ │ ╰─ openTagEnd + ╰─ ╰─ tagName "div" +2╭─ div + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +3╭─ -- `t` and `u` + │ │ ╰─ text "`t` and `u`" + ╰─ ╰─ openTagEnd +4╭─ div -- + │ │ ╰─ openTagEnd + │ ├─ closeTagEnd(div) + ╰─ ╰─ tagName "div" +5╭─ `t` + ╰─ ╰─ text "`t`" +6╭─ + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-hyphen-text-with-backticks/input.marko b/src/__tests__/fixtures/concise-hyphen-text-with-backticks/input.marko new file mode 100644 index 00000000..03e99b6e --- /dev/null +++ b/src/__tests__/fixtures/concise-hyphen-text-with-backticks/input.marko @@ -0,0 +1,5 @@ +div -- `t` +div + -- `t` and `u` +div -- + `t` diff --git a/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/__snapshots__/concise-semicolon-line-without-tag-name.expected.txt b/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/__snapshots__/concise-semicolon-line-without-tag-name.expected.txt new file mode 100644 index 00000000..7a26b6ff --- /dev/null +++ b/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/__snapshots__/concise-semicolon-line-without-tag-name.expected.txt @@ -0,0 +1,6 @@ +1╭─ div + ╰─ ╰─ tagName "div" +2╭─ ; + │ │ ╰─ error(MALFORMED_OPEN_TAG:A concise tag must start with a tag name) + ╰─ ╰─ openTagEnd +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/input.marko b/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/input.marko new file mode 100644 index 00000000..1e9bf22c --- /dev/null +++ b/src/__tests__/fixtures/concise-semicolon-line-without-tag-name/input.marko @@ -0,0 +1,2 @@ +div + ; diff --git a/src/__tests__/fixtures/html-backtick-literal-text/__snapshots__/html-backtick-literal-text.expected.txt b/src/__tests__/fixtures/html-backtick-literal-text/__snapshots__/html-backtick-literal-text.expected.txt new file mode 100644 index 00000000..e9935485 --- /dev/null +++ b/src/__tests__/fixtures/html-backtick-literal-text/__snapshots__/html-backtick-literal-text.expected.txt @@ -0,0 +1,27 @@ +1╭─
`a`
+ │ ││ ││ │ │ ╰─ closeTagEnd(div) + │ ││ ││ │ ╰─ closeTagName "div" + │ ││ ││ ╰─ closeTagStart "`a` + │ ││ ││ │ │ ╰─ closeTagEnd(script) + │ ││ ││ │ ╰─ closeTagName "script" + │ ││ ││ ╰─ closeTagStart " + │ ││ ╰─ openTagEnd + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +4╭─ `a` + ╰─ ╰─ text "\n `a`\n" +5╭─ + │ │ │ ╰─ closeTagEnd(div) + │ │ ╰─ closeTagName "div" + ╰─ ╰─ closeTagStart " `a` + +
+ `a` +
diff --git a/src/__tests__/fixtures/script-concise-invalid-nested-tag/__snapshots__/script-concise-invalid-nested-tag.expected.txt b/src/__tests__/fixtures/script-concise-invalid-nested-tag/__snapshots__/script-concise-invalid-nested-tag.expected.txt index 30afb7a1..e0483839 100644 --- a/src/__tests__/fixtures/script-concise-invalid-nested-tag/__snapshots__/script-concise-invalid-nested-tag.expected.txt +++ b/src/__tests__/fixtures/script-concise-invalid-nested-tag/__snapshots__/script-concise-invalid-nested-tag.expected.txt @@ -1,6 +1,6 @@ 1╭─ script ╰─ ╰─ tagName "script" 2╭─ p - This is not allowed - │ │ ╰─ error(INVALID_LINE_START:A line within a tag that only allows text content must begin with a "-" character) + │ │ ╰─ error(INVALID_LINE_START:A line within a tag that only allows text content must begin with "--" or a backtick) ╰─ ╰─ openTagEnd 3╰─ div \ No newline at end of file diff --git a/src/core/Parser.ts b/src/core/Parser.ts index 304f0306..1c19eb2d 100644 --- a/src/core/Parser.ts +++ b/src/core/Parser.ts @@ -39,6 +39,7 @@ export class Parser { declare public isConcise: boolean; // Set to true if parser is currently in concise mode declare public beginMixedMode?: boolean; // Used as a flag to mark that the next HTML block should enter the parser into HTML mode declare public endingMixedModeAtEOL?: boolean; // Used as a flag to record that the next EOL to exit HTML mode and go back to concise + declare public textDelimiter: undefined | Range; // The delimiter of the concise text block being parsed declare public textPos: number; // Used to buffer text that is found within the body of a tag declare public lines: undefined | number[]; // Keeps track of line indexes to provide line/column info. declare public options: Options; @@ -144,7 +145,11 @@ export class Parser { const start = this.textPos; if (start !== -1) { if (start !== this.pos) { - this.options.onText?.({ start, end: this.pos }); + this.options.onText?.({ + start, + end: this.pos, + delimiter: this.textDelimiter, + }); } this.textPos = -1; } @@ -156,7 +161,11 @@ export class Parser { * return back to the previous parsing mode and to ensure that all * tags within a block are properly closed. */ - beginHtmlBlock(delimiter: string | undefined, singleLine: boolean) { + beginHtmlBlock( + delimiter: string | undefined, + singleLine: boolean, + closingDelimiter?: string, + ) { const content = this.enterState( this.activeTag?.type === TagType.text ? STATE.PARSED_TEXT_CONTENT @@ -165,6 +174,7 @@ export class Parser { content.singleLine = singleLine; content.delimiter = delimiter; + content.closingDelimiter = closingDelimiter; content.indent = this.indent; } @@ -284,7 +294,11 @@ export class Parser { this.textPos = -1; this.isConcise = true; this.beginMixedMode = this.endingMixedModeAtEOL = false; - this.lines = this.activeTag = this.activeAttr = undefined; + this.lines = + this.activeTag = + this.activeAttr = + this.textDelimiter = + undefined; // Drop any state left over from a previous parse so reusing a parser // does not chain (and retain) the old state metas via parent references. this.activeRange = undefined as unknown as Meta; diff --git a/src/states/BEGIN_DELIMITED_HTML_BLOCK.ts b/src/states/BEGIN_DELIMITED_HTML_BLOCK.ts index aa483213..20bea4b1 100644 --- a/src/states/BEGIN_DELIMITED_HTML_BLOCK.ts +++ b/src/states/BEGIN_DELIMITED_HTML_BLOCK.ts @@ -1,5 +1,6 @@ import { htmlEOF, + isIndentCode, isLineCode, type Meta, Parser, @@ -12,10 +13,12 @@ import * as ErrorCode from "../util/error-code.ts"; export interface DelimitedHTMLBlockMeta extends Meta { delimiter: string; indent: string; + delimiterCode: number; } -// In STATE.BEGIN_DELIMITED_HTML_BLOCK we have already found two consecutive hyphens. We expect -// to reach the end of the line with only whitespace characters +// In STATE.BEGIN_DELIMITED_HTML_BLOCK we have already found the start of a delimiter: either two +// consecutive hyphens, or one or more backticks. We expect to reach the end of the line with +// only whitespace characters export const BEGIN_DELIMITED_HTML_BLOCK: StateDefinition = { name: "BEGIN_DELIMITED_HTML_BLOCK", @@ -28,10 +31,13 @@ export const BEGIN_DELIMITED_HTML_BLOCK: StateDefinition end: start, indent: this.indent, delimiter: "", + delimiterCode: this.data.charCodeAt(start), }; }, - exit() {}, + exit() { + this.textDelimiter = undefined; + }, parse(data, maxPos, block) { if (this.pos === maxPos) { @@ -50,24 +56,31 @@ export const BEGIN_DELIMITED_HTML_BLOCK: StateDefinition ? 2 : 1; const prevPos = this.pos; - this.beginHtmlBlock(block.delimiter, false); - handleDelimitedBlockEOL(this, true, len, block); + beginText(this, block, false); + handleDelimitedBlockEOL( + this, + true, + len, + block.indent, + block.delimiter, + ); if (this.pos === prevPos) this.pos += len; // advance past newline if not already advanced return; } - if (code === CODE.HYPHEN) { - block.delimiter += "-"; + if (code === block.delimiterCode) { + block.delimiter += String.fromCharCode(code); this.pos++; continue; } - // Non-hyphen, non-newline: check if whitespace-only remains on line + // Non-delimiter, non-newline: check if whitespace-only remains on line const startPos = this.pos; - if (!this.consumeWhitespaceOnLine()) { + const textOffset = block.delimiterCode === CODE.BACKTICK ? 0 : 1; + if (!this.consumeWhitespaceOnLine(textOffset)) { // Non-whitespace content on this line: start single-line HTML block - this.pos = startPos + 1; - this.beginHtmlBlock(undefined, true); + this.pos = startPos + textOffset; + beginText(this, block, true); return; } // Only whitespace to EOL: consumeWhitespaceOnLine set pos to newline @@ -78,6 +91,24 @@ export const BEGIN_DELIMITED_HTML_BLOCK: StateDefinition return() {}, }; +function beginText( + parser: Parser, + block: DelimitedHTMLBlockMeta, + singleLine: boolean, +) { + parser.textDelimiter = { + start: block.start, + end: block.start + block.delimiter.length, + }; + parser.beginHtmlBlock( + singleLine ? undefined : block.delimiter, + singleLine, + singleLine && block.delimiterCode === CODE.BACKTICK + ? block.delimiter + : undefined, + ); +} + export function handleDelimitedEOL( parser: Parser, newLineLength: number, @@ -91,7 +122,13 @@ export function handleDelimitedEOL( } if (content.delimiter) { - handleDelimitedBlockEOL(parser, false, newLineLength, content); + handleDelimitedBlockEOL( + parser, + false, + newLineLength, + content.indent, + content.delimiter, + ); return true; } @@ -102,13 +139,8 @@ function handleDelimitedBlockEOL( parser: Parser, first: boolean, newLineLength: number, - { - indent, - delimiter, - }: - | STATE.ParsedTextContentMeta - | STATE.HTMLContentMeta - | DelimitedHTMLBlockMeta, + indent: string, + delimiter: string, ) { // If we are within a delimited HTML block then we want to check if the next line is the end // delimiter. Since we are currently positioned at the start of the new line character our lookahead @@ -116,7 +148,14 @@ function handleDelimitedBlockEOL( // the delimiter. const endHtmlBlockLookahead = indent + delimiter; - if (parser.lookAheadFor(endHtmlBlockLookahead, parser.pos + newLineLength)) { + const isClosingLine = + parser.lookAheadFor(endHtmlBlockLookahead, parser.pos + newLineLength) && + (delimiter.charCodeAt(0) !== CODE.BACKTICK || + parser.onlyWhitespaceRemainsOnLine( + newLineLength + endHtmlBlockLookahead.length, + )); + + if (isClosingLine) { parser.endText(); parser.pos += newLineLength + endHtmlBlockLookahead.length; @@ -143,7 +182,7 @@ function handleDelimitedBlockEOL( } else if (indent && !parser.onlyWhitespaceRemainsOnLine(newLineLength)) { // the next line does not have enough indentation // so unless it is blank (whitespace only), - // we will end the block + // we will end the block. A delimiter there is not a closer, it starts the next block. const pos = parser.pos; let cur = parser.pos; while (cur && isLineCode(parser.data.charCodeAt(cur - 1))) { @@ -159,3 +198,37 @@ function handleDelimitedBlockEOL( parser.startText(); } } + +export function checkForBacktickCloser(parser: Parser, delimiter: string) { + const { data } = parser; + const start = parser.pos; + let end = start; + while (data.charCodeAt(end) === CODE.BACKTICK) end++; + if (end - start !== delimiter.length) return false; + + let next = end; + while (isIndentCode(data.charCodeAt(next))) next++; + + const code = data.charCodeAt(next); + const afterSlash = + code === CODE.FORWARD_SLASH ? data.charCodeAt(next + 1) : undefined; + const startsLineComment = afterSlash === CODE.FORWARD_SLASH; + const startsBlockComment = afterSlash === CODE.ASTERISK; + const endsLine = next >= parser.maxPos || isLineCode(code); + if (!endsLine && !startsLineComment && !startsBlockComment) return false; + + parser.endText(); + parser.exitState(); + parser.exitState(); + parser.pos = next; + + if (startsLineComment) { + parser.enterState(STATE.JS_COMMENT_LINE); + parser.pos += 2; + } else if (startsBlockComment) { + parser.enterState(STATE.JS_COMMENT_BLOCK); + parser.pos += 2; + } + + return true; +} diff --git a/src/states/CONCISE_HTML_CONTENT.ts b/src/states/CONCISE_HTML_CONTENT.ts index bfaf39d1..2ed4449e 100644 --- a/src/states/CONCISE_HTML_CONTENT.ts +++ b/src/states/CONCISE_HTML_CONTENT.ts @@ -84,11 +84,15 @@ export const CONCISE_HTML_CONTENT: StateDefinition = { } if (parentTag) { - if (parentTag.type === TagType.text && code !== CODE.HYPHEN) { + if ( + parentTag.type === TagType.text && + code !== CODE.HYPHEN && + code !== CODE.BACKTICK + ) { this.emitError( this.pos, ErrorCode.INVALID_LINE_START, - 'A line within a tag that only allows text content must begin with a "-" character', + 'A line within a tag that only allows text content must begin with "--" or a backtick', ); return; } @@ -129,6 +133,9 @@ export const CONCISE_HTML_CONTENT: StateDefinition = { ); return; } + case CODE.BACKTICK: + this.enterState(STATE.BEGIN_DELIMITED_HTML_BLOCK); + return; // pos stays at the first `, BEGIN_DELIMITED_HTML_BLOCK parses it case CODE.FORWARD_SLASH: switch (data.charCodeAt(this.pos + 1)) { case CODE.FORWARD_SLASH: diff --git a/src/states/HTML_CONTENT.ts b/src/states/HTML_CONTENT.ts index 637d9d60..ffea650f 100644 --- a/src/states/HTML_CONTENT.ts +++ b/src/states/HTML_CONTENT.ts @@ -12,6 +12,7 @@ export interface HTMLContentMeta extends Meta { indent: string; singleLine: boolean; delimiter: undefined | string; + closingDelimiter: undefined | string; } // In STATE.HTML_CONTENT we are looking for tags and placeholders but @@ -29,6 +30,7 @@ export const HTML_CONTENT: StateDefinition = { indent: "", singleLine: false, delimiter: undefined, + closingDelimiter: undefined, }; }, @@ -79,6 +81,17 @@ export const HTML_CONTENT: StateDefinition = { continue; } + if (code === CODE.BACKTICK && content.closingDelimiter) { + if (STATE.checkForBacktickCloser(this, content.closingDelimiter)) + return; + + this.startText(); + do { + this.pos++; + } while (this.pos < maxPos && data.charCodeAt(this.pos) === code); + continue; + } + if (code === CODE.OPEN_ANGLE_BRACKET) { if (STATE.checkForCDATA(this)) return; @@ -165,13 +178,9 @@ export const HTML_CONTENT: StateDefinition = { } this.startText(); - // Eagerly consume the run of chars that cannot match a branch above. - do { - this.pos++; - } while ( - this.pos < maxPos && - !isSpecialHtmlContentCode(data.charCodeAt(this.pos)) - ); + this.pos = content.closingDelimiter + ? skipTextToBacktick(data, this.pos, maxPos) + : skipText(data, this.pos, maxPos); } }, @@ -188,6 +197,27 @@ export const HTML_CONTENT: StateDefinition = { }, }; +// Eagerly consume the run of chars that cannot match a branch above. +function skipText(data: string, pos: number, maxPos: number) { + do { + pos++; + } while (pos < maxPos && !isSpecialHtmlContentCode(data.charCodeAt(pos))); + + return pos; +} + +function skipTextToBacktick(data: string, pos: number, maxPos: number) { + do { + pos++; + } while ( + pos < maxPos && + !isSpecialHtmlContentCode(data.charCodeAt(pos)) && + data.charCodeAt(pos) !== CODE.BACKTICK + ); + + return pos; +} + // Matches every char that can begin one of the parse branches above. function isSpecialHtmlContentCode(code: number) { switch (code) { diff --git a/src/states/OPEN_TAG.ts b/src/states/OPEN_TAG.ts index 4723966e..8bb19e24 100644 --- a/src/states/OPEN_TAG.ts +++ b/src/states/OPEN_TAG.ts @@ -6,6 +6,7 @@ import { matchesCloseParen, matchesPipe, type Meta, + type Parser, type Ranges, STATE, type StateDefinition, @@ -138,7 +139,7 @@ export const OPEN_TAG: StateDefinition = { continue; } - this.exitState(); + exitConciseOpenTag(this, tag); return; } @@ -149,7 +150,7 @@ export const OPEN_TAG: StateDefinition = { if (this.isConcise) { if (code === CODE.SEMICOLON) { this.pos++; // skip ; - this.exitState(); + if (!exitConciseOpenTag(this, tag)) return; if (!this.consumeWhitespaceOnLine(0)) { switch (data.charCodeAt(this.pos)) { case CODE.FORWARD_SLASH: @@ -201,31 +202,15 @@ export const OPEN_TAG: StateDefinition = { return; } - // The open tag is complete; compute nested indent from next line - this.exitState(); - - let curPos = this.pos + 1; - // Skip until the next newline. - while (curPos < maxPos && data.charCodeAt(++curPos) !== CODE.NEWLINE); - // Skip the newline itself. - const indentStart = ++curPos; - - // Count how many spaces/tabs we have after the newline. - while (curPos < maxPos) { - if (isIndentCode(data.charCodeAt(curPos))) { - curPos++; - } else { - break; - } - } - - const indentSize = curPos - indentStart; - if (indentSize > this.indent.length) { - this.indent = data.slice(indentStart, curPos); - } - - this.enterState(STATE.BEGIN_DELIMITED_HTML_BLOCK); - return; // pos still at the first -, BEGIN_DELIMITED_HTML_BLOCK parses it + beginTextAfterOpenTag(this, tag); + return; + } else if ( + code === CODE.BACKTICK && + tag.stage !== TAG_STAGE.ATTR_GROUP && + isWhitespaceCode(data.charCodeAt(this.pos - 1)) + ) { + beginTextAfterOpenTag(this, tag); + return; } else if (code === CODE.OPEN_SQUARE_BRACKET) { if (tag.stage === TAG_STAGE.ATTR_GROUP) { this.emitError( @@ -390,7 +375,7 @@ export const OPEN_TAG: StateDefinition = { return; } - this.exitState(); + exitConciseOpenTag(this, tag); } else { this.emitError( tag, @@ -527,6 +512,41 @@ export const OPEN_TAG: StateDefinition = { }, }; +function exitConciseOpenTag(parser: Parser, tag: OpenTagMeta) { + if (tag.tagName) { + parser.exitState(); + return true; + } + + parser.emitError( + tag, + ErrorCode.MALFORMED_OPEN_TAG, + "A concise tag must start with a tag name", + ); + return false; +} + +function beginTextAfterOpenTag(parser: Parser, tag: OpenTagMeta) { + const { data, maxPos } = parser; + if (!exitConciseOpenTag(parser, tag)) return; + + let curPos = parser.pos; + // Skip until the next newline. + while (curPos < maxPos && data.charCodeAt(curPos) !== CODE.NEWLINE) curPos++; + // Skip the newline itself. + const indentStart = ++curPos; + + // Count how many spaces/tabs we have after the newline. + while (curPos < maxPos && isIndentCode(data.charCodeAt(curPos))) curPos++; + + const indentSize = curPos - indentStart; + if (indentSize > parser.indent.length) { + parser.indent = data.slice(indentStart, curPos); + } + + parser.enterState(STATE.BEGIN_DELIMITED_HTML_BLOCK); +} + function shouldTerminateConciseTagVar( code: number, data: string, diff --git a/src/states/PARSED_TEXT_CONTENT.ts b/src/states/PARSED_TEXT_CONTENT.ts index 2e5464c5..7b4b20da 100644 --- a/src/states/PARSED_TEXT_CONTENT.ts +++ b/src/states/PARSED_TEXT_CONTENT.ts @@ -10,6 +10,7 @@ export interface ParsedTextContentMeta extends Meta { indent: string; singleLine: boolean; delimiter: undefined | string; + closingDelimiter: undefined | string; } // We enter STATE.PARSED_TEXT_CONTENT when we are parsing @@ -27,6 +28,7 @@ export const PARSED_TEXT_CONTENT: StateDefinition = { indent: "", singleLine: false, delimiter: undefined, + closingDelimiter: undefined, }; }, @@ -86,6 +88,13 @@ export const PARSED_TEXT_CONTENT: StateDefinition = { this.pos++; continue; case CODE.BACKTICK: + if ( + content.closingDelimiter && + STATE.checkForBacktickCloser(this, content.closingDelimiter) + ) { + return; + } + this.startText(); this.enterState(STATE.TEMPLATE_STRING); this.pos++; // skip ` diff --git a/src/states/PLACEHOLDER.ts b/src/states/PLACEHOLDER.ts index 082687d4..c1053ced 100644 --- a/src/states/PLACEHOLDER.ts +++ b/src/states/PLACEHOLDER.ts @@ -29,6 +29,7 @@ export const PLACEHOLDER: StateDefinition = { start: placeholder.start, end: placeholder.end, escape: placeholder.escape, + delimiter: this.textDelimiter, value: { start: placeholder.start + (placeholder.escape ? 2 : 3), // ignore ${ or $!{ end: placeholder.end - 1, // ignore } diff --git a/src/util/constants.ts b/src/util/constants.ts index 0ac1ba95..2d6fca94 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -49,8 +49,18 @@ export namespace Ranges { block: boolean; } + export interface Text extends Range { + /** + * The `--` or backtick run that begins the concise text block holding this + * text, or undefined outside of one. + */ + delimiter: Range | undefined; + } + export interface Placeholder extends Value { escape: boolean; + /** See {@link Text.delimiter}. */ + delimiter: Range | undefined; } export interface AttrValue extends Value { @@ -77,7 +87,7 @@ export type TagType = (typeof _TagType)[keyof typeof _TagType]; export interface ParserOptions { onError?(data: Ranges.Error): void; - onText?(data: Range): void; + onText?(data: Ranges.Text): void; onPlaceholder?(data: Ranges.Placeholder): void; onComment?(data: Ranges.Value): void; onCDATA?(data: Ranges.Value): void;