@hyperframes/lint 0.8.106 → 0.8.108

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/browser.js CHANGED
@@ -113,6 +113,28 @@ function readAttr(tagSource, attr) {
113
113
  const match = tagSource.match(new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
114
114
  return match?.[1] || null;
115
115
  }
116
+ function hasAttrName(tagSource, attr) {
117
+ const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ const attrs = tagSource.replace(/^<\s*[a-z][\w:-]*/i, "");
119
+ return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
120
+ }
121
+ function isAudibleVideoTag(tagSource) {
122
+ if (hasAttrName(tagSource, "muted")) return false;
123
+ if (!hasAttrName(tagSource, "data-has-audio")) return true;
124
+ const declared = readAttr(tagSource, "data-has-audio");
125
+ return declared === "true";
126
+ }
127
+ function mediaTimeWindow(tagSource) {
128
+ const start = Number(readAttr(tagSource, "data-start"));
129
+ const duration = Number(readAttr(tagSource, "data-duration"));
130
+ if (!readAttr(tagSource, "data-start") || !readAttr(tagSource, "data-duration")) return null;
131
+ if (!Number.isFinite(start) || !Number.isFinite(duration)) return null;
132
+ return { start, end: start + duration };
133
+ }
134
+ function mediaWindowsOverlap(a, b) {
135
+ if (!a || !b) return true;
136
+ return a.start < b.end && b.start < a.end;
137
+ }
116
138
  function readDecodedAttr(tagSource, attr) {
117
139
  if (!tagSource) return null;
118
140
  let value = null;
@@ -1367,11 +1389,6 @@ function tweensVolumeInSameCall(script, id) {
1367
1389
  function escapeRegExp(value) {
1368
1390
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1369
1391
  }
1370
- function hasAttrName(tagSource, attr) {
1371
- const escaped = escapeRegExp(attr);
1372
- const attrs = tagSource.replace(/^<\s*[a-z][\w:-]*/i, "");
1373
- return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
1374
- }
1375
1392
  var IMAGE_SRC_EXT = /* @__PURE__ */ new Set([
1376
1393
  "jpg",
1377
1394
  "jpeg",
@@ -1772,14 +1789,15 @@ var mediaRules = [
1772
1789
  if (tag.name !== "video") continue;
1773
1790
  const hasMuted = hasAttrName(tag.raw, "muted");
1774
1791
  const hasDeclaredAudio = readAttr(tag.raw, "data-has-audio") === "true";
1775
- if (!hasMuted && !hasDeclaredAudio && readAttr(tag.raw, "data-start")) {
1792
+ const declaresAudioState = hasAttrName(tag.raw, "data-has-audio");
1793
+ if (!hasMuted && !declaresAudioState && readAttr(tag.raw, "data-start")) {
1776
1794
  const elementId = readAttr(tag.raw, "id") || void 0;
1777
1795
  findings.push({
1778
1796
  code: "video_missing_muted",
1779
1797
  severity: "error",
1780
- message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but is not muted. Mark audible videos with data-has-audio="true"; otherwise keep video muted and use a separate <audio> element for sound.`,
1798
+ message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
1781
1799
  elementId,
1782
- fixHint: 'Add the `muted` attribute for silent video, or add data-has-audio="true" when the video track should contribute audio.',
1800
+ fixHint: 'Add data-has-audio="true" when the file has sound (recommended \u2014 keeps picture and sound on one clip), or add `muted` for silent footage.',
1783
1801
  snippet: truncateSnippet(tag.raw)
1784
1802
  });
1785
1803
  }
@@ -2005,31 +2023,31 @@ var mediaRules = [
2005
2023
  // <audio> pointing to the same file, which causes double playback at runtime
2006
2024
  ({ tags }) => {
2007
2025
  const findings = [];
2008
- const videoSources = /* @__PURE__ */ new Map();
2009
- const audioSources = /* @__PURE__ */ new Map();
2026
+ const videos = [];
2027
+ const audios = [];
2010
2028
  for (const tag of tags) {
2011
2029
  if (!readAttr(tag.raw, "data-start")) continue;
2012
2030
  const src = readAttr(tag.raw, "src");
2013
2031
  if (!src) continue;
2014
- const elementId = readAttr(tag.raw, "id") || void 0;
2032
+ const source = { id: readAttr(tag.raw, "id") || void 0, raw: tag.raw };
2015
2033
  if (tag.name === "video") {
2016
- const isMuted = hasAttrName(tag.raw, "muted");
2017
- if (!isMuted) {
2018
- videoSources.set(src, { id: elementId, raw: tag.raw });
2019
- }
2034
+ if (isAudibleVideoTag(tag.raw)) videos.push([src, source]);
2020
2035
  } else if (tag.name === "audio") {
2021
- audioSources.set(src, { id: elementId, raw: tag.raw });
2036
+ audios.push([src, source]);
2022
2037
  }
2023
2038
  }
2024
- for (const [src, audioInfo] of audioSources) {
2025
- const videoInfo = videoSources.get(src);
2026
- if (!videoInfo) continue;
2039
+ for (const [src, audioInfo] of audios) {
2040
+ const match = videos.find(
2041
+ ([videoSrc, video]) => videoSrc === src && mediaWindowsOverlap(mediaTimeWindow(video.raw), mediaTimeWindow(audioInfo.raw))
2042
+ );
2043
+ if (!match) continue;
2044
+ const videoInfo = match[1];
2027
2045
  findings.push({
2028
2046
  code: "video_audio_double_source",
2029
2047
  severity: "error",
2030
- message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source. The unmuted video already provides audio \u2014 the duplicate <audio> will cause double playback and echo.`,
2048
+ message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio \u2014 the duplicate <audio> will cause double playback and echo.`,
2031
2049
  elementId: audioInfo.id,
2032
- fixHint: "Either mute the video (add `muted` attribute) and keep the separate <audio>, or remove the <audio> element and let the video provide its own audio track.",
2050
+ fixHint: "Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
2033
2051
  snippet: truncateSnippet(audioInfo.raw)
2034
2052
  });
2035
2053
  }
@@ -2138,7 +2156,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
2138
2156
  var AUDIO_GROUP_TIMING_ATTRS = ["data-start", "data-duration", "data-track-index"];
2139
2157
  function findAudioGroupNoMembersFindings(ctx) {
2140
2158
  const memberGroupIds = new Set(
2141
- ctx.tags.filter((tag) => tag.name === "audio").map((tag) => readAttr(tag.raw, "data-audio-group")).filter((id) => Boolean(id))
2159
+ 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))
2142
2160
  );
2143
2161
  if (memberGroupIds.size === 0) return [];
2144
2162
  const mayHaveCrossFileMembers = ctx.tags.some(