@valbuild/core 0.139.1 → 0.143.0

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/declarations/src/getSha256.d.ts +55 -0
  3. package/dist/declarations/src/index.d.ts +25 -4
  4. package/dist/declarations/src/initSchema.d.ts +106 -55
  5. package/dist/declarations/src/initVal.d.ts +25 -2
  6. package/dist/declarations/src/mimeType/font.d.ts +36 -0
  7. package/dist/declarations/src/mimeType/index.d.ts +1 -0
  8. package/dist/declarations/src/remote/validationBasis.d.ts +3 -2
  9. package/dist/declarations/src/schema/file.d.ts +7 -0
  10. package/dist/declarations/src/schema/font.d.ts +16 -0
  11. package/dist/declarations/src/schema/fontset.d.ts +40 -0
  12. package/dist/declarations/src/schema/image.d.ts +9 -2
  13. package/dist/declarations/src/schema/index.d.ts +2 -1
  14. package/dist/declarations/src/schema/record.d.ts +16 -2
  15. package/dist/declarations/src/schema/richtext.d.ts +20 -1
  16. package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
  17. package/dist/declarations/src/schema/video.d.ts +249 -0
  18. package/dist/declarations/src/schema/videoset.d.ts +141 -0
  19. package/dist/declarations/src/source/galleries.d.ts +20 -0
  20. package/dist/declarations/src/source/media.d.ts +172 -12
  21. package/dist/declarations/src/val/index.d.ts +4 -2
  22. package/dist/{index-eb9f6480.esm.js → index-02a1ba06.esm.js} +1708 -150
  23. package/dist/{index-7d334094.cjs.prod.js → index-46c72f01.cjs.prod.js} +1713 -149
  24. package/dist/{index-90000f7a.cjs.dev.js → index-aac8c69c.cjs.dev.js} +1713 -149
  25. package/dist/valbuild-core.cjs.dev.js +7 -1
  26. package/dist/valbuild-core.cjs.prod.js +7 -1
  27. package/dist/valbuild-core.esm.js +1 -1
  28. package/package.json +1 -1
  29. package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
  30. package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
  31. package/patch/dist/valbuild-core-patch.esm.js +2 -2
@@ -2754,6 +2754,28 @@ var discriminatedUnion = function discriminatedUnion(key) {
2754
2754
  return new DiscriminatedUnionSchema(key, objects);
2755
2755
  };
2756
2756
 
2757
+ function _objectWithoutPropertiesLoose(r, e) {
2758
+ if (null == r) return {};
2759
+ var t = {};
2760
+ for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
2761
+ if (-1 !== e.indexOf(n)) continue;
2762
+ t[n] = r[n];
2763
+ }
2764
+ return t;
2765
+ }
2766
+
2767
+ function _objectWithoutProperties(e, t) {
2768
+ if (null == e) return {};
2769
+ var o,
2770
+ r,
2771
+ i = _objectWithoutPropertiesLoose(e, t);
2772
+ if (Object.getOwnPropertySymbols) {
2773
+ var n = Object.getOwnPropertySymbols(e);
2774
+ for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
2775
+ }
2776
+ return i;
2777
+ }
2778
+
2757
2779
  var RegEx$1 = /^(https?:\/\/[^/]+)\/file\/p\/([^/]+)\/b\/([^/]+)\/v\/([^/]+)\/h\/([^/]+)\/f\/([^/]+)\/p\/(.+)$/;
2758
2780
  function splitRemoteRef(ref) {
2759
2781
  if (ref[0] === "/") {
@@ -2795,6 +2817,8 @@ function splitRemoteRef(ref) {
2795
2817
  };
2796
2818
  }
2797
2819
 
2820
+ var _excluded = ["poster", "captions"];
2821
+
2798
2822
  /** What must stay in frame when a page crops an image. */
2799
2823
 
2800
2824
  /**
@@ -2803,6 +2827,76 @@ function splitRemoteRef(ref) {
2803
2827
  * here. One place per fact.
2804
2828
  */
2805
2829
 
2830
+ /**
2831
+ * The still shown before a video plays — `<video poster>`.
2832
+ *
2833
+ * Derived, like a video's `width` and `height`: the Studio grabs the frame at
2834
+ * {@link VideoSource.posterTime} and uploads it as an image, so a page can show
2835
+ * it without decoding the video. It is a media object of its own (a `path`,
2836
+ * and a `patch_id` while unpublished) because it is a file of its own.
2837
+ */
2838
+
2839
+ /**
2840
+ * One text track of a video — `<track src srclang label kind default>`.
2841
+ *
2842
+ * `path` is a WebVTT file, uploaded like any other file.
2843
+ */
2844
+
2845
+ /**
2846
+ * What a video FIELD authors, on top of the file it names: the description,
2847
+ * the crop, the poster, the trim and the caption tracks.
2848
+ *
2849
+ * This is the whole value of a field picked from an `s.videoset()`
2850
+ * (`s.video(videosetVal)`) — the mime type, size and length live in the set,
2851
+ * keyed by `path`, and repeating them here is how two copies of one fact get
2852
+ * to disagree. The poster, captions and times stay with the FIELD: one video
2853
+ * used in two places can be trimmed and captioned differently in each.
2854
+ *
2855
+ * Times are in seconds from the start of the file.
2856
+ */
2857
+
2858
+ /**
2859
+ * What one page chooses about a video — as opposed to what is true of the
2860
+ * file. A field (`s.video()`) carries these for itself; the entry of a set
2861
+ * (`s.videoset()`) carries them as DEFAULTS, which every field picked from the
2862
+ * set starts from and may override key by key. See {@link fillFromGallery}.
2863
+ */
2864
+
2865
+ /**
2866
+ * A video: a progressive file (`video/mp4`, `video/webm`) or an HLS stream
2867
+ * (`application/vnd.apple.mpegurl`, whose `path` is the master playlist).
2868
+ *
2869
+ * `mimeType` is REQUIRED on a video of its own: a page has to know which of
2870
+ * the two it is holding before it can play it at all — an `.m3u8` handed to a
2871
+ * `<video src>` plays only in Safari. (What tells a video's TYPE apart from an
2872
+ * image's is its declared keys, see {@link IsVideoSource}, because a field
2873
+ * picked from a set has no `mimeType` of its own.)
2874
+ *
2875
+ * `width`, `height`, `duration` and `mimeType` are read from the bytes;
2876
+ * everything else is authored.
2877
+ */
2878
+
2879
+ /**
2880
+ * Whether a source TYPE is a video's.
2881
+ *
2882
+ * Every media source is "a `path` plus optional fields", so assignability
2883
+ * cannot tell an image from a video: an image is assignable to a set-backed
2884
+ * video and the other way round. The keys a type DECLARES can — only a video
2885
+ * declares `posterTime` — so the readers' conditional types ask this before
2886
+ * they ask about images.
2887
+ */
2888
+
2889
+ /** The mime type of an HLS master playlist. */
2890
+ var HLS_MIME_TYPE = "application/vnd.apple.mpegurl";
2891
+
2892
+ /** Whether a video is an HLS stream rather than a single progressive file. */
2893
+ function isHlsVideo(src) {
2894
+ if (src.mimeType !== undefined) {
2895
+ return src.mimeType === HLS_MIME_TYPE || src.mimeType.toLowerCase() === "application/x-mpegurl";
2896
+ }
2897
+ return src.path.split("?")[0].toLowerCase().endsWith(".m3u8");
2898
+ }
2899
+
2806
2900
  /**
2807
2901
  * The structural supertype of every media source.
2808
2902
  *
@@ -2856,66 +2950,181 @@ function resolveMedia(src) {
2856
2950
  url: mediaUrl(src)
2857
2951
  });
2858
2952
  }
2953
+
2954
+ /**
2955
+ * A video as a reader sees it: every file it names — the video itself, its
2956
+ * poster and each caption track — carries the URL its bytes are served from.
2957
+ *
2958
+ * Each gets its own {@link mediaUrl} because each is its own file with its own
2959
+ * `patch_id`: replacing only the poster drafts the poster, and the video keeps
2960
+ * its published URL.
2961
+ */
2962
+
2963
+ /**
2964
+ * The single implementation of {@link ResolvedVideo}. `url` is passed in as a
2965
+ * function so the reader can tag the video's own URL (stega) without tagging
2966
+ * the poster's and the captions', which are not where an edit lands.
2967
+ */
2968
+ function resolveVideo(src) {
2969
+ var videoUrl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : mediaUrl;
2970
+ var poster = src.poster,
2971
+ captions = src.captions,
2972
+ rest = _objectWithoutProperties(src, _excluded);
2973
+ var resolved = _objectSpread2(_objectSpread2({}, rest), {}, {
2974
+ url: videoUrl(src)
2975
+ });
2976
+ if (poster && _typeof(poster) === "object" && typeof poster.path === "string") {
2977
+ return withCaptions(_objectSpread2(_objectSpread2({}, resolved), {}, {
2978
+ poster: _objectSpread2(_objectSpread2({}, poster), {}, {
2979
+ url: mediaUrl(poster)
2980
+ })
2981
+ }), captions);
2982
+ }
2983
+ return withCaptions(resolved, captions);
2984
+ }
2985
+ function withCaptions(resolved, captions) {
2986
+ if (!Array.isArray(captions)) {
2987
+ return resolved;
2988
+ }
2989
+ return _objectSpread2(_objectSpread2({}, resolved), {}, {
2990
+ captions: captions.filter(function (track) {
2991
+ return !!track && _typeof(track) === "object" && typeof track.path === "string";
2992
+ }).map(function (track) {
2993
+ return _objectSpread2(_objectSpread2({}, track), {}, {
2994
+ url: mediaUrl(track)
2995
+ });
2996
+ })
2997
+ });
2998
+ }
2859
2999
  function isMediaSchema(schema) {
2860
- return _typeof(schema) === "object" && schema !== null && "type" in schema && (schema.type === "image" || schema.type === "file");
3000
+ return _typeof(schema) === "object" && schema !== null && "type" in schema && (schema.type === "image" || schema.type === "file" || schema.type === "video");
2861
3001
  }
2862
3002
 
2863
3003
  /**
2864
- * Fill in what a gallery-backed field does not carry itself.
2865
- *
2866
- * `s.image(galleryModule)` stores only `{path, alt?, hotspot?}` — the
2867
- * dimensions and mime type live in the gallery module, keyed by path. This is
2868
- * the single implementation of that lookup; core, stega/RSC and the Studio all
2869
- * call it rather than each rolling their own.
3004
+ * What a gallery entry holds as a DEFAULT for the fields picked from it, in
3005
+ * the groups a field overrides together.
2870
3006
  *
2871
- * `alt` is filled in too, because a gallery holds the alt text an editor typed
2872
- * once for a file used in several places — but only when the field does not
2873
- * have its own, so a per-image override wins. A gallery whose `alt` schema is a
2874
- * locale record holds an object rather than a string; that is left alone rather
2875
- * than copied into a field typed `string`, and making the override
2876
- * locale-shaped is a separate change.
3007
+ * Per key, not per value: a field that has set its own `startTime` still
3008
+ * takes the gallery's `endTime`. The poster is the one pair — `poster` is the
3009
+ * frame at `posterTime`, so a field with either has chosen its own still, and
3010
+ * taking the other half from the gallery would describe a frame nobody chose.
3011
+ */
3012
+ var GALLERY_DEFAULT_GROUPS = [["alt"], ["hotspot"], ["poster", "posterTime"], ["startTime"], ["endTime"], ["captions"]];
3013
+
3014
+ /** What is true of the file, and the gallery's alone. */
3015
+ var GALLERY_FILE_FACTS = ["width", "height", "mimeType", "duration"];
3016
+
3017
+ /**
3018
+ * The gallery entry a gallery-backed value names, or `null` when it is not
3019
+ * gallery-backed or the gallery has no such entry.
2877
3020
  *
2878
3021
  * The double lookup is load-bearing: a remote gallery keys its entries by the
2879
3022
  * remote URL while the file itself stays on disk under its local path.
2880
3023
  */
2881
- function fillFromGallery(src, schema, getModuleSource) {
3024
+ function galleryEntryOf(src, schema, getModuleSource) {
2882
3025
  if (!isMediaSchema(schema) || !schema.referencedModule) {
2883
- return src;
3026
+ return null;
2884
3027
  }
2885
3028
  var moduleSource = getModuleSource(schema.referencedModule);
2886
- if (!moduleSource || _typeof(moduleSource) !== "object" || Array.isArray(moduleSource)) {
2887
- return src;
3029
+ if (!isPlainObject(moduleSource)) {
3030
+ return null;
2888
3031
  }
2889
- var entries = moduleSource;
2890
- var key = src.path in entries ? src.path : null;
3032
+ var key = src.path in moduleSource ? src.path : null;
2891
3033
  if (key === null) {
2892
3034
  var splitRemoteRefRes = splitRemoteRef(src.path);
2893
- if (splitRemoteRefRes.status === "success" && splitRemoteRefRes.filePath in entries) {
3035
+ if (splitRemoteRefRes.status === "success" && splitRemoteRefRes.filePath in moduleSource) {
2894
3036
  key = splitRemoteRefRes.filePath;
2895
3037
  }
2896
3038
  }
2897
3039
  if (key === null) {
2898
- return src;
3040
+ return null;
2899
3041
  }
2900
- var entry = entries[key];
2901
- if (!entry || _typeof(entry) !== "object" || Array.isArray(entry)) {
3042
+ var entry = moduleSource[key];
3043
+ return isPlainObject(entry) ? entry : null;
3044
+ }
3045
+
3046
+ /**
3047
+ * The value a gallery-backed field means: its own keys, and the gallery's
3048
+ * where it has none.
3049
+ *
3050
+ * `s.image(galleryModule)` and `s.video(videosModule)` store only what one
3051
+ * page chose — the rest lives in the gallery, keyed by path. Two things come
3052
+ * from there:
3053
+ *
3054
+ * - **What is true of the file** — dimensions, mime type, length. The field
3055
+ * never has these, so they are always the gallery's.
3056
+ * - **The defaults** — description, focal point, a video's poster, start, end
3057
+ * and captions ({@link GALLERY_DEFAULT_GROUPS}). An editor sets them once
3058
+ * on the gallery entry; a field that sets its own wins, KEY BY KEY.
3059
+ *
3060
+ * The single implementation of that merge: core, stega/RSC and the Studio all
3061
+ * call it rather than each rolling their own, so a page and the Studio's
3062
+ * preview of it cannot disagree about which poster a video has.
3063
+ *
3064
+ * A gallery whose `alt` schema is a locale record holds an object rather than
3065
+ * a string; that is left alone rather than copied into a field typed
3066
+ * `string`, and making the override locale-shaped is a separate change.
3067
+ */
3068
+ function fillFromGallery(src, schema, getModuleSource) {
3069
+ var entry = galleryEntryOf(src, schema, getModuleSource);
3070
+ if (entry === null) {
2902
3071
  return src;
2903
3072
  }
2904
- var _ref = entry,
2905
- width = _ref.width,
2906
- height = _ref.height,
2907
- mimeType = _ref.mimeType,
2908
- alt = _ref.alt;
2909
- var hasOwnAlt = typeof src.alt === "string";
2910
- return _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({}, src), width !== undefined ? {
2911
- width: width
2912
- } : {}), height !== undefined ? {
2913
- height: height
2914
- } : {}), mimeType !== undefined ? {
2915
- mimeType: mimeType
2916
- } : {}), !hasOwnAlt && typeof alt === "string" ? {
2917
- alt: alt
2918
- } : {});
3073
+ var own = _objectSpread2({}, src);
3074
+ var filled = {};
3075
+ var _iterator = result._createForOfIteratorHelper(GALLERY_FILE_FACTS),
3076
+ _step;
3077
+ try {
3078
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3079
+ var key = _step.value;
3080
+ if (entry[key] !== undefined) {
3081
+ filled[key] = entry[key];
3082
+ }
3083
+ }
3084
+ } catch (err) {
3085
+ _iterator.e(err);
3086
+ } finally {
3087
+ _iterator.f();
3088
+ }
3089
+ var _iterator2 = result._createForOfIteratorHelper(GALLERY_DEFAULT_GROUPS),
3090
+ _step2;
3091
+ try {
3092
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3093
+ var group = _step2.value;
3094
+ if (group.some(function (key) {
3095
+ return own[key] !== undefined;
3096
+ })) {
3097
+ continue;
3098
+ }
3099
+ var _iterator3 = result._createForOfIteratorHelper(group),
3100
+ _step3;
3101
+ try {
3102
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
3103
+ var _key = _step3.value;
3104
+ var value = entry[_key];
3105
+ if (value === undefined || value === null) {
3106
+ continue;
3107
+ }
3108
+ if (_key === "alt" && typeof value !== "string") {
3109
+ continue;
3110
+ }
3111
+ filled[_key] = value;
3112
+ }
3113
+ } catch (err) {
3114
+ _iterator3.e(err);
3115
+ } finally {
3116
+ _iterator3.f();
3117
+ }
3118
+ }
3119
+ } catch (err) {
3120
+ _iterator2.e(err);
3121
+ } finally {
3122
+ _iterator2.f();
3123
+ }
3124
+ return _objectSpread2(_objectSpread2({}, src), filled);
3125
+ }
3126
+ function isPlainObject(value) {
3127
+ return _typeof(value) === "object" && value !== null && !Array.isArray(value);
2919
3128
  }
2920
3129
 
2921
3130
  var EXT_TO_MIME_TYPES = {
@@ -3395,7 +3604,7 @@ var EXT_TO_MIME_TYPES = {
3395
3604
  sxw: "application/vnd.sun.xml.writer",
3396
3605
  sxg: "application/vnd.sun.xml.writer.global",
3397
3606
  stw: "application/vnd.sun.xml.writer.template",
3398
- otf: "application/x-font-otf",
3607
+ otf: "font/otf",
3399
3608
  osfpvg: "application/vnd.yamaha.openscoreformat.osfpvg+xml",
3400
3609
  dp: "application/vnd.osgi.dp",
3401
3610
  pdb: "application/vnd.palm",
@@ -3446,6 +3655,8 @@ var EXT_TO_MIME_TYPES = {
3446
3655
  qam: "application/vnd.epson.quickanime",
3447
3656
  qfx: "application/vnd.intu.qfx",
3448
3657
  mov: "video/quicktime",
3658
+ vtt: "text/vtt",
3659
+ mkv: "video/x-matroska",
3449
3660
  rar: "application/x-rar-compressed",
3450
3661
  ram: "audio/x-pn-realaudio",
3451
3662
  rmp: "audio/x-pn-realaudio-plugin",
@@ -3543,7 +3754,8 @@ var EXT_TO_MIME_TYPES = {
3543
3754
  mxs: "application/vnd.triscape.mxs",
3544
3755
  t: "text/troff",
3545
3756
  tra: "application/vnd.trueapp",
3546
- ttf: "application/x-font-ttf",
3757
+ ttf: "font/ttf",
3758
+ ttc: "font/collection",
3547
3759
  ttl: "text/turtle",
3548
3760
  umj: "application/vnd.umajin",
3549
3761
  uoml: "application/vnd.uoml+xml",
@@ -3570,7 +3782,8 @@ var EXT_TO_MIME_TYPES = {
3570
3782
  wbmp: "image/vnd.wap.wbmp",
3571
3783
  wav: "audio/x-wav",
3572
3784
  davmount: "application/davmount+xml",
3573
- woff: "application/x-font-woff",
3785
+ woff: "font/woff",
3786
+ woff2: "font/woff2",
3574
3787
  wspolicy: "application/wspolicy+xml",
3575
3788
  webp: "image/webp",
3576
3789
  wtb: "application/vnd.webturbo",
@@ -3672,6 +3885,93 @@ function mimeTypeMatchesAccept(mimeType, accept) {
3672
3885
  });
3673
3886
  }
3674
3887
 
3888
+ /**
3889
+ * Fonts, as far as a mime type can tell.
3890
+ *
3891
+ * The four web formats have IANA types under `font/` (RFC 8081), and those are
3892
+ * what Val stores. Browsers and operating systems disagree about them: a file
3893
+ * picker reports `.woff2` as `font/woff2`, `application/font-woff2` or nothing
3894
+ * at all depending on the platform, and nothing at all reads as
3895
+ * `application/octet-stream` once the file is a data URL. That is why an upload
3896
+ * asks the BYTES ({@link sniffFontMimeType}) rather than trusting the picker.
3897
+ */
3898
+
3899
+ /** The formats `s.fontset()` accepts unless told otherwise, best first. */
3900
+ var FONT_MIME_TYPES = ["font/woff2", "font/woff", "font/ttf", "font/otf"];
3901
+
3902
+ /** `s.fontset()`'s `accept` when none is given. */
3903
+ var DEFAULT_FONT_ACCEPT = FONT_MIME_TYPES.join(",");
3904
+
3905
+ /**
3906
+ * The extension of each font type, for a file input: a picker matches an
3907
+ * `accept` of mime types through the operating system's own table, and where
3908
+ * that table does not know `font/woff2` the file is greyed out.
3909
+ */
3910
+ var FONT_MIME_TYPE_EXTENSIONS = {
3911
+ "font/woff2": ".woff2",
3912
+ "font/woff": ".woff",
3913
+ "font/ttf": ".ttf",
3914
+ "font/otf": ".otf",
3915
+ "font/collection": ".ttc"
3916
+ };
3917
+
3918
+ /**
3919
+ * The names the extension table used for fonts before it used RFC 8081's.
3920
+ * Content written then still carries them, and it is the same file.
3921
+ */
3922
+ var LEGACY_FONT_MIME_TYPES = {
3923
+ "application/x-font-ttf": "font/ttf",
3924
+ "application/x-font-otf": "font/otf",
3925
+ "application/x-font-woff": "font/woff"
3926
+ };
3927
+
3928
+ /** `mimeType`, with a legacy font name read as its `font/` type. */
3929
+ function canonicalFontMimeType(mimeType) {
3930
+ var _LEGACY_FONT_MIME_TYP;
3931
+ return (_LEGACY_FONT_MIME_TYP = LEGACY_FONT_MIME_TYPES[mimeType]) !== null && _LEGACY_FONT_MIME_TYP !== void 0 ? _LEGACY_FONT_MIME_TYP : mimeType;
3932
+ }
3933
+
3934
+ /**
3935
+ * Whether a file of `mimeType` is a font a browser can draw: the `font/` types,
3936
+ * and the pre-RFC 8081 names still found in older data.
3937
+ */
3938
+ function isFontMimeType(mimeType) {
3939
+ if (!mimeType) {
3940
+ return false;
3941
+ }
3942
+ return mimeType.startsWith("font/") || mimeType.startsWith("application/x-font-") || mimeType === "application/font-woff" || mimeType === "application/font-woff2" || mimeType === "application/font-sfnt" || mimeType === "application/vnd.ms-opentype";
3943
+ }
3944
+
3945
+ /**
3946
+ * The font type of `bytes`, read from the signature every font format starts
3947
+ * with, or `null` when they are not a font.
3948
+ *
3949
+ * Only the first four bytes are needed: the WOFF signatures (`wOFF`, `wOF2`),
3950
+ * the SFNT versions of TrueType (`0x00010000`, or `true` from old Apple fonts)
3951
+ * and of CFF-flavoured OpenType (`OTTO`), and a collection's tag (`ttcf`).
3952
+ */
3953
+ function sniffFontMimeType(bytes) {
3954
+ if (bytes.length < 4) {
3955
+ return null;
3956
+ }
3957
+ var tag = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
3958
+ switch (tag) {
3959
+ case "wOF2":
3960
+ return "font/woff2";
3961
+ case "wOFF":
3962
+ return "font/woff";
3963
+ case "OTTO":
3964
+ return "font/otf";
3965
+ case "true":
3966
+ case "\0\x01\0\0":
3967
+ return "font/ttf";
3968
+ case "ttcf":
3969
+ return "font/collection";
3970
+ default:
3971
+ return null;
3972
+ }
3973
+ }
3974
+
3675
3975
  /**
3676
3976
  * How an uploaded image is re-encoded in the browser, before it is uploaded.
3677
3977
  *
@@ -4097,6 +4397,19 @@ var ImageSchema = /*#__PURE__*/function (_Schema) {
4097
4397
  /** An image of its own, carrying its own dimensions and mime type. */
4098
4398
 
4099
4399
  function image(options, galleryOptions) {
4400
+ return imageOf(false, options, galleryOptions);
4401
+ }
4402
+
4403
+ /**
4404
+ * `s.image()` for a project whose media is all remote (`files.remote` in
4405
+ * `val.config.ts`): the same schema, with `.remote()` already applied. See
4406
+ * `initSchema`.
4407
+ */
4408
+
4409
+ function remoteImage(options, galleryOptions) {
4410
+ return imageOf(true, options, galleryOptions);
4411
+ }
4412
+ function imageOf(remote, options, galleryOptions) {
4100
4413
  var isModule = !!options && !!Internal.getValPath(options);
4101
4414
  if (isModule) {
4102
4415
  var allModules = {};
@@ -4110,9 +4423,9 @@ function image(options, galleryOptions) {
4110
4423
  }
4111
4424
  return new ImageSchema((galleryOptions === null || galleryOptions === void 0 ? void 0 : galleryOptions.encode) !== undefined ? {
4112
4425
  encode: galleryOptions.encode
4113
- } : {}, false, false, [], allModules);
4426
+ } : {}, false, remote, [], allModules);
4114
4427
  }
4115
- return new ImageSchema(options);
4428
+ return new ImageSchema(options, false, remote);
4116
4429
  }
4117
4430
 
4118
4431
  var RouteSchema = /*#__PURE__*/function (_Schema) {
@@ -4371,6 +4684,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4371
4684
  var isHidden = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
4372
4685
  var description = arguments.length > 5 ? arguments[5] : undefined;
4373
4686
  var previewInput = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : null;
4687
+ var imgRemote = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false;
4374
4688
  _classCallCheck(this, RichTextSchema);
4375
4689
  _this = _callSuper(this, RichTextSchema);
4376
4690
  _this.options = options;
@@ -4380,6 +4694,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4380
4694
  _this.isHidden = isHidden;
4381
4695
  _this.description = description;
4382
4696
  _this.previewInput = previewInput;
4697
+ _this.imgRemote = imgRemote;
4383
4698
  return _this;
4384
4699
  }
4385
4700
 
@@ -4406,7 +4721,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4406
4721
  return _createClass(RichTextSchema, [{
4407
4722
  key: "describe",
4408
4723
  value: function describe(description) {
4409
- return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, description !== null && description !== void 0 ? description : undefined, this.previewInput);
4724
+ return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, description !== null && description !== void 0 ? description : undefined, this.previewInput, this.imgRemote);
4410
4725
  }
4411
4726
 
4412
4727
  /**
@@ -4426,7 +4741,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4426
4741
  value: function maxLength(max) {
4427
4742
  return new RichTextSchema(_objectSpread2(_objectSpread2({}, this.options), {}, {
4428
4743
  maxLength: max
4429
- }), this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput);
4744
+ }), this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput, this.imgRemote);
4430
4745
  }
4431
4746
 
4432
4747
  /**
@@ -4446,7 +4761,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4446
4761
  value: function minLength(min) {
4447
4762
  return new RichTextSchema(_objectSpread2(_objectSpread2({}, this.options), {}, {
4448
4763
  minLength: min
4449
- }), this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput);
4764
+ }), this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput, this.imgRemote);
4450
4765
  }
4451
4766
 
4452
4767
  /**
@@ -4474,7 +4789,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4474
4789
  }, {
4475
4790
  key: "validate",
4476
4791
  value: function validate(validationFunction) {
4477
- return new RichTextSchema(this.options, this.opt, [].concat(_toConsumableArray(this.customValidateFunctions), [validationFunction]), this.isReadonly, this.isHidden, this.description, this.previewInput);
4792
+ return new RichTextSchema(this.options, this.opt, [].concat(_toConsumableArray(this.customValidateFunctions), [validationFunction]), this.isReadonly, this.isHidden, this.description, this.previewInput, this.imgRemote);
4478
4793
  }
4479
4794
  }, {
4480
4795
  key: "executeValidate",
@@ -4649,7 +4964,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4649
4964
  }
4650
4965
  var srcPath = unsafeCreateSourcePath(path, "src");
4651
4966
  var imgSchema = _this2.options.img;
4652
- var imageValidationErrors = _typeof(imgSchema) === "object" ? imgSchema["executeValidate"](srcPath, node.src) : new ImageSchema({}, false, false)["executeValidate"](srcPath, node.src);
4967
+ var imageValidationErrors = _typeof(imgSchema) === "object" ? imgSchema["executeValidate"](srcPath, node.src) : new ImageSchema({}, false, _this2.imgRemote)["executeValidate"](srcPath, node.src);
4653
4968
  if (imageValidationErrors) {
4654
4969
  for (var _validationErrorPathS in imageValidationErrors) {
4655
4970
  var _current$_validationE;
@@ -4918,19 +5233,19 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4918
5233
  }, {
4919
5234
  key: "nullable",
4920
5235
  value: function nullable() {
4921
- return new RichTextSchema(this.options, true, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput);
5236
+ return new RichTextSchema(this.options, true, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.previewInput, this.imgRemote);
4922
5237
  }
4923
5238
  }, {
4924
5239
  key: "readonly",
4925
5240
  value: function readonly() {
4926
5241
  var isReadonly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
4927
- return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, isReadonly, this.isHidden, this.description, this.previewInput);
5242
+ return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, isReadonly, this.isHidden, this.description, this.previewInput, this.imgRemote);
4928
5243
  }
4929
5244
  }, {
4930
5245
  key: "hidden",
4931
5246
  value: function hidden() {
4932
5247
  var isHidden = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
4933
- return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, isHidden, this.description, this.previewInput);
5248
+ return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, isHidden, this.description, this.previewInput, this.imgRemote);
4934
5249
  }
4935
5250
  }, {
4936
5251
  key: "executeCustomValidateAt",
@@ -4958,7 +5273,7 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
4958
5273
  }, {
4959
5274
  key: "preview",
4960
5275
  value: function preview(select) {
4961
- return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, select);
5276
+ return new RichTextSchema(this.options, this.opt, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, select, this.imgRemote);
4962
5277
  }
4963
5278
  }, {
4964
5279
  key: "executePreviewItem",
@@ -5004,7 +5319,10 @@ var RichTextSchema = /*#__PURE__*/function (_Schema) {
5004
5319
  ul: this.options.ul,
5005
5320
  ol: this.options.ol,
5006
5321
  a: serializeAnchorSchema(),
5007
- img: this.options.img && _typeof(this.options.img) === "object" ? this.options.img["executeSerialize"]() : this.options.img
5322
+ img: this.options.img && _typeof(this.options.img) === "object" ? this.options.img["executeSerialize"]() :
5323
+ // The Studio decides local or remote by the serialized image
5324
+ // schema, and `true` carries no such answer.
5325
+ this.options.img && this.imgRemote ? new ImageSchema(undefined, false, true)["executeSerialize"]() : this.options.img
5008
5326
  };
5009
5327
  return {
5010
5328
  type: "richtext",
@@ -5023,6 +5341,14 @@ var richtext = function richtext(options) {
5023
5341
  return new RichTextSchema(options !== null && options !== void 0 ? options : {});
5024
5342
  };
5025
5343
 
5344
+ /**
5345
+ * `s.richtext()` for a project whose media is all remote (`files.remote` in
5346
+ * `val.config.ts`): `img: true` is a remote image. See `initSchema`.
5347
+ */
5348
+ var remoteRichtext = function remoteRichtext(options) {
5349
+ return new RichTextSchema(options !== null && options !== void 0 ? options : {}, false, [], false, false, undefined, null, true);
5350
+ };
5351
+
5026
5352
  function _regeneratorDefine(e, r, n, t) {
5027
5353
  var i = Object.defineProperty;
5028
5354
  try {
@@ -5471,6 +5797,207 @@ function missingDeclaredKeys(declared, present) {
5471
5797
  });
5472
5798
  }
5473
5799
 
5800
+ /**
5801
+ * The checks of what one page chose about a video — its times, focal point,
5802
+ * poster and captions — shared by the two places those choices are made: a
5803
+ * field (`s.video()`) and the entry of a set (`s.videoset()`), which holds the
5804
+ * same choices as the defaults every field picked from it starts from.
5805
+ */
5806
+
5807
+ function isRecord(value) {
5808
+ return _typeof(value) === "object" && value !== null && !Array.isArray(value);
5809
+ }
5810
+ function stripQuery$1(path) {
5811
+ return path.split("?")[0];
5812
+ }
5813
+
5814
+ /**
5815
+ * What is checked, typed as what it IS rather than what it should be: a
5816
+ * field's value and a set's entry both reach here from hand-written JSON
5817
+ * that never saw the type.
5818
+ */
5819
+
5820
+ function isNonNegativeNumber(value) {
5821
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
5822
+ }
5823
+ function validateTimes(src, duration) {
5824
+ var errors = [];
5825
+ var _iterator = result._createForOfIteratorHelper(["posterTime", "startTime", "endTime"]),
5826
+ _step;
5827
+ try {
5828
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
5829
+ var key = _step.value;
5830
+ var value = src[key];
5831
+ if (value === undefined) {
5832
+ continue;
5833
+ }
5834
+ if (!isNonNegativeNumber(value)) {
5835
+ errors.push({
5836
+ message: "'".concat(key, "' must be a number of seconds, 0 or more. Got: ").concat(JSON.stringify(value)),
5837
+ value: src
5838
+ });
5839
+ } else if (duration !== undefined && value > duration) {
5840
+ errors.push({
5841
+ message: "'".concat(key, "' (").concat(value, "s) is after the end of the video (").concat(duration, "s)."),
5842
+ value: src
5843
+ });
5844
+ }
5845
+ }
5846
+ } catch (err) {
5847
+ _iterator.e(err);
5848
+ } finally {
5849
+ _iterator.f();
5850
+ }
5851
+ var startTime = src.startTime,
5852
+ endTime = src.endTime;
5853
+ if (isNonNegativeNumber(startTime) && isNonNegativeNumber(endTime) && startTime >= endTime) {
5854
+ errors.push({
5855
+ message: "'startTime' (".concat(startTime, "s) must be before 'endTime' (").concat(endTime, "s)."),
5856
+ value: src
5857
+ });
5858
+ }
5859
+ return errors;
5860
+ }
5861
+ function validateHotspot(src) {
5862
+ if (src.hotspot === undefined) {
5863
+ return [];
5864
+ }
5865
+ var hotspot = src.hotspot;
5866
+ if (!isRecord(hotspot) || typeof hotspot.x !== "number" || typeof hotspot.y !== "number" || hotspot.x < 0 || hotspot.x > 1 || hotspot.y < 0 || hotspot.y > 1) {
5867
+ return [{
5868
+ message: "'hotspot' must be { x, y } with both between 0 and 1.",
5869
+ value: src
5870
+ }];
5871
+ }
5872
+ return [];
5873
+ }
5874
+ function validatePoster(src) {
5875
+ if (src.poster === undefined) {
5876
+ return [];
5877
+ }
5878
+ var poster = src.poster;
5879
+ if (!isRecord(poster) || typeof poster.path !== "string") {
5880
+ return [{
5881
+ message: "'poster' must be an image object with a 'path'.",
5882
+ value: src
5883
+ }];
5884
+ }
5885
+ var mimeType = filenameToMimeType(stripQuery$1(poster.path));
5886
+ if (!mimeType || !mimeType.startsWith("image/")) {
5887
+ return [{
5888
+ message: "The poster must be an image. Got: ".concat(poster.path),
5889
+ value: src
5890
+ }];
5891
+ }
5892
+ return [];
5893
+ }
5894
+ var CAPTION_KINDS = ["subtitles", "captions"];
5895
+ function validateCaptions(src) {
5896
+ if (src.captions === undefined) {
5897
+ return [];
5898
+ }
5899
+ if (!Array.isArray(src.captions)) {
5900
+ return [{
5901
+ message: "'captions' must be an array.",
5902
+ value: src
5903
+ }];
5904
+ }
5905
+ var errors = [];
5906
+ var defaults = 0;
5907
+ var seen = new Set();
5908
+ src.captions.forEach(function (track, i) {
5909
+ var at = "Caption track ".concat(i + 1);
5910
+ if (!isRecord(track)) {
5911
+ errors.push({
5912
+ message: "".concat(at, " must be an object."),
5913
+ value: src
5914
+ });
5915
+ return;
5916
+ }
5917
+ var t = track;
5918
+ if (typeof t.path !== "string") {
5919
+ errors.push({
5920
+ message: "".concat(at, " has no 'path'."),
5921
+ value: src
5922
+ });
5923
+ } else if (filenameToMimeType(stripQuery$1(t.path)) !== "text/vtt") {
5924
+ errors.push({
5925
+ message: "".concat(at, " must be a WebVTT (.vtt) file. Got: ").concat(t.path),
5926
+ value: src
5927
+ });
5928
+ }
5929
+ if (typeof t.srclang !== "string" || t.srclang.trim() === "") {
5930
+ errors.push({
5931
+ message: "".concat(at, " needs a language ('srclang'), e.g. \"en\"."),
5932
+ value: src
5933
+ });
5934
+ }
5935
+ if (t.label !== undefined && typeof t.label !== "string") {
5936
+ errors.push({
5937
+ message: "".concat(at, ": 'label' must be a string."),
5938
+ value: src
5939
+ });
5940
+ }
5941
+ if (t.kind !== undefined && (typeof t.kind !== "string" || !CAPTION_KINDS.includes(t.kind))) {
5942
+ errors.push({
5943
+ message: "".concat(at, ": 'kind' must be \"subtitles\" or \"captions\"."),
5944
+ value: src
5945
+ });
5946
+ }
5947
+ if (t["default"] !== undefined && typeof t["default"] !== "boolean") {
5948
+ errors.push({
5949
+ message: "".concat(at, ": 'default' must be true or false."),
5950
+ value: src
5951
+ });
5952
+ }
5953
+ if (t["default"] === true) {
5954
+ defaults++;
5955
+ }
5956
+ if (typeof t.srclang === "string") {
5957
+ var _t$kind;
5958
+ var key = "".concat((_t$kind = t.kind) !== null && _t$kind !== void 0 ? _t$kind : "subtitles", ":").concat(t.srclang);
5959
+ if (seen.has(key)) {
5960
+ errors.push({
5961
+ message: "".concat(at, " repeats the language '").concat(t.srclang, "'."),
5962
+ value: src
5963
+ });
5964
+ }
5965
+ seen.add(key);
5966
+ }
5967
+ });
5968
+ if (defaults > 1) {
5969
+ errors.push({
5970
+ message: "At most one caption track can be the default. ".concat(defaults, " are."),
5971
+ value: src
5972
+ });
5973
+ }
5974
+ return errors;
5975
+ }
5976
+
5977
+ function isObject(value) {
5978
+ return _typeof(value) === "object" && value !== null && !Array.isArray(value);
5979
+ }
5980
+
5981
+ /** What a media collection (`s.imageset`, `s.fileset`, `s.videoset`) holds. */
5982
+
5983
+ /**
5984
+ * The fix code a media collection of `type` reports for `check`.
5985
+ *
5986
+ * One function rather than a ternary at each site: there were two kinds of
5987
+ * collection when every site was written as `type === "images" ? … : …`, and
5988
+ * a third kind silently took the files branch.
5989
+ */
5990
+ function mediaCollectionFix(type, check) {
5991
+ return "".concat(type, ":").concat(check);
5992
+ }
5993
+
5994
+ /** The singular a message uses for one entry of a collection of `type`. */
5995
+ var MEDIA_ENTRY_LABEL = {
5996
+ files: "file",
5997
+ images: "image",
5998
+ videos: "video"
5999
+ };
6000
+
5474
6001
  /**
5475
6002
  * `.render(...)` is for records whose entries the generic record editor draws.
5476
6003
  * A router's entries are pages and a gallery's are media, and both have UIs of
@@ -5662,7 +6189,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5662
6189
  _iterator.f();
5663
6190
  }
5664
6191
  if (this.mediaOptions) {
5665
- var checkFix = this.mediaOptions.type === "images" ? "images:check-unique-folder" : "files:check-unique-folder";
6192
+ var checkFix = mediaCollectionFix(this.mediaOptions.type, "check-unique-folder");
5666
6193
  var uniqueCheckError = {
5667
6194
  message: "Gallery directory '".concat(this.mediaOptions.dir, "' must be unique across all galleries"),
5668
6195
  value: {
@@ -5680,7 +6207,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5680
6207
  } else {
5681
6208
  error = _defineProperty({}, path, [uniqueCheckError]);
5682
6209
  }
5683
- var allFilesCheckFix = this.mediaOptions.type === "images" ? "images:check-all-files" : "files:check-all-files";
6210
+ var allFilesCheckFix = mediaCollectionFix(this.mediaOptions.type, "check-all-files");
5684
6211
  var allFilesCheckError = {
5685
6212
  message: "Directory '".concat(this.mediaOptions.dir, "' may have files not tracked by this gallery"),
5686
6213
  value: {
@@ -5734,7 +6261,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5734
6261
  _this2.markKeyErrorsAtPath(keyErr, subPath);
5735
6262
  }
5736
6263
  error = _this2.mergeValidationErrors(error, keyErr);
5737
- var entryErr = _this2.validateMediaEntry(subPath, elem);
6264
+ var entryErr = _this2.validateMediaEntry(subPath, key, elem);
5738
6265
  if (entryErr) {
5739
6266
  _this2.markKeyErrorsAtPath(entryErr, subPath);
5740
6267
  }
@@ -5848,8 +6375,8 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5848
6375
  dir = _this$mediaOptions.dir,
5849
6376
  isRemote = _this$mediaOptions.remote,
5850
6377
  type = _this$mediaOptions.type;
5851
- var mediaLabel = type === "images" ? "images" : "files";
5852
- var checkRemoteFix = type === "images" ? "images:check-remote" : "files:check-remote";
6378
+ var mediaLabel = type;
6379
+ var checkRemoteFix = mediaCollectionFix(type, "check-remote");
5853
6380
  var isRemoteUrl = this.isRemoteUrl(key);
5854
6381
  var isLocalPath = key === dir || key.startsWith(dir + "/");
5855
6382
  if (isRemote) {
@@ -5882,9 +6409,9 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5882
6409
  }]);
5883
6410
  }
5884
6411
  // Local path in a remote gallery: needs to be uploaded to remote.
5885
- var uploadRemoteFix = type === "images" ? "images:upload-remote" : "files:upload-remote";
6412
+ var uploadRemoteFix = mediaCollectionFix(type, "upload-remote");
5886
6413
  return _defineProperty({}, path, [{
5887
- message: "Expected a remote ".concat(type === "images" ? "image" : "file", ", but got a local path. Use Val tooling (CLI --fix, VS Code extension, or Val Studio) to upload it. Got: ").concat(key),
6414
+ message: "Expected a remote ".concat(MEDIA_ENTRY_LABEL[type], ", but got a local path. Use Val tooling (CLI --fix, VS Code extension, or Val Studio) to upload it. Got: ").concat(key),
5888
6415
  value: key,
5889
6416
  fixes: [uploadRemoteFix]
5890
6417
  }]);
@@ -5908,7 +6435,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5908
6435
  }
5909
6436
  }, {
5910
6437
  key: "validateMediaEntry",
5911
- value: function validateMediaEntry(path, entry) {
6438
+ value: function validateMediaEntry(path, key, entry) {
5912
6439
  if (!this.mediaOptions) {
5913
6440
  return false;
5914
6441
  }
@@ -5924,14 +6451,48 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5924
6451
  }
5925
6452
  var entryObj = entry;
5926
6453
  var errors = [];
5927
- if (type === "images") {
5928
- // Validate width
5929
- if (typeof entryObj.width !== "number" || entryObj.width <= 0) {
5930
- errors.push({
5931
- message: "Expected 'width' to be a positive number, got '".concat(entryObj.width, "'"),
5932
- value: entry
5933
- });
5934
- }
6454
+ if (type === "videos") {
6455
+ // What is read from the bytes. Missing is a fix (the CLI reads them,
6456
+ // from disk or — for a remote entry — over HTTP with Range requests);
6457
+ // present and wrong is an error, because a fix that overwrote an
6458
+ // authored number would be guessing which of the two was right.
6459
+ var missing = ["mimeType", "width", "height", "duration"].filter(function (k) {
6460
+ return entryObj[k] === undefined;
6461
+ });
6462
+ if (missing.length > 0) {
6463
+ return _defineProperty({}, path, [{
6464
+ message: "Video metadata is missing: ".concat(missing.join(", "), "."),
6465
+ value: entry,
6466
+ fixes: ["videos:add-metadata"]
6467
+ }]);
6468
+ }
6469
+ var _iterator3 = result._createForOfIteratorHelper(["width", "height", "duration"]),
6470
+ _step3;
6471
+ try {
6472
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
6473
+ var k = _step3.value;
6474
+ var value = entryObj[k];
6475
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
6476
+ errors.push({
6477
+ message: "Expected '".concat(k, "' to be a positive number, got '").concat(JSON.stringify(value), "'"),
6478
+ value: entry
6479
+ });
6480
+ }
6481
+ }
6482
+ } catch (err) {
6483
+ _iterator3.e(err);
6484
+ } finally {
6485
+ _iterator3.f();
6486
+ }
6487
+ }
6488
+ if (type === "images") {
6489
+ // Validate width
6490
+ if (typeof entryObj.width !== "number" || entryObj.width <= 0) {
6491
+ errors.push({
6492
+ message: "Expected 'width' to be a positive number, got '".concat(entryObj.width, "'"),
6493
+ value: entry
6494
+ });
6495
+ }
5935
6496
 
5936
6497
  // Validate height
5937
6498
  if (typeof entryObj.height !== "number" || entryObj.height <= 0) {
@@ -5948,6 +6509,15 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5948
6509
  message: "Expected 'mimeType' to be a string, got '".concat(_typeof(entryObj.mimeType), "'"),
5949
6510
  value: entry
5950
6511
  });
6512
+ } else if (type === "videos" && entryObj.mimeType === HLS_MIME_TYPE) {
6513
+ // A stream is what the Studio made of an upload that WAS accepted, so
6514
+ // `accept` (which is about what may be picked) is not asked about it.
6515
+ if (!key.split("?")[0].toLowerCase().endsWith(".m3u8")) {
6516
+ errors.push({
6517
+ message: "An HLS video must be keyed by its master playlist (.m3u8). Got: ".concat(key),
6518
+ value: entry
6519
+ });
6520
+ }
5951
6521
  } else {
5952
6522
  var mimeTypeError = this.validateMediaMimeType(entryObj.mimeType, accept);
5953
6523
  if (mimeTypeError) {
@@ -5957,6 +6527,26 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5957
6527
  });
5958
6528
  }
5959
6529
  }
6530
+ if (type === "videos") {
6531
+ // The defaults every field picked from the set starts from, checked as
6532
+ // a field's own would be: they are the same choices, made once.
6533
+ var duration = typeof entryObj.duration === "number" && entryObj.duration > 0 ? entryObj.duration : undefined;
6534
+ errors.push.apply(errors, _toConsumableArray(validateTimes(entryObj, duration)).concat(_toConsumableArray(validateHotspot(entryObj)), _toConsumableArray(validatePoster(entryObj)), _toConsumableArray(validateCaptions(entryObj))));
6535
+ var misplaced = this.misplacedEntryFiles(key, entryObj);
6536
+ if (misplaced.length > 0) {
6537
+ var these = misplaced.length === 1 ? "this file is" : "".concat(misplaced.length, " files are");
6538
+ errors.push(this.mediaOptions.remote ? {
6539
+ message: "Expected the entry's files on Val Remote, but ".concat(these, " stored locally: ").concat(misplaced.join(", ")),
6540
+ // The key, as a local key's upload error carries: the fix
6541
+ // rewrites the entry under it.
6542
+ value: key,
6543
+ fixes: ["videos:upload-remote"]
6544
+ } : {
6545
+ message: "Expected the entry's files under ".concat(this.mediaOptions.dir, ", but ").concat(these, " remote: ").concat(misplaced.join(", ")),
6546
+ value: entry
6547
+ });
6548
+ }
6549
+ }
5960
6550
  if (type === "images") {
5961
6551
  // Validate hotspot if present
5962
6552
  if (entryObj.hotspot !== undefined) {
@@ -5968,7 +6558,8 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5968
6558
  });
5969
6559
  }
5970
6560
  }
5971
-
6561
+ }
6562
+ if (type === "images" || type === "videos") {
5972
6563
  // Validate alt using the alt schema
5973
6564
  var altPath = createValPathOfItem(path, "alt");
5974
6565
  if (altPath && altSchema) {
@@ -5983,6 +6574,49 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5983
6574
  }
5984
6575
  return false;
5985
6576
  }
6577
+
6578
+ /**
6579
+ * The poster and caption files of a set's entry that are not where the set
6580
+ * keeps its files. Asked only once the key itself is in place: a local key
6581
+ * in a remote set is reported (and uploaded) as the entry, files and all,
6582
+ * and a second error for its poster would be the same one twice.
6583
+ */
6584
+ }, {
6585
+ key: "misplacedEntryFiles",
6586
+ value: function misplacedEntryFiles(key, entry) {
6587
+ var _this3 = this;
6588
+ if (!this.mediaOptions) {
6589
+ return [];
6590
+ }
6591
+ var remote = this.mediaOptions.remote;
6592
+ if (this.isRemoteUrl(key) !== remote) {
6593
+ return [];
6594
+ }
6595
+ var files = [];
6596
+ var poster = entry.poster;
6597
+ if (isObject(poster) && typeof poster.path === "string") {
6598
+ files.push(poster.path);
6599
+ }
6600
+ if (Array.isArray(entry.captions)) {
6601
+ var _iterator4 = result._createForOfIteratorHelper(entry.captions),
6602
+ _step4;
6603
+ try {
6604
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
6605
+ var track = _step4.value;
6606
+ if (isObject(track) && typeof track.path === "string") {
6607
+ files.push(track.path);
6608
+ }
6609
+ }
6610
+ } catch (err) {
6611
+ _iterator4.e(err);
6612
+ } finally {
6613
+ _iterator4.f();
6614
+ }
6615
+ }
6616
+ return files.filter(function (file) {
6617
+ return _this3.isRemoteUrl(file) !== remote;
6618
+ });
6619
+ }
5986
6620
  }, {
5987
6621
  key: "validateMediaMimeType",
5988
6622
  value: function validateMediaMimeType(mimeType, accept) {
@@ -6346,6 +6980,9 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
6346
6980
  if (this.mediaOptions.encode !== undefined) {
6347
6981
  result.encode = this.mediaOptions.encode;
6348
6982
  }
6983
+ if (this.mediaOptions.stream !== undefined) {
6984
+ result.stream = this.mediaOptions.stream;
6985
+ }
6349
6986
  if (this.mediaOptions.altSchema) {
6350
6987
  result.alt = this.mediaOptions.altSchema["executeSerialize"]();
6351
6988
  }
@@ -10349,49 +10986,548 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10349
10986
  value: src
10350
10987
  }]));
