fix(core): lint leaked head text in compositions (#1727)

* fix(core): lint leaked head text in compositions

* fix(core): harden leaked head text lint

* chore: refresh code scanning status

* fix(core): cover parser-error head close tags
This commit is contained in:
Miguel Ángel
2026-06-25 21:25:37 -04:00
committed by GitHub
parent 92385711dc
commit bafed1fc9a
2 changed files with 284 additions and 0 deletions
+219
View File
@@ -1,6 +1,33 @@
import { describe, it, expect } from "vitest";
import { lintHyperframeHtml } from "../hyperframeLinter.js";
function compositionWithHead(headContent: string): string {
return `
<html>
<head>
${headContent}
</head>
<body>
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
<script>window.__timelines = {};</script>
</body>
</html>`;
}
function templateCompositionWithHead(headContent: string): string {
return `
<template>
<html>
<head>
${headContent}
</head>
<body>
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
</body>
</html>
</template>`;
}
describe("core rules", () => {
it("reports error when root is missing data-composition-id", async () => {
const html = `
@@ -134,6 +161,198 @@ describe("core rules", () => {
expect(finding).toBeUndefined();
});
it("reports error when CSS text is left outside a style block in the document head", async () => {
const html = compositionWithHead(`
<style>
body { margin: 0; }
</style>
</style>
/* Decorative Elements */
.particle {
position: absolute;
width: 4px;
height: 4px;
background: #fff;
}
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
expect(finding?.message).toContain("<head>");
expect(finding?.snippet).toContain(".particle");
});
it("reports error when a stray style close tag is left in the document head", async () => {
const html = compositionWithHead(`
<style>
body { margin: 0; }
</style>
</style>
`);
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 a stray script close tag is left in the document head", async () => {
const html = compositionWithHead(`
<script>
window.__headReady = true;
</script>
</script>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain("</script>");
});
it("does not report leaked head text for valid closing tags with trailing whitespace", async () => {
const html = compositionWithHead(`
<style>
body { margin: 0; }
</style data-parser-error-close>
<script>
window.__headReady = true;
</script
data-parser-error-close>
<title>Particle Field</title >
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("reports error when markdown code fences leak into the document head", async () => {
const withLanguage = compositionWithHead(`
\`\`\`css
.particle {
position: absolute;
}
\`\`\`
`);
const withoutLanguage = compositionWithHead(`
\`\`\`
.particle {
position: absolute;
}
\`\`\`
`);
const withTsxLanguage = compositionWithHead(`
\`\`\`tsx
export function Particle() {
return <div className="particle" />;
}
\`\`\`
`);
const withLanguageResult = await lintHyperframeHtml(withLanguage);
const withoutLanguageResult = await lintHyperframeHtml(withoutLanguage);
const withTsxLanguageResult = await lintHyperframeHtml(withTsxLanguage);
const languageFinding = withLanguageResult.findings.find((f) => f.code === "head_leaked_text");
const unlabeledFinding = withoutLanguageResult.findings.find(
(f) => f.code === "head_leaked_text",
);
const tsxLanguageFinding = withTsxLanguageResult.findings.find(
(f) => f.code === "head_leaked_text",
);
expect(languageFinding).toBeDefined();
expect(languageFinding?.snippet).toContain("```css");
expect(unlabeledFinding).toBeDefined();
expect(unlabeledFinding?.snippet).toContain("```");
expect(tsxLanguageFinding).toBeDefined();
expect(tsxLanguageFinding?.snippet).toContain("```tsx");
});
it("reports error when CSS at-rules leak into the document head", async () => {
const html = compositionWithHead(`
@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("reports leaked CSS when a style block is unclosed in the document head", async () => {
const html = compositionWithHead(`
<style>
.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(".particle");
});
it("does not report leaked head text for commented CSS", async () => {
const html = compositionWithHead(`
<!-- .particle { color: red; } -->
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("does not report leaked head text for valid noscript content", async () => {
const html = compositionWithHead(`
<noscript>
.no-js { display: block; }
</noscript>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("does not report orphan CSS for valid head metadata and style blocks", async () => {
const html = compositionWithHead(`
<title>Particle Field</title>
<meta name="description" content="Particle field">
<link rel="preconnect" href="https://fonts.gstatic.com">
<base href="https://example.com/">
<style>
.particle {
position: absolute;
width: 4px;
height: 4px;
}
</style>
`);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("reports leaked head text inside template-wrapped sub-compositions", async () => {
const html = templateCompositionWithHead(`
</style>
.particle { color: white; }
`);
const result = await lintHyperframeHtml(html, { isSubComposition: true });
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain(".particle");
});
describe("timeline_id_mismatch", () => {
it("accepts dot timeline registration", async () => {
const html = `
+65
View File
@@ -57,6 +57,54 @@ function describeStudioElement(tag: { raw: string; name: string }): string {
return parts.join("");
}
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 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 =
/(?:^|\s)@(?:container|font-face|keyframes|layer|media|page|property|scope|supports)[^{<]*\{[\s\S]*?:[\s\S]*?\}/i;
const ORPHAN_CSS_RULE_PATTERN =
/(?:^|\s)(?:\/\*[\s\S]*?\*\/\s*)?(?:@[a-z-]+[^{}<]*|[.#][\w-]+[^{}<]*|[a-z][\w-]*(?:\s+[.#:[\w-][^{}<]*)?)\s*\{[^{}]*:[^{}]*\}/i;
function findCodeFenceLeak(headWithoutValidBlocks: string): string | null {
return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
}
function findOrphanCssLeak(headContent: string): string | null {
const residualText = headContent
.replace(HEAD_BLOCKS_TO_IGNORE_PATTERN, " ")
.replace(HTML_TAG_PATTERN, " ");
return (
ORPHAN_CSS_AT_RULE_PATTERN.exec(residualText)?.[0] ??
ORPHAN_CSS_RULE_PATTERN.exec(residualText)?.[0] ??
null
);
}
function findStrayCloseLeak(headWithoutValidBlocks: string): string | null {
return STRAY_HEAD_CLOSE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
}
function findLeakedTextInHeadContent(headContent: string): string | null {
const withoutValidBlocks = headContent.replace(HEAD_BLOCKS_TO_IGNORE_PATTERN, " ");
return (
findCodeFenceLeak(withoutValidBlocks) ??
findOrphanCssLeak(headContent) ??
findStrayCloseLeak(withoutValidBlocks)
);
}
function findLeakedTextInHead(rawSource: string): string | null {
const headMatches = [...rawSource.matchAll(HEAD_CONTENT_PATTERN)];
for (const match of headMatches) {
const leakedText = findLeakedTextInHeadContent(match[1] ?? "");
if (leakedText) return leakedText;
}
return null;
}
export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
// root_missing_composition_id + root_missing_dimensions
({ rootTag }) => {
@@ -84,6 +132,23 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
return findings;
},
// head_leaked_text
({ source }) => {
const snippet = findLeakedTextInHead(source);
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.",
fixHint:
"Move CSS into a single `<style>...</style>` block and remove stray close tags, markdown fences, or code text from `<head>`.",
snippet: truncateSnippet(snippet),
},
];
},
// missing_timeline_registry + timeline_registry_missing_init
({ source, rawSource, options }) => {
// Sub-compositions inherit window.__timelines from the host composition