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