mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(lint): flag video/img src pointing at an audio file (#3741)
* feat(lint): flag video/img src pointing at an audio file Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(lint): drop m4a from the audio src kinds Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b0ac581d8e
commit
0d5d3f3eb3
@@ -853,6 +853,62 @@ describe("media_src_kind_mismatch", () => {
|
||||
expect(result.findings.find((f) => f.code === "media_src_kind_mismatch")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("errors when <img> src is an audio file", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<img id="logo" src="https://cdn.example.com/brand-assets/music/theme.mp3?sig=abc">
|
||||
</div>
|
||||
<script>window.__timelines = {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "media_src_kind_mismatch");
|
||||
expect(finding?.severity).toBe("error");
|
||||
expect(finding?.elementId).toBe("logo");
|
||||
expect(finding?.message).toContain("an audio file");
|
||||
});
|
||||
|
||||
it("errors when <video> src is an audio file", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<video id="bg" src="theme.wav" data-start="0" data-duration="5" muted></video>
|
||||
</div>
|
||||
<script>window.__timelines = {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "media_src_kind_mismatch");
|
||||
expect(finding?.severity).toBe("error");
|
||||
expect(finding?.elementId).toBe("bg");
|
||||
});
|
||||
|
||||
it("does not flag <audio> pointing at a video container", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<audio id="sfx" src="whoosh.mp4" data-start="0"></audio>
|
||||
</div>
|
||||
<script>window.__timelines = {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(result.findings.find((f) => f.code === "media_src_kind_mismatch")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not flag .ogg or .m4a, whose containers can carry video too", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<video id="v1" src="clip.ogg" data-start="0" data-duration="5" muted></video>
|
||||
<video id="v2" src="clip.m4a" data-start="0" data-duration="5" muted></video>
|
||||
<img id="i1" src="clip.ogg" data-start="0" data-duration="5" />
|
||||
<img id="i2" src="clip.m4a" data-start="0" data-duration="5" />
|
||||
</div>
|
||||
<script>window.__timelines = {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(result.findings.find((f) => f.code === "media_src_kind_mismatch")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not flag extensionless or audio src", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -67,7 +67,17 @@ const VIDEO_SRC_EXT = new Set([
|
||||
"mpeg",
|
||||
]);
|
||||
|
||||
function srcKind(src: string): "image" | "video" | null {
|
||||
const AUDIO_SRC_EXT = new Set(["mp3", "wav", "aac", "flac", "opus", "aiff", "wma"]);
|
||||
|
||||
type SrcKind = "image" | "video" | "audio";
|
||||
|
||||
const SRC_KIND_NOUN: Record<SrcKind, string> = {
|
||||
image: "an image",
|
||||
video: "a video",
|
||||
audio: "an audio file",
|
||||
};
|
||||
|
||||
function srcKind(src: string): SrcKind | null {
|
||||
const stripped = src.trim();
|
||||
if (!stripped) return null;
|
||||
const lower = stripped.toLowerCase();
|
||||
@@ -76,6 +86,7 @@ function srcKind(src: string): "image" | "video" | null {
|
||||
if (!mime) return null;
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime.startsWith("video/")) return "video";
|
||||
if (mime.startsWith("audio/")) return "audio";
|
||||
return null;
|
||||
}
|
||||
if (lower.startsWith("blob:")) return null;
|
||||
@@ -95,6 +106,7 @@ function srcKind(src: string): "image" | "video" | null {
|
||||
const ext = base.slice(dot + 1).toLowerCase();
|
||||
if (IMAGE_SRC_EXT.has(ext)) return "image";
|
||||
if (VIDEO_SRC_EXT.has(ext)) return "video";
|
||||
if (AUDIO_SRC_EXT.has(ext)) return "audio";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -106,19 +118,18 @@ function findMediaSrcKindMismatchFindings(ctx: LintContext): HyperframeLintFindi
|
||||
if (!src) continue;
|
||||
const kind = srcKind(src);
|
||||
if (kind === null) continue;
|
||||
if (tag.name === "video" && kind !== "image") continue;
|
||||
if (tag.name === "img" && kind !== "video") continue;
|
||||
const elementId = readAttr(tag.raw, "id") || undefined;
|
||||
const expected = tag.name === "video" ? "video" : "image";
|
||||
if (kind === expected) continue;
|
||||
const elementId = readAttr(tag.raw, "id") || undefined;
|
||||
findings.push({
|
||||
code: "media_src_kind_mismatch",
|
||||
severity: "error",
|
||||
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is a ${kind}, not a ${expected}. The producer fail-closes when the tag and file kind disagree.`,
|
||||
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is ${SRC_KIND_NOUN[kind]}, not ${SRC_KIND_NOUN[expected]}. The producer fail-closes when the tag and file kind disagree.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
tag.name === "video"
|
||||
? "Use <img> for a still, or point <video> at a video URL (mp4/webm/mov/…)."
|
||||
: "Use <video> for a video URL, or point <img> at a still (png/jpg/webp/…).",
|
||||
? "Use <img> for a still, <audio> for sound, or point <video> at a video URL (mp4/webm/mov/…)."
|
||||
: "Use <video> for a video URL, <audio> for sound, or point <img> at a still (png/jpg/webp/…).",
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user