Skip to content
Merged
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
23 changes: 12 additions & 11 deletions packages/lint/src/rules/structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<img src="a" data-start="0" />');
expect(has(bare, "media_missing_duration")).toBe(true);
const timed = await codes('<img src="a" data-start="0" data-duration="3" />');
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);
}
});
});
Expand Down
16 changes: 9 additions & 7 deletions packages/lint/src/rules/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)}.`,
Expand Down
Loading