Skip to content

Commit 3f48024

Browse files
committed
fix(runtime): close body before </html> not after, to avoid crossed tags
1 parent 94e915e commit 3f48024

2 files changed

Lines changed: 47 additions & 9 deletions

File tree

packages/runtime/src/index.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,44 @@ describe('buildSrcdoc', () => {
115115
expect(bodyOpen).toBeLessThan(contentIdx);
116116
});
117117

118+
it('shape 2 with </html>: inserts </body> BEFORE </html>, not after', () => {
119+
const html = '<html><head></head><body><p>open only</p></html>';
120+
const out = buildSrcdoc(html);
121+
const bodyClose = out.indexOf('</body>');
122+
const htmlClose = out.indexOf('</html>');
123+
expect(bodyClose).toBeGreaterThanOrEqual(0);
124+
expect(htmlClose).toBeGreaterThan(bodyClose);
125+
expect(out).not.toMatch(/<\/html\s*>[\s\S]*<\/body\s*>/i);
126+
});
127+
128+
it('shape 4 with <html>...</html> shell: <body> after <html>, </body> before </html>', () => {
129+
const html = '<html><p>shell only</p></html>';
130+
const out = buildSrcdoc(html);
131+
const htmlOpen = out.search(/<html[^>]*>/i);
132+
const bodyOpen = out.search(BODY_OPEN_RE);
133+
const content = out.indexOf('<p>shell only</p>');
134+
const bodyClose = out.indexOf('</body>');
135+
const htmlClose = out.indexOf('</html>');
136+
expect(htmlOpen).toBeLessThan(bodyOpen);
137+
expect(bodyOpen).toBeLessThan(content);
138+
expect(content).toBeLessThan(bodyClose);
139+
expect(bodyClose).toBeLessThan(htmlClose);
140+
});
141+
142+
it('shape 4 with </head> + </html>: <body> after </head>, </body> before </html>', () => {
143+
const html = '<html><head><title>t</title></head><p>x</p></html>';
144+
const out = buildSrcdoc(html);
145+
const headClose = out.indexOf('</head>');
146+
const bodyOpen = out.search(BODY_OPEN_RE);
147+
const content = out.indexOf('<p>x</p>');
148+
const bodyClose = out.indexOf('</body>');
149+
const htmlClose = out.indexOf('</html>');
150+
expect(headClose).toBeLessThan(bodyOpen);
151+
expect(bodyOpen).toBeLessThan(content);
152+
expect(content).toBeLessThan(bodyClose);
153+
expect(bodyClose).toBeLessThan(htmlClose);
154+
});
155+
118156
it('shape 4: wraps content in <body>...</body> when neither tag is present', () => {
119157
const out = buildSrcdoc('<div>none</div>');
120158
expect(out).toContain('<body>');

packages/runtime/src/index.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@ const BODY_CLOSE_RE = /<\/body\s*>/i;
2727
* baseline/overlay injection can rely on them. Handles all four input
2828
* shapes: both tags, opener only, closer only, neither.
2929
*/
30+
function closeBody(html: string): string {
31+
return /<\/html\s*>/i.test(html)
32+
? html.replace(/<\/html\s*>/i, '</body></html>')
33+
: `${html}</body>`;
34+
}
35+
3036
function normalizeBodyTags(html: string): string {
3137
const hasOpen = BODY_OPEN_RE.test(html);
3238
const hasClose = BODY_CLOSE_RE.test(html);
3339

3440
if (hasOpen && hasClose) return html;
3541

36-
if (hasOpen && !hasClose) {
37-
return `${html}</body>`;
38-
}
42+
if (hasOpen && !hasClose) return closeBody(html);
3943

4044
if (!hasOpen && hasClose) {
4145
if (HEAD_CLOSE_RE.test(html)) {
@@ -48,12 +52,8 @@ function normalizeBodyTags(html: string): string {
4852
}
4953

5054
// Neither opener nor closer.
51-
if (HEAD_CLOSE_RE.test(html)) {
52-
return `${html.replace(HEAD_CLOSE_RE, (m) => `${m}<body>`)}</body>`;
53-
}
54-
if (HTML_RE.test(html)) {
55-
return `${html.replace(HTML_RE, (m) => `${m}<body>`)}</body>`;
56-
}
55+
if (HEAD_CLOSE_RE.test(html)) return closeBody(html.replace(HEAD_CLOSE_RE, (m) => `${m}<body>`));
56+
if (HTML_RE.test(html)) return closeBody(html.replace(HTML_RE, (m) => `${m}<body>`));
5757
return `<body>${html}</body>`;
5858
}
5959

0 commit comments

Comments
 (0)