10351
10988
  }
10352
-
10353
- // Guarded on `mimeType` being set, as `ImageSchema` guards the same check.
10354
- // Without it, an absent mimeType reported "Mime type and file extension not
10355
- // matching. Mime type is 'undefined'" -- with no fix attached, so no quick
10356
- // fix was offered -- and the `file:add-metadata` branch below could never be
10357
- // reached for any file whose extension is recognized, which is all of them
10358
- // that get this far.
10359
- if (mimeType && fileMimeType !== mimeType) {
10360
- return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
10361
- message: "Mime type and file extension not matching. Mime type is '".concat(mimeType, "' but file extension is '").concat(fileMimeType, "'"),
10362
- value: src
10363
- }]));
10989
+
10990
+ // Guarded on `mimeType` being set, as `ImageSchema` guards the same check.
10991
+ // Without it, an absent mimeType reported "Mime type and file extension not
10992
+ // matching. Mime type is 'undefined'" -- with no fix attached, so no quick
10993
+ // fix was offered -- and the `file:add-metadata` branch below could never be
10994
+ // reached for any file whose extension is recognized, which is all of them
10995
+ // that get this far.
10996
+ // A legacy font name (`application/x-font-ttf`) is what the extension
10997
+ // table itself used to say, so it is not a mismatch.
10998
+ if (mimeType && fileMimeType !== canonicalFontMimeType(mimeType)) {
10999
+ return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
11000
+ message: "Mime type and file extension not matching. Mime type is '".concat(mimeType, "' but file extension is '").concat(fileMimeType, "'"),
11001
+ value: src
11002
+ }]));
11003
+ }
11004
+
11005
+ // The mime type read so far came from the filename. Whether it matches the
11006
+ // bytes can only be answered by reading the file, which this package
11007
+ // deliberately cannot do — so it is always handed on as a fix.
11008
+ if (mimeType !== undefined) {
11009
+ return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
11010
+ message: "File mimeType has not been checked against the file.",
11011
+ value: src,
11012
+ fixes: ["file:check-metadata"]
11013
+ }]));
11014
+ }
11015
+ return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
11016
+ message: "File metadata is missing: mimeType.",
11017
+ value: src,
11018
+ fixes: ["file:add-metadata"]
11019
+ }]));
11020
+ }
11021
+
11022
+ /**
11023
+ * The entries of the gallery this field points at, or null when it is a
11024
+ * standalone field.
11025
+ */
11026
+ }, {
11027
+ key: "galleryEntries",
11028
+ value: function galleryEntries() {
11029
+ var modulePaths = Object.keys(this.moduleMetadata);
11030
+ if (modulePaths.length === 0) {
11031
+ return null;
11032
+ }
11033
+ return this.moduleMetadata[modulePaths[0]];
11034
+ }
11035
+ }, {
11036
+ key: "executeAssert",
11037
+ value: function executeAssert(path, src) {
11038
+ if (this.opt && src === null) {
11039
+ return {
11040
+ success: true,
11041
+ data: src
11042
+ };
11043
+ }
11044
+ if (src === null) {
11045
+ return {
11046
+ success: false,
11047
+ errors: _defineProperty({}, path, [{
11048
+ message: "Expected 'object', got 'null'",
11049
+ typeError: true
11050
+ }])
11051
+ };
11052
+ }
11053
+ if (_typeof(src) !== "object") {
11054
+ return {
11055
+ success: false,
11056
+ errors: _defineProperty({}, path, [{
11057
+ message: "Expected object, got '".concat(_typeof(src), "'"),
11058
+ typeError: true
11059
+ }])
11060
+ };
11061
+ }
11062
+ if (!("path" in src) || typeof src.path !== "string") {
11063
+ return {
11064
+ success: false,
11065
+ errors: _defineProperty({}, path, [{
11066
+ message: "A file must be an object with a 'path' (error type: missing_path)",
11067
+ typeError: true
11068
+ }])
11069
+ };
11070
+ }
11071
+ return {
11072
+ success: true,
11073
+ data: src
11074
+ };
11075
+ }
11076
+ }, {
11077
+ key: "nullable",
11078
+ value: function nullable() {
11079
+ return new FileSchema(this.options, true, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, this.isHidden, this.description, this.previewInput);
11080
+ }
11081
+ }, {
11082
+ key: "readonly",
11083
+ value: function readonly() {
11084
+ var isReadonly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
11085
+ return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, isReadonly, this.isHidden, this.description, this.previewInput);
11086
+ }
11087
+ }, {
11088
+ key: "hidden",
11089
+ value: function hidden() {
11090
+ var isHidden = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
11091
+ return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, isHidden, this.description, this.previewInput);
11092
+ }
11093
+ }, {
11094
+ key: "executeCustomValidateAt",
11095
+ value: function executeCustomValidateAt(path, src) {
11096
+ return this.executeCustomValidateFunctions(src, this.customValidateFunctions, {
11097
+ path: path
11098
+ });
11099
+ }
11100
+
11101
+ /**
11102
+ * How this VALUE is shown where a preview of it is needed — a row in a
11103
+ * sortable list, a reference dropdown, a search hit. Never how the field
11104
+ * itself is edited (that is `render`). See `preview.ts`.
11105
+ *
11106
+ * @example
11107
+ * const schema = s.array(
11108
+ * s.file().preview(({ val }) => ({ title: val.path })),
11109
+ * );
11110
+ * export default c.define("/example.val.ts", schema, [
11111
+ * { path: "/public/val/example.pdf", mimeType: "application/pdf" },
11112
+ * ]);
11113
+ */
11114
+ }, {
11115
+ key: "preview",
11116
+ value: function preview(select) {
11117
+ return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, this.isHidden, this.description, select);
11118
+ }
11119
+ }, {
11120
+ key: "executePreviewItem",
11121
+ value: function executePreviewItem(src) {
11122
+ if (this.previewInput === null) {
11123
+ return null;
11124
+ }
11125
+ return this.previewInput({
11126
+ val: src
11127
+ });
11128
+ }
11129
+ }, {
11130
+ key: "declaresItemPreview",
11131
+ value: function declaresItemPreview() {
11132
+ return this.previewInput !== null;
11133
+ }
11134
+ }, {
11135
+ key: "executeSerialize",
11136
+ value: function executeSerialize() {
11137
+ var _this$customValidateF;
11138
+ var modulePaths = this.moduleMetadata ? Object.keys(this.moduleMetadata) : [];
11139
+ return {
11140
+ type: "file",
11141
+ preview: this.previewInput ? true : undefined,
11142
+ options: this.options,
11143
+ opt: this.opt,
11144
+ remote: this.isRemote,
11145
+ customValidate: this.customValidateFunctions && ((_this$customValidateF = this.customValidateFunctions) === null || _this$customValidateF === void 0 ? void 0 : _this$customValidateF.length) > 0,
11146
+ referencedModule: modulePaths.length > 0 ? modulePaths[0] : undefined,
11147
+ readonly: this.isReadonly,
11148
+ hidden: this.isHidden,
11149
+ description: this.description
11150
+ };
11151
+ }
11152
+ }]);
11153
+ }(Schema);
11154
+
11155
+ /**
11156
+ * A file picked from a gallery. Its mime type lives in the gallery, so the
11157
+ * field carries only the path.
11158
+ */
11159
+
11160
+ /** A file of its own, carrying its own mime type. */
11161
+
11162
+ function file(options) {
11163
+ return fileOf(false, options);
11164
+ }
11165
+
11166
+ /**
11167
+ * `s.file()` for a project whose media is all remote (`files.remote` in
11168
+ * `val.config.ts`): the same schema, with `.remote()` already applied. See
11169
+ * `initSchema`.
11170
+ */
11171
+
11172
+ function remoteFile(options) {
11173
+ return fileOf(true, options);
11174
+ }
11175
+ function fileOf(remote, options) {
11176
+ var isModule = !!options && !!Internal.getValPath(options);
11177
+ if (isModule) {
11178
+ var allModules = {};
11179
+ for (var _i = 0, _arr = [options]; _i < _arr.length; _i++) {
11180
+ var valModule = _arr[_i];
11181
+ var modulePath = getValPath(valModule);
11182
+ if (modulePath === undefined) {
11183
+ throw new Error("Invalid argument passed to s.file(). Expected a ValModule constructed through c.define, but got an object without a valid module path.");
11184
+ }
11185
+ allModules[modulePath] = getSource(valModule);
11186
+ }
11187
+ return new FileSchema({}, false, remote, [], allModules);
11188
+ }
11189
+ return new FileSchema(options, false, remote);
11190
+ }
11191
+
11192
+ /**
11193
+ * A font picked from an `s.fontset()`.
11194
+ *
11195
+ * The same field `s.file(fontsVal)` is — it stores `{ path }` and serializes
11196
+ * identically — with one thing more: it refuses a set that is not a font set,
11197
+ * at definition, so a field called a font cannot end up offering PDFs. What the
11198
+ * Studio does with it (a specimen in its parent, the set to pick from when
11199
+ * opened) it does for any file field backed by a font set.
11200
+ */
11201
+ function font(fontsetModule) {
11202
+ assertFontset(fontsetModule);
11203
+ return file(fontsetModule);
11204
+ }
11205
+
11206
+ /** `s.font()` with `.remote()` applied: see `files.remote` in `initSchema`. */
11207
+ function remoteFont(fontsetModule) {
11208
+ assertFontset(fontsetModule);
11209
+ return remoteFile(fontsetModule);
11210
+ }
11211
+ function assertFontset(fontsetModule) {
11212
+ var _getSchema;
11213
+ var modulePath = getValPath(fontsetModule);
11214
+ var serialized = (_getSchema = getSchema(fontsetModule)) === null || _getSchema === void 0 ? void 0 : _getSchema["executeSerialize"]();
11215
+ var accept = (serialized === null || serialized === void 0 ? void 0 : serialized.type) === "record" && serialized.mediaType === "files" ? serialized.accept : undefined;
11216
+ var isFontset = !!accept && accept.split(",").every(function (type) {
11217
+ return isFontMimeType(type.trim());
11218
+ });
11219
+ if (!isFontset) {
11220
+ throw new Error("s.font(".concat(modulePath !== null && modulePath !== void 0 ? modulePath : "?", "): the module must be an s.fontset(). Use s.file() to pick from a set of other files."));
11221
+ }
11222
+ }
11223
+
11224
+ /**
11225
+ * Turn an upload into an HLS stream, in the browser, before it is uploaded.
11226
+ *
11227
+ * The Studio transcodes the picked file with WebCodecs into one rendition per
11228
+ * entry of `renditions` (never upscaled: a 720p upload gets no 1080p
11229
+ * rendition), segments each into CMAF and writes the playlists. What is stored
11230
+ * is a directory beside the other uploads; the field's `path` is its master
11231
+ * playlist. See `architecture/media.md`.
11232
+ *
11233
+ * `type` is required for the same reason it is on `ImageEncodeOptions`: a
11234
+ * second kind of stream would then be additive.
11235
+ */
11236
+
11237
+ /** `false` (or absent) uploads the file as it was picked. */
11238
+
11239
+ /**
11240
+ * What a SET-BACKED field (`s.video(videosVal)`) may say for itself.
11241
+ *
11242
+ * Not `VideoOptions`: `dir` and `accept` belong to the set. `stream` is the
11243
+ * exception, as `encode` is for an image: it is about what happens to an
11244
+ * upload on its way in, so a field that wants the file as it was picked,
11245
+ * where its set streams, has to be able to say `stream: false`.
11246
+ */
11247
+
11248
+ /**
11249
+ * The set a field picks from: its entries as they were when the module was
11250
+ * evaluated (the published set — see `GalleryImageSource` for why that is
11251
+ * enough), and whether it stores its videos on Val Remote.
11252
+ */
11253
+
11254
+ var DEFAULT_VIDEO_ACCEPT = "video/*";
11255
+ var DEFAULT_VIDEO_RENDITIONS = [1080, 720, 480];
11256
+ var DEFAULT_VIDEO_SEGMENT_DURATION = 6;
11257
+ var VideoSchema = /*#__PURE__*/function (_Schema) {
11258
+ function VideoSchema(options) {
11259
+ var _this;
11260
+ var opt = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
11261
+ var isRemote = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
11262
+ var customValidateFunctions = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
11263
+ var isReadonly = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
11264
+ var isHidden = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
11265
+ var description = arguments.length > 6 ? arguments[6] : undefined;
11266
+ var renderInput = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : null;
11267
+ var previewInput = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : null;
11268
+ var gallery = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : null;
11269
+ _classCallCheck(this, VideoSchema);
11270
+ _this = _callSuper(this, VideoSchema);
11271
+ _this.options = options;
11272
+ _this.opt = opt;
11273
+ _this.isRemote = isRemote;
11274
+ _this.customValidateFunctions = customValidateFunctions;
11275
+ _this.isReadonly = isReadonly;
11276
+ _this.isHidden = isHidden;
11277
+ _this.description = description;
11278
+ _this.renderInput = renderInput;
11279
+ _this.previewInput = previewInput;
11280
+ _this.gallery = gallery;
11281
+ return _this;
11282
+ }
11283
+
11284
+ /**
11285
+ * Describe this field.
11286
+ *
11287
+ * The description is INPUT HELP: it is shown where this field's value is
11288
+ * entered — beside its input in the Val editor, and for a record's key
11289
+ * schema in every form that asks for a key — so it is where you say what an
11290
+ * editor needs to know to fill it in RIGHT, which the field name cannot
11291
+ * carry. It is not a name for the value: that is `.preview(...)`, and it is
11292
+ * read somewhere else. The description also travels in the serialized
11293
+ * schema, which is what the AI assistant and the MCP tools read.
11294
+ *
11295
+ * Pass `null` to clear a description set earlier.
11296
+ *
11297
+ * @example
11298
+ * const schema = s.video().describe("Plays muted behind the headline");
11299
+ * export default c.define("/example.val.ts", schema, {
11300
+ * path: "/public/val/example.mp4",
11301
+ * mimeType: "video/mp4",
11302
+ * });
11303
+ */
11304
+ _inherits(VideoSchema, _Schema);
11305
+ return _createClass(VideoSchema, [{
11306
+ key: "describe",
11307
+ value: function describe(description) {
11308
+ return new VideoSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.isReadonly, this.isHidden, description !== null && description !== void 0 ? description : undefined, this.renderInput, this.previewInput, this.gallery);
11309
+ }
11310
+
11311
+ /**
11312
+ * Store the video on Val's remote content host instead of in your
11313
+ * repository.
11314
+ *
11315
+ * The bytes still go into the patch store when the video is uploaded — the
11316
+ * push to the remote host happens at publish. What changes is where the
11317
+ * published video lives: `path` (and the poster's, and each caption track's)
11318
+ * becomes a remote URL rather than a path under `/public`, so the repository
11319
+ * does not grow with every upload. Upload remote videos in the Studio.
11320
+ *
11321
+ * @example
11322
+ * const schema = s.video().remote();
11323
+ * export default c.define("/example.val.ts", schema, {
11324
+ * path: "https://remote.val.build/file/p/my-project/b/01/v/1.0.0/h/8f2a1c/f/3b9d70/p/public/val/example.mp4",
11325
+ * mimeType: "video/mp4",
11326
+ * });
11327
+ */
11328
+ }, {
11329
+ key: "remote",
11330
+ value: function remote() {
11331
+ return new VideoSchema(this.options, this.opt, true, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.renderInput, this.previewInput, this.gallery);
11332
+ }
11333
+
11334
+ /**
11335
+ * Add a custom validation rule to this field.
11336
+ *
11337
+ * The function is called with the field's value and returns `false` when the
11338
+ * value is fine, or a STRING with the message to show when it is not. Call it
11339
+ * more than once to add more rules — they all run, and every message is
11340
+ * reported.
11341
+ *
11342
+ * Write the check as a ternary, not as `ok || "message"`: that returns `true`
11343
+ * when the value is fine, and `true` is not one of the two answers.
11344
+ *
11345
+ * Validation runs in the Studio as you type, in `npx val validate` and
11346
+ * before a publish.
11347
+ *
11348
+ * The second argument carries the `path` of the field being validated, for
11349
+ * when the message needs to say where the problem is.
11350
+ *
11351
+ * @example
11352
+ * const schema = s.video().validate((val) =>
11353
+ * (val.duration ?? 0) <= 30 ? false : "Keep it under 30 seconds",
11354
+ * );
11355
+ * export default c.define("/example.val.ts", schema, {
11356
+ * path: "/public/val/example.mp4",
11357
+ * mimeType: "video/mp4",
11358
+ * duration: 12,
11359
+ * });
11360
+ */
11361
+ }, {
11362
+ key: "validate",
11363
+ value: function validate(validationFunction) {
11364
+ return new VideoSchema(this.options, this.opt, this.isRemote, [].concat(_toConsumableArray(this.customValidateFunctions), [validationFunction]), this.isReadonly, this.isHidden, this.description, this.renderInput, this.previewInput, this.gallery);
11365
+ }
11366
+ }, {
11367
+ key: "executeValidate",
11368
+ value: function executeValidate(path, src) {
11369
+ var customValidationErrors = this.executeCustomValidateFunctions(src, this.customValidateFunctions, {
11370
+ path: path
11371
+ });
11372
+ var report = function report(errors) {
11373
+ var all = [].concat(_toConsumableArray(customValidationErrors), _toConsumableArray(errors));
11374
+ return all.length > 0 ? _defineProperty({}, path, all) : false;
11375
+ };
11376
+ if (src === null || src === undefined) {
11377
+ if (this.opt) {
11378
+ return report([]);
11379
+ }
11380
+ return report([{
11381
+ message: "Non-optional video was null or undefined.",
11382
+ value: src
11383
+ }]);
11384
+ }
11385
+ if (_typeof(src) !== "object" || typeof src.path !== "string") {
11386
+ return report([{
11387
+ message: "A video must be an object with a 'path' string.",
11388
+ value: src
11389
+ }]);
11390
+ }
11391
+ if (this.gallery) {
11392
+ return report(this.validateFromGallery(src, this.gallery));
11393
+ }
11394
+ return report(this.validateOwn(src));
11395
+ }
11396
+
11397
+ /**
11398
+ * A set-backed field: the file and what was read from it are the set's,
11399
+ * so the field is checked for what is its own, and for pointing at an
11400
+ * entry the set has.
11401
+ */
11402
+ }, {
11403
+ key: "validateFromGallery",
11404
+ value: function validateFromGallery(src, gallery) {
11405
+ var own = ownMetadataOf(src);
11406
+ var repeated = ["mimeType", "width", "height", "duration"].filter(function (key) {
11407
+ return own[key] !== undefined;
11408
+ });
11409
+ if (repeated.length > 0) {
11410
+ return [{
11411
+ message: "A video from a set must not carry its own ".concat(repeated.join(", "), ": ").concat(repeated.length === 1 ? "it is" : "they are", " stored in the set."),
11412
+ value: src
11413
+ }];
11414
+ }
11415
+ var entry = gallery.entries === null ? null : gallery.entries[src.path];
11416
+ if (entry === undefined) {
11417
+ return [{
11418
+ message: "The set does not have a video at '".concat(src.path, "'."),
11419
+ value: src
11420
+ }];
11421
+ }
11422
+ // The poster and the captions are the field's own files, and live where
11423
+ // the set keeps its videos. The video itself is the set's to move.
11424
+ var misplaced = filesOfVideoSource(src).slice(1).filter(function (file) {
11425
+ return isRemoteMediaPath(file) !== gallery.remote;
11426
+ });
11427
+ if (misplaced.length > 0) {
11428
+ return [misplacedError(src, misplaced, gallery.remote)];
11429
+ }
11430
+ var duration = entry !== null && typeof entry.duration === "number" && entry.duration > 0 ? entry.duration : undefined;
11431
+ return [].concat(_toConsumableArray(validateTimes(src, duration)), _toConsumableArray(validateMixedTimes(src, entry)), _toConsumableArray(validateHotspot(src)), _toConsumableArray(validatePoster(src)), _toConsumableArray(validateCaptions(src)));
11432
+ }
11433
+
11434
+ /** A video of its own: it carries the file, and what was read from it. */
11435
+ }, {
11436
+ key: "validateOwn",
11437
+ value: function validateOwn(src) {
11438
+ var _this2 = this;
11439
+ var own = ownMetadataOf(src);
11440
+ // Every file the video names has to be where the schema says, and one fix
11441
+ // moves them all — a video on the content host with its poster still in
11442
+ // the repository is half a migration, not a choice. The error is reported
11443
+ // once for the whole video, never per file.
11444
+ var misplaced = filesOfVideoSource(src).filter(function (file) {
11445
+ return isRemoteMediaPath(file) !== _this2.isRemote;
11446
+ });
11447
+ if (misplaced.length > 0) {
11448
+ return [misplacedError(src, misplaced, this.isRemote)];
11449
+ }
11450
+ var errors = [];
11451
+ var isHls = isHlsVideo({
11452
+ path: src.path,
11453
+ mimeType: stringOrUndefined(own.mimeType)
11454
+ });
11455
+ var extensionMimeType = filenameToMimeType(stripQuery(src.path));
11456
+ if (isHls) {
11457
+ if (!stripQuery(src.path).toLowerCase().endsWith(".m3u8")) {
11458
+ errors.push({
11459
+ message: "An HLS video's path must be its master playlist (.m3u8). Got: ".concat(src.path),
11460
+ value: src
11461
+ });
11462
+ }
11463
+ } else if (!extensionMimeType || !extensionMimeType.startsWith("video/")) {
11464
+ errors.push({
11465
+ message: "Could not tell this is a video from its file extension. Got: ".concat(src.path),
11466
+ value: src
11467
+ });
11468
+ } else if (own.mimeType !== undefined && extensionMimeType !== own.mimeType) {
11469
+ errors.push({
11470
+ message: "Mime type and file extension not matching. Mime type is '".concat(String(own.mimeType), "' but file extension is '").concat(extensionMimeType, "'"),
11471
+ value: src
11472
+ });
11473
+ }
11474
+ if (own.mimeType !== undefined && typeof own.mimeType !== "string") {
11475
+ errors.push({
11476
+ message: "'mimeType' must be a string.",
11477
+ value: src
11478
+ });
11479
+ } else if (own.mimeType !== undefined && !isHls) {
11480
+ var _this$options$accept, _this$options;
11481
+ var accept = (_this$options$accept = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.accept) !== null && _this$options$accept !== void 0 ? _this$options$accept : DEFAULT_VIDEO_ACCEPT;
11482
+ if (!mimeTypeMatchesAccept(own.mimeType, accept)) {
11483
+ errors.push({
11484
+ message: "Mime type mismatch. Found '".concat(own.mimeType, "' but schema accepts '").concat(accept, "'"),
11485
+ value: src
11486
+ });
11487
+ }
11488
+ }
11489
+ var _iterator = result._createForOfIteratorHelper(["width", "height", "duration"]),
11490
+ _step;
11491
+ try {
11492
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
11493
+ var key = _step.value;
11494
+ var value = own[key];
11495
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value <= 0)) {
11496
+ errors.push({
11497
+ message: "'".concat(key, "' must be a positive number. Got: ").concat(JSON.stringify(value)),
11498
+ value: src
11499
+ });
11500
+ }
11501
+ }
11502
+ } catch (err) {
11503
+ _iterator.e(err);
11504
+ } finally {
11505
+ _iterator.f();
11506
+ }
11507
+ var duration = typeof own.duration === "number" && own.duration > 0 ? own.duration : undefined;
11508
+ errors.push.apply(errors, _toConsumableArray(validateTimes(src, duration)));
11509
+ errors.push.apply(errors, _toConsumableArray(validateHotspot(src)));
11510
+ errors.push.apply(errors, _toConsumableArray(validatePoster(src)));
11511
+ errors.push.apply(errors, _toConsumableArray(validateCaptions(src)));
11512
+ if (errors.length > 0) {
11513
+ return errors;
10364
11514
  }
10365
11515
 
10366
- // The mime type read so far came from the filename. Whether it matches the
10367
- // bytes can only be answered by reading the file, which this package
10368
- // deliberately cannot do — so it is always handed on as a fix.
10369
- if (mimeType !== undefined) {
10370
- return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
10371
- message: "File mimeType has not been checked against the file.",
11516
+ // Everything authored is fine. What is left is what is read from the
11517
+ // bytes, which this package cannot do — so it is handed on as a fix. The
11518
+ // CLI reads a local file's headers from disk and a remote one's over
11519
+ // HTTP, with Range requests, so neither is downloaded whole.
11520
+ var missing = ["mimeType", "width", "height", "duration"].filter(function (key) {
11521
+ return own[key] === undefined;
11522
+ });
11523
+ if (missing.length > 0) {
11524
+ return [{
11525
+ message: "Video metadata is missing: ".concat(missing.join(", "), "."),
10372
11526
  value: src,
10373
- fixes: ["file:check-metadata"]
10374
- }]));
10375
- }
10376
- return _defineProperty({}, path, [].concat(_toConsumableArray(customValidationErrors), [{
10377
- message: "File metadata is missing: mimeType.",
10378
- value: src,
10379
- fixes: ["file:add-metadata"]
10380
- }]));
10381
- }
10382
-
10383
- /**
10384
- * The entries of the gallery this field points at, or null when it is a
10385
- * standalone field.
10386
- */
10387
- }, {
10388
- key: "galleryEntries",
10389
- value: function galleryEntries() {
10390
- var modulePaths = Object.keys(this.moduleMetadata);
10391
- if (modulePaths.length === 0) {
10392
- return null;
11527
+ fixes: ["video:add-metadata"]
11528
+ }];
10393
11529
  }
