Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent <style> from getting moved in <html> #974

Merged
merged 19 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-kings-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/compiler": patch
---

Fixes style and script tags sometimes being forcefully put into the body / head tags in the AST
15 changes: 12 additions & 3 deletions internal/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Member Author

@MoustaphaDev MoustaphaDev Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix was to remove this divergence from the html spec in the inHead insertion mode, see the "anything else" section to find the correct steps to be performed

p.addLoc()
p.oe.pop()
p.originalIM = nil
p.im = afterHeadIM
return false
case a.Template:
if !p.oe.contains(a.Template) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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`
Expand Down
29 changes: 29 additions & 0 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type jsonTestcase struct {
name string
source string
want []ASTNode
only bool
}

func TestPrinter(t *testing.T) {
Expand Down Expand Up @@ -3805,6 +3806,26 @@ const c = '\''
source: `<html><body><h1>Hello world!</h1></body></html><style></style>`,
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: `<html></html><style></style>`,
want: []ASTNode{{Type: "element", Name: "html"}, {Type: "element", Name: "style"}},
},
{
name: "style after html with component in head",
source: `<html lang="en"><head><BaseHead /></head></html><style>@use "../styles/global.scss";</style>`,
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: `<html lang="en"><head><BaseHead /></head><body><Header /></body></html><style>@use "../styles/global.scss";</style>`,
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: `<html lang="en"><head><BaseHead /></head><body><Header /></body><style>@use "../styles/global.scss";</style></html>`,
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: `<html><body><h1>Hello world!</h1></body><style></style></html>`,
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions packages/compiler/test/parse/literal.ts
Original file line number Diff line number Diff line change
@@ -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 = `<html><body><h1>Hello world!</h1></body></html>
<style></style>`;
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 = `<html></html>
<style></style>`;
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 = `<html lang="en"><head><BaseHead /></head></html>
<style>@use "../styles/global.scss";</style>`;
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 = `<html lang="en"><head><BaseHead /></head><body><Header /></body></html>
<style>@use "../styles/global.scss";</style>`;
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 = `<html lang="en"><head><BaseHead /></head><body><Header /></body><style>@use "../styles/global.scss";</style></html>`;
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"');
});
64 changes: 64 additions & 0 deletions packages/compiler/test/tsx/literal-style-tag.ts
Original file line number Diff line number Diff line change
@@ -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 = `<html><body><h1>Hello world!</h1></body></html>
<style></style>`;
const output = `${TSXPrefix}<Fragment>
<html><body><h1>Hello world!</h1></body></html>
<style></style>
</Fragment>
export default function __AstroComponent_(_props: Record<string, any>): 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 = `<html></html>
<style></style>`;
const output = `${TSXPrefix}<Fragment>
<html></html>
<style></style>
</Fragment>
export default function __AstroComponent_(_props: Record<string, any>): 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 = `<html lang="en"><head><BaseHead /></head></html>
<style>@use "../styles/global.scss";</style>`;
const output = `${TSXPrefix}<Fragment>
<html lang="en"><head><BaseHead /></head></html>
<style>{\`@use "../styles/global.scss";\`}</style>
</Fragment>
export default function __AstroComponent_(_props: Record<string, any>): 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 = `<html lang="en"><head><BaseHead /></head><body><Header /></body></html>
<style>@use "../styles/global.scss";</style>`;
const output = `${TSXPrefix}<Fragment>
<html lang="en"><head><BaseHead /></head><body><Header /></body></html>
<style>{\`@use "../styles/global.scss";\`}</style>
</Fragment>
export default function __AstroComponent_(_props: Record<string, any>): 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 = `<html lang="en"><head><BaseHead /></head><body><Header /></body><style>@use "../styles/global.scss";</style></html>`;
const output = `${TSXPrefix}<Fragment>
<html lang="en"><head><BaseHead /></head><body><Header /></body><style>{\`@use "../styles/global.scss";\`}</style></html>
</Fragment>
export default function __AstroComponent_(_props: Record<string, any>): any {}\n`;
const { code } = await convertToTSX(input, { sourcemap: 'external' });
assert.snapshot(code, output, 'expected code to match snapshot');
});

test.run();