@moku-labs/web 1.10.0 → 1.12.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.
package/dist/index.cjs CHANGED
@@ -36,9 +36,11 @@ remark_parse = require_convention.__toESM(remark_parse, 1);
36
36
  let remark_rehype = require("remark-rehype");
37
37
  remark_rehype = require_convention.__toESM(remark_rehype, 1);
38
38
  let unist_util_visit = require("unist-util-visit");
39
+ let preact_jsx_runtime = require("preact/jsx-runtime");
39
40
  let hast_util_sanitize = require("hast-util-sanitize");
40
41
  let reading_time = require("reading-time");
41
42
  reading_time = require_convention.__toESM(reading_time, 1);
43
+ let node_process = require("node:process");
42
44
  //#region src/plugins/env/api.ts
43
45
  /** Error prefix for all env API failures. */
44
46
  const ERROR_PREFIX$16 = "[web]";
@@ -1582,14 +1584,15 @@ function validateContentConfig(config) {
1582
1584
  }
1583
1585
  /**
1584
1586
  * Validates the `fileSystemContent` provider options (fail-fast at provider
1585
- * construction). Throws when `mermaid` or `embed` is enabled without
1586
- * `trustedContent: true`: both emit raw HTML (inline SVG / the embed facade),
1587
- * which the sanitize pass (the untrusted-content XSS boundary) would strip — so
1588
- * the combination can never work. Errors use the `[web]` prefix.
1587
+ * construction). Throws when `mermaid`, `embed`, or `gallery` is enabled without
1588
+ * `trustedContent: true`: each emits raw HTML (inline SVG / the embed facade /
1589
+ * the gallery markup), which the sanitize pass (the untrusted-content XSS
1590
+ * boundary) would strip — so the combination can never work. Errors use the
1591
+ * `[web]` prefix.
1589
1592
  *
1590
1593
  * @param options - The provider options to validate.
1591
- * @throws {Error} If `mermaid` or `embed` is enabled while `trustedContent` is
1592
- * not `true`.
1594
+ * @throws {Error} If `mermaid`, `embed`, or `gallery` is enabled while
1595
+ * `trustedContent` is not `true`.
1593
1596
  * @example
1594
1597
  * ```ts
1595
1598
  * validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
@@ -1598,6 +1601,7 @@ function validateContentConfig(config) {
1598
1601
  function validateFileSystemContentOptions(options) {
1599
1602
  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.");
1600
1603
  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.");
1604
+ 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.");
1601
1605
  }
1602
1606
  //#endregion
1603
1607
  //#region src/plugins/content/index.ts
@@ -4053,19 +4057,24 @@ function readCachedContent(ctx) {
4053
4057
  //#endregion
4054
4058
  //#region src/plugins/build/phases/content-images.ts
4055
4059
  /**
4056
- * @file build phase — content-images. Copies each article's co-located image directory
4057
- * (`<contentDir>/<slug>/images/`) to a single shared output dir (`<outDir>/<slug>/images/`) reused by
4058
- * every locale, matching the absolute `/<slug>/images/...` URLs the content renderer emits. Gated by
4059
- * `config.images`.
4060
+ * @file build phase — content-images. Copies each article's co-located asset
4061
+ * directories (`<contentDir>/<slug>/<dir>/`, e.g. `images/` or a pre-built
4062
+ * `game/` embed bundle) to a single shared output location
4063
+ * (`<outDir>/<slug>/<dir>/`) reused by every locale, matching the absolute
4064
+ * `/<slug>/<dir>/...` URLs the content renderer emits (image src + `::embed`
4065
+ * src). Dot- and underscore-prefixed dirs are treated as private and skipped.
4066
+ * Gated by `config.images`.
4060
4067
  */
4061
- /** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
4062
- const ARTICLE_IMAGE_DIR = "images";
4063
4068
  /**
4064
- * Copy every article's co-located `images/` directory to `<outDir>/<slug>/images/`. No-op when
4065
- * `config.images` is false or the content directory does not exist.
4069
+ * Copy every article's co-located asset directories to `<outDir>/<slug>/<dir>/`.
4070
+ * Each direct subdirectory of `<contentDir>/<slug>/` rides along (the
4071
+ * conventional `images/` dir plus any other bundle, like an `::embed` game),
4072
+ * except `.`/`_`-prefixed dirs (private). The `.md` source files are never
4073
+ * copied (only directories are). No-op when `config.images` is false or the
4074
+ * content directory does not exist.
4066
4075
  *
4067
4076
  * @param ctx - Plugin context (provides `config`, `log`, `require`).
4068
- * @returns The number of directories copied (one per article that has an `images/` dir).
4077
+ * @returns The number of articles that had at least one asset directory copied.
4069
4078
  * @example
4070
4079
  * ```ts
4071
4080
  * const copied = await copyContentImages(ctx);
@@ -4084,14 +4093,20 @@ async function copyContentImages(ctx) {
4084
4093
  });
4085
4094
  return 0;
4086
4095
  }
4087
- const entries = await (0, node_fs_promises.readdir)(contentDir, { withFileTypes: true });
4096
+ const articleDirectories = await (0, node_fs_promises.readdir)(contentDir, { withFileTypes: true });
4088
4097
  let copied = 0;
4089
- for (const entry of entries) {
4090
- if (!entry.isDirectory()) continue;
4091
- const source = node_path$1.default.join(contentDir, entry.name, ARTICLE_IMAGE_DIR);
4092
- if (!(0, node_fs.existsSync)(source)) continue;
4093
- await (0, node_fs_promises.cp)(source, node_path$1.default.join(ctx.config.outDir, entry.name, ARTICLE_IMAGE_DIR), { recursive: true });
4094
- copied += 1;
4098
+ for (const article of articleDirectories) {
4099
+ if (!article.isDirectory()) continue;
4100
+ const articleDir = node_path$1.default.join(contentDir, article.name);
4101
+ const assetDirectories = await (0, node_fs_promises.readdir)(articleDir, { withFileTypes: true });
4102
+ let copiedAny = false;
4103
+ for (const asset of assetDirectories) {
4104
+ if (!asset.isDirectory()) continue;
4105
+ if (asset.name.startsWith(".") || asset.name.startsWith("_")) continue;
4106
+ await (0, node_fs_promises.cp)(node_path$1.default.join(articleDir, asset.name), node_path$1.default.join(ctx.config.outDir, article.name, asset.name), { recursive: true });
4107
+ copiedAny = true;
4108
+ }
4109
+ if (copiedAny) copied += 1;
4095
4110
  }
4096
4111
  ctx.log.debug("build:content-images", { copied });
4097
4112
  return copied;
@@ -11352,7 +11367,11 @@ function activateEmbed(figure) {
11352
11367
  }
11353
11368
  /**
11354
11369
  * Shared click handler (module-level so mount/unmount detach the same
11355
- * reference): a click on the facade's button activates the embed.
11370
+ * reference): any click on the not-yet-active facade activates the embed. It
11371
+ * fires on the whole facade — not a specific button class — so a consumer's
11372
+ * custom facade markup (see content `embed.facade`) works without re-wiring;
11373
+ * the default facade's `<button>` keeps it keyboard-accessible. Once active
11374
+ * (`data-embed-active`), clicks fall through to the live iframe.
11356
11375
  *
11357
11376
  * @param event - The click event from the facade figure.
11358
11377
  * @example
@@ -11361,9 +11380,10 @@ function activateEmbed(figure) {
11361
11380
  * ```
11362
11381
  */
11363
11382
  function onFacadeClick(event) {
11364
- if (!event.target.closest("button.lazy-embed-button")) return;
11365
11383
  const figure = event.currentTarget;
11366
- if (figure instanceof HTMLElement) activateEmbed(figure);
11384
+ if (!(figure instanceof HTMLElement)) return;
11385
+ if (figure.dataset.embedActive !== void 0) return;
11386
+ activateEmbed(figure);
11367
11387
  }
11368
11388
  /**
11369
11389
  * Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
@@ -11621,6 +11641,857 @@ function cloudflareBindings() {
11621
11641
  };
11622
11642
  }
11623
11643
  //#endregion
11644
+ //#region node_modules/unist-util-stringify-position/lib/index.js
11645
+ /**
11646
+ * @typedef {import('unist').Node} Node
11647
+ * @typedef {import('unist').Point} Point
11648
+ * @typedef {import('unist').Position} Position
11649
+ */
11650
+ /**
11651
+ * @typedef NodeLike
11652
+ * @property {string} type
11653
+ * @property {PositionLike | null | undefined} [position]
11654
+ *
11655
+ * @typedef PointLike
11656
+ * @property {number | null | undefined} [line]
11657
+ * @property {number | null | undefined} [column]
11658
+ * @property {number | null | undefined} [offset]
11659
+ *
11660
+ * @typedef PositionLike
11661
+ * @property {PointLike | null | undefined} [start]
11662
+ * @property {PointLike | null | undefined} [end]
11663
+ */
11664
+ /**
11665
+ * Serialize the positional info of a point, position (start and end points),
11666
+ * or node.
11667
+ *
11668
+ * @param {Node | NodeLike | Point | PointLike | Position | PositionLike | null | undefined} [value]
11669
+ * Node, position, or point.
11670
+ * @returns {string}
11671
+ * Pretty printed positional info of a node (`string`).
11672
+ *
11673
+ * In the format of a range `ls:cs-le:ce` (when given `node` or `position`)
11674
+ * or a point `l:c` (when given `point`), where `l` stands for line, `c` for
11675
+ * column, `s` for `start`, and `e` for end.
11676
+ * An empty string (`''`) is returned if the given value is neither `node`,
11677
+ * `position`, nor `point`.
11678
+ */
11679
+ function stringifyPosition(value) {
11680
+ if (!value || typeof value !== "object") return "";
11681
+ if ("position" in value || "type" in value) return position(value.position);
11682
+ if ("start" in value || "end" in value) return position(value);
11683
+ if ("line" in value || "column" in value) return point(value);
11684
+ return "";
11685
+ }
11686
+ /**
11687
+ * @param {Point | PointLike | null | undefined} point
11688
+ * @returns {string}
11689
+ */
11690
+ function point(point) {
11691
+ return index(point && point.line) + ":" + index(point && point.column);
11692
+ }
11693
+ /**
11694
+ * @param {Position | PositionLike | null | undefined} pos
11695
+ * @returns {string}
11696
+ */
11697
+ function position(pos) {
11698
+ return point(pos && pos.start) + "-" + point(pos && pos.end);
11699
+ }
11700
+ /**
11701
+ * @param {number | null | undefined} value
11702
+ * @returns {number}
11703
+ */
11704
+ function index(value) {
11705
+ return value && typeof value === "number" ? value : 1;
11706
+ }
11707
+ //#endregion
11708
+ //#region node_modules/vfile-message/lib/index.js
11709
+ /**
11710
+ * @import {Node, Point, Position} from 'unist'
11711
+ */
11712
+ /**
11713
+ * @typedef {object & {type: string, position?: Position | undefined}} NodeLike
11714
+ *
11715
+ * @typedef Options
11716
+ * Configuration.
11717
+ * @property {Array<Node> | null | undefined} [ancestors]
11718
+ * Stack of (inclusive) ancestor nodes surrounding the message (optional).
11719
+ * @property {Error | null | undefined} [cause]
11720
+ * Original error cause of the message (optional).
11721
+ * @property {Point | Position | null | undefined} [place]
11722
+ * Place of message (optional).
11723
+ * @property {string | null | undefined} [ruleId]
11724
+ * Category of message (optional, example: `'my-rule'`).
11725
+ * @property {string | null | undefined} [source]
11726
+ * Namespace of who sent the message (optional, example: `'my-package'`).
11727
+ */
11728
+ /**
11729
+ * Message.
11730
+ */
11731
+ var VFileMessage = class extends Error {
11732
+ /**
11733
+ * Create a message for `reason`.
11734
+ *
11735
+ * > 🪦 **Note**: also has obsolete signatures.
11736
+ *
11737
+ * @overload
11738
+ * @param {string} reason
11739
+ * @param {Options | null | undefined} [options]
11740
+ * @returns
11741
+ *
11742
+ * @overload
11743
+ * @param {string} reason
11744
+ * @param {Node | NodeLike | null | undefined} parent
11745
+ * @param {string | null | undefined} [origin]
11746
+ * @returns
11747
+ *
11748
+ * @overload
11749
+ * @param {string} reason
11750
+ * @param {Point | Position | null | undefined} place
11751
+ * @param {string | null | undefined} [origin]
11752
+ * @returns
11753
+ *
11754
+ * @overload
11755
+ * @param {string} reason
11756
+ * @param {string | null | undefined} [origin]
11757
+ * @returns
11758
+ *
11759
+ * @overload
11760
+ * @param {Error | VFileMessage} cause
11761
+ * @param {Node | NodeLike | null | undefined} parent
11762
+ * @param {string | null | undefined} [origin]
11763
+ * @returns
11764
+ *
11765
+ * @overload
11766
+ * @param {Error | VFileMessage} cause
11767
+ * @param {Point | Position | null | undefined} place
11768
+ * @param {string | null | undefined} [origin]
11769
+ * @returns
11770
+ *
11771
+ * @overload
11772
+ * @param {Error | VFileMessage} cause
11773
+ * @param {string | null | undefined} [origin]
11774
+ * @returns
11775
+ *
11776
+ * @param {Error | VFileMessage | string} causeOrReason
11777
+ * Reason for message, should use markdown.
11778
+ * @param {Node | NodeLike | Options | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
11779
+ * Configuration (optional).
11780
+ * @param {string | null | undefined} [origin]
11781
+ * Place in code where the message originates (example:
11782
+ * `'my-package:my-rule'` or `'my-rule'`).
11783
+ * @returns
11784
+ * Instance of `VFileMessage`.
11785
+ */
11786
+ constructor(causeOrReason, optionsOrParentOrPlace, origin) {
11787
+ super();
11788
+ if (typeof optionsOrParentOrPlace === "string") {
11789
+ origin = optionsOrParentOrPlace;
11790
+ optionsOrParentOrPlace = void 0;
11791
+ }
11792
+ /** @type {string} */
11793
+ let reason = "";
11794
+ /** @type {Options} */
11795
+ let options = {};
11796
+ let legacyCause = false;
11797
+ if (optionsOrParentOrPlace) if ("line" in optionsOrParentOrPlace && "column" in optionsOrParentOrPlace) options = { place: optionsOrParentOrPlace };
11798
+ else if ("start" in optionsOrParentOrPlace && "end" in optionsOrParentOrPlace) options = { place: optionsOrParentOrPlace };
11799
+ else if ("type" in optionsOrParentOrPlace) options = {
11800
+ ancestors: [optionsOrParentOrPlace],
11801
+ place: optionsOrParentOrPlace.position
11802
+ };
11803
+ else options = { ...optionsOrParentOrPlace };
11804
+ if (typeof causeOrReason === "string") reason = causeOrReason;
11805
+ else if (!options.cause && causeOrReason) {
11806
+ legacyCause = true;
11807
+ reason = causeOrReason.message;
11808
+ options.cause = causeOrReason;
11809
+ }
11810
+ if (!options.ruleId && !options.source && typeof origin === "string") {
11811
+ const index = origin.indexOf(":");
11812
+ if (index === -1) options.ruleId = origin;
11813
+ else {
11814
+ options.source = origin.slice(0, index);
11815
+ options.ruleId = origin.slice(index + 1);
11816
+ }
11817
+ }
11818
+ if (!options.place && options.ancestors && options.ancestors) {
11819
+ const parent = options.ancestors[options.ancestors.length - 1];
11820
+ if (parent) options.place = parent.position;
11821
+ }
11822
+ const start = options.place && "start" in options.place ? options.place.start : options.place;
11823
+ /**
11824
+ * Stack of ancestor nodes surrounding the message.
11825
+ *
11826
+ * @type {Array<Node> | undefined}
11827
+ */
11828
+ this.ancestors = options.ancestors || void 0;
11829
+ /**
11830
+ * Original error cause of the message.
11831
+ *
11832
+ * @type {Error | undefined}
11833
+ */
11834
+ this.cause = options.cause || void 0;
11835
+ /**
11836
+ * Starting column of message.
11837
+ *
11838
+ * @type {number | undefined}
11839
+ */
11840
+ this.column = start ? start.column : void 0;
11841
+ /**
11842
+ * State of problem.
11843
+ *
11844
+ * * `true` — error, file not usable
11845
+ * * `false` — warning, change may be needed
11846
+ * * `undefined` — change likely not needed
11847
+ *
11848
+ * @type {boolean | null | undefined}
11849
+ */
11850
+ this.fatal = void 0;
11851
+ /**
11852
+ * Path of a file (used throughout the `VFile` ecosystem).
11853
+ *
11854
+ * @type {string | undefined}
11855
+ */
11856
+ this.file = "";
11857
+ /**
11858
+ * Reason for message.
11859
+ *
11860
+ * @type {string}
11861
+ */
11862
+ this.message = reason;
11863
+ /**
11864
+ * Starting line of error.
11865
+ *
11866
+ * @type {number | undefined}
11867
+ */
11868
+ this.line = start ? start.line : void 0;
11869
+ /**
11870
+ * Serialized positional info of message.
11871
+ *
11872
+ * On normal errors, this would be something like `ParseError`, buit in
11873
+ * `VFile` messages we use this space to show where an error happened.
11874
+ */
11875
+ this.name = stringifyPosition(options.place) || "1:1";
11876
+ /**
11877
+ * Place of message.
11878
+ *
11879
+ * @type {Point | Position | undefined}
11880
+ */
11881
+ this.place = options.place || void 0;
11882
+ /**
11883
+ * Reason for message, should use markdown.
11884
+ *
11885
+ * @type {string}
11886
+ */
11887
+ this.reason = this.message;
11888
+ /**
11889
+ * Category of message (example: `'my-rule'`).
11890
+ *
11891
+ * @type {string | undefined}
11892
+ */
11893
+ this.ruleId = options.ruleId || void 0;
11894
+ /**
11895
+ * Namespace of message (example: `'my-package'`).
11896
+ *
11897
+ * @type {string | undefined}
11898
+ */
11899
+ this.source = options.source || void 0;
11900
+ /**
11901
+ * Stack of message.
11902
+ *
11903
+ * This is used by normal errors to show where something happened in
11904
+ * programming code, irrelevant for `VFile` messages,
11905
+ *
11906
+ * @type {string}
11907
+ */
11908
+ this.stack = legacyCause && options.cause && typeof options.cause.stack === "string" ? options.cause.stack : "";
11909
+ /**
11910
+ * Specify the source value that’s being reported, which is deemed
11911
+ * incorrect.
11912
+ *
11913
+ * @type {string | undefined}
11914
+ */
11915
+ this.actual = void 0;
11916
+ /**
11917
+ * Suggest acceptable values that can be used instead of `actual`.
11918
+ *
11919
+ * @type {Array<string> | undefined}
11920
+ */
11921
+ this.expected = void 0;
11922
+ /**
11923
+ * Long form description of the message (you should use markdown).
11924
+ *
11925
+ * @type {string | undefined}
11926
+ */
11927
+ this.note = void 0;
11928
+ /**
11929
+ * Link to docs for the message.
11930
+ *
11931
+ * > 👉 **Note**: this must be an absolute URL that can be passed as `x`
11932
+ * > to `new URL(x)`.
11933
+ *
11934
+ * @type {string | undefined}
11935
+ */
11936
+ this.url = void 0;
11937
+ }
11938
+ };
11939
+ VFileMessage.prototype.file = "";
11940
+ VFileMessage.prototype.name = "";
11941
+ VFileMessage.prototype.reason = "";
11942
+ VFileMessage.prototype.message = "";
11943
+ VFileMessage.prototype.stack = "";
11944
+ VFileMessage.prototype.column = void 0;
11945
+ VFileMessage.prototype.line = void 0;
11946
+ VFileMessage.prototype.ancestors = void 0;
11947
+ VFileMessage.prototype.cause = void 0;
11948
+ VFileMessage.prototype.fatal = void 0;
11949
+ VFileMessage.prototype.place = void 0;
11950
+ VFileMessage.prototype.ruleId = void 0;
11951
+ VFileMessage.prototype.source = void 0;
11952
+ //#endregion
11953
+ //#region node_modules/vfile/lib/minurl.shared.js
11954
+ /**
11955
+ * Checks if a value has the shape of a WHATWG URL object.
11956
+ *
11957
+ * Using a symbol or instanceof would not be able to recognize URL objects
11958
+ * coming from other implementations (e.g. in Electron), so instead we are
11959
+ * checking some well known properties for a lack of a better test.
11960
+ *
11961
+ * We use `href` and `protocol` as they are the only properties that are
11962
+ * easy to retrieve and calculate due to the lazy nature of the getters.
11963
+ *
11964
+ * We check for auth attribute to distinguish legacy url instance with
11965
+ * WHATWG URL instance.
11966
+ *
11967
+ * @param {unknown} fileUrlOrPath
11968
+ * File path or URL.
11969
+ * @returns {fileUrlOrPath is URL}
11970
+ * Whether it’s a URL.
11971
+ */
11972
+ function isUrl(fileUrlOrPath) {
11973
+ return Boolean(fileUrlOrPath !== null && typeof fileUrlOrPath === "object" && "href" in fileUrlOrPath && fileUrlOrPath.href && "protocol" in fileUrlOrPath && fileUrlOrPath.protocol && fileUrlOrPath.auth === void 0);
11974
+ }
11975
+ //#endregion
11976
+ //#region node_modules/vfile/lib/index.js
11977
+ /**
11978
+ * @import {Node, Point, Position} from 'unist'
11979
+ * @import {Options as MessageOptions} from 'vfile-message'
11980
+ * @import {Compatible, Data, Map, Options, Value} from 'vfile'
11981
+ */
11982
+ /**
11983
+ * @typedef {object & {type: string, position?: Position | undefined}} NodeLike
11984
+ */
11985
+ /**
11986
+ * Order of setting (least specific to most), we need this because otherwise
11987
+ * `{stem: 'a', path: '~/b.js'}` would throw, as a path is needed before a
11988
+ * stem can be set.
11989
+ */
11990
+ const order = [
11991
+ "history",
11992
+ "path",
11993
+ "basename",
11994
+ "stem",
11995
+ "extname",
11996
+ "dirname"
11997
+ ];
11998
+ var VFile = class {
11999
+ /**
12000
+ * Create a new virtual file.
12001
+ *
12002
+ * `options` is treated as:
12003
+ *
12004
+ * * `string` or `Uint8Array` — `{value: options}`
12005
+ * * `URL` — `{path: options}`
12006
+ * * `VFile` — shallow copies its data over to the new file
12007
+ * * `object` — all fields are shallow copied over to the new file
12008
+ *
12009
+ * Path related fields are set in the following order (least specific to
12010
+ * most specific): `history`, `path`, `basename`, `stem`, `extname`,
12011
+ * `dirname`.
12012
+ *
12013
+ * You cannot set `dirname` or `extname` without setting either `history`,
12014
+ * `path`, `basename`, or `stem` too.
12015
+ *
12016
+ * @param {Compatible | null | undefined} [value]
12017
+ * File value.
12018
+ * @returns
12019
+ * New instance.
12020
+ */
12021
+ constructor(value) {
12022
+ /** @type {Options | VFile} */
12023
+ let options;
12024
+ if (!value) options = {};
12025
+ else if (isUrl(value)) options = { path: value };
12026
+ else if (typeof value === "string" || isUint8Array(value)) options = { value };
12027
+ else options = value;
12028
+ /**
12029
+ * Base of `path` (default: `process.cwd()` or `'/'` in browsers).
12030
+ *
12031
+ * @type {string}
12032
+ */
12033
+ this.cwd = "cwd" in options ? "" : node_process.default.cwd();
12034
+ /**
12035
+ * Place to store custom info (default: `{}`).
12036
+ *
12037
+ * It’s OK to store custom data directly on the file but moving it to
12038
+ * `data` is recommended.
12039
+ *
12040
+ * @type {Data}
12041
+ */
12042
+ this.data = {};
12043
+ /**
12044
+ * List of file paths the file moved between.
12045
+ *
12046
+ * The first is the original path and the last is the current path.
12047
+ *
12048
+ * @type {Array<string>}
12049
+ */
12050
+ this.history = [];
12051
+ /**
12052
+ * List of messages associated with the file.
12053
+ *
12054
+ * @type {Array<VFileMessage>}
12055
+ */
12056
+ this.messages = [];
12057
+ /**
12058
+ * Raw value.
12059
+ *
12060
+ * @type {Value}
12061
+ */
12062
+ this.value;
12063
+ /**
12064
+ * Source map.
12065
+ *
12066
+ * This type is equivalent to the `RawSourceMap` type from the `source-map`
12067
+ * module.
12068
+ *
12069
+ * @type {Map | null | undefined}
12070
+ */
12071
+ this.map;
12072
+ /**
12073
+ * Custom, non-string, compiled, representation.
12074
+ *
12075
+ * This is used by unified to store non-string results.
12076
+ * One example is when turning markdown into React nodes.
12077
+ *
12078
+ * @type {unknown}
12079
+ */
12080
+ this.result;
12081
+ /**
12082
+ * Whether a file was saved to disk.
12083
+ *
12084
+ * This is used by vfile reporters.
12085
+ *
12086
+ * @type {boolean}
12087
+ */
12088
+ this.stored;
12089
+ let index = -1;
12090
+ while (++index < order.length) {
12091
+ const field = order[index];
12092
+ if (field in options && options[field] !== void 0 && options[field] !== null) this[field] = field === "history" ? [...options[field]] : options[field];
12093
+ }
12094
+ /** @type {string} */
12095
+ let field;
12096
+ for (field in options) if (!order.includes(field)) this[field] = options[field];
12097
+ }
12098
+ /**
12099
+ * Get the basename (including extname) (example: `'index.min.js'`).
12100
+ *
12101
+ * @returns {string | undefined}
12102
+ * Basename.
12103
+ */
12104
+ get basename() {
12105
+ return typeof this.path === "string" ? node_path$1.default.basename(this.path) : void 0;
12106
+ }
12107
+ /**
12108
+ * Set basename (including extname) (`'index.min.js'`).
12109
+ *
12110
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12111
+ * on windows).
12112
+ * Cannot be nullified (use `file.path = file.dirname` instead).
12113
+ *
12114
+ * @param {string} basename
12115
+ * Basename.
12116
+ * @returns {undefined}
12117
+ * Nothing.
12118
+ */
12119
+ set basename(basename) {
12120
+ assertNonEmpty(basename, "basename");
12121
+ assertPart(basename, "basename");
12122
+ this.path = node_path$1.default.join(this.dirname || "", basename);
12123
+ }
12124
+ /**
12125
+ * Get the parent path (example: `'~'`).
12126
+ *
12127
+ * @returns {string | undefined}
12128
+ * Dirname.
12129
+ */
12130
+ get dirname() {
12131
+ return typeof this.path === "string" ? node_path$1.default.dirname(this.path) : void 0;
12132
+ }
12133
+ /**
12134
+ * Set the parent path (example: `'~'`).
12135
+ *
12136
+ * Cannot be set if there’s no `path` yet.
12137
+ *
12138
+ * @param {string | undefined} dirname
12139
+ * Dirname.
12140
+ * @returns {undefined}
12141
+ * Nothing.
12142
+ */
12143
+ set dirname(dirname) {
12144
+ assertPath(this.basename, "dirname");
12145
+ this.path = node_path$1.default.join(dirname || "", this.basename);
12146
+ }
12147
+ /**
12148
+ * Get the extname (including dot) (example: `'.js'`).
12149
+ *
12150
+ * @returns {string | undefined}
12151
+ * Extname.
12152
+ */
12153
+ get extname() {
12154
+ return typeof this.path === "string" ? node_path$1.default.extname(this.path) : void 0;
12155
+ }
12156
+ /**
12157
+ * Set the extname (including dot) (example: `'.js'`).
12158
+ *
12159
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12160
+ * on windows).
12161
+ * Cannot be set if there’s no `path` yet.
12162
+ *
12163
+ * @param {string | undefined} extname
12164
+ * Extname.
12165
+ * @returns {undefined}
12166
+ * Nothing.
12167
+ */
12168
+ set extname(extname) {
12169
+ assertPart(extname, "extname");
12170
+ assertPath(this.dirname, "extname");
12171
+ if (extname) {
12172
+ if (extname.codePointAt(0) !== 46) throw new Error("`extname` must start with `.`");
12173
+ if (extname.includes(".", 1)) throw new Error("`extname` cannot contain multiple dots");
12174
+ }
12175
+ this.path = node_path$1.default.join(this.dirname, this.stem + (extname || ""));
12176
+ }
12177
+ /**
12178
+ * Get the full path (example: `'~/index.min.js'`).
12179
+ *
12180
+ * @returns {string}
12181
+ * Path.
12182
+ */
12183
+ get path() {
12184
+ return this.history[this.history.length - 1];
12185
+ }
12186
+ /**
12187
+ * Set the full path (example: `'~/index.min.js'`).
12188
+ *
12189
+ * Cannot be nullified.
12190
+ * You can set a file URL (a `URL` object with a `file:` protocol) which will
12191
+ * be turned into a path with `url.fileURLToPath`.
12192
+ *
12193
+ * @param {URL | string} path
12194
+ * Path.
12195
+ * @returns {undefined}
12196
+ * Nothing.
12197
+ */
12198
+ set path(path) {
12199
+ if (isUrl(path)) path = (0, node_url.fileURLToPath)(path);
12200
+ assertNonEmpty(path, "path");
12201
+ if (this.path !== path) this.history.push(path);
12202
+ }
12203
+ /**
12204
+ * Get the stem (basename w/o extname) (example: `'index.min'`).
12205
+ *
12206
+ * @returns {string | undefined}
12207
+ * Stem.
12208
+ */
12209
+ get stem() {
12210
+ return typeof this.path === "string" ? node_path$1.default.basename(this.path, this.extname) : void 0;
12211
+ }
12212
+ /**
12213
+ * Set the stem (basename w/o extname) (example: `'index.min'`).
12214
+ *
12215
+ * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`
12216
+ * on windows).
12217
+ * Cannot be nullified (use `file.path = file.dirname` instead).
12218
+ *
12219
+ * @param {string} stem
12220
+ * Stem.
12221
+ * @returns {undefined}
12222
+ * Nothing.
12223
+ */
12224
+ set stem(stem) {
12225
+ assertNonEmpty(stem, "stem");
12226
+ assertPart(stem, "stem");
12227
+ this.path = node_path$1.default.join(this.dirname || "", stem + (this.extname || ""));
12228
+ }
12229
+ /**
12230
+ * Create a fatal message for `reason` associated with the file.
12231
+ *
12232
+ * The `fatal` field of the message is set to `true` (error; file not usable)
12233
+ * and the `file` field is set to the current file path.
12234
+ * The message is added to the `messages` field on `file`.
12235
+ *
12236
+ * > 🪦 **Note**: also has obsolete signatures.
12237
+ *
12238
+ * @overload
12239
+ * @param {string} reason
12240
+ * @param {MessageOptions | null | undefined} [options]
12241
+ * @returns {never}
12242
+ *
12243
+ * @overload
12244
+ * @param {string} reason
12245
+ * @param {Node | NodeLike | null | undefined} parent
12246
+ * @param {string | null | undefined} [origin]
12247
+ * @returns {never}
12248
+ *
12249
+ * @overload
12250
+ * @param {string} reason
12251
+ * @param {Point | Position | null | undefined} place
12252
+ * @param {string | null | undefined} [origin]
12253
+ * @returns {never}
12254
+ *
12255
+ * @overload
12256
+ * @param {string} reason
12257
+ * @param {string | null | undefined} [origin]
12258
+ * @returns {never}
12259
+ *
12260
+ * @overload
12261
+ * @param {Error | VFileMessage} cause
12262
+ * @param {Node | NodeLike | null | undefined} parent
12263
+ * @param {string | null | undefined} [origin]
12264
+ * @returns {never}
12265
+ *
12266
+ * @overload
12267
+ * @param {Error | VFileMessage} cause
12268
+ * @param {Point | Position | null | undefined} place
12269
+ * @param {string | null | undefined} [origin]
12270
+ * @returns {never}
12271
+ *
12272
+ * @overload
12273
+ * @param {Error | VFileMessage} cause
12274
+ * @param {string | null | undefined} [origin]
12275
+ * @returns {never}
12276
+ *
12277
+ * @param {Error | VFileMessage | string} causeOrReason
12278
+ * Reason for message, should use markdown.
12279
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12280
+ * Configuration (optional).
12281
+ * @param {string | null | undefined} [origin]
12282
+ * Place in code where the message originates (example:
12283
+ * `'my-package:my-rule'` or `'my-rule'`).
12284
+ * @returns {never}
12285
+ * Never.
12286
+ * @throws {VFileMessage}
12287
+ * Message.
12288
+ */
12289
+ fail(causeOrReason, optionsOrParentOrPlace, origin) {
12290
+ const message = this.message(causeOrReason, optionsOrParentOrPlace, origin);
12291
+ message.fatal = true;
12292
+ throw message;
12293
+ }
12294
+ /**
12295
+ * Create an info message for `reason` associated with the file.
12296
+ *
12297
+ * The `fatal` field of the message is set to `undefined` (info; change
12298
+ * likely not needed) and the `file` field is set to the current file path.
12299
+ * The message is added to the `messages` field on `file`.
12300
+ *
12301
+ * > 🪦 **Note**: also has obsolete signatures.
12302
+ *
12303
+ * @overload
12304
+ * @param {string} reason
12305
+ * @param {MessageOptions | null | undefined} [options]
12306
+ * @returns {VFileMessage}
12307
+ *
12308
+ * @overload
12309
+ * @param {string} reason
12310
+ * @param {Node | NodeLike | null | undefined} parent
12311
+ * @param {string | null | undefined} [origin]
12312
+ * @returns {VFileMessage}
12313
+ *
12314
+ * @overload
12315
+ * @param {string} reason
12316
+ * @param {Point | Position | null | undefined} place
12317
+ * @param {string | null | undefined} [origin]
12318
+ * @returns {VFileMessage}
12319
+ *
12320
+ * @overload
12321
+ * @param {string} reason
12322
+ * @param {string | null | undefined} [origin]
12323
+ * @returns {VFileMessage}
12324
+ *
12325
+ * @overload
12326
+ * @param {Error | VFileMessage} cause
12327
+ * @param {Node | NodeLike | null | undefined} parent
12328
+ * @param {string | null | undefined} [origin]
12329
+ * @returns {VFileMessage}
12330
+ *
12331
+ * @overload
12332
+ * @param {Error | VFileMessage} cause
12333
+ * @param {Point | Position | null | undefined} place
12334
+ * @param {string | null | undefined} [origin]
12335
+ * @returns {VFileMessage}
12336
+ *
12337
+ * @overload
12338
+ * @param {Error | VFileMessage} cause
12339
+ * @param {string | null | undefined} [origin]
12340
+ * @returns {VFileMessage}
12341
+ *
12342
+ * @param {Error | VFileMessage | string} causeOrReason
12343
+ * Reason for message, should use markdown.
12344
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12345
+ * Configuration (optional).
12346
+ * @param {string | null | undefined} [origin]
12347
+ * Place in code where the message originates (example:
12348
+ * `'my-package:my-rule'` or `'my-rule'`).
12349
+ * @returns {VFileMessage}
12350
+ * Message.
12351
+ */
12352
+ info(causeOrReason, optionsOrParentOrPlace, origin) {
12353
+ const message = this.message(causeOrReason, optionsOrParentOrPlace, origin);
12354
+ message.fatal = void 0;
12355
+ return message;
12356
+ }
12357
+ /**
12358
+ * Create a message for `reason` associated with the file.
12359
+ *
12360
+ * The `fatal` field of the message is set to `false` (warning; change may be
12361
+ * needed) and the `file` field is set to the current file path.
12362
+ * The message is added to the `messages` field on `file`.
12363
+ *
12364
+ * > 🪦 **Note**: also has obsolete signatures.
12365
+ *
12366
+ * @overload
12367
+ * @param {string} reason
12368
+ * @param {MessageOptions | null | undefined} [options]
12369
+ * @returns {VFileMessage}
12370
+ *
12371
+ * @overload
12372
+ * @param {string} reason
12373
+ * @param {Node | NodeLike | null | undefined} parent
12374
+ * @param {string | null | undefined} [origin]
12375
+ * @returns {VFileMessage}
12376
+ *
12377
+ * @overload
12378
+ * @param {string} reason
12379
+ * @param {Point | Position | null | undefined} place
12380
+ * @param {string | null | undefined} [origin]
12381
+ * @returns {VFileMessage}
12382
+ *
12383
+ * @overload
12384
+ * @param {string} reason
12385
+ * @param {string | null | undefined} [origin]
12386
+ * @returns {VFileMessage}
12387
+ *
12388
+ * @overload
12389
+ * @param {Error | VFileMessage} cause
12390
+ * @param {Node | NodeLike | null | undefined} parent
12391
+ * @param {string | null | undefined} [origin]
12392
+ * @returns {VFileMessage}
12393
+ *
12394
+ * @overload
12395
+ * @param {Error | VFileMessage} cause
12396
+ * @param {Point | Position | null | undefined} place
12397
+ * @param {string | null | undefined} [origin]
12398
+ * @returns {VFileMessage}
12399
+ *
12400
+ * @overload
12401
+ * @param {Error | VFileMessage} cause
12402
+ * @param {string | null | undefined} [origin]
12403
+ * @returns {VFileMessage}
12404
+ *
12405
+ * @param {Error | VFileMessage | string} causeOrReason
12406
+ * Reason for message, should use markdown.
12407
+ * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
12408
+ * Configuration (optional).
12409
+ * @param {string | null | undefined} [origin]
12410
+ * Place in code where the message originates (example:
12411
+ * `'my-package:my-rule'` or `'my-rule'`).
12412
+ * @returns {VFileMessage}
12413
+ * Message.
12414
+ */
12415
+ message(causeOrReason, optionsOrParentOrPlace, origin) {
12416
+ const message = new VFileMessage(causeOrReason, optionsOrParentOrPlace, origin);
12417
+ if (this.path) {
12418
+ message.name = this.path + ":" + message.name;
12419
+ message.file = this.path;
12420
+ }
12421
+ message.fatal = false;
12422
+ this.messages.push(message);
12423
+ return message;
12424
+ }
12425
+ /**
12426
+ * Serialize the file.
12427
+ *
12428
+ * > **Note**: which encodings are supported depends on the engine.
12429
+ * > For info on Node.js, see:
12430
+ * > <https://nodejs.org/api/util.html#whatwg-supported-encodings>.
12431
+ *
12432
+ * @param {string | null | undefined} [encoding='utf8']
12433
+ * Character encoding to understand `value` as when it’s a `Uint8Array`
12434
+ * (default: `'utf-8'`).
12435
+ * @returns {string}
12436
+ * Serialized file.
12437
+ */
12438
+ toString(encoding) {
12439
+ if (this.value === void 0) return "";
12440
+ if (typeof this.value === "string") return this.value;
12441
+ return new TextDecoder(encoding || void 0).decode(this.value);
12442
+ }
12443
+ };
12444
+ /**
12445
+ * Assert that `part` is not a path (as in, does not contain `path.sep`).
12446
+ *
12447
+ * @param {string | null | undefined} part
12448
+ * File path part.
12449
+ * @param {string} name
12450
+ * Part name.
12451
+ * @returns {undefined}
12452
+ * Nothing.
12453
+ */
12454
+ function assertPart(part, name) {
12455
+ if (part && part.includes(node_path$1.default.sep)) throw new Error("`" + name + "` cannot be a path: did not expect `" + node_path$1.default.sep + "`");
12456
+ }
12457
+ /**
12458
+ * Assert that `part` is not empty.
12459
+ *
12460
+ * @param {string | undefined} part
12461
+ * Thing.
12462
+ * @param {string} name
12463
+ * Part name.
12464
+ * @returns {asserts part is string}
12465
+ * Nothing.
12466
+ */
12467
+ function assertNonEmpty(part, name) {
12468
+ if (!part) throw new Error("`" + name + "` cannot be empty");
12469
+ }
12470
+ /**
12471
+ * Assert `path` exists.
12472
+ *
12473
+ * @param {string | undefined} path
12474
+ * Path.
12475
+ * @param {string} name
12476
+ * Dependency name.
12477
+ * @returns {asserts path is string}
12478
+ * Nothing.
12479
+ */
12480
+ function assertPath(path, name) {
12481
+ if (!path) throw new Error("Setting `" + name + "` requires `path` to be set too");
12482
+ }
12483
+ /**
12484
+ * Assert `value` is an `Uint8Array`.
12485
+ *
12486
+ * @param {unknown} value
12487
+ * thing.
12488
+ * @returns {value is Uint8Array}
12489
+ * Whether `value` is an `Uint8Array`.
12490
+ */
12491
+ function isUint8Array(value) {
12492
+ return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value);
12493
+ }
12494
+ //#endregion
11624
12495
  //#region src/plugins/content/pipeline/frontmatter.ts
11625
12496
  /**
11626
12497
  * @file content pipeline — frontmatter parsing.
@@ -11666,15 +12537,45 @@ function parseFrontmatter(raw, config) {
11666
12537
  };
11667
12538
  }
11668
12539
  //#endregion
12540
+ //#region src/plugins/content/pipeline/embed-facade.tsx
12541
+ /** CSS class on the facade's activation button. */
12542
+ const EMBED_BUTTON_CLASS = "lazy-embed-button";
12543
+ /** CSS class on the title span inside the activation button. */
12544
+ const EMBED_TITLE_CLASS = "lazy-embed-title";
12545
+ /**
12546
+ * Default `::embed` facade inner content: a single labelled `<button>` carrying
12547
+ * the embed title. The companion `lazyEmbed` island activates the embed on a
12548
+ * click anywhere in the facade, so the button is the keyboard-accessible
12549
+ * control. Provided as the default and as a composable building block for custom
12550
+ * facades.
12551
+ *
12552
+ * @param props - The embed facade props (only `title` is used by the default).
12553
+ * @returns The facade inner-content VNode.
12554
+ * @example
12555
+ * ```tsx
12556
+ * // Compose the default inside a richer custom facade:
12557
+ * const MyFacade = (p: EmbedFacadeProps) => (
12558
+ * <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
12559
+ * );
12560
+ * ```
12561
+ */
12562
+ function EmbedFacadeButton(props) {
12563
+ return /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("button", {
12564
+ type: "button",
12565
+ class: EMBED_BUTTON_CLASS,
12566
+ "aria-label": `Load embed: ${props.title}`,
12567
+ children: /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("span", {
12568
+ class: EMBED_TITLE_CLASS,
12569
+ children: props.title
12570
+ })
12571
+ });
12572
+ }
12573
+ //#endregion
11669
12574
  //#region src/plugins/content/pipeline/embed.ts
