fix(lint): recognise .avif and .m4a in the media src kind rule (#4779)

The media_src_kind_mismatch rule classified .mp3 and .jpeg but not .m4a or
.avif, so an <img> pointed at an audio file or a <video> at a still slipped
through for those two formats. Add .avif to IMAGE_SRC_EXT and .m4a to
AUDIO_SRC_EXT.

.ogg stays out: its container can carry Theora video as well as Vorbis or
Opus, so the extension does not name the kind. .m4a is the opposite case --
it is audio-only, and video in the same MP4 container is .m4v, which
VIDEO_SRC_EXT already lists. The .ogg case keeps its existing coverage and
gains an explicit unknown-extension case alongside it.

Refs #4382

Co-authored-by: Yi-111-a <Yi-111-a@users.noreply.github.com>
This commit is contained in:
yi111
2026-10-24 00:43:28 +00:00
committed by GitHub
co-authored by Yi-111-a
parent 0e0f7f803a
commit 0648f1ed8d
2 changed files with 91 additions and 4 deletions
+89 -3
View File
@@ -1271,14 +1271,100 @@ describe("media_src_kind_mismatch", () => {
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 () => {
it("errors when <img> src is an .m4a audio file", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<img id="logo" src="voice.m4a" data-start="0" data-duration="5" />
</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 .m4a audio file", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="bg" src="voice.m4a" 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");
expect(finding?.message).toContain("an audio file");
});
it("errors when <video> src is an .avif image", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="bg" src="photo.avif" 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");
expect(finding?.message).toContain("an image");
});
it("does not flag matching .avif and .m4a src kinds", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<img id="i1" src="photo.avif">
<audio id="a1" src="voice.m4a" data-start="0" data-duration="5"></audio>
<video id="v1" src="clip.m4v" data-start="0" data-duration="5" muted></video>
</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("reads .avif and .m4a extensions case-insensitively and past query/hash suffixes", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" src="photo.AVIF?v=2" data-start="0" data-duration="5" muted></video>
<img id="i1" src="voice.M4A#t=5" data-start="0" data-duration="5" />
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const findings = result.findings.filter((f) => f.code === "media_src_kind_mismatch");
expect(findings.map((f) => f.elementId)).toEqual(["v1", "i1"]);
expect(findings[0]?.message).toContain("an image");
expect(findings[1]?.message).toContain("an audio file");
});
it("does not flag .ogg, whose container 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 unknown extensions, whose kind cannot be inferred", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" src="clip.xyz" data-start="0" data-duration="5" muted></video>
<img id="i1" src="still.xyz" data-start="0" data-duration="5" />
</div>
<script>window.__timelines = {};</script>
</body></html>`;
+2 -1
View File
@@ -64,6 +64,7 @@ const IMAGE_SRC_EXT = new Set([
"heif",
"tiff",
"ico",
"avif",
]);
const VIDEO_SRC_EXT = new Set([
"mp4",
@@ -78,7 +79,7 @@ const VIDEO_SRC_EXT = new Set([
"mpeg",
]);
const AUDIO_SRC_EXT = new Set(["mp3", "wav", "aac", "flac", "opus", "aiff", "wma"]);
const AUDIO_SRC_EXT = new Set(["mp3", "wav", "aac", "flac", "opus", "aiff", "wma", "m4a"]);
type SrcKind = "image" | "video" | "audio";