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