fix(lint): avoid unproven media and head leaks

This commit is contained in:
Miguel Ángel
2026-07-13 17:58:05 +00:00
parent 95fa14b2c2
commit 53beb868cd
4 changed files with 54 additions and 8 deletions
+33
View File
@@ -235,3 +235,36 @@ describe("template shell style sources", () => {
expect(findings.some((finding) => finding.code === "texture_mask_asset_not_found")).toBe(true);
});
});
describe("duplicate_audio_track", () => {
async function duplicateAudioFindings(audioMarkup: string): Promise<HyperframeLintFinding[]> {
const project = makeProject(`<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="20">
${audioMarkup}
</div>
<script>window.__timelines = {};</script>
</body></html>`);
const { results } = await lintProject(project);
return results
.flatMap((entry) => entry.result.findings)
.filter((finding) => finding.code === "duplicate_audio_track");
}
it("does not infer overlap when same-track clips omit data-duration", async () => {
const findings = await duplicateAudioFindings(`
<audio id="first" src="voice.wav" data-track-index="0" data-start="0"></audio>
<audio id="second" src="voice.wav" data-track-index="0" data-start="10"></audio>
`);
expect(findings).toHaveLength(0);
});
it("still reports overlap when explicit clip durations overlap", async () => {
const findings = await duplicateAudioFindings(`
<audio id="first" src="voice.wav" data-track-index="0" data-start="0" data-duration="12"></audio>
<audio id="second" src="voice.wav" data-track-index="0" data-start="10" data-duration="5"></audio>
`);
expect(findings).toHaveLength(1);
});
});
+2 -2
View File
@@ -503,11 +503,11 @@ function lintDuplicateAudioTracks(htmlSources: HtmlSource[]): HyperframeLintFind
const startStr = extractAttr(tag, "data-start");
const durStr = extractAttr(tag, "data-duration");
const src = extractAttr(tag, "src") ?? "unknown";
if (!trackStr || !startStr) continue;
if (!trackStr || !startStr || !durStr) continue;
const trackIndex = parseInt(trackStr, 10);
const start = parseFloat(startStr);
const duration = durStr ? parseFloat(durStr) : Infinity;
const duration = parseFloat(durStr);
const key = `${src}:${start}:${duration}:${trackIndex}`;
if (seen.has(key)) continue;
seen.add(key);
+11
View File
@@ -464,6 +464,17 @@ body {
expect(finding).toBeUndefined();
});
it("does not treat a head tag literal inside a template script comment as document head", async () => {
const html = compositionWithBodyPrefix(
"",
`<template><script>// Explain the literal <head> marker.</script></template>`,
);
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "head_leaked_text");
expect(finding).toBeUndefined();
});
it("does not report CSS-looking educational text inside the composition root", async () => {
const html = compositionWithBodyPrefix(
"",
+8 -6
View File
@@ -107,12 +107,14 @@ function findLeakedTextInHeadContent(headContent: string): string | null {
}
function findLeakedTextInHead(rawSource: string): string | null {
const headMatches = [...rawSource.matchAll(HEAD_CONTENT_PATTERN)];
for (const match of headMatches) {
const leakedText = findLeakedTextInHeadContent(match[1] ?? "");
if (leakedText) return leakedText;
}
return null;
const headPattern = new RegExp(HEAD_CONTENT_PATTERN.source, "i");
const match = headPattern.exec(rawSource);
if (!match) return null;
const bodyIndex = rawSource.search(/<body\b/i);
if (bodyIndex >= 0 && match.index > bodyIndex) return null;
return findLeakedTextInHeadContent(match[1] ?? "");
}
function findLeakedTextBetweenHeadAndBody(rawSource: string): string | null {