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();