@dogsbay/format-vitepress 0.2.0-beta.131 → 0.2.0-beta.133
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/README.md +9 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +15 -62
- package/dist/config-node.d.ts +20 -0
- package/dist/config-node.js +51 -0
- package/dist/config.d.ts +14 -20
- package/dist/config.js +7 -51
- package/dist/import.d.ts +28 -0
- package/dist/import.js +76 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +9 -4
- package/dist/loaders/folder.d.ts +31 -0
- package/dist/loaders/folder.js +90 -0
- package/dist/loaders/load.d.ts +49 -21
- package/dist/loaders/load.js +160 -235
- package/dist/migrate.d.ts +40 -0
- package/dist/migrate.js +83 -0
- package/dist/parse.js +5 -1
- package/dist/translate.d.ts +15 -1
- package/dist/translate.js +120 -55
- package/package.json +12 -4
- package/dist/literal.d.ts +0 -54
- package/dist/literal.js +0 -844
package/dist/loaders/load.d.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VitePress corpus discovery over a {@link Corpus} (no Node): find the project (the folder
|
|
3
|
+
* holding `.vitepress`), read its config, resolve `srcDir`, list the pages, and build nav. The
|
|
4
|
+
* CLI runs it over a folder (`folder.ts`), the playground over a map of files.
|
|
5
|
+
*
|
|
6
|
+
* Snippet / include resolution and asset-href rewriting are handed to the translator as
|
|
7
|
+
* closures over the corpus, so the translator itself stays I/O-free and unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* Paths are posix and relative to the corpus root. A corpus is someone else's repo: snippets,
|
|
10
|
+
* includes and `srcDir` stay inside the project, once links resolve (`Corpus.contains`).
|
|
11
|
+
*/
|
|
1
12
|
import type { NavItem } from "@dogsbay/types";
|
|
13
|
+
import type { Corpus } from "@dogsbay/corpus";
|
|
2
14
|
import { type VitePressConfig } from "../config.js";
|
|
3
15
|
import { linkToSlug } from "../nav.js";
|
|
4
16
|
import type { TranslateOptions } from "../translate.js";
|
|
5
17
|
export interface VitepressPageSource {
|
|
6
|
-
|
|
18
|
+
/** The page's file, relative to the corpus root. */
|
|
19
|
+
path: string;
|
|
7
20
|
slug: string;
|
|
8
21
|
}
|
|
22
|
+
/** An asset directory to mirror, relative to the corpus root (`ImportResult.assetMounts`). */
|
|
23
|
+
export interface VitepressAssetDir {
|
|
24
|
+
dir: string;
|
|
25
|
+
into?: string;
|
|
26
|
+
mediaOnly?: boolean;
|
|
27
|
+
}
|
|
9
28
|
export interface VitepressLoadResult {
|
|
10
|
-
/** Project root — the
|
|
29
|
+
/** Project root — the folder that CONTAINS `.vitepress` ("" for the corpus root). */
|
|
11
30
|
root: string;
|
|
12
31
|
/** Content root — `srcDir`, which page slugs are relative to. */
|
|
13
32
|
srcDir: string;
|
|
@@ -17,29 +36,33 @@ export interface VitepressLoadResult {
|
|
|
17
36
|
/** How nav was obtained, for the import summary. */
|
|
18
37
|
navSource: "sidebar" | "directory";
|
|
19
38
|
/** Directories to mirror as assets. */
|
|
20
|
-
assetDirs:
|
|
21
|
-
dir: string;
|
|
22
|
-
into?: string;
|
|
23
|
-
mediaOnly?: boolean;
|
|
24
|
-
}[];
|
|
39
|
+
assetDirs: VitepressAssetDir[];
|
|
25
40
|
warnings: string[];
|
|
26
41
|
}
|
|
27
42
|
export interface VitepressLoadOptions {
|
|
28
43
|
hrefPrefix?: string;
|
|
29
|
-
/**
|
|
30
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The config, already read: the CLI's `--eval-config` imports it (Node). Otherwise it is
|
|
46
|
+
* read statically from the corpus.
|
|
47
|
+
*/
|
|
48
|
+
config?: VitePressConfig;
|
|
49
|
+
/** Notes from reading the config (a failed `--eval-config`), kept with the loader's. */
|
|
50
|
+
warnings?: string[];
|
|
31
51
|
}
|
|
32
|
-
/**
|
|
33
|
-
* `.vitepress/config.*` is the only unambiguous marker. Requiring it is what
|
|
34
|
-
* lets this plugin sit safely in the registry among the other Markdown
|
|
35
|
-
* importers: it cannot claim a plain-Markdown, MkDocs or GitBook corpus.
|
|
36
|
-
*/
|
|
37
|
-
export declare function findVitepressDir(source: string): string | undefined;
|
|
38
|
-
export declare function detectVitepress(source: string): boolean;
|
|
39
52
|
/** `guide/install.md` → `guide/install`; `guide/index.md` → `guide/index`. */
|
|
40
53
|
export declare function pathToSlug(relPath: string): string;
|
|
41
|
-
/**
|
|
42
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Minimal glob matcher for `srcExclude` (`**\/*.md`, `todo.md`, `a/*.md`, `{a,b}`): `*` and
|
|
56
|
+
* `?` stay within a path segment, `**\/` is zero or more folders, `**` anything.
|
|
57
|
+
*
|
|
58
|
+
* Not a RegExp: the globs come from someone else's config, and a backtracking regex of
|
|
59
|
+
* `*a*a*a…b` or `**\/**\/…` took over a minute on one path. Matching is memoized over
|
|
60
|
+
* (glob position, path position), so it is polynomial in both. Returns `{ test }` so a
|
|
61
|
+
* caller can use it where it used a RegExp.
|
|
62
|
+
*/
|
|
63
|
+
export declare function globToRegExp(glob: string): {
|
|
64
|
+
test(path: string): boolean;
|
|
65
|
+
};
|
|
43
66
|
/**
|
|
44
67
|
* Extract a `#region` from a file.
|
|
45
68
|
*
|
|
@@ -56,7 +79,7 @@ export declare function extractRegion(content: string, region: string): string |
|
|
|
56
79
|
*/
|
|
57
80
|
export declare function dedent(text: string): string;
|
|
58
81
|
/** Per-page translate options (snippet resolution + asset rewriting). */
|
|
59
|
-
export declare function translateOptionsFor(page: VitepressPageSource, srcDir: string, root: string
|
|
82
|
+
export declare function translateOptionsFor(corpus: Corpus, page: VitepressPageSource, srcDir: string, root: string): TranslateOptions;
|
|
60
83
|
/**
|
|
61
84
|
* Translate options anchored at `dir`.
|
|
62
85
|
*
|
|
@@ -66,8 +89,13 @@ export declare function translateOptionsFor(page: VitepressPageSource, srcDir: s
|
|
|
66
89
|
* same-named sibling existed next to the page — silently, with no ledger
|
|
67
90
|
* entry — and dropped the include entirely when it did not.
|
|
68
91
|
*/
|
|
69
|
-
export declare function optionsForDir(
|
|
70
|
-
|
|
92
|
+
export declare function optionsForDir(corpus: Corpus, listed: Set<string>, dir: string, srcDir: string, root: string): TranslateOptions;
|
|
93
|
+
/** The VitePress project in `corpus`: its root and config file, or undefined. */
|
|
94
|
+
export declare function findVitepressProject(corpus: Corpus): {
|
|
95
|
+
root: string;
|
|
96
|
+
configPath: string;
|
|
97
|
+
} | undefined;
|
|
98
|
+
export declare function loadVitepressCorpus(corpus: Corpus, opts?: VitepressLoadOptions): VitepressLoadResult;
|
|
71
99
|
/** Used only by the fallback nav, where no page has been parsed yet. */
|
|
72
100
|
export declare function titleFromSlug(slug: string): string;
|
|
73
101
|
export { linkToSlug };
|
package/dist/loaders/load.js
CHANGED
|
@@ -1,77 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* `srcDir`, enumerate pages, and build nav.
|
|
4
|
-
*
|
|
5
|
-
* The only module in this package that touches the filesystem. Snippet /
|
|
6
|
-
* include resolution and asset-href rewriting are handed to the translator as
|
|
7
|
-
* closures so the translator itself stays I/O-free and unit-testable.
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
10
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
11
|
-
import { findConfigFile, readConfig, evalConfig } from "../config.js";
|
|
1
|
+
import { dirOf, isPlainPath, joinPath, normalizePath, vitepressConfigPath, vitepressPage, vitepressRoots, within } from "@dogsbay/detect";
|
|
2
|
+
import { parseConfig } from "../config.js";
|
|
12
3
|
import { buildNav, navFromSlugs, linkToSlug } from "../nav.js";
|
|
13
|
-
const SKIP_DIRS = new Set([
|
|
14
|
-
".vitepress",
|
|
15
|
-
"node_modules",
|
|
16
|
-
"public",
|
|
17
|
-
"dist",
|
|
18
|
-
".git",
|
|
19
|
-
"cache",
|
|
20
|
-
".cache",
|
|
21
|
-
]);
|
|
22
|
-
// ── Detection ─────────────────────────────────────────────────────────────
|
|
23
|
-
/**
|
|
24
|
-
* `.vitepress/config.*` is the only unambiguous marker. Requiring it is what
|
|
25
|
-
* lets this plugin sit safely in the registry among the other Markdown
|
|
26
|
-
* importers: it cannot claim a plain-Markdown, MkDocs or GitBook corpus.
|
|
27
|
-
*/
|
|
28
|
-
export function findVitepressDir(source) {
|
|
29
|
-
const candidates = [source, join(source, "docs"), join(source, "site"), join(source, "website")];
|
|
30
|
-
for (const dir of candidates) {
|
|
31
|
-
const vp = join(dir, ".vitepress");
|
|
32
|
-
if (existsSync(vp) && findConfigFile(vp))
|
|
33
|
-
return vp;
|
|
34
|
-
}
|
|
35
|
-
// One level down, for monorepos whose docs live in an unconventional dir.
|
|
36
|
-
let entries = [];
|
|
37
|
-
try {
|
|
38
|
-
entries = readdirSync(source);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
for (const name of entries) {
|
|
44
|
-
if (name.startsWith(".") || name === "node_modules")
|
|
45
|
-
continue;
|
|
46
|
-
const vp = join(source, name, ".vitepress");
|
|
47
|
-
try {
|
|
48
|
-
if (statSync(vp).isDirectory() && findConfigFile(vp))
|
|
49
|
-
return vp;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
/* not a directory */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
export function detectVitepress(source) {
|
|
58
|
-
try {
|
|
59
|
-
if (!existsSync(source) || !statSync(source).isDirectory())
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return findVitepressDir(source) !== undefined;
|
|
66
|
-
}
|
|
67
4
|
// ── Slugs ─────────────────────────────────────────────────────────────────
|
|
68
5
|
/** `guide/install.md` → `guide/install`; `guide/index.md` → `guide/index`. */
|
|
69
6
|
export function pathToSlug(relPath) {
|
|
70
7
|
return relPath.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
71
8
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let out = "";
|
|
9
|
+
function globTokens(glob) {
|
|
10
|
+
const out = [];
|
|
75
11
|
for (let i = 0; i < glob.length; i++) {
|
|
76
12
|
const c = glob[i];
|
|
77
13
|
if (c === "*") {
|
|
@@ -79,84 +15,96 @@ export function globToRegExp(glob) {
|
|
|
79
15
|
// `**` followed by a separator matches zero or more leading
|
|
80
16
|
// directories; a bare `**` matches anything, separators included.
|
|
81
17
|
if (glob[i + 2] === "/") {
|
|
82
|
-
|
|
18
|
+
// A run of `**/**/` matches what one does
|
|
19
|
+
if (out[out.length - 1]?.t !== "**/")
|
|
20
|
+
out.push({ t: "**/" });
|
|
83
21
|
i += 2;
|
|
84
22
|
}
|
|
85
23
|
else {
|
|
86
|
-
out
|
|
24
|
+
out.push({ t: "**" });
|
|
87
25
|
i += 1;
|
|
88
26
|
}
|
|
89
27
|
continue;
|
|
90
28
|
}
|
|
91
|
-
out
|
|
29
|
+
if (out[out.length - 1]?.t !== "*")
|
|
30
|
+
out.push({ t: "*" });
|
|
92
31
|
continue;
|
|
93
32
|
}
|
|
94
33
|
if (c === "?") {
|
|
95
|
-
out
|
|
34
|
+
out.push({ t: "?" });
|
|
96
35
|
continue;
|
|
97
36
|
}
|
|
98
37
|
// Brace alternation — picomatch (what VitePress uses) supports it.
|
|
99
|
-
//
|
|
38
|
+
// Treating it as literal made `**/*.{draft,tmp}.md` match nothing, so
|
|
100
39
|
// an author's srcExclude silently failed to exclude anything.
|
|
101
40
|
if (c === "{") {
|
|
102
41
|
const close = glob.indexOf("}", i);
|
|
103
42
|
if (close > i) {
|
|
104
|
-
|
|
105
|
-
.slice(i + 1, close)
|
|
106
|
-
.split(",")
|
|
107
|
-
.map((a) => a.replace(/[.+^${}()|[\]\\*?]/g, "\\$&"));
|
|
108
|
-
out += `(?:${alts.join("|")})`;
|
|
43
|
+
out.push({ t: "alt", opts: glob.slice(i + 1, close).split(",") });
|
|
109
44
|
i = close;
|
|
110
45
|
continue;
|
|
111
46
|
}
|
|
112
47
|
}
|
|
113
|
-
out
|
|
48
|
+
out.push({ t: "lit", c });
|
|
114
49
|
}
|
|
115
|
-
return
|
|
50
|
+
return out;
|
|
116
51
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Minimal glob matcher for `srcExclude` (`**\/*.md`, `todo.md`, `a/*.md`, `{a,b}`): `*` and
|
|
54
|
+
* `?` stay within a path segment, `**\/` is zero or more folders, `**` anything.
|
|
55
|
+
*
|
|
56
|
+
* Not a RegExp: the globs come from someone else's config, and a backtracking regex of
|
|
57
|
+
* `*a*a*a…b` or `**\/**\/…` took over a minute on one path. Matching is memoized over
|
|
58
|
+
* (glob position, path position), so it is polynomial in both. Returns `{ test }` so a
|
|
59
|
+
* caller can use it where it used a RegExp.
|
|
60
|
+
*/
|
|
61
|
+
export function globToRegExp(glob) {
|
|
62
|
+
const toks = globTokens(glob);
|
|
63
|
+
return {
|
|
64
|
+
test(path) {
|
|
65
|
+
const n = path.length;
|
|
66
|
+
const memo = new Uint8Array((toks.length + 1) * (n + 1)); // 0 unknown, 1 no, 2 yes
|
|
67
|
+
const match = (g, i) => {
|
|
68
|
+
if (g === toks.length)
|
|
69
|
+
return i === n;
|
|
70
|
+
const slot = g * (n + 1) + i;
|
|
71
|
+
if (memo[slot])
|
|
72
|
+
return memo[slot] === 2;
|
|
73
|
+
const tok = toks[g];
|
|
74
|
+
let ok = false;
|
|
75
|
+
switch (tok.t) {
|
|
76
|
+
case "lit":
|
|
77
|
+
ok = path[i] === tok.c && match(g + 1, i + 1);
|
|
78
|
+
break;
|
|
79
|
+
case "?":
|
|
80
|
+
ok = i < n && path[i] !== "/" && match(g + 1, i + 1);
|
|
81
|
+
break;
|
|
82
|
+
case "*":
|
|
83
|
+
ok = match(g + 1, i);
|
|
84
|
+
for (let j = i; !ok && j < n && path[j] !== "/"; j++)
|
|
85
|
+
ok = match(g + 1, j + 1);
|
|
86
|
+
break;
|
|
87
|
+
case "**":
|
|
88
|
+
ok = match(g + 1, i);
|
|
89
|
+
for (let j = i; !ok && j < n; j++)
|
|
90
|
+
ok = match(g + 1, j + 1);
|
|
91
|
+
break;
|
|
92
|
+
case "**/":
|
|
93
|
+
ok = match(g + 1, i);
|
|
94
|
+
for (let j = i; !ok && j < n; j++)
|
|
95
|
+
if (path[j] === "/")
|
|
96
|
+
ok = match(g + 1, j + 1);
|
|
97
|
+
break;
|
|
98
|
+
case "alt":
|
|
99
|
+
ok = tok.opts.some((o) => path.startsWith(o, i) && match(g + 1, i + o.length));
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
memo[slot] = ok ? 2 : 1;
|
|
103
|
+
return ok;
|
|
104
|
+
};
|
|
105
|
+
return match(0, 0);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
160
108
|
}
|
|
161
109
|
// ── Snippet / include resolution ──────────────────────────────────────────
|
|
162
110
|
/**
|
|
@@ -221,76 +169,42 @@ export function dedent(text) {
|
|
|
221
169
|
return text;
|
|
222
170
|
return lines.map((l) => l.slice(min)).join("\n");
|
|
223
171
|
}
|
|
224
|
-
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
catch {
|
|
241
|
-
realRoot = root;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
realTarget = realpathSync(target);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// Not created yet / broken link — fall back to the lexical path, which is
|
|
248
|
-
// still checked below.
|
|
249
|
-
realTarget = target;
|
|
250
|
-
}
|
|
251
|
-
if (realTarget === realRoot)
|
|
252
|
-
return true;
|
|
253
|
-
const rel = relative(realRoot, realTarget);
|
|
254
|
-
if (!rel)
|
|
255
|
-
return true;
|
|
256
|
-
return rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
172
|
+
/** Is `path` inside the project `root`, once links resolve? */
|
|
173
|
+
function inProject(corpus, listed, path, root) {
|
|
174
|
+
if (path === undefined || !within(path, root))
|
|
175
|
+
return false;
|
|
176
|
+
// A listed file is checked through its real path: a symlink under the docs tree
|
|
177
|
+
// (`docs/guide/link -> /tmp`) must not turn a `<<<` reference into a read outside it.
|
|
178
|
+
return !corpus.contains || !listed.has(path) || corpus.contains(path, root);
|
|
179
|
+
}
|
|
180
|
+
/** `@/x` and `/x` are relative to `srcDir` (VitePress's alias); anything else to `dir`. */
|
|
181
|
+
function snippetPath(spec, dir, srcDir) {
|
|
182
|
+
const target = spec.startsWith("@/")
|
|
183
|
+
? joinPath(srcDir, spec.slice(2))
|
|
184
|
+
: spec.startsWith("/")
|
|
185
|
+
? joinPath(srcDir, spec.slice(1))
|
|
186
|
+
: joinPath(dir, spec);
|
|
187
|
+
return normalizePath(target);
|
|
257
188
|
}
|
|
258
189
|
/**
|
|
259
190
|
* Build the `readSnippet` closure for one page.
|
|
260
191
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* should not read files outside the project it was pointed at.
|
|
192
|
+
* A path escaping `root` is refused — a migration should not read files outside the project
|
|
193
|
+
* it was pointed at.
|
|
264
194
|
*/
|
|
265
|
-
function makeReadSnippet(pageDir, srcDir, root
|
|
195
|
+
function makeReadSnippet(corpus, listed, pageDir, srcDir, root) {
|
|
266
196
|
return (req) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
: resolve(pageDir, raw);
|
|
273
|
-
if (!isInside(root, abs)) {
|
|
274
|
-
warnings.push(`refused to read "${raw}" — outside the project root`);
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
if (!existsSync(abs))
|
|
278
|
-
return undefined;
|
|
279
|
-
let content;
|
|
280
|
-
try {
|
|
281
|
-
content = readFileSync(abs, "utf-8");
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
197
|
+
const path = snippetPath(req.path, pageDir, srcDir);
|
|
198
|
+
if (!inProject(corpus, listed, path, root))
|
|
199
|
+
return { missing: "refused to read: outside the project root" };
|
|
200
|
+
const content = corpus.text(path);
|
|
201
|
+
if (content === undefined)
|
|
284
202
|
return undefined;
|
|
285
|
-
}
|
|
286
|
-
const rel = relative(root, abs);
|
|
287
203
|
if (!req.region)
|
|
288
204
|
return content;
|
|
289
205
|
const extracted = extractRegion(content, req.region);
|
|
290
|
-
if (extracted === undefined)
|
|
291
|
-
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
206
|
+
if (extracted === undefined)
|
|
207
|
+
return { missing: `region "#${req.region}" not found in ${root ? path.slice(root.length + 1) : path}` };
|
|
294
208
|
return extracted;
|
|
295
209
|
};
|
|
296
210
|
}
|
|
@@ -309,16 +223,15 @@ function makeAssetHref(pageDir, srcDir) {
|
|
|
309
223
|
return src;
|
|
310
224
|
if (src.startsWith("/"))
|
|
311
225
|
return `/_assets${src}`;
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
if (rel.startsWith(".."))
|
|
226
|
+
const path = normalizePath(joinPath(pageDir, src));
|
|
227
|
+
if (path === undefined || !within(path, srcDir))
|
|
315
228
|
return src;
|
|
316
|
-
return `/_assets/pages/${
|
|
229
|
+
return `/_assets/pages/${srcDir ? path.slice(srcDir.length + 1) : path}`;
|
|
317
230
|
};
|
|
318
231
|
}
|
|
319
232
|
/** Per-page translate options (snippet resolution + asset rewriting). */
|
|
320
|
-
export function translateOptionsFor(page, srcDir, root
|
|
321
|
-
return optionsForDir(
|
|
233
|
+
export function translateOptionsFor(corpus, page, srcDir, root) {
|
|
234
|
+
return optionsForDir(corpus, new Set(corpus.paths), dirOf(page.path), srcDir, root);
|
|
322
235
|
}
|
|
323
236
|
/**
|
|
324
237
|
* Translate options anchored at `dir`.
|
|
@@ -329,57 +242,59 @@ export function translateOptionsFor(page, srcDir, root, warnings) {
|
|
|
329
242
|
* same-named sibling existed next to the page — silently, with no ledger
|
|
330
243
|
* entry — and dropped the include entirely when it did not.
|
|
331
244
|
*/
|
|
332
|
-
export function optionsForDir(dir, srcDir, root
|
|
245
|
+
export function optionsForDir(corpus, listed, dir, srcDir, root) {
|
|
333
246
|
return {
|
|
334
|
-
readSnippet: makeReadSnippet(dir, srcDir, root
|
|
247
|
+
readSnippet: makeReadSnippet(corpus, listed, dir, srcDir, root),
|
|
335
248
|
assetHref: makeAssetHref(dir, srcDir),
|
|
336
249
|
resolveDir: (spec) => {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
: spec.startsWith("/")
|
|
340
|
-
? resolve(srcDir, spec.slice(1))
|
|
341
|
-
: resolve(dir, spec);
|
|
342
|
-
return isInside(root, abs) ? dirname(abs) : undefined;
|
|
250
|
+
const path = snippetPath(spec, dir, srcDir);
|
|
251
|
+
return inProject(corpus, listed, path, root) ? dirOf(path) : undefined;
|
|
343
252
|
},
|
|
344
|
-
optionsForDir: (nextDir) => optionsForDir(nextDir, srcDir, root
|
|
253
|
+
optionsForDir: (nextDir) => optionsForDir(corpus, listed, nextDir, srcDir, root),
|
|
345
254
|
};
|
|
346
255
|
}
|
|
347
256
|
// ── Load ──────────────────────────────────────────────────────────────────
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
const root = dirname(vitepressDir);
|
|
356
|
-
const configPath = findConfigFile(vitepressDir);
|
|
357
|
-
if (!configPath)
|
|
358
|
-
throw new Error(`No config file in ${vitepressDir}`);
|
|
359
|
-
let config;
|
|
360
|
-
if (opts.evalConfig) {
|
|
361
|
-
try {
|
|
362
|
-
config = await evalConfig(configPath);
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
366
|
-
warnings.push(`--eval-config failed (${msg}); falling back to static reading`);
|
|
367
|
-
config = readConfig(configPath);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
config = readConfig(configPath);
|
|
257
|
+
/** The VitePress project in `corpus`: its root and config file, or undefined. */
|
|
258
|
+
export function findVitepressProject(corpus) {
|
|
259
|
+
for (const root of vitepressRoots(corpus.paths)) {
|
|
260
|
+
const configPath = vitepressConfigPath(corpus.paths, root);
|
|
261
|
+
if (configPath)
|
|
262
|
+
return { root, configPath };
|
|
372
263
|
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
export function loadVitepressCorpus(corpus, opts = {}) {
|
|
267
|
+
const hrefPrefix = opts.hrefPrefix ?? "";
|
|
268
|
+
const warnings = [...(opts.warnings ?? [])];
|
|
269
|
+
const project = findVitepressProject(corpus);
|
|
270
|
+
if (!project)
|
|
271
|
+
throw new Error("No .vitepress/config.* found");
|
|
272
|
+
const { root } = project;
|
|
273
|
+
const config = opts.config ?? parseConfig(corpus.text(project.configPath) ?? "", project.configPath);
|
|
373
274
|
for (const u of config.unresolved) {
|
|
374
275
|
warnings.push(`config: could not read ${u}`);
|
|
375
276
|
}
|
|
376
|
-
|
|
377
|
-
|
|
277
|
+
// `srcDir` comes from the repo being imported: it may not lead out of the project.
|
|
278
|
+
const srcDir = config.srcDir ? normalizePath(joinPath(root, config.srcDir)) : root;
|
|
279
|
+
if (srcDir === undefined || !within(srcDir, root)) {
|
|
280
|
+
throw new Error(`srcDir "${config.srcDir}" is outside the project`);
|
|
281
|
+
}
|
|
282
|
+
const listed = new Set(corpus.paths);
|
|
283
|
+
if (srcDir !== root && !corpus.paths.some((p) => within(p, srcDir))) {
|
|
378
284
|
throw new Error(`srcDir "${config.srcDir}" does not exist (resolved to ${srcDir})`);
|
|
379
285
|
}
|
|
380
|
-
// Pages.
|
|
286
|
+
// Pages: the visible .md files under srcDir that stay in the project once links resolve.
|
|
381
287
|
const found = [];
|
|
382
|
-
|
|
288
|
+
for (const path of corpus.paths) {
|
|
289
|
+
if (!within(path, srcDir) || path === srcDir)
|
|
290
|
+
continue;
|
|
291
|
+
const rel = srcDir ? path.slice(srcDir.length + 1) : path;
|
|
292
|
+
if (!vitepressPage(rel))
|
|
293
|
+
continue;
|
|
294
|
+
if (!inProject(corpus, listed, path, root))
|
|
295
|
+
continue;
|
|
296
|
+
found.push({ path, slug: pathToSlug(rel) });
|
|
297
|
+
}
|
|
383
298
|
const excludes = (config.srcExclude ?? []).map(globToRegExp);
|
|
384
299
|
const rewrites = config.rewrites ?? {};
|
|
385
300
|
// VitePress supports dynamic route params in a rewrite key
|
|
@@ -391,23 +306,32 @@ export async function loadVitepress(source, opts = {}) {
|
|
|
391
306
|
warnings.push(`rewrite rule "${key}" uses dynamic parameters, which are not expanded — pages under it keep their source paths`);
|
|
392
307
|
}
|
|
393
308
|
}
|
|
309
|
+
const rel = (path) => (srcDir ? path.slice(srcDir.length + 1) : path);
|
|
394
310
|
const pages = [];
|
|
395
311
|
const bySlug = new Map();
|
|
396
312
|
for (const p of found) {
|
|
397
313
|
const relMd = `${p.slug}.md`;
|
|
398
314
|
if (excludes.some((re) => re.test(relMd) || re.test(p.slug)))
|
|
399
315
|
continue;
|
|
400
|
-
// `rewrites` remaps a SOURCE path to the served path.
|
|
401
|
-
|
|
402
|
-
|
|
316
|
+
// `rewrites` remaps a SOURCE path to the served path. It comes from the repo too: a
|
|
317
|
+
// rewrite that leads out of the content root is not a slug.
|
|
318
|
+
// Own string keys only: a page named `constructor.md` read Object.prototype's.
|
|
319
|
+
const own = (k) => (Object.hasOwn(rewrites, k) && typeof rewrites[k] === "string" ? rewrites[k] : undefined);
|
|
320
|
+
const rewritten = own(relMd) ?? own(p.slug);
|
|
321
|
+
const normalized = rewritten === undefined ? undefined : normalizePath(rewritten);
|
|
322
|
+
const target = normalized && isPlainPath(normalized) ? normalized : undefined;
|
|
323
|
+
if (rewritten !== undefined && !target) {
|
|
324
|
+
warnings.push(`rewrite of ${relMd} to "${rewritten}" leads out of the site or is not a plain path — kept its source path`);
|
|
325
|
+
}
|
|
326
|
+
const page = target ? { ...p, slug: pathToSlug(target) } : p;
|
|
403
327
|
// Two files landing on one slug means a page is DROPPED by the exporter —
|
|
404
328
|
// the run still says "Imported N pages" and one of them silently isn't
|
|
405
329
|
// there. Name both sources; a migration that loses a page must say so.
|
|
406
330
|
const prior = bySlug.get(page.slug);
|
|
407
331
|
if (prior) {
|
|
408
|
-
warnings.push(`slug collision "${page.slug}": ${
|
|
332
|
+
warnings.push(`slug collision "${page.slug}": ${rel(prior)} and ${rel(page.path)} — only one survives`);
|
|
409
333
|
}
|
|
410
|
-
bySlug.set(page.slug, page.
|
|
334
|
+
bySlug.set(page.slug, page.path);
|
|
411
335
|
pages.push(page);
|
|
412
336
|
}
|
|
413
337
|
pages.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
@@ -426,11 +350,12 @@ export async function loadVitepress(source, opts = {}) {
|
|
|
426
350
|
navSource = "directory";
|
|
427
351
|
}
|
|
428
352
|
const assetDirs = [];
|
|
429
|
-
const
|
|
430
|
-
|
|
353
|
+
const hasDir = (dir) => corpus.paths.some((p) => within(p, dir) && p !== dir);
|
|
354
|
+
const publicDir = joinPath(root, "public");
|
|
355
|
+
if (hasDir(publicDir))
|
|
431
356
|
assetDirs.push({ dir: publicDir });
|
|
432
|
-
const vpPublic =
|
|
433
|
-
if (
|
|
357
|
+
const vpPublic = joinPath(root, ".vitepress/public");
|
|
358
|
+
if (hasDir(vpPublic))
|
|
434
359
|
assetDirs.push({ dir: vpPublic });
|
|
435
360
|
assetDirs.push({ dir: srcDir, into: "pages", mediaOnly: true });
|
|
436
361
|
return { root, srcDir, config, pages, nav, navSource, assetDirs, warnings };
|