fix(lint): catch visible markup comments (#1819)

* fix(lint): catch visible markup comments

* test(lint): cover visible comment exemptions

* fix(lint): harden visible comment scan
This commit is contained in:
Miguel Ángel
2026-06-30 20:49:04 -07:00
committed by GitHub
parent 602590b44d
commit d1038918bb
3 changed files with 158 additions and 1 deletions
+81
View File
@@ -370,6 +370,87 @@ body {
expect(finding).toBeUndefined();
});
it("reports error when CSS block comment syntax leaks into visible markup", async () => {
const html = compositionWithBodyPrefix(
"",
`
/* Main Content Block */
<div class="editorial-block">Hello</div>
`,
);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "visible_markup_comment");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
expect(finding?.message).toContain("visible HTML markup");
expect(finding?.snippet).toContain("Main Content Block");
});
it("reports error when a misbalanced style block leaves block comment syntax visible", async () => {
const html = compositionWithBodyPrefix(
"",
`
<style>
.editorial-block { color: #fff; }
</style>
</style>
/* Main Content Block */
<div class="editorial-block">Hello</div>
`,
);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "visible_markup_comment");
expect(finding).toBeDefined();
expect(finding?.snippet).toContain("Main Content Block");
});
it("does not report block comments inside style or script blocks", async () => {
const html = `
<html>
<head>
<title>/* tab name */ Particle Field</title>
<style>
/* Layout reset */
body { margin: 0; }
</style>
<noscript>/* fallback note */</noscript>
</head>
<body>
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
<script>
/* Timeline registry */
window.__timelines = {};
</script>
</body>
</html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "visible_markup_comment");
expect(finding).toBeUndefined();
});
it("does not report block comments in attributes, html comments, or protected text contexts", async () => {
const html = compositionWithBodyPrefix(
"",
`
<!-- /* hidden implementation note */ -->
<div data-note="/* attribute note */"></div>
<div data-note="a > b /* quoted attribute note */"></div>
<pre>/* visible code sample */</pre>
<code>/* visible inline code sample */</code>
<textarea>/* editable code sample */</textarea>
<template>/* template-only note */</template>
<svg viewBox="0 0 100 20"><text x="0" y="15">/* svg label */</text></svg>
`,
);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "visible_markup_comment");
expect(finding).toBeUndefined();
});
it("reports error when a stray style close tag is left in the document head", async () => {
const html = compositionWithHead(`
<style>
+69
View File
@@ -68,6 +68,14 @@ 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;
interface SourceRange {
start: number;
end: number;
}
function findCodeFenceLeak(headWithoutValidBlocks: string): string | null {
return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
@@ -127,6 +135,50 @@ function findLeakedTextBeforeCompositionRoot(
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)) {
ranges.push({ start: match.index, end: match.index + match[0].length });
}
return ranges;
}
function isInsideSourceRange(index: number, ranges: SourceRange[]): boolean {
return ranges.some((range) => range.start <= index && index < range.end);
}
function isInsideHtmlTag(source: string, index: number): boolean {
let inTag = false;
let quote: '"' | "'" | null = null;
for (let i = 0; i < index; i++) {
const char = source[i];
if (!inTag) {
if (char === "<") inTag = true;
continue;
}
if (quote) {
if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'") {
quote = char;
} else if (char === ">") {
inTag = false;
}
}
return inTag;
}
function findVisibleMarkupCommentLeak(source: string): string | null {
const protectedRanges = findProtectedVisibleMarkupRanges(source);
for (const match of source.matchAll(VISIBLE_MARKUP_COMMENT_PATTERN)) {
if (isInsideHtmlTag(source, match.index)) continue;
if (isInsideSourceRange(match.index, protectedRanges)) continue;
return match[0];
}
return null;
}
export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
// root_missing_composition_id + root_missing_dimensions
({ rootTag }) => {
@@ -174,6 +226,23 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
];
},
// visible_markup_comment
({ source }) => {
const snippet = findVisibleMarkupCommentLeak(source);
if (!snippet) return [];
return [
{
code: "visible_markup_comment",
severity: "error",
message:
"CSS/JS block comment syntax (`/* ... */`) appears in visible HTML markup. HTML only treats `<!-- ... -->` as comments, so this renders as on-screen text.",
fixHint:
"Remove the text or convert it to a real HTML comment (`<!-- ... -->`). Keep CSS comments inside `<style>` and JS comments inside `<script>`.",
snippet: truncateSnippet(snippet),
},
];
},
// missing_timeline_registry + timeline_registry_missing_init
({ source, rawSource, options }) => {
// Sub-compositions inherit window.__timelines from the host composition