Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/lint/src/rules/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"",
`
<div>Scene content</div>
<script>
// Keep the real document <head> untouched.
window.__sceneReady = true;
</script>
`,
);
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 {
Expand Down
17 changes: 12 additions & 5 deletions packages/lint/src/rules/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /<head\b[^>]*>([\s\S]*?)(?:<\/head>|<body\b|$)/gi;
const AFTER_HEAD_BEFORE_BODY_PATTERN = /<\/head(?:\s[^>]*)?>([\s\S]*?)(?=<body\b|$)/gi;
Expand All @@ -78,6 +80,13 @@ interface SourceRange {
end: number;
}

function findSourceRanges(source: string, pattern: RegExp): SourceRange[] {
return [...source.matchAll(pattern)].map((match) => ({
start: match.index,
end: match.index + match[0].length,
}));
}

function findCodeFenceLeak(headWithoutValidBlocks: string): string | null {
return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading