@waveso/docs 0.1.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/LICENSE +21 -0
- package/README.md +518 -0
- package/dist/frontmatter.d.ts +55 -0
- package/dist/frontmatter.js +80 -0
- package/dist/highlighter.d.ts +99 -0
- package/dist/highlighter.js +183 -0
- package/dist/meta.d.ts +75 -0
- package/dist/meta.js +183 -0
- package/dist/next.d.ts +256 -0
- package/dist/next.js +365 -0
- package/dist/plugins/rehype-capture-toc.d.ts +18 -0
- package/dist/plugins/rehype-capture-toc.js +69 -0
- package/dist/plugins/remark-doc-links.d.ts +63 -0
- package/dist/plugins/remark-doc-links.js +122 -0
- package/dist/plugins/remark-unwrap-images.d.ts +11 -0
- package/dist/plugins/remark-unwrap-images.js +25 -0
- package/dist/plugins/remark-youtube.d.ts +22 -0
- package/dist/plugins/remark-youtube.js +84 -0
- package/dist/react/callout.d.ts +37 -0
- package/dist/react/callout.js +113 -0
- package/dist/react/doc-content.d.ts +29 -0
- package/dist/react/doc-content.js +30 -0
- package/dist/react/markdown-components.d.ts +84 -0
- package/dist/react/markdown-components.js +122 -0
- package/dist/react/search-dialog.d.ts +41 -0
- package/dist/react/search-dialog.js +404 -0
- package/dist/react/sidebar.d.ts +29 -0
- package/dist/react/sidebar.js +196 -0
- package/dist/react/skip-link.d.ts +37 -0
- package/dist/react/skip-link.js +37 -0
- package/dist/react/toc.d.ts +35 -0
- package/dist/react/toc.js +87 -0
- package/dist/react/youtube.d.ts +27 -0
- package/dist/react/youtube.js +75 -0
- package/dist/render.d.ts +72 -0
- package/dist/render.js +279 -0
- package/dist/search-index.d.ts +51 -0
- package/dist/search-index.js +274 -0
- package/dist/search-options.d.ts +18 -0
- package/dist/search-options.js +40 -0
- package/dist/source.d.ts +67 -0
- package/dist/source.js +332 -0
- package/dist/styles.css +1033 -0
- package/dist/types.d.ts +334 -0
- package/dist/types.js +0 -0
- package/package.json +166 -0
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DocFile, DocFrontmatter, DocNavNode, DocsConfig, ResolvedDocsConfig } from "./types.js";
|
|
2
|
+
//#region src/source.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A content directory, scanned once and queried many times.
|
|
5
|
+
*
|
|
6
|
+
* Every method is async because the first one to be called performs the scan;
|
|
7
|
+
* subsequent calls resolve from the same in-flight promise.
|
|
8
|
+
*/
|
|
9
|
+
interface DocsSource<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
10
|
+
/** Every page, drafts excluded unless `includeDrafts`. */
|
|
11
|
+
all(): Promise<Array<DocFile<TFrontmatter>>>;
|
|
12
|
+
/**
|
|
13
|
+
* One page by route segments. Returns drafts regardless of config, so a
|
|
14
|
+
* preview route can opt into them without a second source.
|
|
15
|
+
*/
|
|
16
|
+
find(segments: string[]): Promise<DocFile<TFrontmatter> | undefined>;
|
|
17
|
+
/** The navigation tree for the content root. */
|
|
18
|
+
nav(): Promise<DocNavNode[]>;
|
|
19
|
+
/** Route segments for `generateStaticParams`, drafts excluded. */
|
|
20
|
+
slugs(): Promise<string[][]>;
|
|
21
|
+
/**
|
|
22
|
+
* Discard the cached scan, so the next query reads the disk again.
|
|
23
|
+
*
|
|
24
|
+
* A one-shot `next build` never needs this — the tree cannot change while it
|
|
25
|
+
* runs, and caching is the whole point. A long-lived Vite dev server does:
|
|
26
|
+
* without it, a markdown file created after startup could never appear, and
|
|
27
|
+
* the source would be permanently wrong for the rest of the session.
|
|
28
|
+
*/
|
|
29
|
+
invalidate(): void;
|
|
30
|
+
config: ResolvedDocsConfig<TFrontmatter>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Apply {@link DocsConfig} defaults and resolve `contentDir` against
|
|
34
|
+
* `process.cwd()` — the project root under both `next build` and `vite build`.
|
|
35
|
+
*
|
|
36
|
+
* `basePath` is normalised to have a leading slash and no trailing one, so
|
|
37
|
+
* href construction is a plain concatenation everywhere else. Mounting docs at
|
|
38
|
+
* the site root (`'/'`) normalises to `''`, matching the Next.js convention.
|
|
39
|
+
*/
|
|
40
|
+
declare function resolveDocsConfig<TFrontmatter extends DocFrontmatter = DocFrontmatter>(config: DocsConfig<TFrontmatter>): ResolvedDocsConfig<TFrontmatter>;
|
|
41
|
+
/**
|
|
42
|
+
* Create (or reuse) the source for a content directory.
|
|
43
|
+
*/
|
|
44
|
+
declare function createDocsSource<TFrontmatter extends DocFrontmatter = DocFrontmatter>(config: DocsConfig<TFrontmatter>): DocsSource<TFrontmatter>;
|
|
45
|
+
/**
|
|
46
|
+
* A former URL from `aliases` frontmatter, as a route.
|
|
47
|
+
*
|
|
48
|
+
* `'quickstart'` on a site mounted at `/docs` becomes `/docs/quickstart`.
|
|
49
|
+
* Leading and trailing slashes are tolerated because authors write them, but
|
|
50
|
+
* the value is always relative to the base path — an alias of `'/docs/old'` on
|
|
51
|
+
* a `/docs` site would produce `/docs/docs/old`.
|
|
52
|
+
*
|
|
53
|
+
* Shared by both adapters so they agree on which routes exist: an alias is a
|
|
54
|
+
* redirect the host installs, so a link to one resolves, and a link that
|
|
55
|
+
* builds under Next must build under Vite.
|
|
56
|
+
*/
|
|
57
|
+
declare function toAliasRoute(alias: string, basePath: string,
|
|
58
|
+
/**
|
|
59
|
+
* The source path, for the error. A STRING rather than the whole `DocFile`
|
|
60
|
+
* it used to take: this function dereferenced exactly one property of it, and
|
|
61
|
+
* demanding the object meant a cache reader or a manifest-driven redirect
|
|
62
|
+
* table had to fabricate a `DocFile` to agree with the package about which
|
|
63
|
+
* routes exist. That is the reason it is exported at all.
|
|
64
|
+
*/
|
|
65
|
+
sourceLabel: string): string;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { DocsSource, createDocsSource, resolveDocsConfig, toAliasRoute };
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
2
|
+
import { orderNavEntries, readDocsMeta } from "./meta.js";
|
|
3
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
//#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
|
+
/** Markdown only. MDX is deliberately out of scope for this package. */
|
|
16
|
+
const PAGE_EXTENSION = ".md";
|
|
17
|
+
/** A directory whose `index.md` is the directory's own route. */
|
|
18
|
+
const INDEX_NAME = "index";
|
|
19
|
+
/**
|
|
20
|
+
* Apply {@link DocsConfig} defaults and resolve `contentDir` against
|
|
21
|
+
* `process.cwd()` — the project root under both `next build` and `vite build`.
|
|
22
|
+
*
|
|
23
|
+
* `basePath` is normalised to have a leading slash and no trailing one, so
|
|
24
|
+
* href construction is a plain concatenation everywhere else. Mounting docs at
|
|
25
|
+
* the site root (`'/'`) normalises to `''`, matching the Next.js convention.
|
|
26
|
+
*/
|
|
27
|
+
function resolveDocsConfig(config) {
|
|
28
|
+
return {
|
|
29
|
+
contentDir: path.resolve(process.cwd(), config.contentDir),
|
|
30
|
+
basePath: normalizeBasePath(config.basePath ?? "/docs"),
|
|
31
|
+
includeDrafts: config.includeDrafts ?? false,
|
|
32
|
+
assertLinks: config.assertLinks ?? true,
|
|
33
|
+
...config.frontmatterSchema === void 0 ? {} : { frontmatterSchema: config.frontmatterSchema }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sources are memoised by resolved config so that the twenty route files of a
|
|
38
|
+
* docs site share one filesystem scan instead of each starting their own.
|
|
39
|
+
*
|
|
40
|
+
* The values are heterogeneous in `TFrontmatter`, which no `Map` type can
|
|
41
|
+
* express; the read in {@link createDocsSource} is a cast justified by
|
|
42
|
+
* {@link schemaKey} — a key only matches an entry built from the very same
|
|
43
|
+
* schema object, and therefore from the very same `TFrontmatter`.
|
|
44
|
+
*/
|
|
45
|
+
const sources = /* @__PURE__ */ new Map();
|
|
46
|
+
/** Schema identities, assigned on first sight. */
|
|
47
|
+
const schemaIds = /* @__PURE__ */ new WeakMap();
|
|
48
|
+
let schemaIdCount = 0;
|
|
49
|
+
/**
|
|
50
|
+
* A key fragment identifying a `frontmatterSchema`, by reference.
|
|
51
|
+
*
|
|
52
|
+
* Two configs that differ only by schema must not share a source: the cached
|
|
53
|
+
* `DocFile.frontmatter` was parsed by whichever schema arrived first, so
|
|
54
|
+
* sharing would hand one caller the other's fields — silently, and only for the
|
|
55
|
+
* fields the two schemas disagree about. Identity is the only comparison a
|
|
56
|
+
* Standard Schema supports; a validator has no stable serialisation.
|
|
57
|
+
*/
|
|
58
|
+
function schemaKey(schema) {
|
|
59
|
+
if (schema === void 0) return "default-schema";
|
|
60
|
+
const existing = schemaIds.get(schema);
|
|
61
|
+
if (existing !== void 0) return `schema-${existing}`;
|
|
62
|
+
schemaIdCount += 1;
|
|
63
|
+
schemaIds.set(schema, schemaIdCount);
|
|
64
|
+
return `schema-${schemaIdCount}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create (or reuse) the source for a content directory.
|
|
68
|
+
*/
|
|
69
|
+
function createDocsSource(config) {
|
|
70
|
+
const resolved = resolveDocsConfig(config);
|
|
71
|
+
const key = [
|
|
72
|
+
resolved.contentDir,
|
|
73
|
+
resolved.basePath,
|
|
74
|
+
resolved.includeDrafts,
|
|
75
|
+
resolved.assertLinks,
|
|
76
|
+
schemaKey(resolved.frontmatterSchema)
|
|
77
|
+
].join("\0");
|
|
78
|
+
const existing = sources.get(key);
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
const created = buildSource(resolved);
|
|
81
|
+
sources.set(key, created);
|
|
82
|
+
return created;
|
|
83
|
+
}
|
|
84
|
+
function buildSource(config) {
|
|
85
|
+
let cached = null;
|
|
86
|
+
const load = () => cached ??= scan(config);
|
|
87
|
+
const isVisible = (file) => config.includeDrafts || file.frontmatter.draft !== true;
|
|
88
|
+
return {
|
|
89
|
+
config,
|
|
90
|
+
invalidate() {
|
|
91
|
+
cached = null;
|
|
92
|
+
},
|
|
93
|
+
async all() {
|
|
94
|
+
const { files } = await load();
|
|
95
|
+
return files.filter(isVisible);
|
|
96
|
+
},
|
|
97
|
+
async find(segments) {
|
|
98
|
+
const { bySlug } = await load();
|
|
99
|
+
return bySlug.get(segments.join("/"));
|
|
100
|
+
},
|
|
101
|
+
async nav() {
|
|
102
|
+
return (await load()).nav;
|
|
103
|
+
},
|
|
104
|
+
async slugs() {
|
|
105
|
+
const { files } = await load();
|
|
106
|
+
return files.filter(isVisible).map((file) => file.segments);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function scan(config) {
|
|
111
|
+
await assertContentDir(config.contentDir);
|
|
112
|
+
const root = await scanDir(config.contentDir, [], "", config);
|
|
113
|
+
const files = [];
|
|
114
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
115
|
+
collect(root, files, bySlug);
|
|
116
|
+
return {
|
|
117
|
+
files,
|
|
118
|
+
bySlug,
|
|
119
|
+
nav: buildNav(root, config)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function assertContentDir(contentDir) {
|
|
123
|
+
try {
|
|
124
|
+
if ((await stat(contentDir)).isDirectory()) return;
|
|
125
|
+
} catch {}
|
|
126
|
+
throw new Error(`Docs content directory not found: ${contentDir}\nSet \`contentDir\` to a directory of markdown files; relative paths resolve against the working directory (${process.cwd()}).`);
|
|
127
|
+
}
|
|
128
|
+
async function scanDir(absPath, segments, name, config) {
|
|
129
|
+
const [meta, entries] = await Promise.all([readDocsMeta(absPath), readdir(absPath, { withFileTypes: true })]);
|
|
130
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
131
|
+
const pageEntries = sorted.filter((entry) => entry.isFile() && isPageFile(entry.name));
|
|
132
|
+
const dirEntries = sorted.filter((entry) => entry.isDirectory() && !isIgnoredDir(entry.name));
|
|
133
|
+
const [pages, dirs] = await Promise.all([Promise.all(pageEntries.map((entry) => readPage(path.join(absPath, entry.name), segments, config))), Promise.all(dirEntries.map((entry) => scanDir(path.join(absPath, entry.name), [...segments, entry.name], entry.name, config)))]);
|
|
134
|
+
const index = pages.find((page) => baseName(page.filePath) === INDEX_NAME);
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
absPath,
|
|
138
|
+
segments,
|
|
139
|
+
meta,
|
|
140
|
+
metaPath: path.join(absPath, "meta.json"),
|
|
141
|
+
index,
|
|
142
|
+
pages: pages.filter((page) => page !== index),
|
|
143
|
+
dirs
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function readPage(filePath, dirSegments, config) {
|
|
147
|
+
const raw = await readFile(filePath, "utf8");
|
|
148
|
+
const relativePath = toPosix(path.relative(config.contentDir, filePath));
|
|
149
|
+
let data;
|
|
150
|
+
let content;
|
|
151
|
+
try {
|
|
152
|
+
const parsed = matter(raw, { language: "yaml" });
|
|
153
|
+
data = parsed.data;
|
|
154
|
+
content = parsed.content;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
157
|
+
throw new Error(`Could not parse the frontmatter block in ${relativePath}: ${reason}`);
|
|
158
|
+
}
|
|
159
|
+
const name = baseName(filePath);
|
|
160
|
+
const segments = name === INDEX_NAME ? [...dirSegments] : [...dirSegments, name];
|
|
161
|
+
return {
|
|
162
|
+
segments,
|
|
163
|
+
slug: segments.join("/"),
|
|
164
|
+
href: toHref(config.basePath, segments),
|
|
165
|
+
filePath,
|
|
166
|
+
relativePath,
|
|
167
|
+
frontmatter: await parseFrontmatter(data, relativePath, config.frontmatterSchema),
|
|
168
|
+
content
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function collect(dir, files, bySlug) {
|
|
172
|
+
const own = dir.index ? [dir.index, ...dir.pages] : dir.pages;
|
|
173
|
+
for (const file of own) {
|
|
174
|
+
const clash = bySlug.get(file.slug);
|
|
175
|
+
if (clash) throw new Error(`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
|
+
bySlug.set(file.slug, file);
|
|
177
|
+
files.push(file);
|
|
178
|
+
}
|
|
179
|
+
for (const child of dir.dirs) collect(child, files, bySlug);
|
|
180
|
+
}
|
|
181
|
+
function buildNav(dir, config) {
|
|
182
|
+
const entries = [];
|
|
183
|
+
/**
|
|
184
|
+
* `guides.md` beside a `guides/` that has no `index.md` is that directory's
|
|
185
|
+
* own page: both answer to the name `meta.json` addresses them by, and both
|
|
186
|
+
* contribute a nav entry. Merged into one group whose heading links to the
|
|
187
|
+
* page, rather than left to collide — an ambiguous name is how a published
|
|
188
|
+
* route ends up reachable from no link in the sidebar.
|
|
189
|
+
*
|
|
190
|
+
* When the directory *does* have an `index.md`, the two claim the same route
|
|
191
|
+
* and `collect` has already failed the build.
|
|
192
|
+
*/
|
|
193
|
+
const mergeable = new Set(dir.dirs.filter((child) => child.index === void 0).map((child) => child.name));
|
|
194
|
+
const dirPages = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const page of dir.pages) {
|
|
196
|
+
const name = baseName(page.filePath);
|
|
197
|
+
if (mergeable.has(name)) {
|
|
198
|
+
dirPages.set(name, page);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
entries.push(toPageEntry(page, config));
|
|
202
|
+
}
|
|
203
|
+
if (dir.index) entries.push({
|
|
204
|
+
...toPageEntry(dir.index, config),
|
|
205
|
+
isIndex: true
|
|
206
|
+
});
|
|
207
|
+
for (const child of dir.dirs) {
|
|
208
|
+
const children = buildNav(child, config);
|
|
209
|
+
const index = child.index ?? dirPages.get(child.name);
|
|
210
|
+
const title = groupTitle(child, index);
|
|
211
|
+
const href = index && isVisibleIn(index, config) ? index.href : void 0;
|
|
212
|
+
const group = {
|
|
213
|
+
type: "group",
|
|
214
|
+
title,
|
|
215
|
+
children,
|
|
216
|
+
...href !== void 0 ? { href } : {}
|
|
217
|
+
};
|
|
218
|
+
const node = children.length === 0 && index !== void 0 && href !== void 0 ? {
|
|
219
|
+
type: "page",
|
|
220
|
+
title,
|
|
221
|
+
href,
|
|
222
|
+
slug: index.slug
|
|
223
|
+
} : group;
|
|
224
|
+
const order = index?.frontmatter.order;
|
|
225
|
+
entries.push({
|
|
226
|
+
name: child.name,
|
|
227
|
+
title,
|
|
228
|
+
node,
|
|
229
|
+
inlineChildren: children,
|
|
230
|
+
...index !== void 0 && href !== void 0 ? { indexNode: {
|
|
231
|
+
type: "page",
|
|
232
|
+
title: navTitle(index),
|
|
233
|
+
href,
|
|
234
|
+
slug: index.slug
|
|
235
|
+
} } : {},
|
|
236
|
+
...order !== void 0 ? { order } : {}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return orderNavEntries(entries, dir.meta, dir.metaPath);
|
|
240
|
+
}
|
|
241
|
+
function toPageEntry(page, config) {
|
|
242
|
+
const title = navTitle(page);
|
|
243
|
+
const { order } = page.frontmatter;
|
|
244
|
+
return {
|
|
245
|
+
name: baseName(page.filePath),
|
|
246
|
+
title,
|
|
247
|
+
node: {
|
|
248
|
+
type: "page",
|
|
249
|
+
title,
|
|
250
|
+
href: page.href,
|
|
251
|
+
slug: page.slug
|
|
252
|
+
},
|
|
253
|
+
...order !== void 0 ? { order } : {},
|
|
254
|
+
...isVisibleIn(page, config) ? {} : { hidden: true }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** Sidebars are narrow: `label` wins over `title` when the author set one. */
|
|
258
|
+
function navTitle(page) {
|
|
259
|
+
return page.frontmatter.label ?? page.frontmatter.title;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A group heading: its own `meta.json` title, else the title of the page that
|
|
263
|
+
* *is* the directory (`index.md`, or a merged same-named sibling), else the
|
|
264
|
+
* directory name humanised.
|
|
265
|
+
*/
|
|
266
|
+
function groupTitle(dir, index) {
|
|
267
|
+
const fromMeta = dir.meta?.title;
|
|
268
|
+
if (fromMeta !== void 0) return fromMeta;
|
|
269
|
+
if (index) return navTitle(index);
|
|
270
|
+
return humanize(dir.name);
|
|
271
|
+
}
|
|
272
|
+
function isVisibleIn(file, config) {
|
|
273
|
+
return config.includeDrafts || file.frontmatter.draft !== true;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* A former URL from `aliases` frontmatter, as a route.
|
|
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`.
|
|
282
|
+
*
|
|
283
|
+
* Shared by both adapters so they agree on which routes exist: an alias is a
|
|
284
|
+
* redirect the host installs, so a link to one resolves, and a link that
|
|
285
|
+
* builds under Next must build under Vite.
|
|
286
|
+
*/
|
|
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
|
+
/**
|
|
293
|
+
* ⚠️ `_` AND `.` BOTH, MATCHING `isIgnoredDir` BELOW — which is what this did
|
|
294
|
+
* NOT do. A leading dot was skipped and a leading underscore was not, so
|
|
295
|
+
* `_drafts/` was excluded while `_notes.md` beside it was published: routed,
|
|
296
|
+
* listed in the sidebar, written into the sitemap, indexed for search.
|
|
297
|
+
*
|
|
298
|
+
* The asymmetry cannot have been intentional. There is no `ignore` option in
|
|
299
|
+
* `DocsConfig`, and `draft: true` still demands a valid `title`, so the
|
|
300
|
+
* underscore is the ONLY way to keep a markdown file in the tree without
|
|
301
|
+
* publishing it — and it silently did not work for the half people reach for
|
|
302
|
+
* first. Docusaurus and Nextra skip both forms.
|
|
303
|
+
*/
|
|
304
|
+
function isPageFile(name) {
|
|
305
|
+
return !name.startsWith(".") && !name.startsWith("_") && name.endsWith(PAGE_EXTENSION) && name.length > 3;
|
|
306
|
+
}
|
|
307
|
+
/** `_drafts/` and `.git/` are not content. */
|
|
308
|
+
function isIgnoredDir(name) {
|
|
309
|
+
return name.startsWith(".") || name.startsWith("_");
|
|
310
|
+
}
|
|
311
|
+
function baseName(filePath) {
|
|
312
|
+
return path.basename(filePath, PAGE_EXTENSION);
|
|
313
|
+
}
|
|
314
|
+
function toHref(basePath, segments) {
|
|
315
|
+
if (segments.length === 0) return basePath === "" ? "/" : basePath;
|
|
316
|
+
return `${basePath}/${segments.join("/")}`;
|
|
317
|
+
}
|
|
318
|
+
function normalizeBasePath(basePath) {
|
|
319
|
+
const trimmed = basePath.trim().replace(/\/+$/, "");
|
|
320
|
+
if (trimmed === "") return "";
|
|
321
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
322
|
+
}
|
|
323
|
+
/** Windows separators never reach a URL or a `relativePath`. */
|
|
324
|
+
function toPosix(value) {
|
|
325
|
+
return value.split(path.sep).join("/");
|
|
326
|
+
}
|
|
327
|
+
/** `getting-started` -> `Getting Started`. */
|
|
328
|
+
function humanize(name) {
|
|
329
|
+
return name.split(/[-_\s]+/).filter((word) => word !== "").map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ");
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
export { createDocsSource, resolveDocsConfig, toAliasRoute };
|