11670
12575
  /** CSS class on the `<figure>` facade wrapping each embed. */
11671
12576
  const EMBED_FIGURE_CLASS = "lazy-embed";
11672
12577
  /** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
11673
12578
  const EMBED_COMPONENT_NAME = "lazy-embed";
11674
- /** CSS class on the facade's activation button. */
11675
- const EMBED_BUTTON_CLASS = "lazy-embed-button";
11676
- /** CSS class on the title span inside the activation button. */
11677
- const EMBED_TITLE_CLASS = "lazy-embed-title";
11678
12579
  /**
11679
12580
  * Type guard for an `::embed` leaf directive.
11680
12581
  *
@@ -11702,83 +12603,402 @@ function escapeAttribute(value) {
11702
12603
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
11703
12604
  }
11704
12605
  /**
11705
- * Validate an embed `src` URL: only `https:`/`http:` absolute URLs and
11706
- * root-relative paths are embeddable anything else (`javascript:`, `data:`,
11707
- * scheme-relative, …) fails the build.
12606
+ * Validate an embed `src`. Three forms are embeddable: an `http(s)` URL, a
12607
+ * root-relative path (`/x`), or a co-located relative path (`./x`, `../x`,
12608
+ * `x/…`) resolved later against `/<slug>/`. Everything else — protocol-relative
12609
+ * (`//host`), `javascript:`, `data:`, any other scheme — is rejected.
11708
12610
  *
11709
12611
  * @param src - The raw `src` attribute value.
11710
- * @returns `true` when the URL is embeddable.
12612
+ * @returns `true` when the URL/path is embeddable.
11711
12613
  * @example
11712
12614
  * ```ts
11713
12615
  * isEmbeddableUrl("https://game.example.com/"); // true
12616
+ * isEmbeddableUrl("./game/index.html"); // true (co-located)
11714
12617
  * isEmbeddableUrl("javascript:alert(1)"); // false
11715
12618
  * ```
11716
12619
  */
