Skip to content
Open
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
33 changes: 33 additions & 0 deletions packages/lint/src/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 2 additions & 2 deletions packages/lint/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions packages/lint/src/rules/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"",
Expand Down
14 changes: 8 additions & 6 deletions packages/lint/src/rules/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading