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 {