mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +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:
@@ -34,8 +34,15 @@ export function buildLintContext(html: string, options: HyperframeLinterOptions
|
|||||||
// hijack the boundary match below. Linear + fixpoint (see stripHtmlComments) to
|
// hijack the boundary match below. Linear + fixpoint (see stripHtmlComments) to
|
||||||
// stay ReDoS-free and catch markers that re-form when a comment is removed.
|
// stay ReDoS-free and catch markers that re-form when a comment is removed.
|
||||||
let source = stripHtmlComments(rawSource);
|
let source = stripHtmlComments(rawSource);
|
||||||
|
const sourceWithoutTemplates = source.replace(
|
||||||
|
/<template\b[^>]*>[\s\S]*?<\/template(?:\s[^>]*)?>/gi,
|
||||||
|
" ",
|
||||||
|
);
|
||||||
const templateMatch = source.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
const templateMatch = source.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
||||||
if (templateMatch?.[1]) source = templateMatch[1];
|
// Some sub-composition files are HTML shells whose real root lives inside a
|
||||||
|
// <template>. Keep nested templates intact when the visible document already
|
||||||
|
// has a composition root; only unwrap when no root exists outside templates.
|
||||||
|
if (templateMatch?.[1] && !findRootTag(sourceWithoutTemplates)) source = templateMatch[1];
|
||||||
|
|
||||||
const tags = extractOpenTags(source);
|
const tags = extractOpenTags(source);
|
||||||
const styles = [
|
const styles = [
|
||||||
|
|||||||
@@ -370,6 +370,87 @@ body {
|
|||||||
expect(finding).toBeUndefined();
|
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 () => {
|
it("reports error when a stray style close tag is left in the document head", async () => {
|
||||||
const html = compositionWithHead(`
|
const html = compositionWithHead(`
|
||||||
<style>
|
<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;
|
/(?:^|\s)@(?:container|font-face|keyframes|layer|media|page|property|scope|supports)[^{<]*\{[\s\S]*?:[\s\S]*?\}/i;
|
||||||
const ORPHAN_CSS_RULE_PATTERN =
|
const ORPHAN_CSS_RULE_PATTERN =
|
||||||
/(?:^|\s)(?:\/\*[\s\S]*?\*\/\s*)?(?:@[a-z-]+[^{}<]*|[.#][\w-]+[^{}<]*|[a-z][\w-]*(?:\s+[.#:[\w-][^{}<]*)?)\s*\{[^{}]*:[^{}]*\}/i;
|
/(?:^|\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 {
|
function findCodeFenceLeak(headWithoutValidBlocks: string): string | null {
|
||||||
return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
|
return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
|
||||||
@@ -127,6 +135,50 @@ function findLeakedTextBeforeCompositionRoot(
|
|||||||
return findLeakedTextInHeadContent(source.slice(prefixStart, prefixEnd));
|
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[]> = [
|
export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||||
// root_missing_composition_id + root_missing_dimensions
|
// root_missing_composition_id + root_missing_dimensions
|
||||||
({ rootTag }) => {
|
({ 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
|
// missing_timeline_registry + timeline_registry_missing_init
|
||||||
({ source, rawSource, options }) => {
|
({ source, rawSource, options }) => {
|
||||||
// Sub-compositions inherit window.__timelines from the host composition
|
// Sub-compositions inherit window.__timelines from the host composition
|
||||||
|
|||||||
Reference in New Issue
Block a user