@moku-labs/web 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,13 +5,14 @@ import { appendFileSync, existsSync, readFileSync, readdirSync, realpathSync, st
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { homedir, networkInterfaces, tmpdir } from "node:os";
8
- import path from "node:path";
8
+ import minpath, { default as path } from "node:path";
9
9
  import { Feed } from "feed";
10
10
  import { h } from "preact";
11
11
  import { renderToString } from "preact-render-to-string";
12
12
  import { execFileSync } from "node:child_process";
13
13
  import { createInterface } from "node:readline";
14
- import { fileURLToPath } from "node:url";
14
+ import { fileURLToPath, fileURLToPath as urlToPath } from "node:url";
15
+ import minproc from "node:process";
15
16
  import matter from "gray-matter";
16
17
  import rehypeShiki from "@shikijs/rehype";
17
18
  import rehypeSanitize from "rehype-sanitize";
@@ -1570,14 +1571,15 @@ function validateContentConfig(config) {
1570
1571
  }
1571
1572
  /**
1572
1573
  * Validates the `fileSystemContent` provider options (fail-fast at provider
1573
- * construction). Throws when `mermaid` or `embed` is enabled without
1574
- * `trustedContent: true`: both emit raw HTML (inline SVG / the embed facade),
1575
- * which the sanitize pass (the untrusted-content XSS boundary) would strip — so
1576
- * the combination can never work. Errors use the `[web]` prefix.
1574
+ * construction). Throws when `mermaid`, `embed`, or `gallery` is enabled without
1575
+ * `trustedContent: true`: each emits raw HTML (inline SVG / the embed facade /
1576
+ * the gallery markup), which the sanitize pass (the untrusted-content XSS
1577
+ * boundary) would strip — so the combination can never work. Errors use the
1578
+ * `[web]` prefix.
1577
1579
  *
1578
1580
  * @param options - The provider options to validate.
1579
- * @throws {Error} If `mermaid` or `embed` is enabled while `trustedContent` is
1580
- * not `true`.
1581
+ * @throws {Error} If `mermaid`, `embed`, or `gallery` is enabled while
1582
+ * `trustedContent` is not `true`.
1581
1583
  * @example
1582
1584
  * ```ts
1583
1585
  * validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
@@ -1586,6 +1588,7 @@ function validateContentConfig(config) {
1586
1588
  function validateFileSystemContentOptions(options) {
1587
1589
  if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
1588
1590
  if (Boolean(options.embed) && options.trustedContent !== true) throw new Error("[web] content: `embed` requires `trustedContent: true`.\n Embed directives render to a raw-HTML facade, which the sanitize pass would strip\n (and embedding third-party iframes is never safe for untrusted Markdown).\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
1591
+ if (Boolean(options.gallery) && options.trustedContent !== true) throw new Error("[web] content: `gallery` requires `trustedContent: true`.\n Gallery directives render to raw-HTML markup, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
1589
1592
  }
1590
1593
  //#endregion
1591
1594
  //#region src/plugins/content/index.ts
@@ -10623,6 +10626,17 @@ async function performNavigation(pathname, handlers, signal) {
10623
10626
  }
10624
10627
  }
10625
10628
  /**
10629
+ * Whether the user has asked the platform to minimise motion.
10630
+ *
10631
+ * @returns `true` when `(prefers-reduced-motion: reduce)` currently matches; `false` when
10632
+ * it does not, or when `matchMedia` is absent (guards SSR/test environments).
10633
+ * @example
10634
+ * const behavior: ScrollBehavior = prefersReducedMotion() ? "instant" : "smooth";
10635
+ */
10636
+ function prefersReducedMotion() {
10637
+ return typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
10638
+ }
10639
+ /**
10626
10640
  * Run a DOM-mutating swap, optionally wrapped in the View Transitions API when
10627
10641
  * enabled and supported (instant swap otherwise — never throws).
10628
10642
  *
@@ -10643,7 +10657,7 @@ async function performNavigation(pathname, handlers, signal) {
10643
10657
  * runSwap(() => current.replaceWith(next), true, () => scrollTo({ top: 0, behavior: "instant" }));
10644
10658
  */
10645
10659
  function runSwap(doSwap, viewTransitions, beforeCapture) {
10646
- const reduced = typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
10660
+ const reduced = prefersReducedMotion();
10647
10661
  const docWithVt = document;
10648
10662
  const canUseViewTransitions = viewTransitions && !reduced && typeof docWithVt.startViewTransition === "function";
10649
10663
  beforeCapture?.();
@@ -10741,7 +10755,7 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
10741
10755
  if (pathWithSearch(url) === pathWithSearch(location)) {
10742
10756
  window.scrollTo({
10743
10757
  top: 0,
10744
- behavior: "smooth"
10758
+ behavior: prefersReducedMotion() ? "instant" : "smooth"
10745
10759
  });
10746
10760
  return;
10747
10761
  }
@@ -10795,7 +10809,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
10795
10809
  navEvent.intercept({ handler: () => {
10796
10810
  window.scrollTo({
10797
10811
  top: 0,
10798
- behavior: "smooth"
10812
+ behavior: prefersReducedMotion() ? "instant" : "smooth"
10799
10813
  });
10800
10814
  return Promise.resolve();
10801
10815
  } });
@@ -10972,26 +10986,30 @@ function createSpaKernel(state, config, emit, deps) {
10972
10986
  let pendingScrollToTop = true;
10973
10987
  /**
10974
10988
  * Apply the in-flight navigation's scroll intent — the swap's `beforeCapture` hook.
10975
- * For a forward nav it scrolls to top BEFORE the snapshot is captured, so the old and
10976
- * new states share scrollY=0 (no delta the sticky header never un-pins) and there is
10977
- * no pre-fetch scroll pause. Traverse (back/forward) sets `pendingScrollToTop = false`
10978
- * and restores its saved position after the swap instead.
10989
+ * For a forward nav it scrolls to top BEFORE the swap (and, with view transitions on,
10990
+ * before the "old" snapshot is captured), so the old and new states share scrollY=0:
10991
+ * no delta for a transition to animate → a `position: sticky` header never un-pins.
10992
+ * Traverse (back/forward) sets `pendingScrollToTop = false` and restores its saved
10993
+ * position after the swap instead.
10979
10994
  *
10980
- * Scroll behaviour: `"instant"` ONLY when view transitions are enabled — that is what
10981
- * keeps scrollY=0 in the captured snapshot (a `scroll-behavior: smooth` would otherwise
10982
- * animate the reset and re-create the delta sticky-header flicker). With view
10983
- * transitions OFF there is no snapshot to protect, so it honours the page's
10984
- * `scroll-behavior` (`"auto"` = use the CSS value, e.g. a smooth scroll-to-top on nav).
10995
+ * The reset is ALWAYS `"instant"`, never the CSS-driven `"auto"`. It runs synchronously
10996
+ * immediately before the swap, and the swap mutates document height (the outgoing page is
10997
+ * usually taller than the incoming one). A smooth scroll from `behavior: "smooth"` or a
10998
+ * page `scroll-behavior: smooth` that `"auto"` would inherit is still animating when that
10999
+ * height change lands; the browser clamps scrollY to the new, smaller maximum and cancels
11000
+ * the in-flight animation there (worst on WebKit), stranding the page near the OLD position
11001
+ * instead of the top. Instant lands scrollY=0 before the swap, every time. (A smooth
11002
+ * scroll-to-top on the SAME page is unaffected — the router's same-page handler animates
11003
+ * it, where there is no swap to race.)
10985
11004
  *
10986
11005
  * @example
10987
11006
  * runSwap(renderAndMount, viewTransitions, applyPendingScroll);
10988
11007
  */
10989
11008
  const applyPendingScroll = () => {
10990
11009
  if (!pendingScrollToTop) return;
10991
- const behavior = resolved.viewTransitions ? "instant" : "auto";
10992
11010
  window.scrollTo({
10993
11011
  top: 0,
10994
- behavior
11012
+ behavior: "instant"
10995
11013
  });
10996
11014
  };
10997
11015
  /**
@@ -11625,6 +11643,857 @@ function cloudflareBindings() {
11625
11643
  };
11626
11644
  }
11627
11645
  //#endregion
11646
+ //#region node_modules/unist-util-stringify-position/lib/index.js
11647
+ /**
11648
+ * @typedef {import('unist').Node} Node
11649
+ * @typedef {import('unist').Point} Point
11650
+ * @typedef {import('unist').Position} Position
11651
+ */
11652
+ /**
11653
+ * @typedef NodeLike
11654
+ * @property {string} type
11655
+ * @property {PositionLike | null | undefined} [position]
11656
+ *
11657
+ * @typedef PointLike
11658
+ * @property {number | null | undefined} [line]
11659
+ * @property {number | null | undefined} [column]
11660
+ * @property {number | null | undefined} [offset]
11661
+ *
11662
+ * @typedef PositionLike
11663
+ * @property {PointLike | null | undefined} [start]
11664
+ * @property {PointLike | null | undefined} [end]
11665
+ */
11666
+ /**
11667
+ * Serialize the positional info of a point, position (start and end points),
11668
+ * or node.
11669
+ *
11670
+ * @param {Node | NodeLike | Point | PointLike | Position | PositionLike | null | undefined} [value]
11671
+ * Node, position, or point.
11672
+ * @returns {string}
11673
+ * Pretty printed positional info of a node (`string`).
11674
+ *
11675
+ * In the format of a range `ls:cs-le:ce` (when given `node` or `position`)
11676
+ * or a point `l:c` (when given `point`), where `l` stands for line, `c` for
11677
+ * column, `s` for `start`, and `e` for end.
11678
+ * An empty string (`''`) is returned if the given value is neither `node`,
11679
+ * `position`, nor `point`.
11680
+ */
11681
+ function stringifyPosition(value) {
11682
+ if (!value || typeof value !== "object") return "";
11683
+ if ("position" in value || "type" in value) return position(value.position);
11684
+ if ("start" in value || "end" in value) return position(value);
11685
+ if ("line" in value || "column" in value) return point(value);
11686
+ return "";
11687
+ }
11688
+ /**
11689
+ * @param {Point | PointLike | null | undefined} point
11690
+ * @returns {string}
11691
+ */
11692
+ function point(point) {
11693
+ return index(point && point.line) + ":" + index(point && point.column);
11694
+ }
11695
+ /**
11696
+ * @param {Position | PositionLike | null | undefined} pos
11697
+ * @returns {string}
11698
+ */
11699
+ function position(pos) {
11700
+ return point(pos && pos.start) + "-" + point(pos && pos.end);
11701
+ }
11702
+ /**
11703
+ * @param {number | null | undefined} value
11704
+ * @returns {number}
11705
+ */
11706
+ function index(value) {
11707
+ return value && typeof value === "number" ? value : 1;
11708
+ }
11709
+ //#endregion
11710
+ //#region node_modules/vfile-message/lib/index.js
11711
+ /**
11712
+ * @import {Node, Point, Position} from 'unist'
11713
+ */
11714
+ /**
11715
+ * @typedef {object & {type: string, position?: Position | undefined}} NodeLike
11716
+ *
11717
+ * @typedef Options
11718
+ * Configuration.
11719
+ * @property {Array<Node> | null | undefined} [ancestors]
11720
+ * Stack of (inclusive) ancestor nodes surrounding the message (optional).
11721
+ * @property {Error | null | undefined} [cause]
11722
+ * Original error cause of the message (optional).
11723
+ * @property {Point | Position | null | undefined} [place]
11724
+ * Place of message (optional).
11725
+ * @property {string | null | undefined} [ruleId]
11726
+ * Category of message (optional, example: `'my-rule'`).
11727
+ * @property {string | null | undefined} [source]
11728
+ * Namespace of who sent the message (optional, example: `'my-package'`).
11729
+ */
11730
+ /**
11731
+ * Message.
11732
+ */
11733
+ var VFileMessage = class extends Error {
11734
+ /**
11735
+ * Create a message for `reason`.
11736
+ *
11737
+ * > 🪦 **Note**: also has obsolete signatures.
11738
+ *
11739
+ * @overload
11740
+ * @param {string} reason
11741
+ * @param {Options | null | undefined} [options]
11742
+ * @returns
11743
+ *
11744
+ * @overload
11745
+ * @param {string} reason
11746
+ * @param {Node | NodeLike | null | undefined} parent
11747
+ * @param {string | null | undefined} [origin]
11748
+ * @returns
11749
+ *
11750
+ * @overload
11751
+ * @param {string} reason
11752
+ * @param {Point | Position | null | undefined} place
11753
+ * @param {string | null | undefined} [origin]
11754
+ * @returns
11755
+ *
11756
+ * @overload
11757
+ * @param {string} reason
11758
+ * @param {string | null | undefined} [origin]
11759
+ * @returns
11760
+ *
11761
+ * @overload
11762
+ * @param {Error | VFileMessage} cause
11763
+ * @param {Node | NodeLike | null | undefined} parent
11764
+ * @param {string | null | undefined} [origin]
11765
+ * @returns
11766
+ *
11767
+ * @overload
11768
+ * @param {Error | VFileMessage} cause
11769
+ * @param {Point | Position | null | undefined} place
11770
+ * @param {string | null | undefined} [origin]
11771
+ * @returns
11772
+ *
11773
+ * @overload
11774
+ * @param {Error | VFileMessage} cause
11775
+ * @param {string | null | undefined} [origin]
11776
+ * @returns
11777
+ *
11778
+ * @param {Error | VFileMessage | string} causeOrReason
11779
+ * Reason for message, should use markdown.
11780
+ * @param {Node | NodeLike | Options | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
11781
+ * Configuration (optional).
11782
+ * @param {string | null | undefined} [origin]
11783
+ * Place in code where the message originates (example:
11784
+ * `'my-package:my-rule'` or `'my-rule'`).
11785
+ * @returns
11786
+ * Instance of `VFileMessage`.
11787
+ */
11788
+ constructor(causeOrReason, optionsOrParentOrPlace, origin) {
11789
+ super();
11790
+ if (typeof optionsOrParentOrPlace === "string") {
11791
+ origin = optionsOrParentOrPlace;
11792
+ optionsOrParentOrPlace = void 0;
11793
+ }
11794
+ /** @type {string} */
11795
+ let reason = "";
11796
+ /** @type {Options} */
11797
+ let options = {};
11798
+ let legacyCause = false;
11799
+ if (optionsOrParentOrPlace) if ("line" in optionsOrParentOrPlace && "column" in optionsOrParentOrPlace) options = { place: optionsOrParentOrPlace };
11800
+ else if ("start" in optionsOrParentOrPlace && "end" in optionsOrParentOrPlace) options = { place: optionsOrParentOrPlace };
11801
+ else if ("type" in optionsOrParentOrPlace) options = {
11802
+ ancestors: [optionsOrParentOrPlace],
11803
+ place: optionsOrParentOrPlace.position
11804
+ };
11805
+ else options = { ...optionsOrParentOrPlace };
11806
+ if (typeof causeOrReason === "string") reason = causeOrReason;
11807
+ else if (!options.cause && causeOrReason) {
11808
+ legacyCause = true;
11809
+ reason = causeOrReason.message;
11810
+ options.cause = causeOrReason;
11811
+ }
11812
+ if (!options.ruleId && !options.source && typeof origin === "string") {
11813
+ const index = origin.indexOf(":");
11814
+ if (index === -1) options.ruleId = origin;
11815
+ else {
11816
+ options.source = origin.slice(0, index);
11817
+ options.ruleId = origin.slice(index + 1);
11818
+ }
11819
+ }
11820
+ if (!options.place && options.ancestors && options.ancestors) {
11821
+ const parent = options.ancestors[options.ancestors.length - 1];
11822
+ if (parent) options.place = parent.position;
11823
+ }
11824
+ const start = options.place && "start" in options.place ? options.place.start : options.place;
11825
+ /**
11826
+ * Stack of ancestor nodes surrounding the message.
11827
+ *
11828
+ * @type {Array<Node> | undefined}
11829
+ */
11830
+ this.ancestors = options.ancestors || void 0;
11831
+ /**
11832
+ * Original error cause of the message.
11833
+ *
11834
+ * @type {Error | undefined}
11835
+ */
11836
+ this.cause = options.cause || void 0;
11837
+ /**
11838
+ * Starting column of message.
11839
+ *
11840
+ * @type {number | undefined}
11841
+ */
11842
+ this.column = start ? start.column : void 0;
11843
+ /**
11844
+ * State of problem.
11845
+ *
11846
+ * * `true` — error, file not usable
11847
+ * * `false` — warning, change may be needed
11848
+ * * `undefined` — change likely not needed
11849
+ *
11850
+ * @type {boolean | null | undefined}
11851
+ */
11852
+ this.fatal = void 0;
11853
+ /**
11854
+ * Path of a file (used throughout the `VFile` ecosystem).
11855
+ *
11856
+ * @type {string | undefined}
11857
+ */
11858
+ this.file = "";
11859
+ /**
11860
+ * Reason for message.
11861
+ *
11862
+ * @type {string}
11863
+ */
11864
+ this.message = reason;
11865
+ /**
11866
+ * Starting line of error.
11867
+ *
11868
+ * @type {number | undefined}
11869
+ */
11870
+ this.line = start ? start.line : void 0;
11871
+ /**
11872
+ * Serialized positional info of message.
11873
+ *
11874
+ * On normal errors, this would be something like `ParseError`, buit in
11875
+ * `VFile` messages we use this space to show where an error happened.
11876
+ */
11877
+ this.name = stringifyPosition(options.place) || "1:1";
11878
+ /**
11879
+ * Place of message.
11880
+ *
11881
+ * @type {Point | Position | undefined}
11882
+ */
11883
+ this.place = options.place || void 0;
11884
+ /**
11885
+ * Reason for message, should use markdown.
11886
+ *
11887
+ * @type {string}
11888
+ */
11889
+ this.reason = this.message;
11890
+ /**
11891
+ * Category of message (example: `'my-rule'`).
11892
+ *
11893
+ * @type {string | undefined}
11894
+ */
11895
+ this.ruleId = options.ruleId || void 0;
11896
+ /**
11897
+ * Namespace of message (example: `'my-package'`).
11898
+ *
11899
+ * @type {string | undefined}
11900
+ */
11901
+ this.source = options.source || void 0;
11902
+ /**
11903
+ * Stack of message.
11904
+ *
11905
+ * This is used by normal errors to show where something happened in
11906
+ * programming code, irrelevant for `VFile` messages,
11907
+ *
11908
+ * @type {string}
11909
+ */
11910
+ this.stack = legacyCause && options.cause && typeof options.cause.stack === "string" ? options.cause.stack : "";
11911
+ /**
11912
+ * Specify the source value that’s being reported, which is deemed
11913
+ * incorrect.
11914
+ *
11915
+ * @type {string | undefined}
11916
+ */
11917
+ this.actual = void 0;
11918
+ /**
11919
+ * Suggest acceptable values that can be used instead of `actual`.
11920
+ *
11921
+ * @type {Array<string> | undefined}
11922
+ */
11923
+ this.expected = void 0;
11924
+ /**
11925
+ * Long form description of the message (you should use markdown).
11926
+ *
11927
+ * @type {string | undefined}
11928
+ */
11929
+ this.note = void 0;
11930
+ /**
11931
+ * Link to docs for the message.
11932
+ *
11933
+ * > 👉 **Note**: this must be an absolute URL that can be passed as `x`
11934
+ * > to `new URL(x)`.
11935
+ *
11936
+ * @type {string | undefined}
11937
+ */
11938
+ this.url = void 0;
11939
+ }
11940
+ };
11941
+ VFileMessage.prototype.file = "";
11942
+ VFileMessage.prototype.name = "";
11943
+ VFileMessage.prototype.reason = "";
11944
+ VFileMessage.prototype.message = "";
11945
+ VFileMessage.prototype.stack = "";
11946
+ VFileMessage.prototype.column = void 0;
11947
+ VFileMessage.prototype.line = void 0;
11948
+ VFileMessage.prototype.ancestors = void 0;
11949
+ VFileMessage.prototype.cause = void 0;
11950
+ VFileMessage.prototype.fatal = void 0;
11951
+ VFileMessage.prototype.place = void 0;
11952
+ VFileMessage.prototype.ruleId = void 0;
11953
+ VFileMessage.prototype.source = void 0;
11954
+ //#endregion
11955
+ //#region node_modules/vfile/lib/minurl.shared.js
11956
+ /**
11957
+ * Checks if a value has the shape of a WHATWG URL object.
11958
+ *
11959
+ * Using a symbol or instanceof would not be able to recognize URL objects
11960
+ * coming from other implementations (e.g. in Electron), so instead we are
11961
+ * checking some well known properties for a lack of a better test.
11962
+ *
11963
+ * We use `href` and `protocol` as they are the only properties that are
11964
+ * easy to retrieve and calculate due to the lazy nature of the getters.
11965
+ *
11966
+ * We check for auth attribute to distinguish legacy url instance with
11967
+ * WHATWG URL instance.
11968
+ *
11969
+ * @param {unknown} fileUrlOrPath
11970
+ * File path or URL.
11971
+ * @returns {fileUrlOrPath is URL}
11972
+ * Whether it’s a URL.
11973
+ */
11974
+ function isUrl(fileUrlOrPath) {
11975
+ return Boolean(fileUrlOrPath !== null && typeof fileUrlOrPath === "object" && "href" in fileUrlOrPath && fileUrlOrPath.href && "protocol" in fileUrlOrPath && fileUrlOrPath.protocol && fileUrlOrPath.auth === void 0);
11976
+ }
11977
+ //#endregion
11978
+ //#region node_modules/vfile/lib/index.js
11979
+ /**
11980
+ * @import {Node, Point, Position} from 'unist'
11981
+ * @import {Options as MessageOptions} from 'vfile-message'
11982
+ * @import {Compatible, Data, Map, Options, Value} from 'vfile'
11983
+ */
11984
+ /**
11985
+ * @typedef {object & {type: string, position?: Position | undefined}} NodeLike
11986
+ */
11987
+ /**
11988
+ * Order of setting (least specific to most), we need this because otherwise
11989
+ * `{stem: 'a', path: '~/b.js'}` would throw, as a path is needed before a
11990
+ * stem can be set.
11991
+ */
11992
+ const order = [
11993
+ "history",
11994
+ "path",
11995
+ "basename",
11996
+ "stem",
11997
+ "extname",
11998
+ "dirname"
11999
+ ];
12000
+ var VFile = class {
12001
+ /**
12002
+ * Create a new virtual file.
12003
+ *
12004
+ * `options` is treated as:
12005
+ *
12006
+ * * `string` or `Uint8Array` — `{value: options}`
12007
+ * * `URL` — `{path: options}`
12008
+ * * `VFile` — shallow copies its data over to the new file
12009
+ * * `object` — all fields are shallow copied over to the new file
12010
+ *
12011
+ * Path related fields are set in the following order (least specific to
12012
+ * most specific): `history`, `path`, `basename`, `stem`, `extname`,
12013
+ * `dirname`.
12014
+ *
12015
+ * You cannot set `dirname` or `extname` without setting either `history`,
12016
+ * `path`, `basename`, or `stem` too.
12017
+ *
12018
+ * @param {Compatible | null | undefined} [value]
12019
+ * File value.
12020
+ * @returns
12021
+ * New instance.
12022
+ */
12023
+ constructor(value) {
12024
+ /** @type {Options | VFile} */
12025
+ let options;
12026
+ if (!value) options = {};
12027
+ else if (isUrl(value)) options = { path: value };
12028
+ else if (typeof value === "string" || isUint8Array(value)) options = { value };
12029
+ else options = value;
12030
+ /**
12031
+ * Base of `path` (default: `process.cwd()` or `'/'` in browsers).
12032
+ *
12033
+ * @type {string}
12034
+ */
12035
+ this.cwd = "cwd" in options ? "" : minproc.cwd();
12036
+ /**
12037
+ * Place to store custom info (default: `{}`).
12038
+ *
12039
+ * It’s OK to store custom data directly on the file but moving it to
12040
+ * `data` is recommended.
12041
+ *
12042
+ * @type {Data}
12043
+ */
12044
+ this.data = {};
12045
+ /**
12046
+ * List of file paths the file moved between.
12047
+ *
12048
+ * The first is the original path and the last is the current path.
12049
+ *
12050
+ * @type {Array<string>}
12051
+ */
12052
+ this.history = [];
12053
+ /**
12054
+ * List of messages associated with the file.
12055
+ *
12056
+ * @type {Array<VFileMessage>}
12057
+ */
12058
+ this.messages = [];
12059
+ /**
12060
+ * Raw value.
12061
+ *
12062
+ * @type {Value}
12063
+ */
12064
+ this.value;
12065
+ /**
12066
+ * Source map.
12067
+ *
12068
+ * This type is equivalent to the `RawSourceMap` type from the `source-map`
12069
+ * module.
12070
+ *
12071
+ * @type {Map | null | undefined}
12072
+ */
12073
+ this.map;
12074
+ /**
12075
+ * Custom, non-string, compiled, representation.
12076
+ *
12077
+ * This is used by unified to store non-string results.
12078
+ * One example is when turning markdown into React nodes.
12079
+ *
12080
+ * @type {unknown}
12081
+ */
12082
+ this.result;
12083
+ /**
12084
+ * Whether a file was saved to disk.
12085
+ *
12086
+ * This is used by vfile reporters.
12087
+ *
12088
+ * @type {boolean}
12089
+ */
12090
+ this.stored;
12091
+ let index = -1;
12092
+ while (++index < order.length) {
12093
+ const field = order[index];
12094
+ if (field in options && options[field] !== void 0 && options[field] !== null) this[field] = field === "history" ? [...options[field]] : options[field];
12095
+ }
12096
+ /** @type {string} */
12097
+ let field;
12098
+ for (field in options) if (!order.includes(field)) this[field] = options[field];
12099
+ }
12100
+ /**
12101
+ * Get the basename (including extname) (example: `'index.min.js'`).
12102
+ *
12103
+ * @returns {string | undefined}
12104
+ * Basename.
12105
+ */
12106
+ get basename() {
12107
+ return typeof this.path === "string" ? minpath.basename(this.path) : void 0;
12108
+ }
12109
+ /**
12110
+ * Set basename (including extname) (`'index.min.js'`).
12111
+ *
12112
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12113
+ * on windows).
12114
+ * Cannot be nullified (use `file.path = file.dirname` instead).
12115
+ *
12116
+ * @param {string} basename
12117
+ * Basename.
12118
+ * @returns {undefined}
12119
+ * Nothing.
12120
+ */
12121
+ set basename(basename) {
12122
+ assertNonEmpty(basename, "basename");
12123
+ assertPart(basename, "basename");
12124
+ this.path = minpath.join(this.dirname || "", basename);
12125
+ }
12126
+ /**
12127
+ * Get the parent path (example: `'~'`).
12128
+ *
12129
+ * @returns {string | undefined}
12130
+ * Dirname.
12131
+ */
12132
+ get dirname() {
12133
+ return typeof this.path === "string" ? minpath.dirname(this.path) : void 0;
12134
+ }
12135
+ /**
12136
+ * Set the parent path (example: `'~'`).
12137
+ *
12138
+ * Cannot be set if there’s no `path` yet.
12139
+ *
12140
+ * @param {string | undefined} dirname
12141
+ * Dirname.
12142
+ * @returns {undefined}
12143
+ * Nothing.
12144
+ */
12145
+ set dirname(dirname) {
12146
+ assertPath(this.basename, "dirname");
12147
+ this.path = minpath.join(dirname || "", this.basename);
12148
+ }
12149
+ /**
12150
+ * Get the extname (including dot) (example: `'.js'`).
12151
+ *
12152
+ * @returns {string | undefined}
12153
+ * Extname.
12154
+ */
12155
+ get extname() {
12156
+ return typeof this.path === "string" ? minpath.extname(this.path) : void 0;
12157
+ }
12158
+ /**
12159
+ * Set the extname (including dot) (example: `'.js'`).
12160
+ *
12161
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12162
+ * on windows).
12163
+ * Cannot be set if there’s no `path` yet.
12164
+ *
12165
+ * @param {string | undefined} extname
12166
+ * Extname.
12167
+ * @returns {undefined}
12168
+ * Nothing.
12169
+ */
12170
+ set extname(extname) {
12171
+ assertPart(extname, "extname");
12172
+ assertPath(this.dirname, "extname");
12173
+ if (extname) {
12174
+ if (extname.codePointAt(0) !== 46) throw new Error("`extname` must start with `.`");
12175
+ if (extname.includes(".", 1)) throw new Error("`extname` cannot contain multiple dots");
12176
+ }
12177
+ this.path = minpath.join(this.dirname, this.stem + (extname || ""));
12178
+ }
12179
+ /**
12180
+ * Get the full path (example: `'~/index.min.js'`).
12181
+ *
12182
+ * @returns {string}
12183
+ * Path.
12184
+ */
12185
+ get path() {
12186
+ return this.history[this.history.length - 1];
12187
+ }
12188
+ /**
12189
+ * Set the full path (example: `'~/index.min.js'`).
12190
+ *
12191
+ * Cannot be nullified.
12192
+ * You can set a file URL (a `URL` object with a `file:` protocol) which will
12193
+ * be turned into a path with `url.fileURLToPath`.
12194
+ *
12195
+ * @param {URL | string} path
12196
+ * Path.
12197
+ * @returns {undefined}
12198
+ * Nothing.
12199
+ */
12200
+ set path(path) {
12201
+ if (isUrl(path)) path = urlToPath(path);
12202
+ assertNonEmpty(path, "path");
12203
+ if (this.path !== path) this.history.push(path);
12204
+ }
12205
+ /**
12206
+ * Get the stem (basename w/o extname) (example: `'index.min'`).
12207
+ *
12208
+ * @returns {string | undefined}
12209
+ * Stem.
12210
+ */
12211
+ get stem() {
12212
+ return typeof this.path === "string" ? minpath.basename(this.path, this.extname) : void 0;
12213
+ }
12214
+ /**
12215
+ * Set the stem (basename w/o extname) (example: `'index.min'`).
12216
+ *
12217
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12218
+ * on windows).
12219
+ * Cannot be nullified (use `file.path = file.dirname` instead).
12220
+ *
12221
+ * @param {string} stem
12222
+ * Stem.
12223
+ * @returns {undefined}
12224
+ * Nothing.
12225
+ */
12226
+ set stem(stem) {
12227
+ assertNonEmpty(stem, "stem");
12228
+ assertPart(stem, "stem");
12229
+ this.path = minpath.join(this.dirname || "", stem + (this.extname || ""));
12230
+ }
12231
+ /**
12232
+ * Create a fatal message for `reason` associated with the file.
12233
+ *
12234
+ * The `fatal` field of the message is set to `true` (error; file not usable)
12235
+ * and the `file` field is set to the current file path.
12236
+ * The message is added to the `messages` field on `file`.
12237
+ *
12238
+ * > 🪦 **Note**: also has obsolete signatures.
12239
+ *
12240
+ * @overload
12241
+ * @param {string} reason
12242
+ * @param {MessageOptions | null | undefined} [options]
12243
+ * @returns {never}
12244
+ *
12245
+ * @overload
12246
+ * @param {string} reason
12247
+ * @param {Node | NodeLike | null | undefined} parent
12248
+ * @param {string | null | undefined} [origin]
12249
+ * @returns {never}
12250
+ *
12251
+ * @overload
12252
+ * @param {string} reason
12253
+ * @param {Point | Position | null | undefined} place
12254
+ * @param {string | null | undefined} [origin]
12255
+ * @returns {never}
12256
+ *
12257
+ * @overload
12258
+ * @param {string} reason
12259
+ * @param {string | null | undefined} [origin]
12260
+ * @returns {never}
12261
+ *
12262
+ * @overload
12263
+ * @param {Error | VFileMessage} cause
12264
+ * @param {Node | NodeLike | null | undefined} parent
12265
+ * @param {string | null | undefined} [origin]
12266
+ * @returns {never}
12267
+ *
12268
+ * @overload
12269
+ * @param {Error | VFileMessage} cause
12270
+ * @param {Point | Position | null | undefined} place
12271
+ * @param {string | null | undefined} [origin]
12272
+ * @returns {never}
12273
+ *
12274
+ * @overload
12275
+ * @param {Error | VFileMessage} cause
12276
+ * @param {string | null | undefined} [origin]
12277
+ * @returns {never}
12278
+ *
12279
+ * @param {Error | VFileMessage | string} causeOrReason
12280
+ * Reason for message, should use markdown.
12281
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12282
+ * Configuration (optional).
12283
+ * @param {string | null | undefined} [origin]
12284
+ * Place in code where the message originates (example:
12285
+ * `'my-package:my-rule'` or `'my-rule'`).
12286
+ * @returns {never}
12287
+ * Never.
12288
+ * @throws {VFileMessage}
12289
+ * Message.
12290
+ */
12291
+ fail(causeOrReason, optionsOrParentOrPlace, origin) {
12292
+ const message = this.message(causeOrReason, optionsOrParentOrPlace, origin);
12293
+ message.fatal = true;
12294
+ throw message;
12295
+ }
12296
+ /**
12297
+ * Create an info message for `reason` associated with the file.
12298
+ *
12299
+ * The `fatal` field of the message is set to `undefined` (info; change
12300
+ * likely not needed) and the `file` field is set to the current file path.
12301
+ * The message is added to the `messages` field on `file`.
12302
+ *
12303
+ * > 🪦 **Note**: also has obsolete signatures.
12304
+ *
12305
+ * @overload
12306
+ * @param {string} reason
12307
+ * @param {MessageOptions | null | undefined} [options]
12308
+ * @returns {VFileMessage}
12309
+ *
12310
+ * @overload
12311
+ * @param {string} reason
12312
+ * @param {Node | NodeLike | null | undefined} parent
12313
+ * @param {string | null | undefined} [origin]
12314
+ * @returns {VFileMessage}
12315
+ *
12316
+ * @overload
12317
+ * @param {string} reason
12318
+ * @param {Point | Position | null | undefined} place
12319
+ * @param {string | null | undefined} [origin]
12320
+ * @returns {VFileMessage}
12321
+ *
12322
+ * @overload
12323
+ * @param {string} reason
12324
+ * @param {string | null | undefined} [origin]
12325
+ * @returns {VFileMessage}
12326
+ *
12327
+ * @overload
12328
+ * @param {Error | VFileMessage} cause
12329
+ * @param {Node | NodeLike | null | undefined} parent
12330
+ * @param {string | null | undefined} [origin]
12331
+ * @returns {VFileMessage}
12332
+ *
12333
+ * @overload
12334
+ * @param {Error | VFileMessage} cause
12335
+ * @param {Point | Position | null | undefined} place
12336
+ * @param {string | null | undefined} [origin]
12337
+ * @returns {VFileMessage}
12338
+ *
12339
+ * @overload
12340
+ * @param {Error | VFileMessage} cause
12341
+ * @param {string | null | undefined} [origin]
12342
+ * @returns {VFileMessage}
12343
+ *
12344
+ * @param {Error | VFileMessage | string} causeOrReason
12345
+ * Reason for message, should use markdown.
12346
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12347
+ * Configuration (optional).
12348
+ * @param {string | null | undefined} [origin]
12349
+ * Place in code where the message originates (example:
12350
+ * `'my-package:my-rule'` or `'my-rule'`).
12351
+ * @returns {VFileMessage}
12352
+ * Message.
12353
+ */
12354
+ info(causeOrReason, optionsOrParentOrPlace, origin) {
12355
+ const message = this.message(causeOrReason, optionsOrParentOrPlace, origin);
12356
+ message.fatal = void 0;
12357
+ return message;
12358
+ }
12359
+ /**
12360
+ * Create a message for `reason` associated with the file.
12361
+ *
12362
+ * The `fatal` field of the message is set to `false` (warning; change may be
12363
+ * needed) and the `file` field is set to the current file path.
12364
+ * The message is added to the `messages` field on `file`.
12365
+ *
12366
+ * > 🪦 **Note**: also has obsolete signatures.
12367
+ *
12368
+ * @overload
12369
+ * @param {string} reason
12370
+ * @param {MessageOptions | null | undefined} [options]
12371
+ * @returns {VFileMessage}
12372
+ *
12373
+ * @overload
12374
+ * @param {string} reason
12375
+ * @param {Node | NodeLike | null | undefined} parent
12376
+ * @param {string | null | undefined} [origin]
12377
+ * @returns {VFileMessage}
12378
+ *
12379
+ * @overload
12380
+ * @param {string} reason
12381
+ * @param {Point | Position | null | undefined} place
12382
+ * @param {string | null | undefined} [origin]
12383
+ * @returns {VFileMessage}
12384
+ *
12385
+ * @overload
12386
+ * @param {string} reason
12387
+ * @param {string | null | undefined} [origin]
12388
+ * @returns {VFileMessage}
12389
+ *
12390
+ * @overload
12391
+ * @param {Error | VFileMessage} cause
12392
+ * @param {Node | NodeLike | null | undefined} parent
12393
+ * @param {string | null | undefined} [origin]
12394
+ * @returns {VFileMessage}
12395
+ *
12396
+ * @overload
12397
+ * @param {Error | VFileMessage} cause
12398
+ * @param {Point | Position | null | undefined} place
12399
+ * @param {string | null | undefined} [origin]
12400
+ * @returns {VFileMessage}
12401
+ *
12402
+ * @overload
12403
+ * @param {Error | VFileMessage} cause
12404
+ * @param {string | null | undefined} [origin]
12405
+ * @returns {VFileMessage}
12406
+ *
12407
+ * @param {Error | VFileMessage | string} causeOrReason
12408
+ * Reason for message, should use markdown.
12409
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12410
+ * Configuration (optional).
12411
+ * @param {string | null | undefined} [origin]
12412
+ * Place in code where the message originates (example:
12413
+ * `'my-package:my-rule'` or `'my-rule'`).
12414
+ * @returns {VFileMessage}
12415
+ * Message.
12416
+ */
12417
+ message(causeOrReason, optionsOrParentOrPlace, origin) {
12418
+ const message = new VFileMessage(causeOrReason, optionsOrParentOrPlace, origin);
12419
+ if (this.path) {
12420
+ message.name = this.path + ":" + message.name;
12421
+ message.file = this.path;
12422
+ }
12423
+ message.fatal = false;
12424
+ this.messages.push(message);
12425
+ return message;
12426
+ }
12427
+ /**
12428
+ * Serialize the file.
12429
+ *
12430
+ * > **Note**: which encodings are supported depends on the engine.
12431
+ * > For info on Node.js, see:
12432
+ * > <https://nodejs.org/api/util.html#whatwg-supported-encodings>.
12433
+ *
12434
+ * @param {string | null | undefined} [encoding='utf8']
12435
+ * Character encoding to understand `value` as when it’s a `Uint8Array`
12436
+ * (default: `'utf-8'`).
12437
+ * @returns {string}
12438
+ * Serialized file.
12439
+ */
12440
+ toString(encoding) {
12441
+ if (this.value === void 0) return "";
12442
+ if (typeof this.value === "string") return this.value;
12443
+ return new TextDecoder(encoding || void 0).decode(this.value);
12444
+ }
12445
+ };
12446
+ /**
12447
+ * Assert that `part` is not a path (as in, does not contain `path.sep`).
12448
+ *
12449
+ * @param {string | null | undefined} part
12450
+ * File path part.
12451
+ * @param {string} name
12452
+ * Part name.
12453
+ * @returns {undefined}
12454
+ * Nothing.
12455
+ */
12456
+ function assertPart(part, name) {
12457
+ if (part && part.includes(minpath.sep)) throw new Error("`" + name + "` cannot be a path: did not expect `" + minpath.sep + "`");
12458
+ }
12459
+ /**
12460
+ * Assert that `part` is not empty.
12461
+ *
12462
+ * @param {string | undefined} part
12463
+ * Thing.
12464
+ * @param {string} name
12465
+ * Part name.
12466
+ * @returns {asserts part is string}
12467
+ * Nothing.
12468
+ */
12469
+ function assertNonEmpty(part, name) {
12470
+ if (!part) throw new Error("`" + name + "` cannot be empty");
12471
+ }
12472
+ /**
12473
+ * Assert `path` exists.
12474
+ *
12475
+ * @param {string | undefined} path
12476
+ * Path.
12477
+ * @param {string} name
12478
+ * Dependency name.
12479
+ * @returns {asserts path is string}
12480
+ * Nothing.
12481
+ */
12482
+ function assertPath(path, name) {
12483
+ if (!path) throw new Error("Setting `" + name + "` requires `path` to be set too");
12484
+ }
12485
+ /**
12486
+ * Assert `value` is an `Uint8Array`.
12487
+ *
12488
+ * @param {unknown} value
12489
+ * thing.
12490
+ * @returns {value is Uint8Array}
12491
+ * Whether `value` is an `Uint8Array`.
12492
+ */
12493
+ function isUint8Array(value) {
12494
+ return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value);
12495
+ }
12496
+ //#endregion
11628
12497
  //#region src/plugins/content/pipeline/frontmatter.ts
11629
12498
  /**
11630
12499
  * @file content pipeline — frontmatter parsing.
@@ -11795,7 +12664,7 @@ function parseDimensions(width, height) {
11795
12664
  * collectAttributes({ src: "x", poster: "/p.jpg", flag: null }); // { src: "x", poster: "/p.jpg" }
11796
12665
  * ```
11797
12666
  */
11798
- function collectAttributes(attributes) {
12667
+ function collectAttributes$1(attributes) {
11799
12668
  const out = {};
11800
12669
  for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
11801
12670
  return out;
@@ -11868,7 +12737,7 @@ function embedTransform(facade, tree) {
11868
12737
  width: dimensions.width,
11869
12738
  height: dimensions.height
11870
12739
  } : {},
11871
- attributes: collectAttributes(node.attributes)
12740
+ attributes: collectAttributes$1(node.attributes)
11872
12741
  }, dimensions)
11873
12742
  };
11874
12743
  parent.children[index] = html;
@@ -11894,6 +12763,246 @@ function embedPlugin(options = {}) {
11894
12763
  return (tree) => embedTransform(facade, tree);
11895
12764
  }
11896
12765
  //#endregion
12766
+ //#region src/plugins/content/pipeline/gallery-default.tsx
12767
+ /** CSS class on the gallery's slide track. */
12768
+ const GALLERY_TRACK_CLASS = "gallery-track";
12769
+ /**
12770
+ * Default `::gallery` inner content: a single track holding every slide `<img>`
12771
+ * in folder order. A companion gallery island (consumer-provided) can enhance
12772
+ * the track with swipe/keyboard/lightbox; with no island and no CSS it is still
12773
+ * a plain horizontally-scrollable image strip. Provided as the default and as a
12774
+ * composable building block for custom galleries.
12775
+ *
12776
+ * @param props - The gallery props (the resolved `slides`).
12777
+ * @returns The gallery inner-content VNode.
12778
+ * @example
12779
+ * ```tsx
12780
+ * // Compose the default inside a richer custom gallery:
12781
+ * const MyGallery = (p: GalleryProps) => (
12782
+ * <figure><GalleryTrack {...p} /><figcaption>{p.caption}</figcaption></figure>
12783
+ * );
12784
+ * ```
12785
+ */
12786
+ function GalleryTrack(props) {
12787
+ return /* @__PURE__ */ jsx("div", {
12788
+ class: GALLERY_TRACK_CLASS,
12789
+ "data-gallery-track": true,
12790
+ children: props.slides.map((slide) => /* @__PURE__ */ jsx("img", {
12791
+ src: slide.src,
12792
+ alt: slide.alt
12793
+ }, slide.src))
12794
+ });
12795
+ }
12796
+ //#endregion
12797
+ //#region src/plugins/content/pipeline/gallery.ts
12798
+ /**
12799
+ * @file content pipeline — `::gallery` folder galleries.
12800
+ *
12801
+ * Rewrites `::gallery{src="./images/dir/" caption="…"}` leaf directives into a
12802
+ * static swipeable image set at the mdast stage (BEFORE the remark-rehype bridge):
12803
+ * a framework-owned `<div class="gallery" data-component="gallery">` carrying the
12804
+ * island hook, wrapping inner content rendered (at build time, to static markup)
12805
+ * by a Preact component — the built-in {@link GalleryTrack} by default, or a
12806
+ * consumer component via `gallery.component`.
12807
+ *
12808
+ * Unlike `::embed` (one src resolved later by the provider), a gallery's `src` is a
12809
+ * co-located FOLDER that must be listed at build time, which needs the article's
12810
+ * source path. The provider supplies the article `slug` via the VFile `data`
12811
+ * (providers.ts), and the `contentDir` is bound at pipeline-build time — so this
12812
+ * transform reads `<contentDir>/<slug>/<src>` from disk, sorts its images, and
12813
+ * resolves each to its shared `/<slug>/<dir>/<file>` URL (identical from every
12814
+ * locale page, mirroring co-located images). The companion gallery SPA island
12815
+ * (consumer-provided) wires swipe/keyboard/lightbox on `[data-component="gallery"]`.
12816
+ */
12817
+ /** CSS class on the `<div>` wrapping each gallery. */
12818
+ const GALLERY_WRAPPER_CLASS = "gallery";
12819
+ /** `data-component` name binding the gallery to its SPA island. */
12820
+ const GALLERY_COMPONENT_NAME = "gallery";
12821
+ /** Image file extensions a gallery folder expands over. */
12822
+ const IMAGE_EXTENSIONS = new Set([
12823
+ ".webp",
12824
+ ".jpg",
12825
+ ".jpeg",
12826
+ ".png",
12827
+ ".gif",
12828
+ ".avif"
12829
+ ]);
12830
+ /**
12831
+ * Type guard for a `::gallery` leaf directive.
12832
+ *
12833
+ * @param node - AST node to test.
12834
+ * @returns `true` when the node is a `::gallery` leaf directive.
12835
+ * @example
12836
+ * ```ts
12837
+ * if (isGalleryDirective(node)) console.log(node.attributes?.src);
12838
+ * ```
12839
+ */
12840
+ function isGalleryDirective(node) {
12841
+ return node.type === "leafDirective" && node.name === "gallery";
12842
+ }
12843
+ /**
12844
+ * Resolve `.`/`..` segments of a path built from `slug/src/file` into the single
12845
+ * shared absolute URL the content-assets build phase copies the folder to.
12846
+ *
12847
+ * @param slug - Article directory name.
12848
+ * @param src - The directive `src` (co-located relative folder, e.g. `./images/dir/`).
12849
+ * @param file - One image file name inside the folder.
12850
+ * @returns The shared absolute slide URL (`/<slug>/<dir>/<file>`).
12851
+ * @example
12852
+ * ```ts
12853
+ * slideUrl("post", "./images/mk/", "a.webp"); // "/post/images/mk/a.webp"
12854
+ * ```
12855
+ */
12856
+ function slideUrl(slug, src, file) {
12857
+ const resolved = [];
12858
+ for (const segment of `${slug}/${src}/${file}`.split("/")) {
12859
+ if (segment === "" || segment === ".") continue;
12860
+ if (segment === "..") resolved.pop();
12861
+ else resolved.push(segment);
12862
+ }
12863
+ return `/${resolved.join("/")}`;
12864
+ }
12865
+ /**
12866
+ * Read a gallery folder from disk and build its sorted slide list. Each slide
12867
+ * gets the directive `caption` plus a ` · N` index suffix as alt (or just `N`).
12868
+ *
12869
+ * @param contentDir - The provider's content directory.
12870
+ * @param slug - Article directory name (from the VFile data).
12871
+ * @param src - The directive `src` (co-located relative folder).
12872
+ * @param caption - The directive `caption` attribute (may be empty).
12873
+ * @returns The sorted slides.
12874
+ * @throws {Error} When the folder is missing or holds no images.
12875
+ * @example
12876
+ * ```ts
12877
+ * resolveSlides("./content", "post", "./images/mk/", "Our game");
12878
+ * ```
12879
+ */
12880
+ function resolveSlides(contentDir, slug, src, caption) {
12881
+ const folder = path.join(contentDir, slug, src);
12882
+ let entries;
12883
+ try {
12884
+ entries = readdirSync(folder);
12885
+ } catch {
12886
+ throw new Error(`[web] content: \`::gallery\` folder not found: "${src}" (looked in ${folder}).`);
12887
+ }
12888
+ const files = entries.filter((name) => IMAGE_EXTENSIONS.has(path.extname(name).toLowerCase())).toSorted((a, b) => a.localeCompare(b, "en"));
12889
+ if (files.length === 0) throw new Error(`[web] content: \`::gallery\` folder has no images: "${src}" (${folder}).`);
12890
+ return files.map((file, index) => ({
12891
+ src: slideUrl(slug, src, file),
12892
+ alt: caption ? `${caption} · ${index + 1}` : `${index + 1}`
12893
+ }));
12894
+ }
12895
+ /**
12896
+ * Collect the directive's raw attribute bag into a plain string record, dropping
12897
+ * `null`/`undefined` values (so a custom component can read arbitrary extra options).
12898
+ *
12899
+ * @param attributes - The raw directive attributes (or undefined).
12900
+ * @returns A string-valued attribute record.
12901
+ * @example
12902
+ * ```ts
12903
+ * collectAttributes({ src: "x", layout: "dots", flag: null }); // { src: "x", layout: "dots" }
12904
+ * ```
12905
+ */
12906
+ function collectAttributes(attributes) {
12907
+ const out = {};
12908
+ for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
12909
+ return out;
12910
+ }
12911
+ /**
12912
+ * Build the static gallery HTML for one directive: the framework-owned `<div>`
12913
+ * (island hook in `data-component`) wrapping the component's inner content, SSR'd
12914
+ * to static markup.
12915
+ *
12916
+ * @param component - The gallery component (default {@link GalleryTrack}).
12917
+ * @param slides - The resolved slides.
12918
+ * @param caption - The directive `caption` attribute.
12919
+ * @param attributes - The raw directive attribute bag.
12920
+ * @returns The gallery HTML string.
12921
+ * @example
12922
+ * ```ts
12923
+ * galleryHtml(GalleryTrack, slides, "Our game", { src: "./images/mk/" });
12924
+ * ```
12925
+ */
12926
+ function galleryHtml(component, slides, caption, attributes) {
12927
+ return `<div class="${GALLERY_WRAPPER_CLASS}" data-component="${GALLERY_COMPONENT_NAME}">${renderToString(h(component, {
12928
+ slides,
12929
+ caption,
12930
+ attributes
12931
+ }))}</div>`;
12932
+ }
12933
+ /**
12934
+ * Mdast transformer rewriting every `::gallery` leaf directive to its gallery
12935
+ * HTML node. A directive missing `src`, or pointing at a missing/empty folder,
12936
+ * fails the build with the offending value quoted. Skipped entirely when the
12937
+ * VFile carries no `slug` (the standalone `render()` path has no article context).
12938
+ *
12939
+ * @param options - Resolved transform options (component + contentDir).
12940
+ * @param tree - The mdast tree to mutate.
12941
+ * @param file - The VFile (its `data.slug` locates the article on disk).
12942
+ * @throws {Error} When a `::gallery` directive is missing `src`, or its folder is
12943
+ * missing/empty.
12944
+ * @example
12945
+ * ```ts
12946
+ * galleryTransform({ component: GalleryTrack, contentDir: "./content" }, tree, file);
12947
+ * ```
12948
+ */
12949
+ function galleryTransform(options, tree, file) {
12950
+ const slug = typeof file.data.slug === "string" ? file.data.slug : void 0;
12951
+ const component = options.component ?? GalleryTrack;
12952
+ visit(tree, (node, index, parent) => {
12953
+ if (!isGalleryDirective(node)) return;
12954
+ if (parent === void 0 || index === void 0) return;
12955
+ if (slug === void 0) return;
12956
+ const src = node.attributes?.src ?? "";
12957
+ if (src === "") throw new Error("[web] content: `::gallery` requires a `src` folder, e.g. ::gallery{src=\"./images/dir/\"}.");
12958
+ const caption = node.attributes?.caption ?? "";
12959
+ const html = {
12960
+ type: "html",
12961
+ value: galleryHtml(component, resolveSlides(options.contentDir, slug, src, caption), caption, collectAttributes(node.attributes))
12962
+ };
12963
+ parent.children[index] = html;
12964
+ });
12965
+ }
12966
+ /**
12967
+ * Normalize the provider's `gallery` config value (`boolean | options`) plus the
12968
+ * provider `contentDir` into the resolved {@link GalleryTransformOptions} the
12969
+ * transform factory needs.
12970
+ *
12971
+ * @param gallery - The raw `FileSystemContentOptions.gallery` value (truthy).
12972
+ * @param contentDir - The provider's content directory.
12973
+ * @returns The resolved transform options.
12974
+ * @example
12975
+ * ```ts
12976
+ * normalizeGalleryOptions(true, "./content"); // { contentDir: "./content" }
12977
+ * normalizeGalleryOptions({ component: MyGallery }, "./content");
12978
+ * ```
12979
+ */
12980
+ function normalizeGalleryOptions(gallery, contentDir) {
12981
+ return typeof gallery === "boolean" ? { contentDir } : {
12982
+ ...gallery,
12983
+ contentDir
12984
+ };
12985
+ }
12986
+ /**
12987
+ * Remark transform factory: rewrites `::gallery{src="…"}` leaf directives into
12988
+ * static swipeable galleries (see the file header). Opt-in via the provider's
12989
+ * `gallery` option; requires `trustedContent: true` because the markup is raw HTML
12990
+ * the sanitize pass would strip. The inner content is rendered by
12991
+ * `options.component` (a consumer Preact component) or the built-in
12992
+ * {@link GalleryTrack}; folders are read from `options.contentDir` against the
12993
+ * per-article `slug` on the VFile.
12994
+ *
12995
+ * @param options - Resolved transform options (component + contentDir).
12996
+ * @returns An mdast tree transformer.
12997
+ * @example
12998
+ * ```ts
12999
+ * unified().use(galleryPlugin, { component: MyGallery, contentDir: "./content" });
13000
+ * ```
13001
+ */
13002
+ function galleryPlugin(options) {
13003
+ return (tree, file) => galleryTransform(options, tree, file);
13004
+ }
13005
+ //#endregion
11897
13006
  //#region src/plugins/content/pipeline/mermaid.ts
11898
13007
  /** CSS class on the `<figure>` wrapper around each rendered diagram. */
11899
13008
  const MERMAID_FIGURE_CLASS = "mermaid-diagram";
@@ -12203,6 +13312,7 @@ function defaultRemarkPlugins(config) {
12203
13312
  pullQuotePlugin
12204
13313
  ];
12205
13314
  if (config?.embed) plugins.push([embedPlugin, normalizeEmbedOptions(config.embed)]);
13315
+ if (config?.gallery) plugins.push([galleryPlugin, normalizeGalleryOptions(config.gallery, config.contentDir)]);
12206
13316
  if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
12207
13317
  plugins.push([remarkRehype, { allowDangerousHtml: true }]);
12208
13318
  return plugins;
@@ -12604,7 +13714,10 @@ function fileSystemContent(options) {
12604
13714
  state.dirtyPaths.delete(filePath);
12605
13715
  const { frontmatter, body } = parseFrontmatter(raw, options);
12606
13716
  const processor = ensureProcessor(state, options);
12607
- const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(body)), slug), slug);
13717
+ const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(new VFile({
13718
+ value: body,
13719
+ data: { slug }
13720
+ }))), slug), slug);
12608
13721
  const { readingTime, wordCount } = calculateReadingTime(body);
12609
13722
  return {
12610
13723
  frontmatter,
@@ -12728,4 +13841,4 @@ const createApp = core.createApp;
12728
13841
  */
12729
13842
  const createPlugin = core.createPlugin;
12730
13843
  //#endregion
12731
- export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, types_exports$5 as Env, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
13844
+ export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, types_exports$5 as Env, GalleryTrack, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };