diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts index 604c0afc25..e4d7fc2814 100644 --- a/packages/lint/src/rules/core.test.ts +++ b/packages/lint/src/rules/core.test.ts @@ -1,35 +1,6 @@ import { describe, it, expect } from "vitest"; import { lintHyperframeHtml } from "../hyperframeLinter.js"; -function compositionWithHead(headContent: string): string { - return ` - - -${headContent} - - -
- - -`; -} - -function compositionWithHeadBoundary(boundaryContent: string): string { - return ` - - - - -${boundaryContent} - -
- - -`; -} - function compositionWithBodyPrefix(prefixContent: string, rootContent = ""): string { return ` @@ -48,34 +19,6 @@ ${rootContent} `; } -function compositionWithImplicitBodyPrefix(prefixContent: string): string { - return ` - - - - -${prefixContent} -
- -`; -} - -function templateCompositionWithHead(headContent: string): string { - return ` -`; -} - describe("core rules", () => { it("does not lint scripts embedded inside an iframe srcdoc attribute", async () => { const html = ` @@ -218,7 +161,7 @@ describe("core rules", () => { // Regression: a CSS comment referencing an SVG tag name (e.g. `/* wrapper */`) // inside a - - /* 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(""); - 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(""); - expect(finding?.snippet).toContain("body"); - }); - - it("reports error when stray close tags leak between head and body", async () => { - const html = compositionWithHeadBoundary(` - - -`); - const result = await lintHyperframeHtml(html); - const finding = result.findings.find((f) => f.code === "head_leaked_text"); - - expect(finding).toBeDefined(); - expect(finding?.snippet).toContain(""); - }); - - 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(` - - -`); - 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(` - - -`); - 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( - "", - ` -
-      body {
-        margin: 0;
-      }
-    
-`, - ); - const result = await lintHyperframeHtml(html); - const finding = result.findings.find((f) => f.code === "head_leaked_text"); - - expect(finding).toBeUndefined(); - }); - it("reports error when CSS block comment syntax leaks into visible markup", async () => { const html = compositionWithBodyPrefix( "", @@ -585,175 +366,6 @@ body { expect(finding).toBeUndefined(); }); - it("reports error when a stray style close tag is left in the document head", async () => { - const html = compositionWithHead(` - - -`); - const result = await lintHyperframeHtml(html); - const finding = result.findings.find((f) => f.code === "head_leaked_text"); - - expect(finding).toBeDefined(); - expect(finding?.snippet).toContain(""); - }); - - it("reports error when a stray script close tag is left in the document head", async () => { - const html = compositionWithHead(` - - -`); - const result = await lintHyperframeHtml(html); - const finding = result.findings.find((f) => f.code === "head_leaked_text"); - - expect(finding).toBeDefined(); - expect(finding?.snippet).toContain(""); - }); - - it("does not report leaked head text for valid closing tags with trailing whitespace", async () => { - const html = compositionWithHead(` - - - Particle Field -`); - 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
; - } - \`\`\` -`); - 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(` - -`); - 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(` - - .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 = ` diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts index bb3eecaeff..de31a2cfc8 100644 --- a/packages/lint/src/rules/core.ts +++ b/packages/lint/src/rules/core.ts @@ -130,17 +130,6 @@ 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 = /]*>([\s\S]*?)(?:<\/head>|]*)?>([\s\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; const VISIBLE_MARKUP_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g; const VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN = /<(style|script|template|title|noscript|pre|code|textarea|text)\b[^>]*>[\s\S]*?<\/\1(?:\s[^>]*)?>/gi; @@ -150,64 +139,6 @@ interface SourceRange { end: number; } -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; -} - -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 = /]*>/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)); -} - function findProtectedVisibleMarkupRanges(source: string): SourceRange[] { const ranges: SourceRange[] = []; for (const match of source.matchAll(VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN)) { @@ -298,26 +229,6 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ return findings; }, - // head_leaked_text - ({ 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 around the document `` or before the composition root. Browsers render this as visible text in the video.", - fixHint: - "Move CSS into a single `` block and remove stray close tags, markdown fences, or code text from ``, the ``/`` boundary, or the pre-root body prefix.", - snippet: truncateSnippet(snippet), - }, - ]; - }, - // visible_markup_comment ({ source }) => { const snippet = findVisibleMarkupCommentLeak(source);