10394
- return this.moduleMetadata[modulePaths[0]];
11530
+ return [];
10395
11531
  }
10396
11532
  }, {
10397
11533
  key: "executeAssert",
@@ -10424,7 +11560,7 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10424
11560
  return {
10425
11561
  success: false,
10426
11562
  errors: _defineProperty({}, path, [{
10427
- message: "A file must be an object with a 'path' (error type: missing_path)",
11563
+ message: "A video must be an object with a 'path' (error type: missing_path)",
10428
11564
  typeError: true
10429
11565
  }])
10430
11566
  };
@@ -10437,19 +11573,19 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10437
11573
  }, {
10438
11574
  key: "nullable",
10439
11575
  value: function nullable() {
10440
- return new FileSchema(this.options, true, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, this.isHidden, this.description, this.previewInput);
11576
+ return new VideoSchema(this.options, true, this.isRemote, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.renderInput, this.previewInput, this.gallery);
10441
11577
  }
10442
11578
  }, {
10443
11579
  key: "readonly",
10444
11580
  value: function readonly() {
10445
11581
  var isReadonly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
10446
- return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, isReadonly, this.isHidden, this.description, this.previewInput);
11582
+ return new VideoSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, isReadonly, this.isHidden, this.description, this.renderInput, this.previewInput, this.gallery);
10447
11583
  }
10448
11584
  }, {
10449
11585
  key: "hidden",
10450
11586
  value: function hidden() {
10451
11587
  var isHidden = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
10452
- return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, isHidden, this.description, this.previewInput);
11588
+ return new VideoSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.isReadonly, isHidden, this.description, this.renderInput, this.previewInput, this.gallery);
10453
11589
  }
10454
11590
  }, {
10455
11591
  key: "executeCustomValidateAt",
@@ -10459,6 +11595,25 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10459
11595
  });
10460
11596
  }
10461
11597
 
11598
+ /**
11599
+ * How this field is laid out in the editor when it is the item of an array
11600
+ * or record: `{ as: "inline" }` renders the field itself inside each row,
11601
+ * instead of a preview row that navigates to it.
11602
+ *
11603
+ * Static configuration, not a callback — see `render.ts`.
11604
+ *
11605
+ * @example
11606
+ * const schema = s.array(s.video().render({ as: "inline" }));
11607
+ * export default c.define("/example.val.ts", schema, [
11608
+ * { path: "/public/val/example.mp4", mimeType: "video/mp4" },
11609
+ * ]);
11610
+ */
11611
+ }, {
11612
+ key: "render",
11613
+ value: function render(input) {
11614
+ return new VideoSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, input, this.previewInput, this.gallery);
11615
+ }
11616
+
10462
11617
  /**
10463
11618
  * How this VALUE is shown where a preview of it is needed — a row in a
10464
11619
  * sortable list, a reference dropdown, a search hit. Never how the field
@@ -10466,16 +11621,19 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10466
11621
  *
10467
11622
  * @example
10468
11623
  * const schema = s.array(
10469
- * s.file().preview(({ val }) => ({ title: val.path })),
11624
+ * s.video().preview(({ val }) => ({
11625
+ * title: val.alt ?? val.path,
11626
+ * image: val.poster,
11627
+ * })),
10470
11628
  * );
10471
11629
  * export default c.define("/example.val.ts", schema, [
10472
- * { path: "/public/val/example.pdf", mimeType: "application/pdf" },
11630
+ * { path: "/public/val/example.mp4", mimeType: "video/mp4" },
10473
11631
  * ]);
10474
11632
  */
