Skip to content

Commit f1689ea

Browse files
committed
feat: make exact HTML parsing an opt-in experimental feature
This commit makes the previously implemented exact HTML parsing mode an opt-in experimental feature rather than the default parsing mode. The new implementation: 1. Adds an `experimentalExactParsingThingy` flag to control whether to use the more precise HTML parsing mode. Will use a more formal name once we think it through 2. Reverts the default insertion mode from `initialIMExact` back to `initialIM` 3. Updates all WASM bindings to pass the experimental flag through to the parser 4. Adds extensive test coverage with duplicated previously failing test cases that verify the behavior with exact parsing enabled The exact parsing mode preserves the original HTML document structure more faithfully, allowing invalid HTML to pass through to the browser rather than having the parser attempt to normalize it. (Commit written with the assistance of AI)
1 parent 41882e9 commit f1689ea

File tree

31 files changed

+2116
-28
lines changed

31 files changed

+2116
-28
lines changed

cmd/astro-wasm/astro-wasm.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,27 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
143143
experimentalScriptOrder = true
144144
}
145145

146+
experimentalExactParsingThingy := false
147+
if jsBool(options.Get("experimentalExactParsingThingy")) {
148+
experimentalExactParsingThingy = true
149+
}
150+
146151
return transform.TransformOptions{
147-
Filename: filename,
148-
NormalizedFilename: normalizedFilename,
149-
InternalURL: internalURL,
150-
SourceMap: sourcemap,
151-
AstroGlobalArgs: astroGlobalArgs,
152-
Compact: compact,
153-
ResolvePath: resolvePathFn,
154-
PreprocessStyle: preprocessStyle,
155-
ResultScopedSlot: scopedSlot,
156-
ScopedStyleStrategy: scopedStyleStrategy,
157-
TransitionsAnimationURL: transitionsAnimationURL,
158-
AnnotateSourceFile: annotateSourceFile,
159-
RenderScript: renderScript,
160-
ExperimentalScriptOrder: experimentalScriptOrder,
152+
Filename: filename,
153+
NormalizedFilename: normalizedFilename,
154+
InternalURL: internalURL,
155+
SourceMap: sourcemap,
156+
AstroGlobalArgs: astroGlobalArgs,
157+
Compact: compact,
158+
ResolvePath: resolvePathFn,
159+
PreprocessStyle: preprocessStyle,
160+
ResultScopedSlot: scopedSlot,
161+
ScopedStyleStrategy: scopedStyleStrategy,
162+
TransitionsAnimationURL: transitionsAnimationURL,
163+
AnnotateSourceFile: annotateSourceFile,
164+
RenderScript: renderScript,
165+
ExperimentalScriptOrder: experimentalScriptOrder,
166+
ExperimentalExactParsingThingy: experimentalExactParsingThingy,
161167
}
162168
}
163169

@@ -257,7 +263,7 @@ func Parse() any {
257263
h := handler.NewHandler(source, parseOptions.Filename)
258264

259265
var doc *astro.Node
260-
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true))
266+
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true), astro.ParseOptionExperimentalBetterLiteralThingy(transformOptions.ExperimentalExactParsingThingy))
261267
if err != nil {
262268
h.AppendError(err)
263269
}
@@ -281,7 +287,7 @@ func ConvertToTSX() any {
281287
h := handler.NewHandler(source, transformOptions.Filename)
282288

283289
var doc *astro.Node
284-
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true))
290+
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true), astro.ParseOptionExperimentalBetterLiteralThingy(transformOptions.ExperimentalExactParsingThingy))
285291
if err != nil {
286292
h.AppendError(err)
287293
}
@@ -335,7 +341,7 @@ func Transform() any {
335341
}
336342
}()
337343