11717
12620
  function isEmbeddableUrl(src) {
11718
- if (src.startsWith("/") && !src.startsWith("//")) return true;
11719
- return /^https?:\/\//i.test(src);
12621
+ if (src === "") return false;
12622
+ if (src.startsWith("//")) return false;
12623
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src)) return /^https?:\/\//i.test(src);
12624
+ return true;
11720
12625
  }
11721
12626
  /**
11722
- * Build the static facade HTML for one embed: the `<figure>` carrying the
11723
- * target in data attributes plus the activation `<button>` (the button label
11724
- * is the embed's title; visual chrome is consumer CSS).
12627
+ * Parse + validate the optional `width`/`height` directive attributes. Both
12628
+ * must be supplied together, each a positive integer count of pixels; the pair
12629
+ * is used to reserve the facade box at its true aspect ratio. Returns
12630
+ * `undefined` when neither is set.
11725
12631
  *
11726
- * @param src - The validated embed URL.
11727
- * @param title - The human-readable embed title (button label, iframe title).
12632
+ * @param width - Raw `width` attribute (or undefined).
12633
+ * @param height - Raw `height` attribute (or undefined).
12634
+ * @returns The parsed dimensions, or `undefined` when both are absent.
12635
+ * @throws {Error} When only one of the pair is set, or a value is not a
12636
+ * positive integer.
12637
+ * @example
12638
+ * ```ts
12639
+ * parseDimensions("400", "711"); // { width: 400, height: 711 }
12640
+ * parseDimensions(undefined, undefined); // undefined
12641
+ * ```
12642
+ */
12643
+ function parseDimensions(width, height) {
12644
+ const hasWidth = width !== void 0 && width !== null && width !== "";
12645
+ const hasHeight = height !== void 0 && height !== null && height !== "";
12646
+ if (!hasWidth && !hasHeight) return void 0;
12647
+ if (!hasWidth || !hasHeight) throw new Error("[web] content: `::embed` width and height must be set together (got only one).");
12648
+ if (!/^\d+$/.test(width) || !/^\d+$/.test(height) || width === "0" || height === "0") throw new Error(`[web] content: \`::embed\` width/height must be positive integers in pixels (got "${width}"×"${height}").`);
12649
+ return {
12650
+ width: Number(width),
12651
+ height: Number(height)
12652
+ };
12653
+ }
12654
+ /**
12655
+ * Collect the directive's raw attribute bag into a plain string record, dropping
12656
+ * `null`/`undefined` values (so a custom facade can read arbitrary extra options).
12657
+ *
12658
+ * @param attributes - The raw directive attributes (or undefined).
12659
+ * @returns A string-valued attribute record.
12660
+ * @example
12661
+ * ```ts
12662
+ * collectAttributes({ src: "x", poster: "/p.jpg", flag: null }); // { src: "x", poster: "/p.jpg" }
12663
+ * ```
12664
+ */
12665
+ function collectAttributes$1(attributes) {
12666
+ const out = {};
12667
+ for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
12668
+ return out;
12669
+ }
12670
+ /**
12671
+ * Build the static facade HTML for one embed: the framework-owned `<figure>`
12672
+ * (island hooks in data attributes; optional reserved-box `aspect-ratio`/`max-width`
12673
+ * inline style when dimensions are given) wrapping the facade component's inner
12674
+ * content, SSR'd to static markup. The wrapper carries `data-embed-src` (raw —
12675
+ * the provider resolves a relative src) so neither the island contract nor the
12676
+ * URL rewrite depend on the consumer's markup.
12677
+ *
12678
+ * @param facade - The facade component (default {@link EmbedFacadeButton}).
12679
+ * @param props - The facade props (`src`, `title`, optional `width`/`height`, raw `attributes`).
12680
+ * @param dimensions - Optional reserved-box pixel dimensions.
11728
12681
  * @returns The facade HTML string.
11729
12682
  * @example
11730
12683
  * ```ts
11731
- * embedFacadeHtml("https://game.example.com/", "My Game");
12684
+ * embedFacadeHtml(EmbedFacadeButton, { src: "https://g/", title: "G", attributes: {} });
11732
12685
  * ```
11733
12686
  */
