mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user