@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/browser.d.mts +127 -6
- package/dist/browser.mjs +8 -3
- package/dist/index.cjs +1340 -62
- package/dist/index.d.cts +167 -6
- package/dist/index.d.mts +167 -6
- package/dist/index.mjs +1341 -65
- package/package.json +1 -1
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 `
|
|
1573
|
-
* `trustedContent: true`:
|
|
1574
|
-
* which the sanitize pass (the untrusted-content XSS
|
|
1575
|
-
* the combination can never work. Errors use the
|
|
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 `
|
|
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
|
|
4044
|
-
* (`<contentDir>/<slug
|
|
4045
|
-
*
|
|
4046
|
-
*
|
|
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
|
|
4052
|
-
*
|
|
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
|
|
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
|
|
4083
|
+
const articleDirectories = await readdir(contentDir, { withFileTypes: true });
|
|
4075
4084
|
let copied = 0;
|
|
4076
|
-
for (const
|
|
4077
|
-
if (!
|
|
4078
|
-
const
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
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):
|
|
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)
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11690
12591
|
}
|
|
11691
12592
|
/**
|
|
11692
|
-
* Validate an embed `src
|
|
11693
|
-
* root-relative
|
|
11694
|
-
*
|
|
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
|
|
11706
|
-
|
|
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
|
-
*
|
|
11710
|
-
*
|
|
11711
|
-
* is the
|
|
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
|
|
11714
|
-
* @param
|
|
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://
|
|
12671
|
+
* embedFacadeHtml(EmbedFacadeButton, { src: "https://g/", title: "G", attributes: {} });
|
|
11719
12672
|
* ```
|
|
11720
12673
|
*/
|
|
11721
|
-
function embedFacadeHtml(
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
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`,
|
|
11729
|
-
*
|
|
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`,
|
|
11733
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
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 };
|