@waveso/docs 0.1.0 → 0.3.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/CHANGELOG.md +194 -0
- package/README.md +592 -88
- package/dist/code-frame.d.ts +29 -0
- package/dist/code-frame.js +41 -0
- package/dist/code-meta.d.ts +48 -0
- package/dist/code-meta.js +72 -0
- package/dist/docs-content-id.d.ts +19 -0
- package/dist/docs-content-id.js +19 -0
- package/dist/docs-error.d.ts +19 -0
- package/dist/docs-error.js +28 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.js +45 -0
- package/dist/frontmatter.d.ts +39 -7
- package/dist/frontmatter.js +51 -24
- package/dist/highlighter.d.ts +2 -2
- package/dist/highlighter.js +3 -2
- package/dist/map-pooled.d.ts +26 -0
- package/dist/map-pooled.js +45 -0
- package/dist/meta.d.ts +7 -3
- package/dist/meta.js +61 -15
- package/dist/next.d.ts +182 -35
- package/dist/next.js +177 -49
- package/dist/plugins/rehype-capture-toc.js +52 -20
- package/dist/plugins/rehype-code-frame.d.ts +10 -0
- package/dist/plugins/rehype-code-frame.js +88 -0
- package/dist/plugins/rehype-code-language.d.ts +24 -0
- package/dist/plugins/rehype-code-language.js +54 -0
- package/dist/plugins/rehype-fallback-heading-ids.d.ts +6 -0
- package/dist/plugins/rehype-fallback-heading-ids.js +51 -0
- package/dist/plugins/rehype-flatten-roots.d.ts +7 -0
- package/dist/plugins/rehype-flatten-roots.js +39 -0
- package/dist/plugins/remark-doc-links.d.ts +12 -1
- package/dist/plugins/remark-doc-links.js +147 -20
- package/dist/react/code-runtime.d.ts +14 -0
- package/dist/react/code-runtime.js +161 -0
- package/dist/react/doc-content.d.ts +39 -2
- package/dist/react/doc-content.js +42 -10
- package/dist/react/layout.d.ts +44 -0
- package/dist/react/layout.js +65 -0
- package/dist/react/markdown-components.js +71 -6
- package/dist/react/nav.d.ts +28 -0
- package/dist/react/nav.js +70 -0
- package/dist/react/nearest-scroll-top.d.ts +45 -0
- package/dist/react/nearest-scroll-top.js +44 -0
- package/dist/react/next-link.d.ts +34 -0
- package/dist/react/next-link.js +30 -0
- package/dist/react/next-nav.d.ts +11 -0
- package/dist/react/next-nav.js +32 -0
- package/dist/react/next-search.d.ts +22 -0
- package/dist/react/next-search.js +52 -0
- package/dist/react/search-dialog.d.ts +35 -7
- package/dist/react/search-dialog.js +55 -33
- package/dist/react/shell-labels.d.ts +43 -0
- package/dist/react/shell-labels.js +27 -0
- package/dist/react/sidebar.d.ts +38 -3
- package/dist/react/sidebar.js +104 -12
- package/dist/react/skip-link.d.ts +1 -9
- package/dist/react/skip-link.js +6 -5
- package/dist/react/toc.d.ts +12 -4
- package/dist/react/toc.js +46 -12
- package/dist/react/youtube.d.ts +31 -5
- package/dist/react/youtube.js +76 -52
- package/dist/render.d.ts +78 -10
- package/dist/render.js +137 -54
- package/dist/route-path.d.ts +46 -0
- package/dist/route-path.js +51 -0
- package/dist/search-index.d.ts +22 -21
- package/dist/search-index.js +27 -78
- package/dist/search-options.d.ts +32 -1
- package/dist/search-options.js +66 -3
- package/dist/section-boundary.d.ts +17 -0
- package/dist/section-boundary.js +43 -0
- package/dist/sitemap-limit.d.ts +34 -0
- package/dist/sitemap-limit.js +37 -0
- package/dist/source.d.ts +12 -22
- package/dist/source.js +165 -72
- package/dist/styles.css +1117 -125
- package/dist/types.d.ts +52 -29
- package/package.json +70 -34
package/dist/source.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
2
3
|
import { orderNavEntries, readDocsMeta } from "./meta.js";
|
|
3
|
-
import {
|
|
4
|
+
import { encodeSegments, toAliasRoute } from "./route-path.js";
|
|
5
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
5
|
-
import
|
|
7
|
+
import { VFile } from "vfile";
|
|
8
|
+
import { matter } from "vfile-matter";
|
|
6
9
|
//#region src/source.ts
|
|
7
|
-
/**
|
|
8
|
-
* The source layer: a content directory on disk becomes `DocFile[]` plus a
|
|
9
|
-
* navigation tree.
|
|
10
|
-
*
|
|
11
|
-
* Node-only, and the only module that touches the filesystem. Everything it
|
|
12
|
-
* produces is plain data, so the result crosses the RSC boundary, a Vite
|
|
13
|
-
* virtual module or a JSON cache file without ceremony.
|
|
14
|
-
*/
|
|
15
10
|
/** Markdown only. MDX is deliberately out of scope for this package. */
|
|
16
11
|
const PAGE_EXTENSION = ".md";
|
|
17
12
|
/** A directory whose `index.md` is the directory's own route. */
|
|
@@ -26,7 +21,11 @@ const INDEX_NAME = "index";
|
|
|
26
21
|
*/
|
|
27
22
|
function resolveDocsConfig(config) {
|
|
28
23
|
return {
|
|
29
|
-
contentDir: path.resolve(
|
|
24
|
+
contentDir: path.resolve(
|
|
25
|
+
/*turbopackIgnore: true*/
|
|
26
|
+
process.cwd(),
|
|
27
|
+
config.contentDir
|
|
28
|
+
),
|
|
30
29
|
basePath: normalizeBasePath(config.basePath ?? "/docs"),
|
|
31
30
|
includeDrafts: config.includeDrafts ?? false,
|
|
32
31
|
assertLinks: config.assertLinks ?? true,
|
|
@@ -83,10 +82,43 @@ function createDocsSource(config) {
|
|
|
83
82
|
}
|
|
84
83
|
function buildSource(config) {
|
|
85
84
|
let cached = null;
|
|
86
|
-
const load = () =>
|
|
85
|
+
const load = () => {
|
|
86
|
+
cached ??= scan(config).catch((err) => {
|
|
87
|
+
cached = null;
|
|
88
|
+
throw err;
|
|
89
|
+
});
|
|
90
|
+
return cached;
|
|
91
|
+
};
|
|
87
92
|
const isVisible = (file) => config.includeDrafts || file.frontmatter.draft !== true;
|
|
88
93
|
return {
|
|
89
94
|
config,
|
|
95
|
+
/**
|
|
96
|
+
* Throw the scan away. The next query reads the disk again.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ A STAT-WALK DIRTY CHECK WAS BUILT HERE AND MEASURED AND REMOVED. The
|
|
99
|
+
* idea is obvious and the roadmap called for it: mark dirty, then compare
|
|
100
|
+
* a stat-only fingerprint of the tree against the cached one and skip the
|
|
101
|
+
* re-read when nothing changed. It rests on stat being much cheaper than
|
|
102
|
+
* read, and on this corpus it is not — the fingerprint has to `readdir`
|
|
103
|
+
* every directory and `stat` every file, which is nearly everything the
|
|
104
|
+
* scan does apart from the read and the parse.
|
|
105
|
+
*
|
|
106
|
+
* Measured over 501 pages, median of six, against a full rescan:
|
|
107
|
+
*
|
|
108
|
+
* ~1.4 KB pages 28.3 ms vs 26.8 ms 0.95x (slower)
|
|
109
|
+
* ~20 KB pages 28.8 ms vs 27.4 ms 0.95x (slower)
|
|
110
|
+
* ~120 KB pages 39.1 ms vs 45.1 ms 1.15x (faster)
|
|
111
|
+
*
|
|
112
|
+
* Documentation pages are the first two rows. So it is a small regression
|
|
113
|
+
* plus a new class of invalidation bug, in exchange for a win on a corpus
|
|
114
|
+
* nobody has. There is no cheaper correct fingerprint either: statting
|
|
115
|
+
* only directories catches an added or renamed file but not an edited one,
|
|
116
|
+
* which is the common case in a dev server.
|
|
117
|
+
*
|
|
118
|
+
* If this is ever revisited, the thing to change is the *scan*, not the
|
|
119
|
+
* check — patch only the files whose mtime moved and re-derive the nav in
|
|
120
|
+
* memory, which is a different item with a much harder correctness story.
|
|
121
|
+
*/
|
|
90
122
|
invalidate() {
|
|
91
123
|
cached = null;
|
|
92
124
|
},
|
|
@@ -94,6 +126,10 @@ function buildSource(config) {
|
|
|
94
126
|
const { files } = await load();
|
|
95
127
|
return files.filter(isVisible);
|
|
96
128
|
},
|
|
129
|
+
async drafts() {
|
|
130
|
+
const { files } = await load();
|
|
131
|
+
return files.filter((file) => file.frontmatter.draft === true);
|
|
132
|
+
},
|
|
97
133
|
async find(segments) {
|
|
98
134
|
const { bySlug } = await load();
|
|
99
135
|
return bySlug.get(segments.join("/"));
|
|
@@ -109,7 +145,7 @@ function buildSource(config) {
|
|
|
109
145
|
}
|
|
110
146
|
async function scan(config) {
|
|
111
147
|
await assertContentDir(config.contentDir);
|
|
112
|
-
const root = await scanDir(config.contentDir, [], "", config);
|
|
148
|
+
const root = await scanDir(config.contentDir, [], "", config, /* @__PURE__ */ new Set([await realpath(config.contentDir)]));
|
|
113
149
|
const files = [];
|
|
114
150
|
const bySlug = /* @__PURE__ */ new Map();
|
|
115
151
|
collect(root, files, bySlug);
|
|
@@ -123,15 +159,21 @@ async function assertContentDir(contentDir) {
|
|
|
123
159
|
try {
|
|
124
160
|
if ((await stat(contentDir)).isDirectory()) return;
|
|
125
161
|
} catch {}
|
|
126
|
-
throw
|
|
162
|
+
throw docsError("missing-content-dir", `Docs content directory not found: ${contentDir}\nSet \`contentDir\` to a directory of markdown files; relative paths resolve against the working directory (${process.cwd()}).`);
|
|
127
163
|
}
|
|
128
|
-
|
|
164
|
+
const SKIP = { kind: "skip" };
|
|
165
|
+
async function scanDir(absPath, segments, name, config, ancestors) {
|
|
129
166
|
const [meta, entries] = await Promise.all([readDocsMeta(absPath), readdir(absPath, { withFileTypes: true })]);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
167
|
+
const classified = await Promise.all(entries.map((entry) => ({
|
|
168
|
+
entry,
|
|
169
|
+
name: entry.name.normalize("NFC")
|
|
170
|
+
})).sort((a, b) => a.name.localeCompare(b.name, "en")).map((listed) => classifyEntry(absPath, listed.entry, listed.name)));
|
|
171
|
+
const pageEntries = [];
|
|
172
|
+
const dirEntries = [];
|
|
173
|
+
for (const entry of classified) if (entry.kind === "page") pageEntries.push(entry);
|
|
174
|
+
else if (entry.kind === "dir" && !ancestors.has(entry.realPath)) dirEntries.push(entry);
|
|
175
|
+
const [pages, dirs] = await Promise.all([Promise.all(pageEntries.map((entry) => readPage(entry.absPath, entry.name, segments, config))), Promise.all(dirEntries.map((entry) => scanDir(entry.absPath, [...segments, entry.name], entry.name, config, new Set(ancestors).add(entry.realPath))))]);
|
|
176
|
+
const index = pages.find((page) => page.name === INDEX_NAME);
|
|
135
177
|
return {
|
|
136
178
|
name,
|
|
137
179
|
absPath,
|
|
@@ -143,36 +185,83 @@ async function scanDir(absPath, segments, name, config) {
|
|
|
143
185
|
dirs
|
|
144
186
|
};
|
|
145
187
|
}
|
|
146
|
-
|
|
188
|
+
/**
|
|
189
|
+
* What kind of content is this directory entry?
|
|
190
|
+
*
|
|
191
|
+
* ⚠️ `isFile()` AND `isDirectory()` ARE BOTH FALSE FOR A SYMBOLIC LINK, and
|
|
192
|
+
* `readdir` has no follow option — so a symlinked page, or a whole symlinked
|
|
193
|
+
* section, was absent from `all()`, `nav()`, the sitemap and the search index
|
|
194
|
+
* without a word. The cascade is worse than the omission: the first link to
|
|
195
|
+
* the missing page fails the build with `no such page exists`, which sends the
|
|
196
|
+
* author after a link that is perfectly correct.
|
|
197
|
+
*
|
|
198
|
+
* `stat` follows the link, so the target's kind decides — and the name filters
|
|
199
|
+
* still apply to the link's own name, which is what the URL is built from.
|
|
200
|
+
*/
|
|
201
|
+
async function classifyEntry(dirPath, entry, name) {
|
|
202
|
+
const absPath = path.join(dirPath, entry.name);
|
|
203
|
+
let isFile = entry.isFile();
|
|
204
|
+
let isDir = entry.isDirectory();
|
|
205
|
+
if (!isFile && !isDir) {
|
|
206
|
+
if (!entry.isSymbolicLink()) return SKIP;
|
|
207
|
+
let target;
|
|
208
|
+
try {
|
|
209
|
+
target = await stat(absPath);
|
|
210
|
+
} catch {
|
|
211
|
+
if (!isPageFile(name)) return SKIP;
|
|
212
|
+
throw docsError("broken-symlink", `@waveso/docs: ${absPath} is a broken symbolic link, and it names a markdown page — so skipping it would delete a route nobody asked to delete. Point it at an existing file, or remove the link.`);
|
|
213
|
+
}
|
|
214
|
+
isFile = target.isFile();
|
|
215
|
+
isDir = target.isDirectory();
|
|
216
|
+
}
|
|
217
|
+
if (isFile) return isPageFile(name) ? {
|
|
218
|
+
kind: "page",
|
|
219
|
+
absPath,
|
|
220
|
+
name: stripExtension(name)
|
|
221
|
+
} : SKIP;
|
|
222
|
+
if (isDir && !isIgnoredDir(name)) return {
|
|
223
|
+
kind: "dir",
|
|
224
|
+
absPath,
|
|
225
|
+
name,
|
|
226
|
+
realPath: await realpath(absPath)
|
|
227
|
+
};
|
|
228
|
+
return SKIP;
|
|
229
|
+
}
|
|
230
|
+
async function readPage(filePath, name, dirSegments, config) {
|
|
147
231
|
const raw = await readFile(filePath, "utf8");
|
|
148
232
|
const relativePath = toPosix(path.relative(config.contentDir, filePath));
|
|
149
233
|
let data;
|
|
150
234
|
let content;
|
|
151
235
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
236
|
+
const file = new VFile({ value: raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw });
|
|
237
|
+
matter(file, { strip: true });
|
|
238
|
+
data = file.data.matter;
|
|
239
|
+
content = String(file);
|
|
155
240
|
} catch (err) {
|
|
156
241
|
const reason = err instanceof Error ? err.message : String(err);
|
|
157
|
-
throw
|
|
242
|
+
throw docsError("invalid-frontmatter", `Could not parse the frontmatter block in ${relativePath}: ${reason}`, { cause: err });
|
|
158
243
|
}
|
|
159
|
-
const name = baseName(filePath);
|
|
160
244
|
const segments = name === INDEX_NAME ? [...dirSegments] : [...dirSegments, name];
|
|
245
|
+
const frontmatter = await parseFrontmatter(data, relativePath, config.frontmatterSchema);
|
|
246
|
+
for (const alias of frontmatter.aliases ?? []) toAliasRoute(alias, config.basePath, relativePath);
|
|
161
247
|
return {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
248
|
+
name,
|
|
249
|
+
doc: {
|
|
250
|
+
segments,
|
|
251
|
+
slug: segments.join("/"),
|
|
252
|
+
href: toHref(config.basePath, segments),
|
|
253
|
+
filePath,
|
|
254
|
+
relativePath,
|
|
255
|
+
frontmatter,
|
|
256
|
+
content
|
|
257
|
+
}
|
|
169
258
|
};
|
|
170
259
|
}
|
|
171
260
|
function collect(dir, files, bySlug) {
|
|
172
261
|
const own = dir.index ? [dir.index, ...dir.pages] : dir.pages;
|
|
173
|
-
for (const file of own) {
|
|
262
|
+
for (const { doc: file } of own) {
|
|
174
263
|
const clash = bySlug.get(file.slug);
|
|
175
|
-
if (clash) throw
|
|
264
|
+
if (clash) throw docsError("route-collision", `Two files claim the route "${file.href}": ${clash.relativePath} and ${file.relativePath}. Rename one, or delete the other — a directory index and a same-named sibling file collide.`);
|
|
176
265
|
bySlug.set(file.slug, file);
|
|
177
266
|
files.push(file);
|
|
178
267
|
}
|
|
@@ -193,9 +282,8 @@ function buildNav(dir, config) {
|
|
|
193
282
|
const mergeable = new Set(dir.dirs.filter((child) => child.index === void 0).map((child) => child.name));
|
|
194
283
|
const dirPages = /* @__PURE__ */ new Map();
|
|
195
284
|
for (const page of dir.pages) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
dirPages.set(name, page);
|
|
285
|
+
if (mergeable.has(page.name)) {
|
|
286
|
+
dirPages.set(page.name, page);
|
|
199
287
|
continue;
|
|
200
288
|
}
|
|
201
289
|
entries.push(toPageEntry(page, config));
|
|
@@ -207,51 +295,61 @@ function buildNav(dir, config) {
|
|
|
207
295
|
for (const child of dir.dirs) {
|
|
208
296
|
const children = buildNav(child, config);
|
|
209
297
|
const index = child.index ?? dirPages.get(child.name);
|
|
210
|
-
|
|
211
|
-
|
|
298
|
+
/**
|
|
299
|
+
* ⚠️ A DRAFT INDEX IS NOT A PUBLIC PAGE, AND ITS TITLE IS NOT EITHER.
|
|
300
|
+
*
|
|
301
|
+
* Only `href` used to be gated on visibility, so `secret/index.md` with
|
|
302
|
+
* `draft: true` still supplied the group heading — an unreleased codename
|
|
303
|
+
* rendered into the sidebar of a production build, with no link on it, so
|
|
304
|
+
* no click reveals it and only view-source shows it at all. Its `order`
|
|
305
|
+
* also still positioned the group among published ones.
|
|
306
|
+
*/
|
|
307
|
+
const visible = index && isVisibleIn(index.doc, config) ? index : void 0;
|
|
308
|
+
const title = groupTitle(child, visible?.doc);
|
|
309
|
+
const href = visible?.doc.href;
|
|
212
310
|
const group = {
|
|
213
311
|
type: "group",
|
|
214
312
|
title,
|
|
215
313
|
children,
|
|
216
314
|
...href !== void 0 ? { href } : {}
|
|
217
315
|
};
|
|
218
|
-
const node = children.length === 0 &&
|
|
316
|
+
const node = children.length === 0 && visible !== void 0 && href !== void 0 ? {
|
|
219
317
|
type: "page",
|
|
220
318
|
title,
|
|
221
319
|
href,
|
|
222
|
-
slug:
|
|
320
|
+
slug: visible.doc.slug
|
|
223
321
|
} : group;
|
|
224
|
-
const order =
|
|
322
|
+
const order = visible?.doc.frontmatter.order;
|
|
225
323
|
entries.push({
|
|
226
324
|
name: child.name,
|
|
227
325
|
title,
|
|
228
326
|
node,
|
|
229
327
|
inlineChildren: children,
|
|
230
|
-
...
|
|
328
|
+
...visible !== void 0 && href !== void 0 ? { indexNode: {
|
|
231
329
|
type: "page",
|
|
232
|
-
title: navTitle(
|
|
330
|
+
title: navTitle(visible.doc),
|
|
233
331
|
href,
|
|
234
|
-
slug:
|
|
332
|
+
slug: visible.doc.slug
|
|
235
333
|
} } : {},
|
|
236
334
|
...order !== void 0 ? { order } : {}
|
|
237
335
|
});
|
|
238
336
|
}
|
|
239
|
-
return orderNavEntries(entries, dir.meta, dir.metaPath);
|
|
337
|
+
return orderNavEntries(entries, dir.meta, dir.metaPath, dir.segments.length);
|
|
240
338
|
}
|
|
241
339
|
function toPageEntry(page, config) {
|
|
242
|
-
const title = navTitle(page);
|
|
243
|
-
const { order } = page.frontmatter;
|
|
340
|
+
const title = navTitle(page.doc);
|
|
341
|
+
const { order } = page.doc.frontmatter;
|
|
244
342
|
return {
|
|
245
|
-
name:
|
|
343
|
+
name: page.name,
|
|
246
344
|
title,
|
|
247
345
|
node: {
|
|
248
346
|
type: "page",
|
|
249
347
|
title,
|
|
250
|
-
href: page.href,
|
|
251
|
-
slug: page.slug
|
|
348
|
+
href: page.doc.href,
|
|
349
|
+
slug: page.doc.slug
|
|
252
350
|
},
|
|
253
351
|
...order !== void 0 ? { order } : {},
|
|
254
|
-
...isVisibleIn(page, config) ? {} : { hidden: true }
|
|
352
|
+
...isVisibleIn(page.doc, config) ? {} : { hidden: true }
|
|
255
353
|
};
|
|
256
354
|
}
|
|
257
355
|
/** Sidebars are narrow: `label` wins over `title` when the author set one. */
|
|
@@ -273,22 +371,16 @@ function isVisibleIn(file, config) {
|
|
|
273
371
|
return config.includeDrafts || file.frontmatter.draft !== true;
|
|
274
372
|
}
|
|
275
373
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* `'quickstart'` on a site mounted at `/docs` becomes `/docs/quickstart`.
|
|
279
|
-
* Leading and trailing slashes are tolerated because authors write them, but
|
|
280
|
-
* the value is always relative to the base path — an alias of `'/docs/old'` on
|
|
281
|
-
* a `/docs` site would produce `/docs/docs/old`.
|
|
374
|
+
* ⚠️ CHARACTERS `path-to-regexp` READS AS PATTERN SYNTAX.
|
|
282
375
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
376
|
+
* Next compiles every `redirects()` source with it, so an alias is not the
|
|
377
|
+
* literal URL it looks like. `v1:beta` compiles to `/docs/v1([^/]+?)`, which
|
|
378
|
+
* `next build` accepts without a word and which then 308s the genuinely
|
|
379
|
+
* prerendered `/docs/v1-guide` away — config redirects run before filesystem
|
|
380
|
+
* routes, so the real page is unreachable in production and nothing reports
|
|
381
|
+
* it. `c++` is the loud sibling: the build aborts with `Unexpected MODIFIER at
|
|
382
|
+
* 7`, naming an offset into a string the author never wrote and no file.
|
|
286
383
|
*/
|
|
287
|
-
function toAliasRoute(alias, basePath, sourceLabel) {
|
|
288
|
-
const trimmed = alias.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
289
|
-
if (trimmed === "") throw new Error(`@waveso/docs: ${sourceLabel} has an empty entry in its \`aliases\` frontmatter. Each alias is a former URL for this page, relative to the docs base path — e.g. \`aliases: [quickstart]\`.`);
|
|
290
|
-
return `${basePath}/${trimmed}`;
|
|
291
|
-
}
|
|
292
384
|
/**
|
|
293
385
|
* ⚠️ `_` AND `.` BOTH, MATCHING `isIgnoredDir` BELOW — which is what this did
|
|
294
386
|
* NOT do. A leading dot was skipped and a leading underscore was not, so
|
|
@@ -302,18 +394,19 @@ function toAliasRoute(alias, basePath, sourceLabel) {
|
|
|
302
394
|
* first. Docusaurus and Nextra skip both forms.
|
|
303
395
|
*/
|
|
304
396
|
function isPageFile(name) {
|
|
305
|
-
return !name.startsWith(".") && !name.startsWith("_") && name.
|
|
397
|
+
return !name.startsWith(".") && !name.startsWith("_") && path.extname(name).toLowerCase() === PAGE_EXTENSION;
|
|
306
398
|
}
|
|
307
399
|
/** `_drafts/` and `.git/` are not content. */
|
|
308
400
|
function isIgnoredDir(name) {
|
|
309
401
|
return name.startsWith(".") || name.startsWith("_");
|
|
310
402
|
}
|
|
311
|
-
|
|
312
|
-
|
|
403
|
+
/** `getting-started.MD` -> `getting-started`. */
|
|
404
|
+
function stripExtension(name) {
|
|
405
|
+
return name.slice(0, name.length - path.extname(name).length);
|
|
313
406
|
}
|
|
314
407
|
function toHref(basePath, segments) {
|
|
315
408
|
if (segments.length === 0) return basePath === "" ? "/" : basePath;
|
|
316
|
-
return `${basePath}/${segments
|
|
409
|
+
return `${basePath}/${encodeSegments(segments)}`;
|
|
317
410
|
}
|
|
318
411
|
function normalizeBasePath(basePath) {
|
|
319
412
|
const trimmed = basePath.trim().replace(/\/+$/, "");
|
|
@@ -329,4 +422,4 @@ function humanize(name) {
|
|
|
329
422
|
return name.split(/[-_\s]+/).filter((word) => word !== "").map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ");
|
|
330
423
|
}
|
|
331
424
|
//#endregion
|
|
332
|
-
export { createDocsSource, resolveDocsConfig
|
|
425
|
+
export { createDocsSource, resolveDocsConfig };
|