diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts index 1419c545ee..dedd37c9a6 100644 --- a/packages/lint/src/rules/core.test.ts +++ b/packages/lint/src/rules/core.test.ts @@ -421,6 +421,23 @@ body { expect(finding).toBeUndefined(); }); + it("does not treat a head tag literal inside a body script comment as document markup", async () => { + const html = compositionWithBodyPrefix( + "", + ` +
Scene content
+ +`, + ); + 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 { diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts index f4b6b8a300..7518bcfd8d 100644 --- a/packages/lint/src/rules/core.ts +++ b/packages/lint/src/rules/core.ts @@ -60,6 +60,8 @@ function describeStudioElement(tag: { raw: string; name: string }): string { const HEAD_BLOCKS_TO_IGNORE_PATTERN = /<(?:style|script|template|title|noscript)\b[^>]*>[\s\S]*?<\/(?:style|script|template|title|noscript)(?:\s[^>]*)?>/gi; +const HEAD_MARKUP_PROTECTED_BLOCK_PATTERN = + /<(style|script|title|noscript|pre|code|textarea|text)\b[^>]*>[\s\S]*?<\/\1(?:\s[^>]*)?>/gi; const HTML_TAG_PATTERN = /<[^>]+>/g; const HEAD_CONTENT_PATTERN = /]*>([\s\S]*?)(?:<\/head>|]*)?>([\s\S]*?)(?= ({ + start: match.index, + end: match.index + match[0].length, + })); +} + function findCodeFenceLeak(headWithoutValidBlocks: string): string | null { return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null; } @@ -107,8 +116,10 @@ function findLeakedTextInHeadContent(headContent: string): string | null { } function findLeakedTextInHead(rawSource: string): string | null { + const protectedRanges = findSourceRanges(rawSource, HEAD_MARKUP_PROTECTED_BLOCK_PATTERN); const headMatches = [...rawSource.matchAll(HEAD_CONTENT_PATTERN)]; for (const match of headMatches) { + if (isInsideSourceRange(match.index, protectedRanges)) continue; const leakedText = findLeakedTextInHeadContent(match[1] ?? ""); if (leakedText) return leakedText; } @@ -137,11 +148,7 @@ function findLeakedTextBeforeCompositionRoot( } function findProtectedVisibleMarkupRanges(source: string): SourceRange[] { - const ranges: SourceRange[] = []; - for (const match of source.matchAll(VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN)) { - ranges.push({ start: match.index, end: match.index + match[0].length }); - } - return ranges; + return findSourceRanges(source, VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN); } function isInsideSourceRange(index: number, ranges: SourceRange[]): boolean {