@hyperframes/lint 0.8.107 → 0.8.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -122,7 +122,7 @@ function isAudibleVideoTag(tagSource) {
122
122
  if (hasAttrName(tagSource, "muted")) return false;
123
123
  if (!hasAttrName(tagSource, "data-has-audio")) return true;
124
124
  const declared = readAttr(tagSource, "data-has-audio");
125
- return declared !== null && declared.trim().toLowerCase() !== "false";
125
+ return declared === "true";
126
126
  }
127
127
  function mediaTimeWindow(tagSource) {
128
128
  const start = Number(readAttr(tagSource, "data-start"));
@@ -1372,6 +1372,93 @@ function cssErrorOffset(source, error) {
1372
1372
  import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
1373
1373
  import { extractMediaSrcMutations } from "@hyperframes/parsers/composition";
1374
1374
  import { parseHTML } from "linkedom";
1375
+
1376
+ // src/rules/linkedClips.ts
1377
+ var SYNC_TOLERANCE_S = 1e-3;
1378
+ var LINKED_TIMING_FIELDS = [
1379
+ { field: "start", attrs: ["data-start"], fallback: 0 },
1380
+ { field: "duration", attrs: ["data-duration"], fallback: 0 },
1381
+ { field: "media-start", attrs: ["data-playback-start", "data-media-start"], fallback: 0 },
1382
+ { field: "playback-rate", attrs: ["data-playback-rate"], fallback: 1 }
1383
+ ];
1384
+ function readTimingField(tag, attrs, fallback) {
1385
+ for (const attr of attrs) {
1386
+ const raw = readAttr(tag.raw, attr);
1387
+ if (raw === null || raw.trim() === "") continue;
1388
+ const value = Number(raw);
1389
+ if (Number.isFinite(value)) return value;
1390
+ }
1391
+ return fallback;
1392
+ }
1393
+ function driftingFields(members) {
1394
+ return LINKED_TIMING_FIELDS.filter(({ attrs, fallback }) => {
1395
+ const values = members.map((tag) => readTimingField(tag, attrs, fallback));
1396
+ const first = values[0] ?? fallback;
1397
+ return values.some((value) => Math.abs(value - first) > SYNC_TOLERANCE_S);
1398
+ }).map(({ field }) => field);
1399
+ }
1400
+ function compositionScopeOf(tag, compositions) {
1401
+ let scope = -1;
1402
+ for (const composition of compositions) {
1403
+ const end = composition.endIndex ?? composition.closeIndex ?? Number.POSITIVE_INFINITY;
1404
+ const encloses = composition.index < tag.index && tag.index < end;
1405
+ if (composition !== tag && encloses && composition.index > scope) scope = composition.index;
1406
+ }
1407
+ return scope;
1408
+ }
1409
+ function groupByLink(tags) {
1410
+ const compositions = tags.filter(
1411
+ (tag) => ["data-composition-id", "data-composition-file"].some(
1412
+ (name) => readAttr(tag.raw, name) !== null
1413
+ )
1414
+ );
1415
+ const groups = /* @__PURE__ */ new Map();
1416
+ for (const tag of tags) {
1417
+ const link = readAttr(tag.raw, "data-link");
1418
+ if (!link) continue;
1419
+ const key = `${compositionScopeOf(tag, compositions)}\0${link}`;
1420
+ const group = groups.get(key) ?? { link, members: [] };
1421
+ group.members.push(tag);
1422
+ groups.set(key, group);
1423
+ }
1424
+ return groups;
1425
+ }
1426
+ var memberLabel = (tag) => {
1427
+ const id = readAttr(tag.raw, "id");
1428
+ return id ? `#${id}` : `<${tag.name}>`;
1429
+ };
1430
+ function findLinkedClipFindings(ctx) {
1431
+ const findings = [];
1432
+ for (const { link, members } of groupByLink(ctx.tags).values()) {
1433
+ const first = members[0];
1434
+ if (!first) continue;
1435
+ const elementId = readAttr(first.raw, "id") || void 0;
1436
+ if (members.length === 1) {
1437
+ findings.push({
1438
+ code: "linked_clip_orphan",
1439
+ severity: "warning",
1440
+ message: `${memberLabel(first)} is the only clip with data-link="${link}"; its linked partner is gone.`,
1441
+ elementId,
1442
+ fixHint: `Remove data-link="${link}" from ${memberLabel(first)}, or restore the partner clip.`,
1443
+ snippet: truncateSnippet(first.raw)
1444
+ });
1445
+ continue;
1446
+ }
1447
+ const drift = driftingFields(members);
1448
+ if (drift.length === 0) continue;
1449
+ findings.push({
1450
+ code: "linked_clips_out_of_sync",
1451
+ severity: "warning",
1452
+ message: `Linked clips ${members.map(memberLabel).join(", ")} (data-link="${link}") differ in ${drift.join(", ")}.`,
1453
+ elementId,
1454
+ fixHint: "Linked clips are edited as one: give every member the same data-start, data-duration, data-media-start and data-playback-rate, or remove data-link from all of them to unlink.",
1455
+ snippet: truncateSnippet(first.raw)
1456
+ });
1457
+ }
1458
+ return findings;
1459
+ }
1460
+
1461
+ // src/rules/media.ts
1375
1462
  function tweensVolumeInSameCall(script, id) {
1376
1463
  const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
1377
1464
  for (let hit = selector.exec(script); hit; hit = selector.exec(script)) {
@@ -2070,7 +2157,8 @@ var mediaRules = [
2070
2157
  // audio_group_timing_attrs
2071
2158
  findAudioGroupTimingAttrFindings,
2072
2159
  // audio_group_carve_attr
2073
- findAudioGroupCarveAttrFindings
2160
+ findAudioGroupCarveAttrFindings,
2161
+ findLinkedClipFindings
2074
2162
  ];
2075
2163
  function findVolumeTweenOverridesGainFindings(ctx) {
2076
2164
  const boosted = ctx.tags.filter((tag) => isMediaTag(tag.name)).map((tag) => ({ tag, volume: Number(readAttr(tag.raw, "data-volume") ?? "1") })).filter((entry) => Number.isFinite(entry.volume) && entry.volume !== 1).filter((entry) => !readDecodedAttr(entry.tag.raw, "data-automation")).map((entry) => ({ ...entry, id: readAttr(entry.tag.raw, "id") })).filter((entry) => Boolean(entry.id));
@@ -2159,7 +2247,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
2159
2247
  var AUDIO_GROUP_TIMING_ATTRS = ["data-start", "data-duration", "data-track-index"];
2160
2248
  function findAudioGroupNoMembersFindings(ctx) {
2161
2249
  const memberGroupIds = new Set(
2162
- ctx.tags.filter((tag) => tag.name === "audio").map((tag) => readAttr(tag.raw, "data-audio-group")).filter((id) => Boolean(id))
2250
+ ctx.tags.filter((tag) => tag.name === "audio" || tag.name === "video" && isAudibleVideoTag(tag.raw)).map((tag) => readAttr(tag.raw, "data-audio-group")).filter((id) => Boolean(id))
2163
2251
  );
2164
2252
  if (memberGroupIds.size === 0) return [];
2165
2253
  const mayHaveCrossFileMembers = ctx.tags.some(