diff --git a/packages/lint/src/rules/structure.test.ts b/packages/lint/src/rules/structure.test.ts
index 9f3392d7c0..f4d4f619f1 100644
--- a/packages/lint/src/rules/structure.test.ts
+++ b/packages/lint/src/rules/structure.test.ts
@@ -95,17 +95,18 @@ describe("legacy data-end", () => {
});
describe("media_missing_duration", () => {
- it("flags img, video and audio without data-duration and passes them with one", async () => {
- for (const tag of ["img", "video", "audio"]) {
- expect(
- has(await codes(`<${tag} src="a" data-start="0">${tag}>`), "media_missing_duration"),
- ).toBe(true);
- expect(
- has(
- await codes(`<${tag} src="a" data-start="0" data-duration="3">${tag}>`),
- "media_missing_duration",
- ),
- ).toBe(false);
+ it("flags a timed img without data-duration and passes it with one", async () => {
+ const bare = await codes('
');
+ expect(has(bare, "media_missing_duration")).toBe(true);
+ const timed = await codes('
');
+ expect(has(timed, "media_missing_duration")).toBe(false);
+ });
+
+ it("takes video and audio length from the file, so data-start alone passes", async () => {
+ for (const tag of ["video", "audio"]) {
+ const found = await codes(`<${tag} src="a" data-start="0" data-track-index="1">${tag}>`);
+ expect(has(found, "media_missing_duration")).toBe(false);
+ expect(has(found, "timeline_element_missing_timing")).toBe(false);
}
});
});
diff --git a/packages/lint/src/rules/structure.ts b/packages/lint/src/rules/structure.ts
index 7273ee09a0..37f065afd3 100644
--- a/packages/lint/src/rules/structure.ts
+++ b/packages/lint/src/rules/structure.ts
@@ -45,7 +45,8 @@ const OPAQUE_TAGS = new Set([
]);
// Never layout: their content is code or inert markup.
const NON_LAYOUT_TAGS = new Set(["style", "script", "template", "noscript"]);
-const MEDIA_TAGS = new Set(["img", "video", "audio"]);
+// Their length comes from the media file, so data-start alone is enough.
+const FILE_LENGTH_TAGS = new Set(["video", "audio"]);
const NODE_ATTRS = [
"id",
"class",
@@ -124,26 +125,27 @@ function nestedStructureFindings(rows: TagNode[], severity: Severity): Hyperfram
}
function missingDurationFindings(rows: TagNode[], severity: Severity): HyperframeLintFinding[] {
- // A bare media element with no timing is a static layer, not a timeline clip.
+ // A bare img with no timing is a static layer, not a timeline clip.
const intendedClip = (row: TagNode) =>
- !MEDIA_TAGS.has(row.tag) ||
+ row.tag !== "img" ||
row.attrs["data-start"] !== undefined ||
row.attrs["data-track-index"] !== undefined;
return rows
.filter(
(row) =>
+ !FILE_LENGTH_TAGS.has(row.tag) &&
row.attrs["data-duration"] === undefined &&
row.attrs["data-end"] === undefined &&
!isSubCompositionHost(row) &&
intendedClip(row),
)
.map((row) => {
- const isMedia = MEDIA_TAGS.has(row.tag);
+ const isImage = row.tag === "img";
return {
- code: isMedia ? "media_missing_duration" : "timeline_element_missing_timing",
+ code: isImage ? "media_missing_duration" : "timeline_element_missing_timing",
severity,
- message: isMedia
- ? `${describe(row)} is media on the timeline without data-duration, so its clip has no length.`
+ message: isImage
+ ? `${describe(row)} is an image on the timeline without data-duration, and an image has no length of its own.`
: `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
elementId: row.attrs.id,
fixHint: `Add data-duration (in seconds) to ${describe(row)}.`,