Skip to content

Commit

Permalink
Prevent <style> from getting moved in <html> (#974)
Browse files Browse the repository at this point in the history
* chore: `only` helper for json test

* fix: remove divergence from html spec

* test: add test for #712

* `addLoc` before popping the stack of oe

* test: add test

* literal parsing after `body` and `html`

* test: add test for style tag after body

* test: add tsx tests

* test: add missing semicolon in test

* test: add more tests from duplicates

* nit: formatting

* chore: changeset

* test: add AST tests

---------

Co-authored-by: Nate Moore <[email protected]>
Co-authored-by: Erika <[email protected]>
  • Loading branch information
3 people authored Jul 16, 2024
1 parent d1ba06d commit 9549bb7
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 3 deletions.
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())
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();

0 comments on commit 9549bb7

Please sign in to comment.