@hyperframes/lint 0.8.27 → 0.8.28

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
@@ -717,7 +717,7 @@ var coreRules = [
717
717
  {
718
718
  pattern: /crypto\.getRandomValues\s*\(/,
719
719
  label: "crypto.getRandomValues()",
720
- hint: "Remove time-dependent code. Use a seeded PRNG for deterministic renders."
720
+ hint: "Use a seeded PRNG (e.g. a simple mulberry32) so renders are deterministic across frames."
721
721
  },
722
722
  {
723
723
  pattern: /gsap\.utils\.random\s*\(/,
@@ -781,6 +781,84 @@ function hasAttrName(tagSource, attr) {
781
781
  const attrs = tagSource.replace(/^<\s*[a-z][\w:-]*/i, "");
782
782
  return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
783
783
  }
784
+ var IMAGE_SRC_EXT = /* @__PURE__ */ new Set([
785
+ "jpg",
786
+ "jpeg",
787
+ "png",
788
+ "gif",
789
+ "bmp",
790
+ "webp",
791
+ "svg",
792
+ "heic",
793
+ "heif",
794
+ "tiff",
795
+ "ico"
796
+ ]);
797
+ var VIDEO_SRC_EXT = /* @__PURE__ */ new Set([
798
+ "mp4",
799
+ "mov",
800
+ "avi",
801
+ "webm",
802
+ "mkv",
803
+ "flv",
804
+ "wmv",
805
+ "m4v",
806
+ "mpg",
807
+ "mpeg"
808
+ ]);
809
+ function srcKind(src) {
810
+ const stripped = src.trim();
811
+ if (!stripped) return null;
812
+ const lower = stripped.toLowerCase();
813
+ if (lower.startsWith("data:")) {
814
+ const mime = /^data:([^;,]+)/i.exec(stripped)?.[1]?.toLowerCase();
815
+ if (!mime) return null;
816
+ if (mime.startsWith("image/")) return "image";
817
+ if (mime.startsWith("video/")) return "video";
818
+ return null;
819
+ }
820
+ if (lower.startsWith("blob:")) return null;
821
+ let pathname = stripped;
822
+ try {
823
+ if (/^https?:/i.test(stripped)) {
824
+ pathname = decodeURIComponent(new URL(stripped).pathname);
825
+ } else {
826
+ pathname = stripped.split("?")[0]?.split("#")[0] ?? stripped;
827
+ }
828
+ } catch {
829
+ pathname = stripped.split("?")[0]?.split("#")[0] ?? stripped;
830
+ }
831
+ const base = pathname.split("/").pop() ?? "";
832
+ const dot = base.lastIndexOf(".");
833
+ if (dot < 0) return null;
834
+ const ext = base.slice(dot + 1).toLowerCase();
835
+ if (IMAGE_SRC_EXT.has(ext)) return "image";
836
+ if (VIDEO_SRC_EXT.has(ext)) return "video";
837
+ return null;
838
+ }
839
+ function findMediaSrcKindMismatchFindings(ctx) {
840
+ const findings = [];
841
+ for (const tag of ctx.tags) {
842
+ if (tag.name !== "video" && tag.name !== "img") continue;
843
+ const src = readAttr(tag.raw, "src");
844
+ if (!src) continue;
845
+ const kind = srcKind(src);
846
+ if (kind === null) continue;
847
+ if (tag.name === "video" && kind !== "image") continue;
848
+ if (tag.name === "img" && kind !== "video") continue;
849
+ const elementId = readAttr(tag.raw, "id") || void 0;
850
+ const expected = tag.name === "video" ? "video" : "image";
851
+ findings.push({
852
+ code: "media_src_kind_mismatch",
853
+ severity: "error",
854
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is a ${kind}, not a ${expected}. The producer fail-closes when the tag and file kind disagree.`,
855
+ elementId,
856
+ fixHint: tag.name === "video" ? "Use <img> for a still, or point <video> at a video URL (mp4/webm/mov/\u2026)." : "Use <video> for a video URL, or point <img> at a still (png/jpg/webp/\u2026).",
857
+ snippet: truncateSnippet(tag.raw)
858
+ });
859
+ }
860
+ return findings;
861
+ }
784
862
  function mediaHasResolvableSrc(tag, tags) {
785
863
  if (readAttr(tag.raw, "src")) return true;
786
864
  const end = tag.closeIndex ?? tag.endIndex;
@@ -1138,6 +1216,8 @@ var mediaRules = [
1138
1216
  }
1139
1217
  return findings;
1140
1218
  },
1219
+ // media_src_kind_mismatch
1220
+ findMediaSrcKindMismatchFindings,
1141
1221
  // placeholder_media_url
1142
1222
  ({ tags }) => {
1143
1223
  const findings = [];