diff --git a/.changeset/long-yaks-smell.md b/.changeset/long-yaks-smell.md new file mode 100644 index 00000000..21e19e6b --- /dev/null +++ b/.changeset/long-yaks-smell.md @@ -0,0 +1,6 @@ +--- +"htmljs-parser": minor +--- + +Add support for type parameter/argument parsing. +This adds a new `onTagTypeParams`, `onTagTypeArgs` events and a `.typeParams` property on the `AttrMethod` range. diff --git a/README.md b/README.md index cd803a70..5fbf1af4 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,18 @@ const parser = createParser({ range.expressions; // A list of placeholder ranges (similar to whats emitted via onPlaceholder). }, + /** + * Called after the type arguments for a tag have been parsed. + * + * @example + * 1╭─ > + * │ │╰─ tagTypeArgs.value "string" + * ╰─ ╰─ tagTypeArgs "" + */ + onTagTypeArgs(range) { + range.value; // Another range that includes only the type arguments themselves and not the angle brackets. + }, + /** * Called after a tag variable has been parsed. * @@ -273,6 +285,18 @@ const parser = createParser({ range.value; // Another range that includes only the args themselves and not the outer parenthesis. }, + /** + * Called after type parameters for the tag parameters have been parsed. + * + * @example + * 1╭─ |input: { name: T }|> + * │ │╰─ tagBodyTypeParams.value + * ╰─ ╰─ tagBodyTypeParams "" + */ + onTagBodyTypeParams(range) { + range.value; // Another range that includes only the type params themselves and not the angle brackets. + }, + /** * Called after tag parameters have been parsed. * @@ -281,7 +305,7 @@ const parser = createParser({ * │ │╰─ tagParams.value "item" * ╰─ ╰─ tagParams "|item|" */ - onTagParams(range) { + onTagBodyParams(range) { range.value; // Another range that includes only the params themselves and not the outer pipes. }, @@ -334,6 +358,9 @@ const parser = createParser({ * ╰─ ╰─ attrMethod "(ev) { foo(); }" */ onAttrMethod(range) { + range.typeParams; // Another range which includes the type params for the method. + range.typeParams.value; // Another range which includes the type params without outer angle brackets. + range.params; // Another range which includes the params for the method. range.params.value; // Another range which includes the method params without outer parenthesis. diff --git a/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/__snapshots__/attr-method-shorthand-with-type-parameters.expected.txt b/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/__snapshots__/attr-method-shorthand-with-type-parameters.expected.txt new file mode 100644 index 00000000..eab7dcf9 --- /dev/null +++ b/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/__snapshots__/attr-method-shorthand-with-type-parameters.expected.txt @@ -0,0 +1,128 @@ +1╭─ (event: A){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A" + │ ││ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │╰─ attrMethod.typeParams.value + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod "(event: A){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +3╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +4├─ +5╭─ (event: A){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A" + │ ││ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │╰─ attrMethod.typeParams.value + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod " (event: A){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +6╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +7╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +8├─ +9╭─ (event: A){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A" + │ ││ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │╰─ attrMethod.typeParams.value + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod "(event: A){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +10╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +11╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +12├─ +13╭─ (event: A){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A" + │ ││ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │╰─ attrMethod.typeParams.value + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod " (event: A){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +14╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +15╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +16├─ +17╭─ (event: A & B){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod "(event: A & B){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +18╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +19╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +20├─ +21╭─ (event: A & B){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod " (event: A & B){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +22╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +23╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +24├─ +25╭─ (event: A & B){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod "(event: A & B){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +26╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +27╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +28├─ +29╭─ (event: A & B){ + │ ││ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ ├─ attrMethod.typeParams "" + │ ││ ├─ attrMethod " (event: A & B){\n console.log(event.type)\n}" + │ ││ ├─ attrName + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +30╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +31╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +32╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/input.marko b/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/input.marko new file mode 100644 index 00000000..ec84e712 --- /dev/null +++ b/src/__tests__/fixtures/attr-method-shorthand-with-type-parameters/input.marko @@ -0,0 +1,31 @@ +(event: A){ + console.log(event.type) +}/> + + (event: A){ + console.log(event.type) +}/> + +(event: A){ + console.log(event.type) +}/> + + (event: A){ + console.log(event.type) +}/> + +(event: A & B){ + console.log(event.type) +}/> + + (event: A & B){ + console.log(event.type) +}/> + +(event: A & B){ + console.log(event.type) +}/> + + (event: A & B){ + console.log(event.type) +}/> diff --git a/src/__tests__/fixtures/attr-method-with-type-parameters/__snapshots__/attr-method-with-type-parameters.expected.txt b/src/__tests__/fixtures/attr-method-with-type-parameters/__snapshots__/attr-method-with-type-parameters.expected.txt new file mode 100644 index 00000000..81cc872b --- /dev/null +++ b/src/__tests__/fixtures/attr-method-with-type-parameters/__snapshots__/attr-method-with-type-parameters.expected.txt @@ -0,0 +1,120 @@ +1╭─ (event: A){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A" + │ ││ │ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │ │╰─ attrMethod.typeParams.value + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod "(event: A){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +3╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +4├─ +5╭─ (event: A){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A" + │ ││ │ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │ │╰─ attrMethod.typeParams.value + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod " (event: A){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +6╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +7╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +8├─ +9╭─ (event: A){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A" + │ ││ │ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │ │╰─ attrMethod.typeParams.value + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod "(event: A){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +10╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +11╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +12├─ +13╭─ (event: A){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A" + │ ││ │ ││ ╰─ attrMethod.params "(event: A)" + │ ││ │ │╰─ attrMethod.typeParams.value + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod " (event: A){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +14╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +15╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +16├─ +17╭─ (event: A & B){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ │ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod "(event: A & B){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +18╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +19╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +20├─ +21╭─ (event: A & B){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ │ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod " (event: A & B){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +22╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +23╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +24├─ +25╭─ (event: A & B){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ │ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod "(event: A & B){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +26╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +27╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +28├─ +29╭─ (event: A & B){ + │ ││ │ ││ ││ ╰─ attrMethod.body "{\n console.log(event.type)\n}" + │ ││ │ ││ │╰─ attrMethod.params.value "event: A & B" + │ ││ │ ││ ╰─ attrMethod.params "(event: A & B)" + │ ││ │ │╰─ attrMethod.typeParams.value "A, B = string" + │ ││ │ ├─ attrMethod.typeParams "" + │ ││ │ ╰─ attrMethod " (event: A & B){\n console.log(event.type)\n}" + │ ││ ╰─ attrName "onclick" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +30╭─ console.log(event.type) + ╰─ ╰─ attrMethod.body.value "\n console.log(event.type)\n" +31╭─ }/> + ╰─ ╰─ openTagEnd:selfClosed "/>" +32╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/attr-method-with-type-parameters/input.marko b/src/__tests__/fixtures/attr-method-with-type-parameters/input.marko new file mode 100644 index 00000000..daa8049e --- /dev/null +++ b/src/__tests__/fixtures/attr-method-with-type-parameters/input.marko @@ -0,0 +1,31 @@ +(event: A){ + console.log(event.type) +}/> + + (event: A){ + console.log(event.type) +}/> + +(event: A){ + console.log(event.type) +}/> + + (event: A){ + console.log(event.type) +}/> + +(event: A & B){ + console.log(event.type) +}/> + + (event: A & B){ + console.log(event.type) +}/> + +(event: A & B){ + console.log(event.type) +}/> + + (event: A & B){ + console.log(event.type) +}/> diff --git a/src/__tests__/fixtures/attr-name-with-html-chars/__snapshots__/attr-name-with-html-chars.expected.txt b/src/__tests__/fixtures/attr-name-with-html-chars/__snapshots__/attr-name-with-html-chars.expected.txt index 4ea9914b..55a28383 100644 --- a/src/__tests__/fixtures/attr-name-with-html-chars/__snapshots__/attr-name-with-html-chars.expected.txt +++ b/src/__tests__/fixtures/attr-name-with-html-chars/__snapshots__/attr-name-with-html-chars.expected.txt @@ -1,3 +1,7 @@ 1╭─ tagname hello frank - │ │ ╰─ error(INVALID_ATTRIBUTE_NAME:Invalid attribute name. Attribute name cannot begin with the "<" character.) + │ │ ││ │ │ ╰─ error(INVALID_ATTR_TYPE_PARAMS:Attribute cannot contain type parameters unless it is a shorthand method) "/span" + │ │ ││ │ ╰─ attrName "frank" + │ │ ││ ╰─ attrName "hello" + │ │ │╰─ tagTypeArgs.value "span" + │ │ ╰─ tagTypeArgs "" ╰─ ╰─ tagName "tagname" \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-missing-tag-var/__snapshots__/invalid-missing-tag-var.expected.txt b/src/__tests__/fixtures/invalid-missing-tag-var/__snapshots__/invalid-missing-tag-var.expected.txt new file mode 100644 index 00000000..8edd5151 --- /dev/null +++ b/src/__tests__/fixtures/invalid-missing-tag-var/__snapshots__/invalid-missing-tag-var.expected.txt @@ -0,0 +1,5 @@ +1╭─ + │ ││ ╰─ error(MISSING_TAG_VARIABLE:A slash was found that was not followed by a variable name or lhs expression) + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-missing-tag-var/input.marko b/src/__tests__/fixtures/invalid-missing-tag-var/input.marko new file mode 100644 index 00000000..520513e7 --- /dev/null +++ b/src/__tests__/fixtures/invalid-missing-tag-var/input.marko @@ -0,0 +1 @@ + diff --git a/src/__tests__/fixtures/invalid-multiple-tag-params/__snapshots__/invalid-multiple-tag-params.expected.txt b/src/__tests__/fixtures/invalid-multiple-tag-params/__snapshots__/invalid-multiple-tag-params.expected.txt new file mode 100644 index 00000000..0c049e5e --- /dev/null +++ b/src/__tests__/fixtures/invalid-multiple-tag-params/__snapshots__/invalid-multiple-tag-params.expected.txt @@ -0,0 +1,9 @@ +1╭─ + │ ││ ││ ╰─ error(INVALID_TAG_PARAMS:A tag can only specify parameters once) + │ ││ │╰─ tagParams.value + │ ││ ╰─ tagParams "|a|" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2├─ +3├─ +4╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-multiple-tag-params/input.marko b/src/__tests__/fixtures/invalid-multiple-tag-params/input.marko new file mode 100644 index 00000000..8d62d67c --- /dev/null +++ b/src/__tests__/fixtures/invalid-multiple-tag-params/input.marko @@ -0,0 +1,3 @@ + + + diff --git a/src/__tests__/fixtures/invalid-multiple-tag-type-params/__snapshots__/invalid-multiple-tag-type-params.expected.txt b/src/__tests__/fixtures/invalid-multiple-tag-type-params/__snapshots__/invalid-multiple-tag-type-params.expected.txt new file mode 100644 index 00000000..fa27119e --- /dev/null +++ b/src/__tests__/fixtures/invalid-multiple-tag-type-params/__snapshots__/invalid-multiple-tag-type-params.expected.txt @@ -0,0 +1,7 @@ +1╭─ /> + │ ││ ││ ╰─ error(INVALID_TAG_TYPE_PARAMS:Unexpected type parameters. Type parameters must directly follow a tag name, or precede a method / tag parameters.) + │ ││ │╰─ tagTypeArgs.value + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-multiple-tag-type-params/input.marko b/src/__tests__/fixtures/invalid-multiple-tag-type-params/input.marko new file mode 100644 index 00000000..d67d905b --- /dev/null +++ b/src/__tests__/fixtures/invalid-multiple-tag-type-params/input.marko @@ -0,0 +1 @@ +/> diff --git a/src/__tests__/fixtures/invalid-type-params-after-args/__snapshots__/invalid-type-params-after-args.expected.txt b/src/__tests__/fixtures/invalid-type-params-after-args/__snapshots__/invalid-type-params-after-args.expected.txt new file mode 100644 index 00000000..2a957c7e --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-args/__snapshots__/invalid-type-params-after-args.expected.txt @@ -0,0 +1,7 @@ +1╭─ /> + │ ││ ││ ╰─ error(INVALID_TAG_TYPE_PARAMS:Unexpected type parameters. Type parameters must directly follow a tag name, or precede a method / tag parameters.) + │ ││ │╰─ tagArgs.value + │ ││ ╰─ tagArgs "(a)" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-type-params-after-args/input.marko b/src/__tests__/fixtures/invalid-type-params-after-args/input.marko new file mode 100644 index 00000000..38f8a929 --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-args/input.marko @@ -0,0 +1 @@ +/> diff --git a/src/__tests__/fixtures/invalid-type-params-after-method/__snapshots__/invalid-type-params-after-method.expected.txt b/src/__tests__/fixtures/invalid-type-params-after-method/__snapshots__/invalid-type-params-after-method.expected.txt new file mode 100644 index 00000000..c203e905 --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-method/__snapshots__/invalid-type-params-after-method.expected.txt @@ -0,0 +1,11 @@ +1╭─ /> + │ ││ ││ ││╰─ error(INVALID_ATTRIBUTE_NAME:Invalid attribute name. Attribute name cannot begin with the "<" character.) + │ ││ ││ │╰─ attrMethod.body.value + │ ││ ││ ╰─ attrMethod.body "{}" + │ ││ │╰─ attrMethod.params.value + │ ││ ├─ attrMethod.params "(a)" + │ ││ ├─ attrMethod "(a){}" + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-type-params-after-method/input.marko b/src/__tests__/fixtures/invalid-type-params-after-method/input.marko new file mode 100644 index 00000000..bcc8c528 --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-method/input.marko @@ -0,0 +1 @@ +/> diff --git a/src/__tests__/fixtures/invalid-type-params-after-params/__snapshots__/invalid-type-params-after-params.expected.txt b/src/__tests__/fixtures/invalid-type-params-after-params/__snapshots__/invalid-type-params-after-params.expected.txt new file mode 100644 index 00000000..06ff4fd9 --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-params/__snapshots__/invalid-type-params-after-params.expected.txt @@ -0,0 +1,7 @@ +1╭─ /> + │ ││ ││ ╰─ error(INVALID_TAG_TYPE_PARAMS:Unexpected type parameters. Type parameters must directly follow a tag name, or precede a method / tag parameters.) + │ ││ │╰─ tagParams.value + │ ││ ╰─ tagParams "|a|" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-type-params-after-params/input.marko b/src/__tests__/fixtures/invalid-type-params-after-params/input.marko new file mode 100644 index 00000000..e5fcae7c --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-after-params/input.marko @@ -0,0 +1 @@ +/> diff --git a/src/__tests__/fixtures/invalid-type-params-attr-arg/__snapshots__/invalid-type-params-attr-arg.expected.txt b/src/__tests__/fixtures/invalid-type-params-attr-arg/__snapshots__/invalid-type-params-attr-arg.expected.txt new file mode 100644 index 00000000..97a4921f --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-attr-arg/__snapshots__/invalid-type-params-attr-arg.expected.txt @@ -0,0 +1,6 @@ +1╭─ (b) c/> + │ ││ │ ╰─ error(INVALID_ATTRIBUTE_ARGUMENT:An attribute cannot have both type parameters and arguments) + │ ││ ╰─ attrName + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/invalid-type-params-attr-arg/input.marko b/src/__tests__/fixtures/invalid-type-params-attr-arg/input.marko new file mode 100644 index 00000000..263e3c3d --- /dev/null +++ b/src/__tests__/fixtures/invalid-type-params-attr-arg/input.marko @@ -0,0 +1 @@ +(b) c/> diff --git a/src/__tests__/fixtures/tag-params-with-type-parameters/__snapshots__/tag-params-with-type-parameters.expected.txt b/src/__tests__/fixtures/tag-params-with-type-parameters/__snapshots__/tag-params-with-type-parameters.expected.txt new file mode 100644 index 00000000..a98f1f83 --- /dev/null +++ b/src/__tests__/fixtures/tag-params-with-type-parameters/__snapshots__/tag-params-with-type-parameters.expected.txt @@ -0,0 +1,120 @@ +1╭─ |data: A|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A" + │ ││ ││ ╰─ tagParams "|data: A|" + │ ││ │╰─ tagTypeParams.value + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╭─ hi + ╰─ ╰─ text "\n hi\n" +3╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " |data: A|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A" + │ ││ ││ ╰─ tagParams "|data: A|" + │ ││ │╰─ tagTypeParams.value + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +6╭─ hi + ╰─ ╰─ text "\n hi\n" +7╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "|data: A|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A" + │ ││ ││ ╰─ tagParams "|data: A|" + │ ││ │╰─ tagTypeParams.value + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +10╭─ hi + ╰─ ╰─ text "\n hi\n" +11╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " |data: A|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A" + │ ││ ││ ╰─ tagParams "|data: A|" + │ ││ │╰─ tagTypeParams.value + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +14╭─ hi + ╰─ ╰─ text "\n hi\n" +15╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "|data: A & B|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A & B" + │ ││ ││ ╰─ tagParams "|data: A & B|" + │ ││ │╰─ tagTypeParams.value "A, B = string" + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +18╭─ hi + ╰─ ╰─ text "\n hi\n" +19╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " |data: A & B|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A & B" + │ ││ ││ ╰─ tagParams "|data: A & B|" + │ ││ │╰─ tagTypeParams.value "A, B = string" + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +22╭─ hi + ╰─ ╰─ text "\n hi\n" +23╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "|data: A & B|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A & B" + │ ││ ││ ╰─ tagParams "|data: A & B|" + │ ││ │╰─ tagTypeParams.value "A, B = string" + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +26╭─ hi + ╰─ ╰─ text "\n hi\n" +27╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " |data: A & B|> + │ ││ ││ ││ ╰─ openTagEnd + │ ││ ││ │╰─ tagParams.value "data: A & B" + │ ││ ││ ╰─ tagParams "|data: A & B|" + │ ││ │╰─ tagTypeParams.value "A, B = string" + │ ││ ╰─ tagTypeParams "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +30╭─ hi + ╰─ ╰─ text "\n hi\n" +31╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "|data: A|> + hi + + + |data: A|> + hi + + +|data: A|> + hi + + + |data: A|> + hi + + +|data: A & B|> + hi + + + |data: A & B|> + hi + + +|data: A & B|> + hi + + + |data: A & B|> + hi + diff --git a/src/__tests__/fixtures/tag-with-type-arguments/__snapshots__/tag-with-type-arguments.expected.txt b/src/__tests__/fixtures/tag-with-type-arguments/__snapshots__/tag-with-type-arguments.expected.txt new file mode 100644 index 00000000..4e25313b --- /dev/null +++ b/src/__tests__/fixtures/tag-with-type-arguments/__snapshots__/tag-with-type-arguments.expected.txt @@ -0,0 +1,104 @@ +1╭─ > + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +2╭─ hi + ╰─ ╰─ text "\n hi\n" +3╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " > + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +6╭─ hi + ╰─ ╰─ text "\n hi\n" +7╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "> + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +10╭─ hi + ╰─ ╰─ text "\n hi\n" +11╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " > + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +14╭─ hi + ╰─ ╰─ text "\n hi\n" +15╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "> + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value "A, B = string" + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +18╭─ hi + ╰─ ╰─ text "\n hi\n" +19╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " > + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value "A, B = string" + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +22╭─ hi + ╰─ ╰─ text "\n hi\n" +23╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "> + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value "A, B = string" + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +26╭─ hi + ╰─ ╰─ text "\n hi\n" +27╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart " > + │ ││ ││ ╰─ openTagEnd + │ ││ │╰─ tagTypeArgs.value "A, B = string" + │ ││ ╰─ tagTypeArgs "" + │ │╰─ tagName "foo" + ╰─ ╰─ openTagStart +30╭─ hi + ╰─ ╰─ text "\n hi\n" +31╭─ + │ │ ├─ closeTagEnd(foo) + │ │ ╰─ closeTagName + ╰─ ╰─ closeTagStart "> + hi + + + > + hi + + +> + hi + + + > + hi + + +> + hi + + + > + hi + + +> + hi + + + > + hi + diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index 9f027045..80c70a67 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -137,6 +137,9 @@ for (const entry of fs.readdirSync(FIXTURES)) { onTagShorthandClass(range) { addTemplateRange("tagShorthandClass", range); }, + onTagTypeArgs(range) { + addValueRange("tagTypeArgs", range); + }, onTagVar(range) { addValueRange("tagVar", range); }, @@ -146,6 +149,9 @@ for (const entry of fs.readdirSync(FIXTURES)) { onTagParams(range) { addValueRange("tagParams", range); }, + onTagTypeParams(range) { + addValueRange("tagTypeParams", range); + }, onAttrName(range) { addRange("attrName", range); }, @@ -157,6 +163,8 @@ for (const entry of fs.readdirSync(FIXTURES)) { }, onAttrMethod(range) { addRange("attrMethod", range); + if (range.typeParams) + addValueRange("attrMethod.typeParams", range.typeParams); addValueRange("attrMethod.params", range.params); addValueRange("attrMethod.body", range.body); }, diff --git a/src/states/ATTRIBUTE.ts b/src/states/ATTRIBUTE.ts index 5418ff2a..0bce3433 100644 --- a/src/states/ATTRIBUTE.ts +++ b/src/states/ATTRIBUTE.ts @@ -8,6 +8,7 @@ import { Ranges, Meta, ErrorCode, + matchesCloseAngleBracket, matchesCloseCurlyBrace, matchesCloseParen, } from "../internal"; @@ -18,6 +19,7 @@ const enum ATTR_STAGE { NAME, VALUE, ARGUMENT, + TYPE_PARAMS, BLOCK, } @@ -26,6 +28,7 @@ export interface AttrMeta extends Meta { name: undefined | Range; valueStart: number; args: boolean | Ranges.AttrMethod["params"]; + typeParams: undefined | Ranges.Value; spread: boolean; bound: boolean; } @@ -45,6 +48,7 @@ export const ATTRIBUTE: StateDefinition = { stage: ATTR_STAGE.UNKNOWN, name: undefined, args: false, + typeParams: undefined, bound: false, spread: false, }); @@ -94,6 +98,15 @@ export const ATTRIBUTE: StateDefinition = { this.pos++; // skip ( this.forward = 0; this.enterState(STATE.EXPRESSION).shouldTerminate = matchesCloseParen; + } else if ( + code === CODE.OPEN_ANGLE_BRACKET && + attr.stage === ATTR_STAGE.NAME + ) { + attr.stage = ATTR_STAGE.TYPE_PARAMS; + this.pos++; // skip < + this.forward = 0; + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseAngleBracket; } else if (code === CODE.OPEN_CURLY_BRACE && attr.args) { ensureAttrName(this, attr); attr.stage = ATTR_STAGE.BLOCK; @@ -102,6 +115,14 @@ export const ATTRIBUTE: StateDefinition = { this.enterState(STATE.EXPRESSION).shouldTerminate = matchesCloseCurlyBrace; } else if (attr.stage === ATTR_STAGE.UNKNOWN) { + if (code === CODE.OPEN_ANGLE_BRACKET) { + return this.emitError( + this.pos, + ErrorCode.INVALID_ATTRIBUTE_NAME, + 'Invalid attribute name. Attribute name cannot begin with the "<" character.' + ); + } + attr.stage = ATTR_STAGE.NAME; this.forward = 0; const expr = this.enterState(STATE.EXPRESSION); @@ -172,6 +193,12 @@ export const ATTRIBUTE: StateDefinition = { end, value, }; + } else if (attr.typeParams) { + this.emitError( + child, + ErrorCode.INVALID_ATTRIBUTE_ARGUMENT, + "An attribute cannot have both type parameters and arguments" + ); } else { attr.args = true; this.options.onAttrArgs?.({ @@ -185,12 +212,15 @@ export const ATTRIBUTE: StateDefinition = { } case ATTR_STAGE.BLOCK: { const params = attr.args as Ranges.Value; - const start = params.start; const end = ++this.pos; // include } + const { typeParams } = attr; + const start = typeParams ? typeParams.start : params.start; + this.options.onAttrMethod?.({ start, end, params, + typeParams, body: { start: child.start - 1, // include { end, @@ -204,6 +234,30 @@ export const ATTRIBUTE: StateDefinition = { break; } + case ATTR_STAGE.TYPE_PARAMS: { + const start = child.start - 1; // include < + const end = ++this.pos; // include > + + if (!this.consumeWhitespaceIfBefore("(")) { + return this.emitError( + child, + ErrorCode.INVALID_ATTR_TYPE_PARAMS, + "Attribute cannot contain type parameters unless it is a shorthand method" + ); + } + + attr.typeParams = { + start, + end, + value: { + start: child.start, + end: child.end, + }, + }; + + break; + } + case ATTR_STAGE.VALUE: { if (child.start === child.end) { return this.emitError( @@ -256,6 +310,7 @@ function shouldTerminateHtmlAttrName(code: number, data: string, pos: number) { case CODE.EQUAL: case CODE.OPEN_PAREN: case CODE.CLOSE_ANGLE_BRACKET: + case CODE.OPEN_ANGLE_BRACKET: return true; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; @@ -297,6 +352,7 @@ function shouldTerminateConciseAttrName( case CODE.EQUAL: case CODE.OPEN_PAREN: case CODE.SEMICOLON: + case CODE.OPEN_ANGLE_BRACKET: return true; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; @@ -339,6 +395,7 @@ function shouldTerminateConciseGroupedAttrName( case CODE.EQUAL: case CODE.OPEN_PAREN: case CODE.CLOSE_SQUARE_BRACKET: + case CODE.OPEN_ANGLE_BRACKET: return true; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; diff --git a/src/states/OPEN_TAG.ts b/src/states/OPEN_TAG.ts index ca844836..73018e62 100644 --- a/src/states/OPEN_TAG.ts +++ b/src/states/OPEN_TAG.ts @@ -9,12 +9,14 @@ import { ErrorCode, matchesPipe, matchesCloseParen, + matchesCloseAngleBracket, } from "../internal"; export enum TAG_STAGE { UNKNOWN, VAR, ARGUMENT, + TYPE_PARAMS, PARAMS, ATTR_GROUP, } @@ -28,6 +30,8 @@ export interface OpenTagMeta extends Meta { shorthandEnd: number; hasArgs: boolean; hasAttrs: boolean; + hasParams: boolean; + hasTypeParams: boolean; hasShorthandId: boolean; selfClosed: boolean; indent: string; @@ -52,6 +56,8 @@ export const OPEN_TAG: StateDefinition = { hasShorthandId: false, hasArgs: false, hasAttrs: false, + hasParams: false, + hasTypeParams: false, selfClosed: false, shorthandEnd: -1, tagName: undefined!, @@ -251,14 +257,6 @@ export const OPEN_TAG: StateDefinition = { return; } - if (code === CODE.OPEN_ANGLE_BRACKET) { - return this.emitError( - this.pos, - ErrorCode.INVALID_ATTRIBUTE_NAME, - 'Invalid attribute name. Attribute name cannot begin with the "<" character.' - ); - } - if (code === CODE.FORWARD_SLASH) { // Check next character to see if we are in a comment switch (this.lookAtCharCodeAhead(1)) { @@ -279,119 +277,170 @@ export const OPEN_TAG: StateDefinition = { this.pos++; // skip , this.forward = 0; this.consumeWhitespace(); - } else if (code === CODE.FORWARD_SLASH && !tag.hasAttrs) { - tag.stage = TAG_STAGE.VAR; - this.pos++; // skip / - this.forward = 0; - - if (isWhitespaceCode(this.lookAtCharCodeAhead(0))) { - return this.emitError( - this.pos, - ErrorCode.MISSING_TAG_VARIABLE, - "A slash was found that was not followed by a variable name or lhs expression" - ); - } - - const expr = this.enterState(STATE.EXPRESSION); - expr.operators = true; - expr.terminatedByWhitespace = true; - expr.shouldTerminate = this.isConcise - ? shouldTerminateConciseTagVar - : shouldTerminateHtmlTagVar; - } else if (code === CODE.OPEN_PAREN && !tag.hasAttrs) { - if (tag.hasArgs) { - this.emitError( - this.pos, - ErrorCode.INVALID_TAG_ARGUMENT, - "A tag can only have one argument" - ); - return; - } - tag.stage = TAG_STAGE.ARGUMENT; - this.pos++; // skip ( - this.forward = 0; - this.enterState(STATE.EXPRESSION).shouldTerminate = matchesCloseParen; - } else if (code === CODE.PIPE && !tag.hasAttrs) { - tag.stage = TAG_STAGE.PARAMS; - this.pos++; // skip | - this.forward = 0; - this.enterState(STATE.EXPRESSION).shouldTerminate = matchesPipe; } else { this.forward = 0; - if (tag.tagName) { + if (tag.hasAttrs) { this.enterState(STATE.ATTRIBUTE); - tag.hasAttrs = true; - } else { - this.enterState(STATE.TAG_NAME); - } - } - }, + } else if (tag.tagName) { + switch (code) { + case CODE.FORWARD_SLASH: { + tag.stage = TAG_STAGE.VAR; + this.pos++; // skip / - return(child, tag) { - switch (child.state) { - case STATE.JS_COMMENT_BLOCK: { - /* Ignore comments within an open tag */ - break; - } - case STATE.EXPRESSION: { - switch (tag.stage) { - case TAG_STAGE.VAR: { - if (child.start === child.end) { + if (isWhitespaceCode(this.lookAtCharCodeAhead(0))) { return this.emitError( - child, + this.pos, ErrorCode.MISSING_TAG_VARIABLE, "A slash was found that was not followed by a variable name or lhs expression" ); } - this.options.onTagVar?.({ - start: child.start - 1, // include /, - end: child.end, - value: { - start: child.start, - end: child.end, - }, - }); + const expr = this.enterState(STATE.EXPRESSION); + expr.operators = true; + expr.terminatedByWhitespace = true; + expr.shouldTerminate = this.isConcise + ? shouldTerminateConciseTagVar + : shouldTerminateHtmlTagVar; break; } - case TAG_STAGE.ARGUMENT: { - const start = child.start - 1; // include ( - const end = ++this.pos; // include ) - const value = { - start: child.start, - end: child.end, - }; - - if (this.consumeWhitespaceIfBefore("{")) { - const attr = this.enterState(STATE.ATTRIBUTE); - attr.start = start; - attr.args = { start, end, value }; - tag.hasAttrs = true; - this.forward = 0; - } else { - tag.hasArgs = true; - this.options.onTagArgs?.({ - start, - end, - value, - }); + + case CODE.OPEN_PAREN: + if (tag.hasArgs) { + this.emitError( + this.pos, + ErrorCode.INVALID_TAG_ARGUMENT, + "A tag can only have one argument" + ); + return; } + tag.stage = TAG_STAGE.ARGUMENT; + this.pos++; // skip ( + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseParen; break; - } - case TAG_STAGE.PARAMS: { - const end = ++this.pos; // include closing | - this.options.onTagParams?.({ - start: child.start - 1, // include leading | - end, - value: { - start: child.start, - end: child.end, - }, - }); + + case CODE.PIPE: + if (tag.hasParams) { + this.emitError( + this.pos, + ErrorCode.INVALID_TAG_PARAMS, + "A tag can only specify parameters once" + ); + return; + } + tag.stage = TAG_STAGE.PARAMS; + this.pos++; // skip | + this.enterState(STATE.EXPRESSION).shouldTerminate = matchesPipe; break; - } + + case CODE.OPEN_ANGLE_BRACKET: + tag.stage = TAG_STAGE.TYPE_PARAMS; + this.pos++; // skip < + this.enterState(STATE.EXPRESSION).shouldTerminate = + matchesCloseAngleBracket; + break; + + default: + tag.hasAttrs = true; + this.enterState(STATE.ATTRIBUTE); + } + } else { + this.enterState(STATE.TAG_NAME); + } + } + }, + + return(child, tag) { + if (child.state !== STATE.EXPRESSION) return; + + switch (tag.stage) { + case TAG_STAGE.VAR: { + if (child.start === child.end) { + return this.emitError( + child, + ErrorCode.MISSING_TAG_VARIABLE, + "A slash was found that was not followed by a variable name or lhs expression" + ); } + + this.options.onTagVar?.({ + start: child.start - 1, // include /, + end: child.end, + value: { + start: child.start, + end: child.end, + }, + }); + break; + } + case TAG_STAGE.ARGUMENT: { + const start = child.start - 1; // include ( + const end = ++this.pos; // include ) + const value = { + start: child.start, + end: child.end, + }; + + if (this.consumeWhitespaceIfBefore("{")) { + const attr = this.enterState(STATE.ATTRIBUTE); + attr.args = { start, end, value }; + attr.start = start; + this.forward = 0; + tag.hasAttrs = true; + } else { + tag.hasArgs = true; + this.options.onTagArgs?.({ + start, + end, + value, + }); + } + break; + } + case TAG_STAGE.TYPE_PARAMS: { + const end = ++this.pos; // include > + const types = { + start: child.start - 1, // include < + end, + value: { + start: child.start, + end: child.end, + }, + }; + + if (!tag.hasArgs && this.consumeWhitespaceIfBefore("(")) { + const attr = this.enterState(STATE.ATTRIBUTE); + attr.typeParams = types; + attr.start = types.start; + this.forward = 0; + tag.hasArgs = true; + tag.hasAttrs = true; + } else if (!tag.hasParams && this.consumeWhitespaceIfBefore("|")) { + this.options.onTagTypeParams?.(types); + } else if (!(tag.hasArgs || tag.hasParams || tag.hasTypeParams)) { + this.options.onTagTypeArgs?.(types); + tag.hasTypeParams = true; + } else { + this.emitError( + child, + ErrorCode.INVALID_TAG_TYPE_PARAMS, + "Unexpected type parameters. Type parameters must directly follow a tag name, or precede a method / tag parameters." + ); + } + break; + } + case TAG_STAGE.PARAMS: { + const end = ++this.pos; // include closing | + this.options.onTagParams?.({ + start: child.start - 1, + end, + value: { + start: child.start, + end: child.end, + }, + }); + tag.hasParams = true; break; } } @@ -405,6 +454,7 @@ function shouldTerminateConciseTagVar(code: number, data: string, pos: number) { case CODE.PIPE: case CODE.OPEN_PAREN: case CODE.SEMICOLON: + case CODE.OPEN_ANGLE_BRACKET: return true; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; @@ -420,6 +470,7 @@ function shouldTerminateHtmlTagVar(code: number, data: string, pos: number) { case CODE.EQUAL: case CODE.OPEN_PAREN: case CODE.CLOSE_ANGLE_BRACKET: + case CODE.OPEN_ANGLE_BRACKET: return true; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; diff --git a/src/states/TAG_NAME.ts b/src/states/TAG_NAME.ts index 183e1746..56df0c6c 100644 --- a/src/states/TAG_NAME.ts +++ b/src/states/TAG_NAME.ts @@ -11,7 +11,7 @@ import { } from "../internal"; export interface TagNameMeta extends Meta, Ranges.Template { - shorthandCode?: CODE.NUMBER_SIGN | CODE.PERIOD; + shorthandCode: -1 | CODE.NUMBER_SIGN | CODE.PERIOD; } // We enter STATE.TAG_NAME after we encounter a "<" @@ -25,6 +25,7 @@ export const TAG_NAME: StateDefinition = { parent, start, end: start, + shorthandCode: -1, expressions: [], quasis: [{ start, end: start }], }; @@ -122,6 +123,7 @@ export const TAG_NAME: StateDefinition = { code === CODE.OPEN_PAREN || code === CODE.FORWARD_SLASH || code === CODE.PIPE || + code === CODE.OPEN_ANGLE_BRACKET || (this.isConcise ? code === CODE.SEMICOLON : code === CODE.CLOSE_ANGLE_BRACKET) diff --git a/src/util/constants.ts b/src/util/constants.ts index 9ac8a85d..75171752 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -102,6 +102,7 @@ export namespace Ranges { export interface AttrMethod extends Range { body: Value; params: Value; + typeParams: Value | undefined; } export interface OpenTagEnd extends Range { @@ -136,6 +137,9 @@ export enum ErrorCode { MISSING_TAG_VARIABLE, RESERVED_TAG_NAME, ROOT_TAG_ONLY, + INVALID_TAG_PARAMS, + INVALID_TAG_TYPE_PARAMS, + INVALID_ATTR_TYPE_PARAMS, } export const enum TagType { @@ -158,8 +162,10 @@ export interface ParserOptions { onOpenTagName?(data: Ranges.Template): TagType | void; onTagShorthandId?(data: Ranges.Template): void; onTagShorthandClass?(data: Ranges.Template): void; + onTagTypeArgs?(data: Ranges.Value): void; onTagVar?(data: Ranges.Value): void; onTagArgs?(data: Ranges.Value): void; + onTagTypeParams?(data: Ranges.Value): void; onTagParams?(data: Ranges.Value): void; onAttrName?(data: Range): void; onAttrArgs?(data: Ranges.Value): void; diff --git a/src/util/util.ts b/src/util/util.ts index 090d568f..fe8a6166 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -68,6 +68,10 @@ export function htmlEOF(this: Parser) { } } +export function matchesCloseAngleBracket(code: number) { + return code === CODE.CLOSE_ANGLE_BRACKET; +} + export function matchesCloseParen(code: number) { return code === CODE.CLOSE_PAREN; }