@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 +40 -22
- package/dist/browser.js.map +1 -1
- package/dist/index.js +48 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.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;
|
|
@@ -1370,11 +1392,6 @@ function tweensVolumeInSameCall(script, id) {
|
|
|
1370
1392
|
function escapeRegExp(value) {
|
|
1371
1393
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1372
1394
|
}
|
|
1373
|
-
function hasAttrName(tagSource, attr) {
|
|
1374
|
-
const escaped = escapeRegExp(attr);
|
|
1375
|
-
const attrs = tagSource.replace(/^<\s*[a-z][\w:-]*/i, "");
|
|
1376
|
-
return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
|
|
1377
|
-
}
|
|
1378
1395
|
var IMAGE_SRC_EXT = /* @__PURE__ */ new Set([
|
|
1379
1396
|
"jpg",
|
|
1380
1397
|
"jpeg",
|
|
@@ -1775,14 +1792,15 @@ var mediaRules = [
|
|
|
1775
1792
|
if (tag.name !== "video") continue;
|
|
1776
1793
|
const hasMuted = hasAttrName(tag.raw, "muted");
|
|
1777
1794
|
const hasDeclaredAudio = readAttr(tag.raw, "data-has-audio") === "true";
|
|
1778
|
-
|
|
1795
|
+
const declaresAudioState = hasAttrName(tag.raw, "data-has-audio");
|
|
1796
|
+
if (!hasMuted && !declaresAudioState && readAttr(tag.raw, "data-start")) {
|
|
1779
1797
|
const elementId = readAttr(tag.raw, "id") || void 0;
|
|
1780
1798
|
findings.push({
|
|
1781
1799
|
code: "video_missing_muted",
|
|
1782
1800
|
severity: "error",
|
|
1783
|
-
message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but
|
|
1801
|
+
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.`,
|
|
1784
1802
|
elementId,
|
|
1785
|
-
fixHint: 'Add
|
|
1803
|
+
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.',
|
|
1786
1804
|
snippet: truncateSnippet(tag.raw)
|
|
1787
1805
|
});
|
|
1788
1806
|
}
|
|
@@ -2008,31 +2026,31 @@ var mediaRules = [
|
|
|
2008
2026
|
// <audio> pointing to the same file, which causes double playback at runtime
|
|
2009
2027
|
({ tags }) => {
|
|
2010
2028
|
const findings = [];
|
|
2011
|
-
const
|
|
2012
|
-
const
|
|
2029
|
+
const videos = [];
|
|
2030
|
+
const audios = [];
|
|
2013
2031
|
for (const tag of tags) {
|
|
2014
2032
|
if (!readAttr(tag.raw, "data-start")) continue;
|
|
2015
2033
|
const src = readAttr(tag.raw, "src");
|
|
2016
2034
|
if (!src) continue;
|
|
2017
|
-
const
|
|
2035
|
+
const source = { id: readAttr(tag.raw, "id") || void 0, raw: tag.raw };
|
|
2018
2036
|
if (tag.name === "video") {
|
|
2019
|
-
|
|
2020
|
-
if (!isMuted) {
|
|
2021
|
-
videoSources.set(src, { id: elementId, raw: tag.raw });
|
|
2022
|
-
}
|
|
2037
|
+
if (isAudibleVideoTag(tag.raw)) videos.push([src, source]);
|
|
2023
2038
|
} else if (tag.name === "audio") {
|
|
2024
|
-
|
|
2039
|
+
audios.push([src, source]);
|
|
2025
2040
|
}
|
|
2026
2041
|
}
|
|
2027
|
-
for (const [src, audioInfo] of
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2042
|
+
for (const [src, audioInfo] of audios) {
|
|
2043
|
+
const match = videos.find(
|
|
2044
|
+
([videoSrc, video]) => videoSrc === src && mediaWindowsOverlap(mediaTimeWindow(video.raw), mediaTimeWindow(audioInfo.raw))
|
|
2045
|
+
);
|
|
2046
|
+
if (!match) continue;
|
|
2047
|
+
const videoInfo = match[1];
|
|
2030
2048
|
findings.push({
|
|
2031
2049
|
code: "video_audio_double_source",
|
|
2032
2050
|
severity: "error",
|
|
2033
|
-
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.`,
|
|
2051
|
+
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.`,
|
|
2034
2052
|
elementId: audioInfo.id,
|
|
2035
|
-
fixHint: "
|
|
2053
|
+
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.",
|
|
2036
2054
|
snippet: truncateSnippet(audioInfo.raw)
|
|
2037
2055
|
});
|
|
2038
2056
|
}
|
|
@@ -2141,7 +2159,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
|
|
|
2141
2159
|
var AUDIO_GROUP_TIMING_ATTRS = ["data-start", "data-duration", "data-track-index"];
|
|
2142
2160
|
function findAudioGroupNoMembersFindings(ctx) {
|
|
2143
2161
|
const memberGroupIds = new Set(
|
|
2144
|
-
ctx.tags.filter((tag) => tag.name === "audio").map((tag) => readAttr(tag.raw, "data-audio-group")).filter((id) => Boolean(id))
|
|
2162
|
+
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))
|
|
2145
2163
|
);
|
|
2146
2164
|
if (memberGroupIds.size === 0) return [];
|
|
2147
2165
|
const mayHaveCrossFileMembers = ctx.tags.some(
|
|
@@ -6616,7 +6634,10 @@ function lintProjectAudioFiles(projectDir, htmlSources) {
|
|
|
6616
6634
|
}
|
|
6617
6635
|
if (audioFiles.length === 0) return findings;
|
|
6618
6636
|
const hasAudioElement = htmlSources.some(({ html }) => /<audio\b/i.test(html));
|
|
6619
|
-
|
|
6637
|
+
const hasAudibleVideo = htmlSources.some(
|
|
6638
|
+
({ html }) => (html.match(/<video\b[^>]*>/gi) ?? []).some(isAudibleVideoTag)
|
|
6639
|
+
);
|
|
6640
|
+
if (!hasAudioElement && !hasAudibleVideo) {
|
|
6620
6641
|
findings.push({
|
|
6621
6642
|
code: "audio_file_without_element",
|
|
6622
6643
|
severity: "warning",
|
|
@@ -6766,10 +6787,11 @@ function lintDuplicateAudioTracks(htmlSources) {
|
|
|
6766
6787
|
const tracks = [];
|
|
6767
6788
|
const seen = /* @__PURE__ */ new Set();
|
|
6768
6789
|
for (const { html } of htmlSources) {
|
|
6769
|
-
const
|
|
6790
|
+
const soundTagRe = /<(?:audio|video)\b[^>]*>/gi;
|
|
6770
6791
|
let match;
|
|
6771
|
-
while ((match =
|
|
6792
|
+
while ((match = soundTagRe.exec(html)) !== null) {
|
|
6772
6793
|
const tag = match[0];
|
|
6794
|
+
if (/^<video/i.test(tag) && !isAudibleVideoTag(tag)) continue;
|
|
6773
6795
|
const trackStr = extractAttr(tag, "data-track-index");
|
|
6774
6796
|
const startStr = extractAttr(tag, "data-start");
|
|
6775
6797
|
const durStr = extractAttr(tag, "data-duration");
|
|
@@ -6793,7 +6815,7 @@ function lintDuplicateAudioTracks(htmlSources) {
|
|
|
6793
6815
|
findings.push({
|
|
6794
6816
|
code: "duplicate_audio_track",
|
|
6795
6817
|
severity: "warning",
|
|
6796
|
-
message: `Multiple <audio> elements on track ${a.trackIndex} overlap (${a.src} at ${a.start}-${Number.isFinite(a.end) ? a.end.toFixed(1) : "end"}s, ${b.src} at ${b.start}-${Number.isFinite(b.end) ? b.end.toFixed(1) : "end"}s). This causes layered audio playback.`,
|
|
6818
|
+
message: `Multiple audible <audio>/<video> elements on track ${a.trackIndex} overlap (${a.src} at ${a.start}-${Number.isFinite(a.end) ? a.end.toFixed(1) : "end"}s, ${b.src} at ${b.start}-${Number.isFinite(b.end) ? b.end.toFixed(1) : "end"}s). This causes layered audio playback.`,
|
|
6797
6819
|
fixHint: "Use non-overlapping time windows or different track indices."
|
|
6798
6820
|
});
|
|
6799
6821
|
}
|