11734
- function embedFacadeHtml(src, title) {
11735
- const safeSource = escapeAttribute(src);
11736
- const safeTitle = escapeAttribute(title);
11737
- return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${safeSource}" data-embed-title="${safeTitle}"><button type="button" class="${EMBED_BUTTON_CLASS}" aria-label="Load embed: ${safeTitle}"><span class="${EMBED_TITLE_CLASS}">${safeTitle}</span></button></figure>`;
12687
+ function embedFacadeHtml(facade, props, dimensions) {
12688
+ return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${escapeAttribute(props.src)}" data-embed-title="${escapeAttribute(props.title)}"${dimensions ? ` data-embed-width="${dimensions.width}" data-embed-height="${dimensions.height}" style="aspect-ratio: ${dimensions.width} / ${dimensions.height}; max-width: ${dimensions.width}px;"` : ""}>${(0, preact_render_to_string.renderToString)((0, preact.h)(facade, props))}</figure>`;
12689
+ }
12690
+ /**
12691
+ * Normalize the provider's `embed` config value (`boolean | options`) to a plain
12692
+ * {@link EmbedOptions} object for the transform factory.
12693
+ *
12694
+ * @param embed - The raw `FileSystemContentOptions.embed` value (truthy).
12695
+ * @returns The options object (`{}` for the bare `true` form).
12696
+ * @example
12697
+ * ```ts
12698
+ * normalizeEmbedOptions(true); // {}
12699
+ * normalizeEmbedOptions({ facade: MyFacade });
12700
+ * ```
12701
+ */
12702
+ function normalizeEmbedOptions(embed) {
12703
+ return typeof embed === "boolean" ? {} : embed;
11738
12704
  }
11739
12705
  /**
11740
12706
  * Mdast transformer rewriting every `::embed` leaf directive to its facade
11741
- * HTML node. A directive missing `src`/`title`, or carrying a non-embeddable
11742
- * URL, fails the build with the offending value quoted.
12707
+ * HTML node. A directive missing `src`/`title`, carrying a non-embeddable URL,
12708
+ * or carrying invalid `width`/`height`, fails the build with the offending
12709
+ * value quoted.
11743
12710
  *
12711
+ * @param facade - The facade component to render the inner content with.
11744
12712
  * @param tree - The mdast tree to mutate.
11745
- * @throws {Error} When an `::embed` directive is missing `src` or `title`, or
11746
- * its `src` is not an embeddable URL.
12713
+ * @throws {Error} When an `::embed` directive is missing `src` or `title`, its
12714
+ * `src` is not embeddable, or its dimensions are invalid.
11747
12715
  * @example
11748
12716
  * ```ts
11749
- * embedTransform(tree);
12717
+ * embedTransform(EmbedFacadeButton, tree);
11750
12718
  * ```
11751
12719
  */
11752
- function embedTransform(tree) {
12720
+ function embedTransform(facade, tree) {
11753
12721
  (0, unist_util_visit.visit)(tree, (node, index, parent) => {
11754
12722
  if (!isEmbedDirective(node)) return;
11755
12723
  if (parent === void 0 || index === void 0) return;
11756
12724
  const src = node.attributes?.src ?? "";
11757
12725
  const title = node.attributes?.title ?? "";
11758
12726
  if (src === "" || title === "") throw new Error("[web] content: `::embed` requires both `src` and `title` attributes, e.g. ::embed{src=\"https://…\" title=\"…\"}.");
11759
- if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL or a root-relative path (got "${src}").`);
12727
+ if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL, a root-relative path, or a co-located relative path (got "${src}").`);
12728
+ const dimensions = parseDimensions(node.attributes?.width, node.attributes?.height);
11760
12729
  const html = {
11761
12730
  type: "html",
11762
- value: embedFacadeHtml(src, title)
12731
+ value: embedFacadeHtml(facade, {
12732
+ src,
12733
+ title,
12734
+ ...dimensions ? {
12735
+ width: dimensions.width,
12736
+ height: dimensions.height
12737
+ } : {},
12738
+ attributes: collectAttributes$1(node.attributes)
12739
+ }, dimensions)
11763
12740
  };
11764
12741
  parent.children[index] = html;
11765
12742
  });
11766
12743
  }
11767
12744
  /**
11768
- * Remark transform: rewrites `::embed{src="…" title="…"}` leaf directives into
11769
- * static click-to-activate facades (no iframe until the reader clicks — see
12745
+ * Remark transform factory: rewrites `::embed{src="…" title="…"}` leaf directives
12746
+ * into static click-to-activate facades (no iframe until the reader clicks — see
11770
12747
  * the file header). Opt-in via the provider's `embed` option; requires
11771
- * `trustedContent: true` because the facade is raw HTML the sanitize pass
11772
- * would strip.
12748
+ * `trustedContent: true` because the facade is raw HTML the sanitize pass would
12749
+ * strip. The facade's inner content is rendered by `options.facade` (a consumer
12750
+ * Preact component) or the built-in {@link EmbedFacadeButton}.
11773
12751
  *
12752
+ * @param options - Embed options (the optional `facade` component).
11774
12753
  * @returns An mdast tree transformer.
11775
12754
  * @example
11776
12755
  * ```ts
11777
- * unified().use(embedPlugin);
12756
+ * unified().use(embedPlugin, { facade: MyFacade });
11778
12757
  * ```
11779
12758
  */
11780
- function embedPlugin() {
11781
- return embedTransform;
12759
+ function embedPlugin(options = {}) {
12760
+ const facade = options.facade ?? EmbedFacadeButton;
12761
+ return (tree) => embedTransform(facade, tree);
12762
+ }
12763
+ //#endregion
12764
+ //#region src/plugins/content/pipeline/gallery-default.tsx
12765
+ /** CSS class on the gallery's slide track. */
12766
+ const GALLERY_TRACK_CLASS = "gallery-track";
12767
+ /**
12768
+ * Default `::gallery` inner content: a single track holding every slide `<img>`
12769
+ * in folder order. A companion gallery island (consumer-provided) can enhance
12770
+ * the track with swipe/keyboard/lightbox; with no island and no CSS it is still
12771
+ * a plain horizontally-scrollable image strip. Provided as the default and as a
12772
+ * composable building block for custom galleries.
12773
+ *
12774
+ * @param props - The gallery props (the resolved `slides`).
12775
+ * @returns The gallery inner-content VNode.
12776
+ * @example
12777
+ * ```tsx
12778
+ * // Compose the default inside a richer custom gallery:
12779
+ * const MyGallery = (p: GalleryProps) => (
12780
+ * <figure><GalleryTrack {...p} /><figcaption>{p.caption}</figcaption></figure>
12781
+ * );
12782
+ * ```
12783
+ */
12784
+ function GalleryTrack(props) {
12785
+ return /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("div", {
12786
+ class: GALLERY_TRACK_CLASS,
12787
+ "data-gallery-track": true,
12788
+ children: props.slides.map((slide) => /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("img", {
12789
+ src: slide.src,
12790
+ alt: slide.alt
12791
+ }, slide.src))
12792
+ });
12793
+ }
12794
+ //#endregion
12795
+ //#region src/plugins/content/pipeline/gallery.ts
12796
+ /**
12797
+ * @file content pipeline — `::gallery` folder galleries.
12798
+ *
12799
+ * Rewrites `::gallery{src="./images/dir/" caption="…"}` leaf directives into a
12800
+ * static swipeable image set at the mdast stage (BEFORE the remark-rehype bridge):
12801
+ * a framework-owned `<div class="gallery" data-component="gallery">` carrying the
12802
+ * island hook, wrapping inner content rendered (at build time, to static markup)
12803
+ * by a Preact component — the built-in {@link GalleryTrack} by default, or a
12804
+ * consumer component via `gallery.component`.
12805
+ *
12806
+ * Unlike `::embed` (one src resolved later by the provider), a gallery's `src` is a
12807
+ * co-located FOLDER that must be listed at build time, which needs the article's
12808
+ * source path. The provider supplies the article `slug` via the VFile `data`
12809
+ * (providers.ts), and the `contentDir` is bound at pipeline-build time — so this
12810
+ * transform reads `<contentDir>/<slug>/<src>` from disk, sorts its images, and
12811
+ * resolves each to its shared `/<slug>/<dir>/<file>` URL (identical from every
12812
+ * locale page, mirroring co-located images). The companion gallery SPA island
12813
+ * (consumer-provided) wires swipe/keyboard/lightbox on `[data-component="gallery"]`.
12814
+ */
12815
+ /** CSS class on the `<div>` wrapping each gallery. */
12816
+ const GALLERY_WRAPPER_CLASS = "gallery";
12817
+ /** `data-component` name binding the gallery to its SPA island. */
12818
+ const GALLERY_COMPONENT_NAME = "gallery";
12819
+ /** Image file extensions a gallery folder expands over. */
12820
+ const IMAGE_EXTENSIONS = new Set([
12821
+ ".webp",
12822
+ ".jpg",
12823
+ ".jpeg",
12824
+ ".png",
12825
+ ".gif",
12826
+ ".avif"
12827
+ ]);
12828
+ /**
12829
+ * Type guard for a `::gallery` leaf directive.
12830
+ *
12831
+ * @param node - AST node to test.
12832
+ * @returns `true` when the node is a `::gallery` leaf directive.
12833
+ * @example
12834
+ * ```ts
12835
+ * if (isGalleryDirective(node)) console.log(node.attributes?.src);
12836
+ * ```
12837
+ */
12838
+ function isGalleryDirective(node) {
12839
+ return node.type === "leafDirective" && node.name === "gallery";
12840
+ }
12841
+ /**
12842
+ * Resolve `.`/`..` segments of a path built from `slug/src/file` into the single
12843
+ * shared absolute URL the content-assets build phase copies the folder to.
12844
+ *
12845
+ * @param slug - Article directory name.
12846
+ * @param src - The directive `src` (co-located relative folder, e.g. `./images/dir/`).
12847
+ * @param file - One image file name inside the folder.
12848
+ * @returns The shared absolute slide URL (`/<slug>/<dir>/<file>`).
12849
+ * @example
12850
+ * ```ts
12851
+ * slideUrl("post", "./images/mk/", "a.webp"); // "/post/images/mk/a.webp"
12852
+ * ```
12853
+ */
12854
+ function slideUrl(slug, src, file) {
12855
+ const resolved = [];
12856
+ for (const segment of `${slug}/${src}/${file}`.split("/")) {
12857
+ if (segment === "" || segment === ".") continue;
12858
+ if (segment === "..") resolved.pop();
12859
+ else resolved.push(segment);
12860
+ }
12861
+ return `/${resolved.join("/")}`;
12862
+ }
12863
+ /**
12864
+ * Read a gallery folder from disk and build its sorted slide list. Each slide
12865
+ * gets the directive `caption` plus a ` · N` index suffix as alt (or just `N`).
12866
+ *
12867
+ * @param contentDir - The provider's content directory.
12868
+ * @param slug - Article directory name (from the VFile data).
12869
+ * @param src - The directive `src` (co-located relative folder).
12870
+ * @param caption - The directive `caption` attribute (may be empty).
12871
+ * @returns The sorted slides.
12872
+ * @throws {Error} When the folder is missing or holds no images.
12873
+ * @example
12874
+ * ```ts
12875
+ * resolveSlides("./content", "post", "./images/mk/", "Our game");
12876
+ * ```
12877
+ */
12878
+ function resolveSlides(contentDir, slug, src, caption) {
12879
+ const folder = node_path$1.default.join(contentDir, slug, src);
12880
+ let entries;
12881
+ try {
12882
+ entries = (0, node_fs.readdirSync)(folder);
12883
+ } catch {
12884
+ throw new Error(`[web] content: \`::gallery\` folder not found: "${src}" (looked in ${folder}).`);
12885
+ }
12886
+ const files = entries.filter((name) => IMAGE_EXTENSIONS.has(node_path$1.default.extname(name).toLowerCase())).toSorted((a, b) => a.localeCompare(b, "en"));
12887
+ if (files.length === 0) throw new Error(`[web] content: \`::gallery\` folder has no images: "${src}" (${folder}).`);
12888
+ return files.map((file, index) => ({
12889
+ src: slideUrl(slug, src, file),
12890
+ alt: caption ? `${caption} · ${index + 1}` : `${index + 1}`
12891
+ }));
12892
+ }
12893
+ /**
12894
+ * Collect the directive's raw attribute bag into a plain string record, dropping
12895
+ * `null`/`undefined` values (so a custom component can read arbitrary extra options).
12896
+ *
12897
+ * @param attributes - The raw directive attributes (or undefined).
12898
+ * @returns A string-valued attribute record.
12899
+ * @example
12900
+ * ```ts
12901
+ * collectAttributes({ src: "x", layout: "dots", flag: null }); // { src: "x", layout: "dots" }
12902
+ * ```
12903
+ */
12904
+ function collectAttributes(attributes) {
12905
+ const out = {};
12906
+ for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
12907
+ return out;
12908
+ }
12909
+ /**
12910
+ * Build the static gallery HTML for one directive: the framework-owned `<div>`
12911
+ * (island hook in `data-component`) wrapping the component's inner content, SSR'd
12912
+ * to static markup.
12913
+ *
12914
+ * @param component - The gallery component (default {@link GalleryTrack}).
12915
+ * @param slides - The resolved slides.
12916
+ * @param caption - The directive `caption` attribute.
12917
+ * @param attributes - The raw directive attribute bag.
12918
+ * @returns The gallery HTML string.
12919
+ * @example
12920
+ * ```ts
12921
+ * galleryHtml(GalleryTrack, slides, "Our game", { src: "./images/mk/" });
12922
+ * ```
12923
+ */
12924
+ function galleryHtml(component, slides, caption, attributes) {
12925
+ return `<div class="${GALLERY_WRAPPER_CLASS}" data-component="${GALLERY_COMPONENT_NAME}">${(0, preact_render_to_string.renderToString)((0, preact.h)(component, {
12926
+ slides,
12927
+ caption,
12928
+ attributes
12929
+ }))}</div>`;
12930
+ }
12931
+ /**
12932
+ * Mdast transformer rewriting every `::gallery` leaf directive to its gallery
12933
+ * HTML node. A directive missing `src`, or pointing at a missing/empty folder,
12934
+ * fails the build with the offending value quoted. Skipped entirely when the
12935
+ * VFile carries no `slug` (the standalone `render()` path has no article context).
12936
+ *
12937
+ * @param options - Resolved transform options (component + contentDir).
12938
+ * @param tree - The mdast tree to mutate.
12939
+ * @param file - The VFile (its `data.slug` locates the article on disk).
12940
+ * @throws {Error} When a `::gallery` directive is missing `src`, or its folder is
12941
+ * missing/empty.
12942
+ * @example
12943
+ * ```ts
12944
+ * galleryTransform({ component: GalleryTrack, contentDir: "./content" }, tree, file);
12945
+ * ```
12946
+ */
12947
+ function galleryTransform(options, tree, file) {
12948
+ const slug = typeof file.data.slug === "string" ? file.data.slug : void 0;
12949
+ const component = options.component ?? GalleryTrack;
12950
+ (0, unist_util_visit.visit)(tree, (node, index, parent) => {
12951
+ if (!isGalleryDirective(node)) return;
12952
+ if (parent === void 0 || index === void 0) return;
12953
+ if (slug === void 0) return;
12954
+ const src = node.attributes?.src ?? "";
12955
+ if (src === "") throw new Error("[web] content: `::gallery` requires a `src` folder, e.g. ::gallery{src=\"./images/dir/\"}.");
12956
+ const caption = node.attributes?.caption ?? "";
12957
+ const html = {
12958
+ type: "html",
12959
+ value: galleryHtml(component, resolveSlides(options.contentDir, slug, src, caption), caption, collectAttributes(node.attributes))
12960
+ };
12961
+ parent.children[index] = html;
12962
+ });
12963
+ }
12964
+ /**
12965
+ * Normalize the provider's `gallery` config value (`boolean | options`) plus the
12966
+ * provider `contentDir` into the resolved {@link GalleryTransformOptions} the
12967
+ * transform factory needs.
12968
+ *
12969
+ * @param gallery - The raw `FileSystemContentOptions.gallery` value (truthy).
12970
+ * @param contentDir - The provider's content directory.
12971
+ * @returns The resolved transform options.
12972
+ * @example
12973
+ * ```ts
12974
+ * normalizeGalleryOptions(true, "./content"); // { contentDir: "./content" }
12975
+ * normalizeGalleryOptions({ component: MyGallery }, "./content");
12976
+ * ```
12977
+ */
12978
+ function normalizeGalleryOptions(gallery, contentDir) {
12979
+ return typeof gallery === "boolean" ? { contentDir } : {
12980
+ ...gallery,
12981
+ contentDir
12982
+ };
12983
+ }
12984
+ /**
12985
+ * Remark transform factory: rewrites `::gallery{src="…"}` leaf directives into
12986
+ * static swipeable galleries (see the file header). Opt-in via the provider's
12987
+ * `gallery` option; requires `trustedContent: true` because the markup is raw HTML
12988
+ * the sanitize pass would strip. The inner content is rendered by
12989
+ * `options.component` (a consumer Preact component) or the built-in
12990
+ * {@link GalleryTrack}; folders are read from `options.contentDir` against the
12991
+ * per-article `slug` on the VFile.
12992
+ *
12993
+ * @param options - Resolved transform options (component + contentDir).
12994
+ * @returns An mdast tree transformer.
12995
+ * @example
12996
+ * ```ts
12997
+ * unified().use(galleryPlugin, { component: MyGallery, contentDir: "./content" });
12998
+ * ```
12999
+ */
13000
+ function galleryPlugin(options) {
13001
+ return (tree, file) => galleryTransform(options, tree, file);
11782
13002
  }
11783
13003
  //#endregion
11784
13004
  //#region src/plugins/content/pipeline/mermaid.ts
@@ -12089,7 +13309,8 @@ function defaultRemarkPlugins(config) {
12089
13309
  remark_directive.default,
12090
13310
  pullQuotePlugin
12091
13311
  ];
12092
- if (config?.embed) plugins.push(embedPlugin);
13312
+ if (config?.embed) plugins.push([embedPlugin, normalizeEmbedOptions(config.embed)]);
13313
+ if (config?.gallery) plugins.push([galleryPlugin, normalizeGalleryOptions(config.gallery, config.contentDir)]);
12093
13314
  if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
12094
13315
  plugins.push([remark_rehype.default, { allowDangerousHtml: true }]);
12095
13316
  return plugins;
@@ -12331,6 +13552,8 @@ function calculateReadingTime(text) {
12331
13552
  */
12332
13553
  /** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
12333
13554
  const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
13555
+ /** Matches the `data-embed-src` of an `::embed` facade (value captured for path resolution). */
13556
+ const EMBED_SRC_ATTR = /(\bdata-embed-src=")([^"]*)(")/g;
12334
13557
  /**
12335
13558
  * Build a canonical article URL for a locale + slug.
12336
13559
  *
@@ -12361,6 +13584,56 @@ function rewriteImageUrls(html, slug) {
12361
13584
  return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
12362
13585
  }
12363
13586
  /**
13587
+ * Resolve an `::embed` `src` to the URL the iframe should load. Absolute targets
13588
+ * (`http(s)://…`, root-relative `/…`) pass through unchanged; a co-located
13589
+ * relative path (`./game/index.html`, `../x`, `game/x`) is resolved against the
13590
+ * article base `/<slug>/` into the single shared absolute path the content-assets
13591
+ * build phase copies the bundle to — so it loads identically from every locale
13592
+ * page (mirroring how co-located images resolve). Any `?query`/`#hash` is
13593
+ * preserved verbatim.
13594
+ *
13595
+ * @param value - The raw `data-embed-src` value.
13596
+ * @param slug - Article directory name.
13597
+ * @returns The resolved embed URL.
13598
+ * @example
13599
+ * ```ts
13600
+ * resolveEmbedSource("./game/index.html", "post"); // "/post/game/index.html"
13601
+ * resolveEmbedSource("https://x.dev/", "post"); // "https://x.dev/"
13602
+ * ```
13603
+ */
13604
+ function resolveEmbedSource(value, slug) {
13605
+ if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
13606
+ const tailIndex = value.search(/[?#]/);
13607
+ const rawPath = tailIndex === -1 ? value : value.slice(0, tailIndex);
13608
+ const tail = tailIndex === -1 ? "" : value.slice(tailIndex);
13609
+ const out = [];
13610
+ for (const segment of `${slug}/${rawPath}`.split("/")) {
13611
+ if (segment === "" || segment === ".") continue;
13612
+ if (segment === "..") {
13613
+ out.pop();
13614
+ continue;
13615
+ }
13616
+ out.push(segment);
13617
+ }
13618
+ const trailingSlash = rawPath === "" || rawPath.endsWith("/") ? "/" : "";
13619
+ return `/${out.join("/")}${trailingSlash}${tail}`;
13620
+ }
13621
+ /**
13622
+ * Rewrite every `::embed` facade's relative `data-embed-src` to its shared
13623
+ * absolute `/<slug>/…` path (no-op for already-absolute targets).
13624
+ *
13625
+ * @param html - The rendered article HTML.
13626
+ * @param slug - Article directory name.
13627
+ * @returns The HTML with embed `src`s resolved.
13628
+ * @example
13629
+ * ```ts
13630
+ * rewriteEmbedUrls('<figure data-embed-src="./g/">', "post"); // '… data-embed-src="/post/g/"'
13631
+ * ```
13632
+ */
13633
+ function rewriteEmbedUrls(html, slug) {
13634
+ return html.replaceAll(EMBED_SRC_ATTR, (_match, prefix, value, suffix) => `${prefix}${resolveEmbedSource(value, slug)}${suffix}`);
13635
+ }
13636
+ /**
12364
13637
  * Discover slug-like subdirectories of the content root (direct children not
12365
13638
  * starting with `.` or `_`), sorted alphabetically for deterministic ordering.
12366
13639
  *
@@ -12439,7 +13712,10 @@ function fileSystemContent(options) {
12439
13712
  state.dirtyPaths.delete(filePath);
12440
13713
  const { frontmatter, body } = parseFrontmatter(raw, options);
12441
13714
  const processor = ensureProcessor(state, options);
12442
- const html = rewriteImageUrls(String(await processor.process(body)), slug);
13715
+ const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(new VFile({
13716
+ value: body,
13717
+ data: { slug }
13718
+ }))), slug), slug);
12443
13719
  const { readingTime, wordCount } = calculateReadingTime(body);
12444
13720
  return {
12445
13721
  frontmatter,
@@ -12593,12 +13869,14 @@ Object.defineProperty(exports, "Deploy", {
12593
13869
  return types_exports$4;
12594
13870
  }
12595
13871
  });
13872
+ exports.EmbedFacadeButton = EmbedFacadeButton;
12596
13873
  Object.defineProperty(exports, "Env", {
12597
13874
  enumerable: true,
12598
13875
  get: function() {
12599
13876
  return types_exports$5;
12600
13877
  }
12601
13878
  });
13879
+ exports.GalleryTrack = GalleryTrack;
12602
13880
  Object.defineProperty(exports, "Head", {
12603
13881
  enumerable: true,
12604
13882
  get: function() {