10475
11633
  }, {
10476
11634
  key: "preview",
10477
11635
  value: function preview(select) {
10478
- return new FileSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.moduleMetadata, this.isReadonly, this.isHidden, this.description, select);
11636
+ return new VideoSchema(this.options, this.opt, this.isRemote, this.customValidateFunctions, this.isReadonly, this.isHidden, this.description, this.renderInput, select, this.gallery);
10479
11637
  }
10480
11638
  }, {
10481
11639
  key: "executePreviewItem",
@@ -10495,16 +11653,16 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10495
11653
  }, {
10496
11654
  key: "executeSerialize",
10497
11655
  value: function executeSerialize() {
10498
- var _this$customValidateF;
10499
- var modulePaths = this.moduleMetadata ? Object.keys(this.moduleMetadata) : [];
11656
+ var _this$renderInput, _this$customValidateF, _this$gallery;
10500
11657
  return {
10501
- type: "file",
11658
+ type: "video",
11659
+ render: (_this$renderInput = this.renderInput) !== null && _this$renderInput !== void 0 ? _this$renderInput : undefined,
10502
11660
  preview: this.previewInput ? true : undefined,
10503
11661
  options: this.options,
10504
11662
  opt: this.opt,
10505
11663
  remote: this.isRemote,
10506
11664
  customValidate: this.customValidateFunctions && ((_this$customValidateF = this.customValidateFunctions) === null || _this$customValidateF === void 0 ? void 0 : _this$customValidateF.length) > 0,
10507
- referencedModule: modulePaths.length > 0 ? modulePaths[0] : undefined,
11665
+ referencedModule: (_this$gallery = this.gallery) === null || _this$gallery === void 0 ? void 0 : _this$gallery.modulePath,
10508
11666
  readonly: this.isReadonly,
10509
11667
  hidden: this.isHidden,
10510
11668
  description: this.description
@@ -10514,27 +11672,137 @@ var FileSchema = /*#__PURE__*/function (_Schema) {
10514
11672
  }(Schema);
10515
11673
 
10516
11674
  /**
10517
- * A file picked from a gallery. Its mime type lives in the gallery, so the
10518
- * field carries only the path.
11675
+ * Every file a video value names: the video, its poster and each caption
11676
+ * track. An HLS stream's segments are not here — they are named by its
11677
+ * playlists, not by the value.
10519
11678
  */
10520
-
10521
- /** A file of its own, carrying its own mime type. */
10522
-
10523
- function file(options) {
10524
- var isModule = !!options && !!Internal.getValPath(options);
10525
- if (isModule) {
10526
- var allModules = {};
10527
- for (var _i = 0, _arr = [options]; _i < _arr.length; _i++) {
10528
- var valModule = _arr[_i];
10529
- var modulePath = getValPath(valModule);
10530
- if (modulePath === undefined) {
10531
- throw new Error("Invalid argument passed to s.file(). Expected a ValModule constructed through c.define, but got an object without a valid module path.");
11679
+ function filesOfVideoSource(src) {
11680
+ var files = [src.path];
11681
+ if (src.poster && _typeof(src.poster) === "object" && typeof src.poster.path === "string") {
11682
+ files.push(src.poster.path);
11683
+ }
11684
+ if (Array.isArray(src.captions)) {
11685
+ var _iterator2 = result._createForOfIteratorHelper(src.captions),
11686
+ _step2;
11687
+ try {
11688
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
11689
+ var track = _step2.value;
11690
+ if (track && _typeof(track) === "object" && typeof track.path === "string") {
11691
+ files.push(track.path);
11692
+ }
10532
11693
  }
10533
- allModules[modulePath] = getSource(valModule);
11694
+ } catch (err) {
11695
+ _iterator2.e(err);
11696
+ } finally {
11697
+ _iterator2.f();
10534
11698
  }
10535
- return new FileSchema({}, false, false, [], allModules);
10536
11699
  }
10537
- return new FileSchema(options);
11700
+ return files;
11701
+ }
11702
+
11703
+ /**
11704
+ * What the value says about its file, typed as what it IS rather than what
11705
+ * it should be: a set-backed field's type has none of these, and is checked
11706
+ * for carrying them anyway, because hand-written JSON never saw the type.
11707
+ */
11708
+ function ownMetadataOf(src) {
11709
+ var value = _objectSpread2({}, src);
11710
+ return {
11711
+ mimeType: value.mimeType,
11712
+ width: value.width,
11713
+ height: value.height,
11714
+ duration: value.duration
11715
+ };
11716
+ }
11717
+
11718
+ /**
11719
+ * A start the field set against an end the set holds, or the reverse: each is
11720
+ * fine alone, and the field's own times and the set's are each checked where
11721
+ * they are written, so what is left is the pair the page will actually play.
11722
+ */
11723
+ function validateMixedTimes(src, entry) {
11724
+ var _src$startTime, _src$endTime;
11725
+ if (entry === null || src.startTime === undefined === (src.endTime === undefined)) {
11726
+ return [];
11727
+ }
11728
+ var start = (_src$startTime = src.startTime) !== null && _src$startTime !== void 0 ? _src$startTime : entry.startTime;
11729
+ var end = (_src$endTime = src.endTime) !== null && _src$endTime !== void 0 ? _src$endTime : entry.endTime;
11730
+ if (typeof start !== "number" || typeof end !== "number" || start < end) {
11731
+ return [];
11732
+ }
11733
+ var whose = src.startTime === undefined ? "the start is the set's" : "the end is the set's";
11734
+ return [{
11735
+ message: "'startTime' (".concat(start, "s) must be before 'endTime' (").concat(end, "s) \u2014 ").concat(whose, "."),
11736
+ value: src
11737
+ }];
11738
+ }
11739
+ function stringOrUndefined(value) {
11740
+ return typeof value === "string" ? value : undefined;
11741
+ }
11742
+ function misplacedError(src, misplaced, shouldBeRemote) {
11743
+ var these = misplaced.length === 1 ? "this file is" : "".concat(misplaced.length, " files are");
11744
+ return shouldBeRemote ? {
11745
+ message: "Expected a remote video, but ".concat(these, " stored locally: ").concat(misplaced.join(", ")),
11746
+ value: src,
11747
+ fixes: ["video:upload-remote"]
11748
+ } : {
11749
+ message: "Expected a local video (files under /public), but ".concat(these, " remote: ").concat(misplaced.join(", ")),
11750
+ value: src,
11751
+ fixes: ["video:download-remote"]
11752
+ };
11753
+ }
11754
+ function stripQuery(path) {
11755
+ return path.split("?")[0];
11756
+ }
11757
+
11758
+ /**
11759
+ * A video picked from a set (`s.videoset()`). What is true of the file —
11760
+ * its type, size and length — lives in the set; the field carries what one
11761
+ * page chose about it.
11762
+ */
11763
+
11764
+ /**
11765
+ * Define a video: an `.mp4` / `.webm` file, or an HLS stream the Studio makes
11766
+ * from an upload when `stream` is set.
11767
+ */
11768
+
11769
+ function video(options, galleryOptions) {
11770
+ return videoOf(options, galleryOptions);
11771
+ }
11772
+
11773
+ /**
11774
+ * `s.video()` for a project whose media is all remote (`files.remote` in
11775
+ * `val.config.ts`): the same schema, with `.remote()` already applied. See
11776
+ * `initSchema`.
11777
+ */
11778
+
11779
+ function remoteVideo(options, galleryOptions) {
11780
+ return videoOf(options, galleryOptions).remote();
11781
+ }
11782
+ function videoOf(options, galleryOptions) {
11783
+ var _options$GetSchema, _setSchema$remote, _setSchema$remote2;
11784
+ if (options === undefined || !isVideosetModule(options)) {
11785
+ return new VideoSchema(options);
11786
+ }
11787
+ var modulePath = getValPath(options);
11788
+ if (modulePath === undefined) {
11789
+ throw new Error("Invalid argument passed to s.video(). Expected a ValModule constructed through c.define, but got an object without a valid module path.");
11790
+ }
11791
+ var setSchema = (_options$GetSchema = options[GetSchema]) === null || _options$GetSchema === void 0 ? void 0 : _options$GetSchema["executeSerialize"]();
11792
+ if ((setSchema === null || setSchema === void 0 ? void 0 : setSchema.type) !== "record" || setSchema.mediaType !== "videos") {
11793
+ var _setSchema$mediaType, _setSchema$type;
11794
+ throw new Error("s.video(".concat(modulePath, "): the module must be an s.videoset(). Got ").concat((setSchema === null || setSchema === void 0 ? void 0 : setSchema.type) === "record" ? "a record of ".concat((_setSchema$mediaType = setSchema.mediaType) !== null && _setSchema$mediaType !== void 0 ? _setSchema$mediaType : "values") : (_setSchema$type = setSchema === null || setSchema === void 0 ? void 0 : setSchema.type) !== null && _setSchema$type !== void 0 ? _setSchema$type : "no schema", "."));
11795
+ }
11796
+ return new VideoSchema((galleryOptions === null || galleryOptions === void 0 ? void 0 : galleryOptions.stream) !== undefined ? {
11797
+ stream: galleryOptions.stream
11798
+ } : undefined, false, (_setSchema$remote = setSchema.remote) !== null && _setSchema$remote !== void 0 ? _setSchema$remote : false, [], false, false, undefined, null, null, {
11799
+ modulePath: modulePath,
11800
+ entries: getSource(options),
11801
+ remote: (_setSchema$remote2 = setSchema.remote) !== null && _setSchema$remote2 !== void 0 ? _setSchema$remote2 : false
11802
+ });
11803
+ }
11804
+ function isVideosetModule(value) {
11805
+ return GetSchema in value;
10538
11806
  }
10539
11807
 
10540
11808
  /**
@@ -10576,6 +11844,50 @@ var fileset = function fileset(options) {
10576
11844
  });
10577
11845
  };
10578
11846
 
11847
+ /**
11848
+ * Options for s.fontset()
11849
+ */
11850
+
11851
+ /**
11852
+ * Define a collection of fonts.
11853
+ *
11854
+ * A fontset IS a fileset — the same source, the same `s.file(fontsVal)` field
11855
+ * to pick from it, the same checks and fixes — with `accept` defaulting to the
11856
+ * web font formats. What a font gets that any other file does not is decided
11857
+ * by its mime type rather than by the set: the Studio previews any file it
11858
+ * knows is a font, wherever it is shown.
11859
+ *
11860
+ * Remote is off by default: call `.remote()` on the result to allow remote files.
11861
+ *
11862
+ * @example
11863
+ * ```typescript
11864
+ * const schema = s.fontset({ dir: "/public/val/fonts" });
11865
+ * export default c.define("/content/fonts.val.ts", schema, {
11866
+ * "/public/val/fonts/inter_a1b2c.woff2": {
11867
+ * mimeType: "font/woff2",
11868
+ * },
11869
+ * });
11870
+ * ```
11871
+ */
11872
+ var fontset = function fontset(options) {
11873
+ var _options$accept;
11874
+ var accept = (_options$accept = options.accept) !== null && _options$accept !== void 0 ? _options$accept : DEFAULT_FONT_ACCEPT;
11875
+ var notFonts = accept.split(",").map(function (type) {
11876
+ return type.trim();
11877
+ }).filter(function (type) {
11878
+ return !isFontMimeType(type);
11879
+ });
11880
+ if (notFonts.length > 0) {
11881
+ // A fontset that took a PDF would be a fileset under a name that says
11882
+ // otherwise, and the Studio would draw a specimen of nothing.
11883
+ throw new Error("s.fontset(): 'accept' may only name font types (font/woff2, font/woff, font/ttf, font/otf or font/*). Got: ".concat(notFonts.join(", "), ". Use s.fileset() for other files."));
11884
+ }
11885
+ return fileset({
11886
+ dir: options.dir,
11887
+ accept: accept
11888
+ });
11889
+ };
11890
+
10579
11891
  var DateSchema = /*#__PURE__*/function (_Schema) {
10580
11892
  function DateSchema(options) {
10581
11893
  var _this;
@@ -11516,6 +12828,99 @@ var imageset = function imageset(options) {
11516
12828
  });
11517
12829
  };
11518
12830
 
12831
+ /**
12832
+ * Options for s.videoset()
12833
+ */
12834
+
12835
+ /** See `ImagesetAltOption`: `alt` is optional only when it is the default. */
12836
+
12837
+ /**
12838
+ * One video of a set, keyed by its file path (an HLS stream by its master
12839
+ * playlist).
12840
+ *
12841
+ * Two kinds of thing are here. What is TRUE OF THE FILE — what it is, how
12842
+ * big, how long — is read from the bytes and is the set's alone. The rest —
12843
+ * the description, focal point, poster, start and end, and captions — is what
12844
+ * a page chooses about a video, and an entry holds it as the DEFAULT: every
12845
+ * field picked from the set (`s.video(videosVal)`) starts from it and may
12846
+ * override it key by key, so a video described and trimmed once is described
12847
+ * and trimmed everywhere it is used. `fillFromGallery` is the one merge.
12848
+ *
12849
+ * The poster is also the set's thumbnail of the entry: there is no separate
12850
+ * still for the gallery, because two stills of one video would disagree.
12851
+ */
12852
+
12853
+ /**
12854
+ * Define a collection of videos — the video twin of `s.imageset()`.
12855
+ *
12856
+ * A field picks from it with `s.video(videosVal)`. An entry may hold the
12857
+ * description, poster, start and end, focal point and captions as defaults,
12858
+ * and a field overrides any of them by setting its own. Uploading in such a
12859
+ * field adds to the set. Call `.remote()` on the result
12860
+ * to store the set on Val Remote.
12861
+ *
12862
+ * @example
12863
+ * ```typescript
12864
+ * const schema = s.videoset({
12865
+ * dir: "/public/val/videos",
12866
+ * stream: { type: "hls" },
12867
+ * });
12868
+ * export default c.define("/content/videos.val.ts", schema, {
12869
+ * "/public/val/videos/intro_51df2.mp4": {
12870
+ * mimeType: "video/mp4",
12871
+ * width: 1280,
12872
+ * height: 720,
12873
+ * duration: 12.5,
12874
+ * alt: "The team, introducing itself",
12875
+ * startTime: 1.5,
12876
+ * },
12877
+ * });
12878
+ * ```
12879
+ */
12880
+ var videoset = function videoset(options) {
12881
+ var _options$alt, _options$accept;
12882
+ // The same assertion as in `imageset()`, and sound for the same reason:
12883
+ // `VideosetAltOption` makes `alt` required unless `Alt` is the default.
12884
+ var altSchema = (_options$alt = options.alt) !== null && _options$alt !== void 0 ? _options$alt : string().nullable();
12885
+ var itemSchema = new ObjectSchema({
12886
+ mimeType: new StringSchema({}, false),
12887
+ width: new NumberSchema(undefined, false),
12888
+ height: new NumberSchema(undefined, false),
12889
+ duration: new NumberSchema(undefined, false),
12890
+ alt: altSchema
12891
+ }, false);
12892
+ return new RecordSchema(itemSchema, false, [], null, null, {
12893
+ type: "videos",
12894
+ accept: (_options$accept = options.accept) !== null && _options$accept !== void 0 ? _options$accept : DEFAULT_VIDEO_ACCEPT,
12895
+ dir: options.dir,
12896
+ remote: false,
12897
+ altSchema: altSchema,
12898
+ stream: options.stream
12899
+ });
12900
+ };
12901
+
12902
+ /**
12903
+ * The video schema a set's entry is hashed against, for a remote ref.
12904
+ *
12905
+ * The set's item schema is an object, but a remote ref's validation hash is
12906
+ * computed over a MEDIA schema, so one is synthesized from the set — the one
12907
+ * definition the CLI's upload (`videos:upload-remote`) and the Studio's (the
12908
+ * set's gallery) both use, because two copies are two hashes for the same
12909
+ * file. `stream` is left out: it says what is done to an upload on its way
12910
+ * in, not whether the bytes that arrived are valid.
12911
+ */
12912
+ function videosetEntryVideoSchema(set) {
12913
+ return {
12914
+ type: "video",
12915
+ opt: false,
12916
+ options: _objectSpread2(_objectSpread2({}, set.accept ? {
12917
+ accept: set.accept
12918
+ } : {}), set.dir ? {
12919
+ dir: set.dir
12920
+ } : {})
12921
+ };
12922
+ }
12923
+
11519
12924
  // import type { F } from "ts-toolbelt";
11520
12925
  // import { i18n, I18n } from "./schema/future/i18n";
11521
12926
  // import { oneOf } from "./schema/future/oneOf";
@@ -11523,7 +12928,37 @@ var imageset = function imageset(options) {
11523
12928
  // export type InitSchemaLocalized<Locales extends readonly string[]> = {
11524
12929
  // readonly i18n: I18n<Locales>;
11525
12930
  // };
11526
- function initSchema() {
12931
+
12932
+ function initSchema(options) {
12933
+ var media = (options === null || options === void 0 ? void 0 : options.remote) === true ? {
12934
+ image: remoteImage,
12935
+ file: remoteFile,
12936
+ font: remoteFont,
12937
+ video: remoteVideo,
12938
+ richtext: remoteRichtext,
12939
+ imageset: function (options) {
12940
+ return imageset(options).remote();
12941
+ },
12942
+ fileset: function (options) {
12943
+ return fileset(options).remote();
12944
+ },
12945
+ fontset: function (options) {
12946
+ return fontset(options).remote();
12947
+ },
12948
+ videoset: function (options) {
12949
+ return videoset(options).remote();
12950
+ }
12951
+ } : {
12952
+ image: image,
12953
+ file: file,
12954
+ font: font,
12955
+ video: video,
12956
+ richtext: richtext,
12957
+ imageset: imageset,
12958
+ fileset: fileset,
12959
+ fontset: fontset,
12960
+ videoset: videoset
12961
+ };
11527
12962
  // locales: F.Narrow<Locales>
11528
12963
  return {
11529
12964
  string: string,
@@ -11535,14 +12970,17 @@ function initSchema() {
11535
12970
  "enum": enumSchema,
11536
12971
  union: union,
11537
12972
  // oneOf,
11538
- richtext: richtext,
11539
- image: image,
12973
+ richtext: media.richtext,
12974
+ image: media.image,
11540
12975
  literal: literal,
11541
12976
  keyOf: keyOf,
11542
12977
  view: view,
11543
12978
  record: record,
11544
- file: file,
11545
- fileset: fileset,
12979
+ file: media.file,
12980
+ font: media.font,
12981
+ video: media.video,
12982
+ fileset: media.fileset,
12983
+ fontset: media.fontset,
11546
12984
  date: date,
11547
12985
  datetime: datetime,
11548
12986
  color: color,
@@ -11550,7 +12988,8 @@ function initSchema() {
11550
12988
  locale: locale,
11551
12989
  route: route,
11552
12990
  router: router,
11553
- imageset: imageset,
12991
+ imageset: media.imageset,
12992
+ videoset: media.videoset,
11554
12993
  settings: settings
11555
12994
  // i18n: i18n(locales),
11556
12995
  };
@@ -11568,8 +13007,11 @@ function initSchema() {
11568
13007
 
11569
13008
  // TODO: Rename to createValSystem (only to be used by internal things), we can then export * from '@valbuild/core' in the next package then.
11570
13009
  var initVal = function initVal(config) {
13010
+ var _config$files;
11571
13011
  // const locales = options?.locales;
11572
- var s = initSchema();
13012
+ var s = initSchema({
13013
+ remote: (config === null || config === void 0 || (_config$files = config.files) === null || _config$files === void 0 ? void 0 : _config$files.remote) === true
13014
+ });
11573
13015
  // if (locales?.required) {
11574
13016
  // console.error("Locales / i18n currently not implemented");
11575
13017
  // return {
@@ -12670,7 +14112,7 @@ function _extractValModules() {
12670
14112
  * instead would reintroduce exactly the silent skip described above.
12671
14113
  */
12672
14114
  function hasRemoteFileSchema(schema) {
12673
- if (schema.type === "file" || schema.type === "image") {
14115
+ if (schema.type === "file" || schema.type === "image" || schema.type === "video") {
12674
14116
  return !!schema.remote;
12675
14117
  } else if (schema.type === "richtext") {
12676
14118
  var _schema$options;
@@ -12749,7 +14191,7 @@ function hasRemoteFileSchema(schema) {
12749
14191
  * already has a data-loss incident attached to it (see above).
12750
14192
  */
12751
14193
  function hasMediaSchema(schema) {
12752
- if (schema.type === "file" || schema.type === "image") {
14194
+ if (schema.type === "file" || schema.type === "image" || schema.type === "video") {
12753
14195
  return true;
12754
14196
  } else if (schema.type === "richtext") {
12755
14197
  var _schema$options2;
@@ -13115,6 +14557,10 @@ function getValidationBasis(coreVersion, schema, fileExt, metadata, fileHash) {
13115
14557
  // bytes that arrived are valid. Leaving it in would mean that changing a
13116
14558
  // quality setting re-validates every remote file in the project.
13117
14559
  delete options.encode;
14560
+ // `stream` likewise: it is what the Studio made of an upload (an HLS
14561
+ // directory instead of one file), and every file it made is validated on its
14562
+ // own merits.
14563
+ delete options.stream;
13118
14564
  var schemaValidationBasis = {
13119
14565
  type: schema.type,
13120
14566
  opt: schema.opt,
@@ -13134,6 +14580,93 @@ function getFileHash(text) {
13134
14580
  return hashToRemoteFileHash(getSHA256Hash(new Uint8Array(text)));
13135
14581
  }
13136
14582
 
14583
+ var cache$1 = new WeakMap();
14584
+
14585
+ /**
14586
+ * The PUBLISHED entries of every gallery a module's fields pick from — an
14587
+ * `s.image(galleryVal)`, `s.file(filesVal)` or `s.video(videosVal)` — by the
14588
+ * gallery's module path.
14589
+ *
14590
+ * A gallery-backed value names its file and nothing else that is the
14591
+ * gallery's, so a reader has to look the entry up (`fillFromGallery`). In
14592
+ * draft mode the reader has every module's draft source to look in; outside
14593
+ * it, nothing — and the page got a set-backed video with no type, size,
14594
+ * poster or description, and an image with no dimensions. But the field's
14595
+ * schema was built from the gallery module (`s.video(videosVal)` reads it at
14596
+ * definition), so the published entries are already here, on the schema
14597
+ * instance. This is where they are read from.
14598
+ *
14599
+ * Stops at a view: the module it points at is read on its own, with its own
14600
+ * galleries.
14601
+ */
14602
+ function galleriesOf(schema) {
14603
+ if (schema === undefined) {
14604
+ return new Map();
14605
+ }
14606
+ var cached = cache$1.get(schema);
14607
+ if (cached) {
14608
+ return cached;
14609
+ }
14610
+ var galleries = new Map();
14611
+ var seen = new Set();
14612
+ function walk(node) {
14613
+ if (_typeof(node) !== "object" || node === null || seen.has(node)) {
14614
+ return;
14615
+ }
14616
+ seen.add(node);
14617
+ if (node instanceof ValViewSchema) {
14618
+ return;
14619
+ }
14620
+ if (node instanceof ImageSchema) {
14621
+ for (var _i = 0, _Object$entries = Object.entries(node["moduleMetadata"]); _i < _Object$entries.length; _i++) {
14622
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
14623
+ modulePath = _Object$entries$_i[0],
14624
+ entries = _Object$entries$_i[1];
14625
+ galleries.set(modulePath, entries);
14626
+ }
14627
+ return;
14628
+ }
14629
+ if (node instanceof FileSchema) {
14630
+ for (var _i2 = 0, _Object$entries2 = Object.entries(node["moduleMetadata"]); _i2 < _Object$entries2.length; _i2++) {
14631
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
14632
+ _modulePath = _Object$entries2$_i[0],
14633
+ _entries = _Object$entries2$_i[1];
14634
+ galleries.set(_modulePath, _entries);
14635
+ }
14636
+ return;
14637
+ }
14638
+ if (node instanceof VideoSchema) {
14639
+ var gallery = node["gallery"];
14640
+ if (gallery && gallery.entries !== null) {
14641
+ galleries.set(gallery.modulePath, gallery.entries);
14642
+ }
14643
+ return;
14644
+ }
14645
+ if (Array.isArray(node)) {
14646
+ var _iterator = result._createForOfIteratorHelper(node),
14647
+ _step;
14648
+ try {
14649
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
14650
+ var item = _step.value;
14651
+ walk(item);
14652
+ }
14653
+ } catch (err) {
14654
+ _iterator.e(err);
14655
+ } finally {
14656
+ _iterator.f();
14657
+ }
14658
+ return;
14659
+ }
14660
+ for (var _i3 = 0, _Object$values = Object.values(node); _i3 < _Object$values.length; _i3++) {
14661
+ var value = _Object$values[_i3];
14662
+ walk(value);
14663
+ }
14664
+ }
14665
+ walk(schema);
14666
+ cache$1.set(schema, galleries);
14667
+ return galleries;
14668
+ }
14669
+
13137
14670
  /**
13138
14671
  * The variant of a discriminated union that a value takes, by its tag.
13139
14672
  *
@@ -13339,7 +14872,7 @@ function deserializeSchema(serialized) {
13339
14872
  return schema;
13340
14873
  }
13341
14874
  function deserializeSchemaImpl(serialized) {
13342
- var _serialized$options, _serialized$options2, _serialized$multiline, _serialized$render, _serialized$accept, _serialized$dir, _serialized$remote, _serialized$jsonValue, _serialized$render2, _serialized$external, _serialized$render3, _serialized$readonly, _serialized$hidden;
14875
+ var _serialized$options, _serialized$options2, _serialized$multiline, _serialized$render, _serialized$accept, _serialized$dir, _serialized$remote, _serialized$jsonValue, _serialized$render2, _serialized$external, _serialized$render3, _serialized$render4, _serialized$remote2, _serialized$readonly, _serialized$hidden;
13343
14876
  switch (serialized.type) {
13344
14877
  case "string":
13345
14878
  return new StringSchema(_objectSpread2(_objectSpread2({}, serialized.options), {}, {
@@ -13398,6 +14931,8 @@ function deserializeSchemaImpl(serialized) {
13398
14931
  accept: (_serialized$accept = serialized.accept) !== null && _serialized$accept !== void 0 ? _serialized$accept : "*/*",
13399
14932
  dir: (_serialized$dir = serialized.dir) !== null && _serialized$dir !== void 0 ? _serialized$dir : "/public/val",
13400
14933
  remote: (_serialized$remote = serialized.remote) !== null && _serialized$remote !== void 0 ? _serialized$remote : false,
14934
+ encode: serialized.encode,
14935
+ stream: serialized.stream,
13401
14936
  altSchema: serialized.alt ? deserializeSchema(serialized.alt) : undefined
13402
14937
  } : undefined, false, false, serialized.description, (_serialized$jsonValue = serialized.jsonValues) !== null && _serialized$jsonValue !== void 0 ? _serialized$jsonValue : false,
13403
14938
  // The preview closure cannot survive serialization; the render can.
@@ -13423,6 +14958,12 @@ function deserializeSchemaImpl(serialized) {
13423
14958
  return new FileSchema(serialized.options, serialized.opt, serialized.remote, [], {}, false, false, serialized.description);
13424
14959
  case "image":
13425
14960
  return new ImageSchema(serialized.options, serialized.opt, serialized.remote, [], {}, false, false, serialized.description);
14961
+ case "video":
14962
+ return new VideoSchema(serialized.options, serialized.opt, serialized.remote, [], false, false, serialized.description, (_serialized$render4 = serialized.render) !== null && _serialized$render4 !== void 0 ? _serialized$render4 : null, null, serialized.referencedModule ? {
14963
+ modulePath: serialized.referencedModule,
14964
+ entries: null,
14965
+ remote: (_serialized$remote2 = serialized.remote) !== null && _serialized$remote2 !== void 0 ? _serialized$remote2 : false
14966
+ } : null);
13426
14967
  case "date":
13427
14968
  return new DateSchema(serialized.options, serialized.opt, [], false, false, serialized.description);
13428
14969
  case "dateTime":
@@ -14118,7 +15659,7 @@ function isPageRouter(routerId) {
14118
15659
 
14119
15660
  var corePackageJson = {
14120
15661
  name: "@valbuild/core",
14121
- version: "0.139.1",
15662
+ version: "0.143.0",
14122
15663
  "private": false,
14123
15664
  description: "Val - supercharged hard-coded content",
14124
15665
  repository: {
@@ -14207,6 +15748,7 @@ var Internal = {
14207
15748
  VERSION: {
14208
15749
  core: corePackageJson.version
14209
15750
  },
15751
+ videosetEntryVideoSchema: videosetEntryVideoSchema,
14210
15752
  // A view's read-path plumbing: `Internal` rather than the public surface,
14211
15753
  // because an app never builds or unwraps a handle — it passes one to
14212
15754
  // `useVal`.
@@ -14217,9 +15759,15 @@ var Internal = {
14217
15759
  viewModulesOf: viewModulesOf,
14218
15760
  mediaUrl: mediaUrl,
14219
15761
  resolveMedia: resolveMedia,
15762
+ resolveVideo: resolveVideo,
14220
15763
  isRemoteMediaPath: isRemoteMediaPath,
14221
15764
  media: {
14222
- fillFromGallery: fillFromGallery
15765
+ fillFromGallery: fillFromGallery,
15766
+ galleriesOf: galleriesOf,
15767
+ galleryEntryOf: galleryEntryOf,
15768
+ GALLERY_DEFAULT_GROUPS: GALLERY_DEFAULT_GROUPS,
15769
+ isHlsVideo: isHlsVideo,
15770
+ HLS_MIME_TYPE: HLS_MIME_TYPE
14223
15771
  },
14224
15772
  getSchema: getSchema,
14225
15773
  getValPath: getValPath,
@@ -14267,6 +15815,13 @@ var Internal = {
14267
15815
  isExternalFileRef: isExternalFileRef,
14268
15816
  createValPathOfItem: createValPathOfItem,
14269
15817
  getSHA256Hash: getSHA256Hash,
15818
+ /**
15819
+ * The same hash as `getSHA256Hash`, fed a chunk at a time: what a caller
15820
+ * hashing hundreds of megabytes uses to give the thread back between chunks.
15821
+ */
15822
+ createSHA256Hash: function createSHA256Hash() {
15823
+ return createHash("sha256");
15824
+ },
14270
15825
  initSchema: initSchema,
14271
15826
  getMimeType: getMimeType,
14272
15827
  mimeTypeToFileExt: mimeTypeToFileExt,
@@ -14274,6 +15829,9 @@ var Internal = {
14274
15829
  mimeTypeMatchesAccept: mimeTypeMatchesAccept,
14275
15830
  EXT_TO_MIME_TYPES: EXT_TO_MIME_TYPES,
14276
15831
  MIME_TYPES_TO_EXT: MIME_TYPES_TO_EXT,
15832
+ FONT_MIME_TYPE_EXTENSIONS: FONT_MIME_TYPE_EXTENSIONS,
15833
+ isFontMimeType: isFontMimeType,
15834
+ sniffFontMimeType: sniffFontMimeType,
14277
15835
  ModuleFilePathSep: ModuleFilePathSep,
14278
15836
  notFileOp: function notFileOp(op) {
14279
15837
  return op.op !== "file";
@@ -14335,13 +15893,18 @@ exports.ColorSchema = ColorSchema;
14335
15893
  exports.DEFAULT_APP_HOST = DEFAULT_APP_HOST;
14336
15894
  exports.DEFAULT_COLOR_FORMAT = DEFAULT_COLOR_FORMAT;
14337
15895
  exports.DEFAULT_CONTENT_HOST = DEFAULT_CONTENT_HOST;
15896
+ exports.DEFAULT_FONT_ACCEPT = DEFAULT_FONT_ACCEPT;
14338
15897
  exports.DEFAULT_STATIC_HOST = DEFAULT_STATIC_HOST;
14339
15898
  exports.DEFAULT_VAL_REMOTE_HOST = DEFAULT_VAL_REMOTE_HOST;
15899
+ exports.DEFAULT_VIDEO_ACCEPT = DEFAULT_VIDEO_ACCEPT;
15900
+ exports.DEFAULT_VIDEO_RENDITIONS = DEFAULT_VIDEO_RENDITIONS;
15901
+ exports.DEFAULT_VIDEO_SEGMENT_DURATION = DEFAULT_VIDEO_SEGMENT_DURATION;
14340
15902
  exports.DateSchema = DateSchema;
14341
15903
  exports.DateTimeSchema = DateTimeSchema;
14342
15904
  exports.DiscriminatedUnionSchema = DiscriminatedUnionSchema;
14343
15905
  exports.EnumSchema = EnumSchema;
14344
15906
  exports.FATAL_ERROR_TYPES = FATAL_ERROR_TYPES;
15907
+ exports.FONT_MIME_TYPES = FONT_MIME_TYPES;
14345
15908
  exports.FileSchema = FileSchema;
14346
15909
  exports.GenericSelector = GenericSelector;
14347
15910
  exports.ImageSchema = ImageSchema;
@@ -14366,6 +15929,7 @@ exports.THEME_RADIUS_STEPS = THEME_RADIUS_STEPS;
14366
15929
  exports.UNSAFE_URL_SCHEMES = UNSAFE_URL_SCHEMES;
14367
15930
  exports.VAL_EXTENSION = VAL_EXTENSION;
14368
15931
  exports.ValViewSchema = ValViewSchema;
15932
+ exports.VideoSchema = VideoSchema;
14369
15933
  exports._classCallCheck = _classCallCheck;
14370
15934
  exports._createClass = _createClass;
14371
15935
  exports._slicedToArray = _slicedToArray;