diff --git a/.changeset/lazy-beers-develop.md b/.changeset/lazy-beers-develop.md new file mode 100644 index 000000000..6565f6644 --- /dev/null +++ b/.changeset/lazy-beers-develop.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Fixes an issue where HTML and JSX comments lead to subsequent content being incorrectly treated as plain text when they have parent expressions. diff --git a/.gitignore b/.gitignore index adbc7f4a0..3ee059e40 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules *.wasm /astro debug.test +__debug_bin packages/compiler/sourcemap.mjs diff --git a/internal/helpers/js_comment_utils.go b/internal/helpers/js_comment_utils.go new file mode 100644 index 000000000..7c2930a11 --- /dev/null +++ b/internal/helpers/js_comment_utils.go @@ -0,0 +1,45 @@ +package helpers + +import ( + "strings" +) + +func peekIs(input string, cur int, assert byte) bool { + return cur+1 < len(input) && input[cur+1] == assert +} + +// RemoveComments removes both block and inline comments from a string +func RemoveComments(input string) string { + var ( + sb = strings.Builder{} + inComment = false + ) + for cur := 0; cur < len(input); cur++ { + if input[cur] == '/' && !inComment { + if peekIs(input, cur, '*') { + inComment = true + cur++ + } else if peekIs(input, cur, '/') { + // Skip until the end of line for inline comments + for cur < len(input) && input[cur] != '\n' { + cur++ + } + continue + } + } else if input[cur] == '*' && inComment && peekIs(input, cur, '/') { + inComment = false + cur++ + continue + } + + if !inComment { + sb.WriteByte(input[cur]) + } + } + + if inComment { + return "" + } + + return strings.TrimSpace(sb.String()) +} diff --git a/internal/parser.go b/internal/parser.go index 6f6b3da0b..5de65c269 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -2785,6 +2785,13 @@ func inExpressionIM(p *parser) bool { p.oe.pop() p.im = textIM return true + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + Loc: p.generateLoc(), + }) + return true } p.im = p.originalIM p.originalIM = nil diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go index fcf644104..f0e9c30a9 100644 --- a/internal/printer/print-to-js.go +++ b/internal/printer/print-to-js.go @@ -12,6 +12,7 @@ import ( . "github.com/withastro/compiler/internal" "github.com/withastro/compiler/internal/handler" + "github.com/withastro/compiler/internal/helpers" "github.com/withastro/compiler/internal/js_scanner" "github.com/withastro/compiler/internal/loc" "github.com/withastro/compiler/internal/sourcemap" @@ -87,13 +88,18 @@ func printToJs(p *printer, n *Node, cssLen int, opts transform.TransformOptions) const whitespace = " \t\r\n\f" // Returns true if the expression only contains a comment block (e.g. {/* a comment */}) -func expressionOnlyHasCommentBlock(n *Node) bool { - clean, _ := removeComments(n.FirstChild.Data) - return n.FirstChild.NextSibling == nil && +func expressionOnlyHasComment(n *Node) bool { + if n.FirstChild == nil { + return false + } + clean := helpers.RemoveComments(n.FirstChild.Data) + trimmedData := strings.TrimLeft(n.FirstChild.Data, whitespace) + result := n.FirstChild.NextSibling == nil && n.FirstChild.Type == TextNode && - // removeComments iterates over text and most of the time we won't be parsing comments so lets check if text starts with /* before iterating - strings.HasPrefix(strings.TrimLeft(n.FirstChild.Data, whitespace), "/*") && + // RemoveComments iterates over text and most of the time we won't be parsing comments so lets check if text starts with /* or // before iterating + (strings.HasPrefix(trimmedData, "/*") || strings.HasPrefix(trimmedData, "//")) && len(clean) == 0 + return result } func emptyTextNodeWithoutSiblings(n *Node) bool { @@ -311,7 +317,7 @@ func render1(p *printer, n *Node, opts RenderOptions) { if n.Expression { if n.FirstChild == nil { p.print("${(void 0)") - } else if expressionOnlyHasCommentBlock(n) { + } else if expressionOnlyHasComment(n) { // we do not print expressions that only contain comment blocks return } else { @@ -604,9 +610,12 @@ func render1(p *printer, n *Node, opts RenderOptions) { } } - // Only slot ElementNodes or non-empty TextNodes! - // CommentNode and others should not be slotted - if c.Type == ElementNode || (c.Type == TextNode && !emptyTextNodeWithoutSiblings(c)) { + // Only slot ElementNodes (except expressions containing only comments) or non-empty TextNodes! + // CommentNode, JSX comments and others should not be slotted + if expressionOnlyHasComment(c) { + continue + } + if c.Type == ElementNode || c.Type == TextNode && !emptyTextNodeWithoutSiblings(c) { slottedChildren[slotProp] = append(slottedChildren[slotProp], c) } } diff --git a/internal/printer/print-to-tsx.go b/internal/printer/print-to-tsx.go index ea1b6c8d4..0a45a0577 100644 --- a/internal/printer/print-to-tsx.go +++ b/internal/printer/print-to-tsx.go @@ -8,6 +8,7 @@ import ( . "github.com/withastro/compiler/internal" astro "github.com/withastro/compiler/internal" "github.com/withastro/compiler/internal/handler" + "github.com/withastro/compiler/internal/helpers" "github.com/withastro/compiler/internal/js_scanner" "github.com/withastro/compiler/internal/loc" "github.com/withastro/compiler/internal/sourcemap" @@ -229,7 +230,7 @@ declare const Astro: Readonly", }, }, + { + name: "HTML comment in component inside expression I", + source: "{(() => )}", + want: want{ + code: "${(() => $$render`${$$renderComponent($$result,'Component',Component,{},{})}`)}", + }, + }, + { + name: "HTML comment in component inside expression II", + source: "{list.map(() => )}", + want: want{ + code: "${list.map(() => $$render`${$$renderComponent($$result,'Component',Component,{},{})}`)}", + }, + }, { name: "nested expressions", source: `
{(previous || next) && }
`, @@ -989,7 +1003,7 @@ const name = "world"; }, }, { - name: "head expression and conditional renderin of fragment", + name: "head expression and conditional rendering of fragment", source: `--- const testBool = true; --- @@ -2761,12 +2775,50 @@ const items = ["Dog", "Cat", "Platipus"]; }, }, { - name: "comment only expressions are removed", + name: "comment only expressions are removed I", source: `{/* a comment 1 */}

{/* a comment 2*/}Hello

`, want: want{ code: `${$$maybeRenderHead($$result)}

Hello

`, }, }, + { + name: "comment only expressions are removed II", + source: `{ + list.map((i) => ( + + { + // hello + } + + )) +}`, + want: want{ + code: `${ + list.map((i) => ( + $$render` + BACKTICK + `${$$renderComponent($$result,'Component',Component,{},{})}` + BACKTICK + ` + )) +}`, + }, + }, + { + name: "comment only expressions are removed III", + source: `{ + list.map((i) => ( + + { + /* hello */ + } + + )) +}`, + want: want{ + code: `${ + list.map((i) => ( + $$render` + BACKTICK + `${$$renderComponent($$result,'Component',Component,{},{})}` + BACKTICK + ` + )) +}`, + }, + }, { name: "component with only a script", source: "", diff --git a/internal/printer/utils.go b/internal/printer/utils.go index 7da77c159..74f27b4aa 100644 --- a/internal/printer/utils.go +++ b/internal/printer/utils.go @@ -1,7 +1,6 @@ package printer import ( - "errors" "fmt" "regexp" "strings" @@ -95,32 +94,6 @@ func encodeDoubleQuote(str string) string { return strings.Replace(str, `"`, """, -1) } -// Remove comment blocks from string (e.g. "/* a comment */aProp" => "aProp") -func removeComments(input string) (string, error) { - var ( - sb = strings.Builder{} - inComment = false - ) - for cur := 0; cur < len(input); cur++ { - peekIs := func(assert byte) bool { return cur+1 < len(input) && input[cur+1] == assert } - if input[cur] == '/' && !inComment && peekIs('*') { - inComment = true - cur++ - } else if input[cur] == '*' && inComment && peekIs('/') { - inComment = false - cur++ - } else if !inComment { - sb.WriteByte(input[cur]) - } - } - - if inComment { - return "", errors.New("unterminated comment") - } - - return strings.TrimSpace(sb.String()), nil -} - func convertAttributeValue(n *astro.Node, attrName string) string { expr := `""` if transform.HasAttr(n, attrName) {