diff --git a/.changeset/ninety-kings-tease.md b/.changeset/ninety-kings-tease.md new file mode 100644 index 000000000..ba3995f0d --- /dev/null +++ b/.changeset/ninety-kings-tease.md @@ -0,0 +1,5 @@ +--- +"@astrojs/compiler": patch +--- + +Fixes style and script tags sometimes being forcefully put into the body / head tags in the AST diff --git a/internal/parser.go b/internal/parser.go index 12c610163..ac01859b9 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -903,8 +903,10 @@ func inHeadIM(p *parser) bool { p.im = afterHeadIM return true case a.Body, a.Html, a.Br: - p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) p.addLoc() + p.oe.pop() + p.originalIM = nil + p.im = afterHeadIM return false case a.Template: if !p.oe.contains(a.Template) { @@ -1439,12 +1441,18 @@ func inBodyIM(p *parser) bool { if p.elementInScope(defaultScope, a.Body) { p.im = afterBodyIM } + if p.literal { + p.oe.pop() + } case a.Html: p.addLoc() if p.elementInScope(defaultScope, a.Body) { p.parseImpliedToken(EndTagToken, a.Body, a.Body.String()) return false } + if p.literal { + p.oe.pop() + } return true case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul: p.addLoc() @@ -2702,9 +2710,10 @@ func inLiteralIM(p *parser) bool { p.addLoc() p.oe.pop() p.acknowledgeSelfClosingTag() + } else { + // always continue `inLiteralIM` + return true } - // always continue `inLiteralIM` - return true case StartExpressionToken: p.addExpression() // always continue `inLiteralIM` diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 454ceaee4..2c2e6af8d 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -89,6 +89,7 @@ type jsonTestcase struct { name string source string want []ASTNode + only bool } func TestPrinter(t *testing.T) { @@ -3805,6 +3806,26 @@ const c = '\'' source: `

Hello world!

`, want: []ASTNode{{Type: "element", Name: "html", Children: []ASTNode{{Type: "element", Name: "body", Children: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello world!"}}}}}}}, {Type: "element", Name: "style"}}, }, + { + name: "style after empty html", + source: ``, + want: []ASTNode{{Type: "element", Name: "html"}, {Type: "element", Name: "style"}}, + }, + { + name: "style after html with component in head", + source: ``, + want: []ASTNode{{Type: "element", Name: "html", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "lang", Value: "en", Raw: "\"en\""}}, Children: []ASTNode{{Type: "element", Name: "head", Children: []ASTNode{{Type: "component", Name: "BaseHead"}}}}}, {Type: "element", Name: "style", Children: []ASTNode{{Type: "text", Value: "@use \"../styles/global.scss\";"}}}}, + }, + { + name: "style after html with component in head and body", + source: `
`, + want: []ASTNode{{Type: "element", Name: "html", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "lang", Value: "en", Raw: "\"en\""}}, Children: []ASTNode{{Type: "element", Name: "head", Children: []ASTNode{{Type: "component", Name: "BaseHead"}}}, {Type: "element", Name: "body", Children: []ASTNode{{Type: "component", Name: "Header"}}}}}, {Type: "element", Name: "style", Children: []ASTNode{{Type: "text", Value: "@use \"../styles/global.scss\";"}}}}, + }, + { + name: "style after body with component in head and body", + source: `
`, + want: []ASTNode{{Type: "element", Name: "html", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "lang", Value: "en", Raw: "\"en\""}}, Children: []ASTNode{{Type: "element", Name: "head", Children: []ASTNode{{Type: "component", Name: "BaseHead"}}}, {Type: "element", Name: "body", Children: []ASTNode{{Type: "component", Name: "Header"}}}, {Type: "element", Name: "style", Children: []ASTNode{{Type: "text", Value: "@use \"../styles/global.scss\";"}}}}}}, + }, { name: "style in html", source: `

Hello world!

`, @@ -3832,6 +3853,14 @@ const c = '\'' }, } + for _, tt := range tests { + if tt.only { + tests = make([]jsonTestcase, 0) + tests = append(tests, tt) + break + } + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // transform output from source diff --git a/packages/compiler/test/parse/literal.ts b/packages/compiler/test/parse/literal.ts new file mode 100644 index 000000000..6602e2307 --- /dev/null +++ b/packages/compiler/test/parse/literal.ts @@ -0,0 +1,70 @@ +import { parse } from '@astrojs/compiler'; +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import type { ElementNode } from '../../types.js'; + +test('preserve style tag position I', async () => { + const input = `

Hello world!

+`; + const { ast } = await parse(input); + + const lastChildren = ast.children.at(-1) as ElementNode; + + assert.equal(lastChildren.type, 'element', 'Expected last child node to be of type "element"'); + assert.equal(lastChildren.name, 'style', 'Expected last child node to be of type "style"'); +}); + +test('preserve style tag position II', async () => { + const input = ` +`; + const { ast } = await parse(input); + + const lastChildren = ast.children.at(-1) as ElementNode; + + assert.equal(lastChildren.type, 'element', 'Expected last child node to be of type "element"'); + assert.equal(lastChildren.name, 'style', 'Expected last child node to be of type "style"'); +}); + +test('preserve style tag position III', async () => { + const input = ` +`; + const { ast } = await parse(input); + + const lastChildren = ast.children.at(-1) as ElementNode; + + assert.equal(lastChildren.type, 'element', 'Expected last child node to be of type "element"'); + assert.equal(lastChildren.name, 'style', 'Expected last child node to be of type "style"'); + assert.equal( + lastChildren.children[0].type, + 'text', + 'Expected last child node to be of type "text"' + ); +}); + +test('preserve style tag position IV', async () => { + const input = `
+`; + const { ast } = await parse(input); + + const lastChildren = ast.children.at(-1) as ElementNode; + + assert.equal(lastChildren.type, 'element', 'Expected last child node to be of type "element"'); + assert.equal(lastChildren.name, 'style', 'Expected last child node to be of type "style"'); + assert.equal( + lastChildren.children[0].type, + 'text', + 'Expected last child node to be of type "text"' + ); +}); + +test('preserve style tag position V', async () => { + const input = `
`; + const { ast } = await parse(input); + + const firstChild = ast.children.at(0) as ElementNode; + const lastChild = firstChild.children.at(-1) as ElementNode; + + assert.equal(lastChild.type, 'element', 'Expected last child node to be of type "element"'); + assert.equal(lastChild.name, 'style', 'Expected last child node to be of type "style"'); + assert.equal(lastChild.children[0].type, 'text', 'Expected last child node to be of type "text"'); +}); diff --git a/packages/compiler/test/tsx/literal-style-tag.ts b/packages/compiler/test/tsx/literal-style-tag.ts new file mode 100644 index 000000000..e945f053a --- /dev/null +++ b/packages/compiler/test/tsx/literal-style-tag.ts @@ -0,0 +1,64 @@ +import { convertToTSX } from '@astrojs/compiler'; +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { TSXPrefix } from '../utils.js'; + +test('preserve style tag position I', async () => { + const input = `

Hello world!

+`; + const output = `${TSXPrefix} +

Hello world!

+ +
+export default function __AstroComponent_(_props: Record): any {}\n`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test('preserve style tag position II', async () => { + const input = ` +`; + const output = `${TSXPrefix} + + + +export default function __AstroComponent_(_props: Record): any {}\n`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test('preserve style tag position III', async () => { + const input = ` +`; + const output = `${TSXPrefix} + + + +export default function __AstroComponent_(_props: Record): any {}\n`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test('preserve style tag position IV', async () => { + const input = `
+`; + const output = `${TSXPrefix} +
+ + +export default function __AstroComponent_(_props: Record): any {}\n`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test('preserve style tag position V', async () => { + const input = `
`; + const output = `${TSXPrefix} +
+ +export default function __AstroComponent_(_props: Record): any {}\n`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test.run();