diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts
index 4ab12647b..2a44596ec 100644
--- a/packages/lint/src/rules/composition.test.ts
+++ b/packages/lint/src/rules/composition.test.ts
@@ -173,6 +173,22 @@ describe("composition rules", () => {
expect(finding).toBeUndefined();
});
+ it("does not count transcript caption cues as dense track elements", async () => {
+ const html = `
+
+
+`;
+
+ const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
+ const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
+ expect(finding).toBeUndefined();
+ });
+
it("does not count root composition or mounted sub-compositions as dense elements", async () => {
const html = `
diff --git a/packages/lint/src/rules/composition.ts b/packages/lint/src/rules/composition.ts
index eee2ae9a8..a2ffa4f69 100644
--- a/packages/lint/src/rules/composition.ts
+++ b/packages/lint/src/rules/composition.ts
@@ -15,6 +15,8 @@ import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers/composition";
const MAX_COMPOSITION_LINES = 300;
const MAX_TIMED_ELEMENTS_PER_TRACK = 3;
const TRACK_DENSITY_EXEMPT_TAGS = new Set(["audio", "script", "style", "video"]);
+const CAPTION_CUE_TOKEN =
+ /^(?:caption(?:[-_](?:group|word|line|block|cue|text))?|subtitle(?:[-_](?:group|line|cue|text))?|cg-.+)$/i;
// `parseFloat("0.1") + parseFloat("0.2") = 0.30000000000000004`. Sub-second
// authored adjacencies survive parse + add as a value a few ulps above the
@@ -36,6 +38,15 @@ function countStructuralLines(source: string): number {
return countPhysicalLines(source.replace(/"));
}
+function isCaptionCue(tag: OpenTag): boolean {
+ const classTokens = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
+ const id = readAttr(tag.raw, "id");
+ return (
+ classTokens.some((token) => CAPTION_CUE_TOKEN.test(token)) ||
+ Boolean(id && CAPTION_CUE_TOKEN.test(id))
+ );
+}
+
export function isRegistrySourceFile(filePath?: string): boolean {
if (!filePath) return false;
@@ -294,6 +305,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
const trackCounts = new Map();
for (const tag of tags) {
if (TRACK_DENSITY_EXEMPT_TAGS.has(tag.name)) continue;
+ if (isCaptionCue(tag)) continue;
if (isCompositionRootOrMount(tag.raw)) continue;
if (!readAttr(tag.raw, "data-start")) continue;