338-
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h))
344+
doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true), astro.ParseOptionExperimentalBetterLiteralThingy(transformOptions.ExperimentalExactParsingThingy))
339345
if err != nil {
340346
reject.Invoke(wasm_utils.ErrorToJSError(h, err))
341347
return

internal/parser.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3171,10 +3171,8 @@ func ParseWithOptions(r io.Reader, opts ...ParseOption) (*Node, error) {
31713171
Type: DocumentNode,
31723172
HydrationDirectives: make(map[string]bool),
31733173
},
3174-
framesetOK: true,
3175-
// TODO: use the experimental flag
3176-
// to choose the correct initial insertion mode
3177-
im: initialIMExact,
3174+
framesetOK: true,
3175+
im: initialIM,
31783176
frontmatterState: FrontmatterInitial,
31793177
exitLiteralIM: func() bool { return false },
31803178
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
[TestPrinter/Preserve_namespaces_in_expressions_-_with_exact_parsing - 1]
3+
## Input
4+
5+
```
6+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><rect xlink:href={`#${iconId}`}></svg>
7+
```
8+
9+
## Output
10+
11+
```js
12+
import {
13+
Fragment,
14+
render as $$render,
15+
createAstro as $$createAstro,
16+
createComponent as $$createComponent,
17+
renderComponent as $$renderComponent,
18+
renderHead as $$renderHead,
19+
maybeRenderHead as $$maybeRenderHead,
20+
unescapeHTML as $$unescapeHTML,
21+
renderSlot as $$renderSlot,
22+
mergeSlots as $$mergeSlots,
23+
addAttribute as $$addAttribute,
24+
spreadAttributes as $$spreadAttributes,
25+
defineStyleVars as $$defineStyleVars,
26+
defineScriptVars as $$defineScriptVars,
27+
renderTransition as $$renderTransition,
28+
createTransitionScope as $$createTransitionScope,
29+
renderScript as $$renderScript,
30+
createMetadata as $$createMetadata
31+
} from "http://localhost:3000/";
32+
33+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] });
34+
35+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
36+
37+
return $$render`${$$maybeRenderHead($$result)}<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><rect${$$addAttribute(`#${iconId}`, "xlink:href")}></rect></svg>`;
38+
}, undefined, undefined);
39+
export default $$Component;
40+
```
41+
---
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
[TestPrinter/React_framework_example_-_with_exact_parsing - 1]
3+
## Input
4+
5+
```
6+
/-/-/-/
7+
// Component Imports
8+
import Counter from '../components/Counter.jsx'
9+
const someProps = {
10+
count: 0,
11+
}
12+
13+
// Full Astro Component Syntax:
14+
// https://docs.astro.build/core-concepts/astro-components/
15+
/-/-/-/
16+
<html lang="en">
17+
<head>
18+
<meta charset="utf-8" />
19+
<meta
20+
name="viewport"
21+
content="width=device-width"
22+
/>
23+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
24+
<style>
25+
:global(:root) {
26+
font-family: system-ui;
27+
padding: 2em 0;
28+
}
29+
:global(.counter) {
30+
display: grid;
31+
grid-template-columns: repeat(3, minmax(0, 1fr));
32+
place-items: center;
33+
font-size: 2em;
34+
margin-top: 2em;
35+
}
36+
:global(.children) {
37+
display: grid;
38+
place-items: center;
39+
margin-bottom: 2em;
40+
}
41+
</style>
42+
</head>
43+
<body>
44+
<main>
45+
<Counter {...someProps} client:visible>
46+
<h1>Hello React!</h1>
47+
</Counter>
48+
</main>
49+
</body>
50+
</html>
51+
```
52+
53+
## Output
54+
55+
```js
56+
import {
57+
Fragment,
58+
render as $$render,
59+
createAstro as $$createAstro,
60+
createComponent as $$createComponent,
61+
renderComponent as $$renderComponent,
62+
renderHead as $$renderHead,
63+
maybeRenderHead as $$maybeRenderHead,
64+
unescapeHTML as $$unescapeHTML,
65+
renderSlot as $$renderSlot,
66+
mergeSlots as $$mergeSlots,
67+
addAttribute as $$addAttribute,
68+
spreadAttributes as $$spreadAttributes,
69+
defineStyleVars as $$defineStyleVars,
70+
defineScriptVars as $$defineScriptVars,
71+
renderTransition as $$renderTransition,
72+
createTransitionScope as $$createTransitionScope,
73+
renderScript as $$renderScript,
74+
createMetadata as $$createMetadata
75+
} from "http://localhost:3000/";
76+
import Counter from '../components/Counter.jsx'
77+
78+
79+
import * as $$module1 from '../components/Counter.jsx';
80+
81+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [{ module: $$module1, specifier: '../components/Counter.jsx', assert: {} }], hydratedComponents: [Counter], clientOnlyComponents: [], hydrationDirectives: new Set(['visible']), hoisted: [] });
82+
83+
const $$Astro = $$createAstro('https://astro.build');
84+
const Astro = $$Astro;
85+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
86+
const Astro = $$result.createAstro($$Astro, $$props, $$slots);
87+
Astro.self = $$Component;
88+
89+
// Component Imports
90+
91+
const someProps = {
92+
count: 0,
93+
}
94+
95+
// Full Astro Component Syntax:
96+
// https://docs.astro.build/core-concepts/astro-components/
97+
98+
return $$render`<html lang="en" class="astro-hmnnhvcq">
99+
<head>
100+
<meta charset="utf-8">
101+
<meta name="viewport" content="width=device-width">
102+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
103+
104+
${$$renderHead($$result)}</head>
105+
<body class="astro-hmnnhvcq">
106+
<main class="astro-hmnnhvcq">
107+
${$$renderComponent($$result,'Counter',Counter,{...(someProps),"client:visible":true,"client:component-hydration":"visible","client:component-path":("../components/Counter.jsx"),"client:component-export":("default"),"class":"astro-hmnnhvcq"},{"default": () => $$render`
108+
<h1 class="astro-hmnnhvcq">Hello React!</h1>
109+
`,})}
110+
</main>
111+
</body>
112+
</html>`;
113+
}, undefined, undefined);
114+
export default $$Component;
115+
```
116+
---
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
[TestPrinter/client:only_component_(default)_-_with_exact_parsing - 1]
3+
## Input
4+
5+
```
6+
/-/-/-/
7+
import Component from '../components';
8+
/-/-/-/
9+
<html>
10+
<head>
11+
<title>Hello world</title>
12+
</head>
13+
<body>
14+
<Component client:only />
15+
</body>
16+
</html>
17+
```
18+
19+
## Output
20+
21+
```js
22+
import {
23+
Fragment,
24+
render as $$render,
25+
createAstro as $$createAstro,
26+
createComponent as $$createComponent,
27+
renderComponent as $$renderComponent,
28+
renderHead as $$renderHead,
29+
maybeRenderHead as $$maybeRenderHead,
30+
unescapeHTML as $$unescapeHTML,
31+
renderSlot as $$renderSlot,
32+
mergeSlots as $$mergeSlots,
33+
addAttribute as $$addAttribute,
34+
spreadAttributes as $$spreadAttributes,
35+
defineStyleVars as $$defineStyleVars,
36+
defineScriptVars as $$defineScriptVars,
37+
renderTransition as $$renderTransition,
38+
createTransitionScope as $$createTransitionScope,
39+
renderScript as $$renderScript,
40+
createMetadata as $$createMetadata
41+
} from "http://localhost:3000/";
42+
import Component from '../components';
43+
44+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });
45+
46+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
47+
48+
return $$render`<html>
49+
<head>
50+
<title>Hello world</title>
51+
${$$renderHead($$result)}</head>
52+
<body>
53+
${$$renderComponent($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
54+
</body>
55+
</html>`;
56+
}, undefined, undefined);
57+
export default $$Component;
58+
```
59+
---
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
[TestPrinter/client:only_component_(multiple)_-_with_exact_parsing - 1]
3+
## Input
4+
5+
```
6+
/-/-/-/
7+
import Component from '../components';
8+
/-/-/-/
9+
<html>
10+
<head>
11+
<title>Hello world</title>
12+
</head>
13+
<body>
14+
<Component test="a" client:only />
15+
<Component test="b" client:only />
16+
<Component test="c" client:only />
17+
</body>
18+
</html>
19+
```
20+
21+
## Output
22+
23+
```js
24+
import {
25+
Fragment,
26+
render as $$render,
27+
createAstro as $$createAstro,
28+
createComponent as $$createComponent,
29+
renderComponent as $$renderComponent,
30+
renderHead as $$renderHead,
31+
maybeRenderHead as $$maybeRenderHead,
32+
unescapeHTML as $$unescapeHTML,
33+
renderSlot as $$renderSlot,
34+
mergeSlots as $$mergeSlots,
35+
addAttribute as $$addAttribute,
36+
spreadAttributes as $$spreadAttributes,
37+
defineStyleVars as $$defineStyleVars,
38+
defineScriptVars as $$defineScriptVars,
39+
renderTransition as $$renderTransition,
40+
createTransitionScope as $$createTransitionScope,
41+
renderScript as $$renderScript,
42+
createMetadata as $$createMetadata
43+
} from "http://localhost:3000/";
44+
import Component from '../components';
45+
46+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });
47+
48+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
49+
50+
return $$render`<html>
51+
<head>
52+
<title>Hello world</title>
53+
${$$renderHead($$result)}</head>
54+
<body>
55+
${$$renderComponent($$result,'Component',null,{"test":"a","client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
56+
${$$renderComponent($$result,'Component',null,{"test":"b","client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
57+
${$$renderComponent($$result,'Component',null,{"test":"c","client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
58+
</body>
59+
</html>`;
60+
}, undefined, undefined);
61+
export default $$Component;
62+
```
63+
---

0 commit comments

Comments
 (0)