diff --git a/.changeset/concise-attr-group-without-name.md b/.changeset/concise-attr-group-without-name.md new file mode 100644 index 00000000..7fe57f06 --- /dev/null +++ b/.changeset/concise-attr-group-without-name.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +Read an attribute group (`[`) at the start of a concise line as following an empty tag name, as `.class` and `|params|` are, rather than reading its first attribute as the tag name. diff --git a/.changeset/concise-semicolon-line.md b/.changeset/concise-semicolon-line.md new file mode 100644 index 00000000..621eee5d --- /dev/null +++ b/.changeset/concise-semicolon-line.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +Ignore a concise line that starts with `;`, which opened a tag with no name. As after a tag's `;`, only comments may follow it. diff --git a/.changeset/concise-tag-name-hyphen.md b/.changeset/concise-tag-name-hyphen.md new file mode 100644 index 00000000..76678c96 --- /dev/null +++ b/.changeset/concise-tag-name-hyphen.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +Read a concise line starting with `-` and a word character (eg `-if`) as a tag whose name starts with `-`, as `<-if>` already is in HTML mode. A single `-` followed by anything else is still an error, and so is any single `-` within a tag that only allows text. diff --git a/.changeset/open-tag-ended-before-name.md b/.changeset/open-tag-ended-before-name.md new file mode 100644 index 00000000..56fe243f --- /dev/null +++ b/.changeset/open-tag-ended-before-name.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +Name an open tag that ends before its name (eg a concise line of just `,`, or `<,>`) empty, so a tag never ends without `onOpenTagName`; the compiler and editor tooling crashed on these, and `<,>` crashed the parser at the end of the template. diff --git a/src/__tests__/fixtures/concise-attr-group-without-name/__snapshots__/concise-attr-group-without-name.expected.txt b/src/__tests__/fixtures/concise-attr-group-without-name/__snapshots__/concise-attr-group-without-name.expected.txt new file mode 100644 index 00000000..53c5cdb5 --- /dev/null +++ b/src/__tests__/fixtures/concise-attr-group-without-name/__snapshots__/concise-attr-group-without-name.expected.txt @@ -0,0 +1,21 @@ +1╭─ [a=1] + │ │││╰─ attrValue.value + │ ││╰─ attrValue "=1" + │ │╰─ attrName + ╰─ ╰─ tagName +2╭─ [ + │ ├─ closeTagEnd() + │ ├─ openTagEnd + ╰─ ╰─ tagName +3╭─ class="x" + │ │ │╰─ attrValue.value "\"x\"" + │ │ ╰─ attrValue "=\"x\"" + ╰─ ╰─ attrName "class" +4├─ ] +5╭─ [] + │ ├─ closeTagEnd() + │ ├─ openTagEnd + ╰─ ╰─ tagName +6╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd() \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-attr-group-without-name/input.marko b/src/__tests__/fixtures/concise-attr-group-without-name/input.marko new file mode 100644 index 00000000..01a038b9 --- /dev/null +++ b/src/__tests__/fixtures/concise-attr-group-without-name/input.marko @@ -0,0 +1,5 @@ +[a=1] +[ + class="x" +] +[] diff --git a/src/__tests__/fixtures/concise-semicolon-line/__snapshots__/concise-semicolon-line.expected.txt b/src/__tests__/fixtures/concise-semicolon-line/__snapshots__/concise-semicolon-line.expected.txt new file mode 100644 index 00000000..8e47af73 --- /dev/null +++ b/src/__tests__/fixtures/concise-semicolon-line/__snapshots__/concise-semicolon-line.expected.txt @@ -0,0 +1,19 @@ +1╭─ ; +2╭─ div + ╰─ ╰─ tagName "div" +3╭─ ; // comment + │ │ │ ╰─ comment.value " comment" + │ │ ╰─ comment "// comment" + ╰─ ╰─ openTagEnd +4╭─ span + ╰─ ╰─ tagName "span" +5╭─ ; /* block */ + │ │ │ ╰─ comment.value " block " + │ │ ╰─ comment "/* block */" + │ ├─ closeTagEnd(div) + │ ├─ closeTagEnd(span) + ╰─ ╰─ openTagEnd +6╭─ ; + │ │ ╰─ comment.value " html " + ╰─ ╰─ comment "" +7╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-semicolon-line/input.marko b/src/__tests__/fixtures/concise-semicolon-line/input.marko new file mode 100644 index 00000000..71fb2972 --- /dev/null +++ b/src/__tests__/fixtures/concise-semicolon-line/input.marko @@ -0,0 +1,6 @@ +; +div + ; // comment + span +; /* block */ +; diff --git a/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/__snapshots__/concise-tag-name-hyphen-in-text.expected.txt b/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/__snapshots__/concise-tag-name-hyphen-in-text.expected.txt new file mode 100644 index 00000000..11030b6c --- /dev/null +++ b/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/__snapshots__/concise-tag-name-hyphen-in-text.expected.txt @@ -0,0 +1,6 @@ +1╭─ style + ╰─ ╰─ tagName "style" +2╭─ -foo + │ │ ╰─ error(INVALID_LINE_START:A line in concise mode cannot start with a single hyphen. Use "--" instead. See: https://github.com/marko-js/htmljs-parser/issues/43) + ╰─ ╰─ openTagEnd +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/input.marko b/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/input.marko new file mode 100644 index 00000000..573f2091 --- /dev/null +++ b/src/__tests__/fixtures/concise-tag-name-hyphen-in-text/input.marko @@ -0,0 +1,2 @@ +style + -foo diff --git a/src/__tests__/fixtures/concise-tag-name-hyphen/__snapshots__/concise-tag-name-hyphen.expected.txt b/src/__tests__/fixtures/concise-tag-name-hyphen/__snapshots__/concise-tag-name-hyphen.expected.txt new file mode 100644 index 00000000..9c71f55e --- /dev/null +++ b/src/__tests__/fixtures/concise-tag-name-hyphen/__snapshots__/concise-tag-name-hyphen.expected.txt @@ -0,0 +1,21 @@ +1╭─ -if(x) + │ │ │╰─ tagArgs.value + │ │ ╰─ tagArgs "(x)" + ╰─ ╰─ tagName "-if" +2╭─ span + │ │ ╰─ tagName "span" + ╰─ ╰─ openTagEnd +3╭─ -else-if(y) + │ │ │╰─ tagArgs.value + │ │ ╰─ tagArgs "(y)" + │ ├─ closeTagEnd(-if) + │ ├─ closeTagEnd(span) + │ ├─ openTagEnd + ╰─ ╰─ tagName "-else-if" +4╭─ -my-tag + │ │ ╰─ tagName "-my-tag" + ╰─ ╰─ openTagEnd +5╭─ + │ ├─ openTagEnd + │ ├─ closeTagEnd(-else-if) + ╰─ ╰─ closeTagEnd(-my-tag) \ No newline at end of file diff --git a/src/__tests__/fixtures/concise-tag-name-hyphen/input.marko b/src/__tests__/fixtures/concise-tag-name-hyphen/input.marko new file mode 100644 index 00000000..4c5e0aec --- /dev/null +++ b/src/__tests__/fixtures/concise-tag-name-hyphen/input.marko @@ -0,0 +1,4 @@ +-if(x) + span +-else-if(y) + -my-tag diff --git a/src/__tests__/fixtures/open-tag-ended-before-name/__snapshots__/open-tag-ended-before-name.expected.txt b/src/__tests__/fixtures/open-tag-ended-before-name/__snapshots__/open-tag-ended-before-name.expected.txt new file mode 100644 index 00000000..e4395d10 --- /dev/null +++ b/src/__tests__/fixtures/open-tag-ended-before-name/__snapshots__/open-tag-ended-before-name.expected.txt @@ -0,0 +1,20 @@ +1╭─ ,; +2╭─ <,/> + │ │ ├─ openTagEnd:selfClosed "/>" + │ │ ╰─ tagName + │ ├─ closeTagEnd() + │ ├─ openTagEnd + │ ├─ tagName + ╰─ ╰─ openTagStart +3╭─ <,> + │ │ ││ ├─ closeTagEnd() + │ │ ││ ╰─ closeTagName + │ │ │╰─ closeTagStart " +<,> +, diff --git a/src/states/CONCISE_HTML_CONTENT.ts b/src/states/CONCISE_HTML_CONTENT.ts index bfaf39d1..060189d7 100644 --- a/src/states/CONCISE_HTML_CONTENT.ts +++ b/src/states/CONCISE_HTML_CONTENT.ts @@ -1,6 +1,7 @@ import { htmlEOF, isWhitespaceCode, + isWordCode, STATE, type StateDefinition, } from "../internal.ts"; @@ -106,6 +107,11 @@ export const CONCISE_HTML_CONTENT: StateDefinition = { } switch (code) { + case CODE.SEMICOLON: + // An empty line, as after a tag ended by ";". + this.pos++; // skip ; + STATE.endConciseLine(this, data); + return; case CODE.OPEN_ANGLE_BRACKET: this.beginMixedMode = true; this.beginHtmlBlock(undefined, false); // pos stays at < @@ -121,6 +127,11 @@ export const CONCISE_HTML_CONTENT: StateDefinition = { if (data.charCodeAt(this.pos + 1) === CODE.HYPHEN) { this.enterState(STATE.BEGIN_DELIMITED_HTML_BLOCK); return; // pos stays at the first -, BEGIN_DELIMITED_HTML_BLOCK parses it + } else if ( + parentTag?.type !== TagType.text && + isWordCode(data.charCodeAt(this.pos + 1)) + ) { + break; // a tag name starting with "-" (eg "-if") } else { this.emitError( this.pos, diff --git a/src/states/OPEN_TAG.ts b/src/states/OPEN_TAG.ts index 4723966e..8891eee1 100644 --- a/src/states/OPEN_TAG.ts +++ b/src/states/OPEN_TAG.ts @@ -2,10 +2,12 @@ import { isIndentCode, isLineCode, isWhitespaceCode, + isWordCode, matchesCloseAngleBracket, matchesCloseParen, matchesPipe, type Meta, + type Parser, type Ranges, STATE, type StateDefinition, @@ -69,9 +71,22 @@ export const OPEN_TAG: StateDefinition = { exit(tag) { const { selfClosed } = tag; + const start = this.pos - (this.isConcise ? 0 : selfClosed ? 2 : 1); + + if (!tag.tagName) { + // An open tag can end before a name (eg a concise line of just ",", or + // "<,>"), so name it empty where it ends, as one starting with "|" is. + tag.tagName = { + start, + end: start, + quasis: [{ start, end: start }], + expressions: [], + }; + tag.type = this.options.onOpenTagName?.(tag.tagName) || tag.type; + } this.options.onOpenTagEnd?.({ - start: this.pos - (this.isConcise ? 0 : selfClosed ? 2 : 1), + start, end: this.pos, selfClosed, }); @@ -150,40 +165,17 @@ export const OPEN_TAG: StateDefinition = { if (code === CODE.SEMICOLON) { this.pos++; // skip ; this.exitState(); - if (!this.consumeWhitespaceOnLine(0)) { - switch (data.charCodeAt(this.pos)) { - case CODE.FORWARD_SLASH: - switch (data.charCodeAt(this.pos + 1)) { - case CODE.FORWARD_SLASH: - this.enterState(STATE.JS_COMMENT_LINE); - this.pos += 2; // skip // - return; - case CODE.ASTERISK: - this.enterState(STATE.JS_COMMENT_BLOCK); - this.pos += 2; // skip /* - return; - } - break; - case CODE.OPEN_ANGLE_BRACKET: - if (this.lookAheadFor("!--")) { - this.enterState(STATE.HTML_COMMENT); - this.pos += 4; // skip