fix(lint): catch visible css before composition root (#1751)

This commit is contained in:
Miguel Ángel
2026-06-26 18:41:38 -04:00
committed by GitHub
parent 1d3c68ef69
commit c7830b54c6
2 changed files with 215 additions and 4 deletions
+186
View File
@@ -14,6 +14,54 @@ ${headContent}
</html>`;
}
function compositionWithHeadBoundary(boundaryContent: string): string {
return `
<html>
<head>
<style>
body { margin: 0; }
</style>
</head>
${boundaryContent}
<body>
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
<script>window.__timelines = {};</script>
</body>
</html>`;
}
function compositionWithBodyPrefix(prefixContent: string, rootContent = ""): string {
return `
<html>
<head>
<style>
body { margin: 0; }
</style>
</head>
<body>
${prefixContent}
<div data-composition-id="c1" data-width="1920" data-height="1080">
${rootContent}
</div>
<script>window.__timelines = {};</script>
</body>
</html>`;
}
function compositionWithImplicitBodyPrefix(prefixContent: string): string {
return `
<html>
<head>
<style>
body { margin: 0; }
</style>
</head>
${prefixContent}
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
<script>window.__timelines = {};</script>
</html>`;
}
function templateCompositionWithHead(headContent: string): string {
return `
<template>
@@ -184,6 +232,144 @@ describe("core rules", () => {
expect(finding?.snippet).toContain(".particle");
});
it("reports error when CSS variables leak between head and body", async () => {
const html = compositionWithHeadBoundary(`
--bg-color: #F5F1E8;
--text-color: #212121;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.message).toContain("<head>");
expect(finding?.snippet).toContain("body");
});
it("reports error when stray close tags leak between head and body", async () => {
const html = compositionWithHeadBoundary(`
</style>
</script>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain("</style>");
});
it("reports error when markdown code fences leak between head and body", async () => {
const html = compositionWithHeadBoundary(`
\`\`\`css
.particle {
color: white;
}
\`\`\`
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain("```css");
});
it("reports error when CSS at-rules leak between head and body", async () => {
const html = compositionWithHeadBoundary(`
@media (min-width: 800px) {
.particle {
transform: scale(1.2);
}
}
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain("@media");
});
it("does not report leaked text for valid script and style blocks around the head boundary", async () => {
const html = compositionWithHeadBoundary(`
<script>
window.__headReady = true;
</script>
<template>
<style>
.template-only { color: red; }
</style>
</template>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("reports error when CSS text leaks before the composition root", async () => {
const html = compositionWithBodyPrefix(`
.orphan {
position: absolute;
inset: 0;
}
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain(".orphan");
});
it("reports error when CSS text leaks before the composition root without an explicit body", async () => {
const html = compositionWithImplicitBodyPrefix(`
.implicit-body-orphan {
position: absolute;
inset: 0;
}
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain(".implicit-body-orphan");
});
it("does not report leaked text for valid script and style blocks before the composition root", async () => {
const html = compositionWithBodyPrefix(`
<style>
.pre-root-helper { color: red; }
</style>
<script>
window.__preRootReady = true;
</script>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("does not report CSS-looking educational text inside the composition root", async () => {
const html = compositionWithBodyPrefix(
"",
`
<pre>
body {
margin: 0;
}
</pre>
`,
);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("reports error when a stray style close tag is left in the document head", async () => {
const html = compositionWithHead(`
<style>
+29 -4
View File
@@ -61,6 +61,7 @@ const HEAD_BLOCKS_TO_IGNORE_PATTERN =
/<(?:style|script|template|title|noscript)\b[^>]*>[\s\S]*?<\/(?:style|script|template|title|noscript)(?:\s[^>]*)?>/gi;
const HTML_TAG_PATTERN = /<[^>]+>/g;
const HEAD_CONTENT_PATTERN = /<head\b[^>]*>([\s\S]*?)(?:<\/head>|<body\b|$)/gi;
const AFTER_HEAD_BEFORE_BODY_PATTERN = /<\/head(?:\s[^>]*)?>([\s\S]*?)(?=<body\b|$)/gi;
const STRAY_HEAD_CLOSE_PATTERN = /<\/(?:style|script)(?:\s[^>]*)?>/i;
const MARKDOWN_CODE_FENCE_PATTERN = /```[^\r\n`]*(?:\r?\n|$)[\s\S]*?```/i;
const ORPHAN_CSS_AT_RULE_PATTERN =
@@ -105,6 +106,27 @@ function findLeakedTextInHead(rawSource: string): string | null {
return null;
}
function findLeakedTextBetweenHeadAndBody(rawSource: string): string | null {
const boundaryMatches = [...rawSource.matchAll(AFTER_HEAD_BEFORE_BODY_PATTERN)];
for (const match of boundaryMatches) {
const leakedText = findLeakedTextInHeadContent(match[1] ?? "");
if (leakedText) return leakedText;
}
return null;
}
function findLeakedTextBeforeCompositionRoot(
source: string,
rootTag: LintContext["rootTag"],
): string | null {
if (!rootTag || rootTag.name === "body") return null;
const bodyOpenMatch = /<body\b[^>]*>/i.exec(source);
const prefixStart = bodyOpenMatch ? bodyOpenMatch.index + bodyOpenMatch[0].length : 0;
const prefixEnd = rootTag.index;
if (prefixEnd <= prefixStart) return null;
return findLeakedTextInHeadContent(source.slice(prefixStart, prefixEnd));
}
export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
// root_missing_composition_id + root_missing_dimensions
({ rootTag }) => {
@@ -133,17 +155,20 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
},
// head_leaked_text
({ source }) => {
const snippet = findLeakedTextInHead(source);
({ source, rootTag }) => {
const snippet =
findLeakedTextInHead(source) ??
findLeakedTextBetweenHeadAndBody(source) ??
findLeakedTextBeforeCompositionRoot(source, rootTag);
if (!snippet) return [];
return [
{
code: "head_leaked_text",
severity: "error",
message:
"Detected leaked code or CSS text in the document `<head>`. Browsers render this as visible text in the video.",
"Detected leaked code or CSS text around the document `<head>` or before the composition root. Browsers render this as visible text in the video.",
fixHint:
"Move CSS into a single `<style>...</style>` block and remove stray close tags, markdown fences, or code text from `<head>`.",
"Move CSS into a single `<style>...</style>` block and remove stray close tags, markdown fences, or code text from `<head>`, the `</head>`/`<body>` boundary, or the pre-root body prefix.",
snippet: truncateSnippet(snippet),
},
];