lildocs 0.0.0 → 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/README.md +67 -0
- package/dist/cli.mjs +2315 -0
- package/dist/core/github-pages.mjs +2 -0
- package/dist/github-pages-0s3VHkYL.mjs +74 -0
- package/dist/render/copy-code.js +51 -0
- package/dist/render/github-icon.svg +6 -0
- package/dist/render/navigation.js +14 -0
- package/dist/render/search.js +3239 -0
- package/dist/render/styles.css +650 -0
- package/package.json +51 -10
- package/readme.md +0 -1
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as renderGitHubPagesWorkflow, r as repoRelativeInputPath, t as normalizeBasePath } from "./github-pages-0s3VHkYL.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { command, flag, option, optional, positional, run, string, subcommands } from "cmd-ts";
|
|
6
|
+
import { createReadStream, existsSync, watch } from "node:fs";
|
|
7
|
+
import { access, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
import { Marked, Parser, Renderer, marked } from "marked";
|
|
12
|
+
import markedShiki from "marked-shiki";
|
|
13
|
+
import { bundledThemes, codeToHtml } from "shiki";
|
|
14
|
+
import { renderMermaidSVG } from "beautiful-mermaid";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import { generateMarkdownForModule } from "exports-md";
|
|
17
|
+
import { clampGamut, converter, formatHex, parse, wcagContrast } from "culori";
|
|
18
|
+
import { createJiti } from "jiti";
|
|
19
|
+
import { render } from "preact-render-to-string";
|
|
20
|
+
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
21
|
+
import http from "node:http";
|
|
22
|
+
//#region src/core/errors.ts
|
|
23
|
+
var LildocsError = class extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "LildocsError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/core/config.ts
|
|
31
|
+
async function readDocsConfig(docsRoot) {
|
|
32
|
+
const configPath = path.join(docsRoot, "config.json");
|
|
33
|
+
let rawConfig;
|
|
34
|
+
try {
|
|
35
|
+
rawConfig = await readFile(configPath, "utf8");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (isMissingFileError(error)) return {};
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(rawConfig);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new LildocsError(`Invalid docs config JSON at ${configPath}: ${errorMessage$3(error)}`);
|
|
45
|
+
}
|
|
46
|
+
return validateDocsConfig(parsed, configPath);
|
|
47
|
+
}
|
|
48
|
+
function mergeConfigOptions(options) {
|
|
49
|
+
return {
|
|
50
|
+
projectName: options.config.projectName,
|
|
51
|
+
theme: options.theme ?? options.config.theme,
|
|
52
|
+
fonts: {
|
|
53
|
+
heading: options.fonts?.heading ?? options.config.font?.heading,
|
|
54
|
+
body: options.fonts?.body ?? options.config.font?.body,
|
|
55
|
+
code: options.fonts?.code ?? options.config.font?.code
|
|
56
|
+
},
|
|
57
|
+
favicon: options.favicon ?? options.config.favicon,
|
|
58
|
+
logo: {
|
|
59
|
+
image: options.logo?.image ?? options.config.logo?.image,
|
|
60
|
+
text: options.logo?.text ?? options.config.logo?.text,
|
|
61
|
+
font: options.logo?.font ?? options.config.logo?.font
|
|
62
|
+
},
|
|
63
|
+
background: {
|
|
64
|
+
image: options.background?.image ?? options.config.background?.image,
|
|
65
|
+
gradient: options.background?.gradient ?? options.config.background?.gradient,
|
|
66
|
+
blendMode: options.background?.blendMode ?? options.config.background?.blendMode
|
|
67
|
+
},
|
|
68
|
+
link: { underline: options.link?.underline ?? options.config.link?.underline },
|
|
69
|
+
reference: { packageJson: options.config.reference?.packageJson },
|
|
70
|
+
navigation: {
|
|
71
|
+
transition: options.navigation?.transition ?? options.config.navigation?.transition,
|
|
72
|
+
duration: options.navigation?.duration ?? options.config.navigation?.duration,
|
|
73
|
+
easing: options.navigation?.easing ?? options.config.navigation?.easing,
|
|
74
|
+
order: options.config.navigation?.order
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function validateDocsConfig(value, configPath) {
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new LildocsError(`Docs config must be a JSON object: ${configPath}`);
|
|
80
|
+
const config = value;
|
|
81
|
+
assertOptionalString(config.$schema, "$schema", configPath);
|
|
82
|
+
assertOptionalString(config.projectName, "projectName", configPath);
|
|
83
|
+
assertOptionalThemeConfig(config.theme, configPath);
|
|
84
|
+
assertOptionalString(config.favicon, "favicon", configPath);
|
|
85
|
+
if (config.font !== void 0) {
|
|
86
|
+
if (!config.font || typeof config.font !== "object" || Array.isArray(config.font)) throw new LildocsError(`Docs config "font" must be an object: ${configPath}`);
|
|
87
|
+
assertOptionalString(config.font.heading, "font.heading", configPath);
|
|
88
|
+
assertOptionalString(config.font.body, "font.body", configPath);
|
|
89
|
+
assertOptionalString(config.font.code, "font.code", configPath);
|
|
90
|
+
}
|
|
91
|
+
if (config.logo !== void 0) {
|
|
92
|
+
if (!config.logo || typeof config.logo !== "object" || Array.isArray(config.logo)) throw new LildocsError(`Docs config "logo" must be an object: ${configPath}`);
|
|
93
|
+
assertOptionalString(config.logo.image, "logo.image", configPath);
|
|
94
|
+
assertOptionalString(config.logo.text, "logo.text", configPath);
|
|
95
|
+
assertOptionalString(config.logo.font, "logo.font", configPath);
|
|
96
|
+
}
|
|
97
|
+
if (config.background !== void 0) {
|
|
98
|
+
if (!config.background || typeof config.background !== "object" || Array.isArray(config.background)) throw new LildocsError(`Docs config "background" must be an object: ${configPath}`);
|
|
99
|
+
assertOptionalString(config.background.image, "background.image", configPath);
|
|
100
|
+
assertOptionalString(config.background.gradient, "background.gradient", configPath);
|
|
101
|
+
assertOptionalString(config.background.blendMode, "background.blendMode", configPath);
|
|
102
|
+
}
|
|
103
|
+
if (config.link !== void 0) {
|
|
104
|
+
if (!config.link || typeof config.link !== "object" || Array.isArray(config.link)) throw new LildocsError(`Docs config "link" must be an object: ${configPath}`);
|
|
105
|
+
assertOptionalEnum(config.link.underline, "link.underline", [
|
|
106
|
+
"always",
|
|
107
|
+
"hover",
|
|
108
|
+
"none"
|
|
109
|
+
], configPath);
|
|
110
|
+
}
|
|
111
|
+
if (config.navigation !== void 0) {
|
|
112
|
+
if (!config.navigation || typeof config.navigation !== "object" || Array.isArray(config.navigation)) throw new LildocsError(`Docs config "navigation" must be an object: ${configPath}`);
|
|
113
|
+
assertOptionalEnum(config.navigation.transition, "navigation.transition", [
|
|
114
|
+
"fade",
|
|
115
|
+
"slide",
|
|
116
|
+
"scale",
|
|
117
|
+
"instant"
|
|
118
|
+
], configPath);
|
|
119
|
+
assertOptionalNumber(config.navigation.duration, "navigation.duration", configPath);
|
|
120
|
+
assertOptionalString(config.navigation.easing, "navigation.easing", configPath);
|
|
121
|
+
assertOptionalStringArray(config.navigation.order, "navigation.order", configPath);
|
|
122
|
+
}
|
|
123
|
+
if (config.reference !== void 0) {
|
|
124
|
+
if (!config.reference || typeof config.reference !== "object" || Array.isArray(config.reference)) throw new LildocsError(`Docs config "reference" must be an object: ${configPath}`);
|
|
125
|
+
assertOptionalString(config.reference.packageJson, "reference.packageJson", configPath);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
projectName: config.projectName,
|
|
129
|
+
theme: config.theme,
|
|
130
|
+
favicon: config.favicon,
|
|
131
|
+
font: config.font ? {
|
|
132
|
+
heading: config.font.heading,
|
|
133
|
+
body: config.font.body,
|
|
134
|
+
code: config.font.code
|
|
135
|
+
} : void 0,
|
|
136
|
+
logo: config.logo ? {
|
|
137
|
+
image: config.logo.image,
|
|
138
|
+
text: config.logo.text,
|
|
139
|
+
font: config.logo.font
|
|
140
|
+
} : void 0,
|
|
141
|
+
background: config.background ? {
|
|
142
|
+
image: config.background.image,
|
|
143
|
+
gradient: config.background.gradient,
|
|
144
|
+
blendMode: config.background.blendMode
|
|
145
|
+
} : void 0,
|
|
146
|
+
link: config.link ? { underline: config.link.underline } : void 0,
|
|
147
|
+
reference: config.reference ? { packageJson: config.reference.packageJson } : void 0,
|
|
148
|
+
navigation: config.navigation ? {
|
|
149
|
+
transition: config.navigation.transition,
|
|
150
|
+
duration: config.navigation.duration,
|
|
151
|
+
easing: config.navigation.easing,
|
|
152
|
+
order: config.navigation.order
|
|
153
|
+
} : void 0
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function assertOptionalString(value, name, configPath) {
|
|
157
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) throw new LildocsError(`Docs config "${name}" must be a non-empty string: ${configPath}`);
|
|
158
|
+
}
|
|
159
|
+
function assertOptionalThemeConfig(value, configPath) {
|
|
160
|
+
if (value === void 0) return;
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
assertOptionalString(value, "theme", configPath);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new LildocsError(`Docs config "theme" must be a string or an object: ${configPath}`);
|
|
166
|
+
const theme = value;
|
|
167
|
+
assertOptionalString(theme.light, "theme.light", configPath);
|
|
168
|
+
assertOptionalString(theme.dark, "theme.dark", configPath);
|
|
169
|
+
}
|
|
170
|
+
function assertOptionalEnum(value, name, allowed, configPath) {
|
|
171
|
+
if (value !== void 0 && !allowed.includes(String(value))) throw new LildocsError(`Docs config "${name}" must be one of ${allowed.join(", ")}: ${configPath}`);
|
|
172
|
+
}
|
|
173
|
+
function assertOptionalNumber(value, name, configPath) {
|
|
174
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) throw new LildocsError(`Docs config "${name}" must be a non-negative finite number: ${configPath}`);
|
|
175
|
+
}
|
|
176
|
+
function assertOptionalStringArray(value, name, configPath) {
|
|
177
|
+
if (value === void 0) return;
|
|
178
|
+
if (!Array.isArray(value)) throw new LildocsError(`Docs config "${name}" must be an array: ${configPath}`);
|
|
179
|
+
for (const [index, item] of value.entries()) assertOptionalString(item, `${name}[${index}]`, configPath);
|
|
180
|
+
}
|
|
181
|
+
function isMissingFileError(error) {
|
|
182
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
183
|
+
}
|
|
184
|
+
function errorMessage$3(error) {
|
|
185
|
+
return error instanceof Error ? error.message : String(error);
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/core/paths.ts
|
|
189
|
+
function toPosixPath(value) {
|
|
190
|
+
return value.split(path.sep).join("/");
|
|
191
|
+
}
|
|
192
|
+
function isHiddenOrSystemPath(relativePath) {
|
|
193
|
+
return toPosixPath(relativePath).split("/").some((part) => part.startsWith(".") || part === "node_modules" || part === ".DS_Store");
|
|
194
|
+
}
|
|
195
|
+
function relativeUrl(fromRoute, toRoute) {
|
|
196
|
+
const fromDir = path.posix.dirname(fromRoute);
|
|
197
|
+
const relative = path.posix.relative(fromDir, toRoute);
|
|
198
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
199
|
+
}
|
|
200
|
+
function isExternalOrAnchorUrl(href) {
|
|
201
|
+
return /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("#") || href.startsWith("mailto:");
|
|
202
|
+
}
|
|
203
|
+
function resolveMarkdownDocumentPath(fromRelativePath, href) {
|
|
204
|
+
if (isExternalOrAnchorUrl(href)) return;
|
|
205
|
+
const [hrefPath] = href.split("#");
|
|
206
|
+
if (!hrefPath?.toLowerCase().endsWith(".md")) return;
|
|
207
|
+
return path.posix.normalize(path.posix.join(path.posix.dirname(fromRelativePath), hrefPath.replace(/\\/g, "/")));
|
|
208
|
+
}
|
|
209
|
+
function pageDepth(route) {
|
|
210
|
+
const dir = path.posix.dirname(route);
|
|
211
|
+
return dir === "." ? 0 : dir.split("/").length;
|
|
212
|
+
}
|
|
213
|
+
function rootRelativeUrl(route, target) {
|
|
214
|
+
return `${pageDepth(route) === 0 ? "." : Array.from({ length: pageDepth(route) }, () => "..").join("/")}/${target}`;
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/core/input.ts
|
|
218
|
+
const HOME_PAGE_CANDIDATES = [
|
|
219
|
+
"index.md",
|
|
220
|
+
"intro.md",
|
|
221
|
+
"introduction.md",
|
|
222
|
+
"getting-started.md",
|
|
223
|
+
"quickstart.md",
|
|
224
|
+
"readme.md",
|
|
225
|
+
"README.md"
|
|
226
|
+
];
|
|
227
|
+
const README_FIRST_HOME_PAGE_CANDIDATES = ["README.md", ...HOME_PAGE_CANDIDATES.filter((candidate) => candidate !== "README.md")];
|
|
228
|
+
async function resolveInput(input, cwd, options = {}) {
|
|
229
|
+
const inputPath = path.resolve(cwd, input);
|
|
230
|
+
let inputStat;
|
|
231
|
+
try {
|
|
232
|
+
inputStat = await stat(inputPath);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new LildocsError(`Input path does not exist: ${input}`);
|
|
235
|
+
}
|
|
236
|
+
if (inputStat.isFile()) {
|
|
237
|
+
if (path.extname(inputPath).toLowerCase() !== ".md") throw new LildocsError(`Input file must be Markdown: ${input}`);
|
|
238
|
+
return {
|
|
239
|
+
docsRoot: path.dirname(inputPath),
|
|
240
|
+
homePage: inputPath
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!inputStat.isDirectory()) throw new LildocsError(`Input path must be a Markdown file or directory: ${input}`);
|
|
244
|
+
const homePage = await findHomePage(inputPath, options.homePagePreference);
|
|
245
|
+
if (!homePage) throw new LildocsError(`No Markdown files found in docs directory: ${input}`);
|
|
246
|
+
return {
|
|
247
|
+
docsRoot: inputPath,
|
|
248
|
+
homePage
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function findHomePage(docsRoot, preference = "default") {
|
|
252
|
+
const homePageCandidates = preference === "readme-first" ? README_FIRST_HOME_PAGE_CANDIDATES : HOME_PAGE_CANDIDATES;
|
|
253
|
+
const homePage = (await Promise.all(homePageCandidates.map(async (candidate) => {
|
|
254
|
+
const candidatePath = path.join(docsRoot, candidate);
|
|
255
|
+
try {
|
|
256
|
+
return (await stat(candidatePath)).isFile() ? candidatePath : void 0;
|
|
257
|
+
} catch {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}))).find(Boolean);
|
|
261
|
+
if (homePage) return homePage;
|
|
262
|
+
return (await collectMarkdownPaths(docsRoot, docsRoot))[0];
|
|
263
|
+
}
|
|
264
|
+
async function collectMarkdownPaths(docsRoot, currentDir = docsRoot) {
|
|
265
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
266
|
+
return (await Promise.all(entries.toSorted((a, b) => a.name.localeCompare(b.name)).map(async (entry) => {
|
|
267
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
268
|
+
if (isHiddenOrSystemPath(path.relative(docsRoot, fullPath))) return [];
|
|
269
|
+
if (entry.isDirectory()) return collectMarkdownPaths(docsRoot, fullPath);
|
|
270
|
+
if (entry.isFile() && path.extname(entry.name).toLowerCase() === ".md") return [fullPath];
|
|
271
|
+
return [];
|
|
272
|
+
}))).flat();
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/core/content.ts
|
|
276
|
+
async function buildContentModel(input, outDirName, navigationOrder = [], additionalPages = []) {
|
|
277
|
+
const paths = await collectMarkdownPaths(input.docsRoot);
|
|
278
|
+
const pages = [...await Promise.all(paths.map((sourcePath) => readPage(input, sourcePath))), ...additionalPages.map((page) => pageFromMarkdown(input, page))];
|
|
279
|
+
const orderEntries = normalizeNavigationOrder(navigationOrder);
|
|
280
|
+
const visiblePages = pages.filter((page) => !isInsideOutput(page.relativePath, outDirName));
|
|
281
|
+
const entryPointOrder = entryPointLinkOrder(visiblePages, input.homePage, orderEntries);
|
|
282
|
+
const sortedPages = visiblePages.toSorted((a, b) => navigationRank(a, orderEntries) - navigationRank(b, orderEntries) || routeRank(a, input.homePage) - routeRank(b, input.homePage) || entryPointLinkRank(a, entryPointOrder) - entryPointLinkRank(b, entryPointOrder) || a.route.localeCompare(b.route));
|
|
283
|
+
validateNavigationOrder(orderEntries, sortedPages);
|
|
284
|
+
ensureUniqueRoutes(sortedPages);
|
|
285
|
+
return {
|
|
286
|
+
docsRoot: input.docsRoot,
|
|
287
|
+
homePage: input.homePage,
|
|
288
|
+
pages: sortedPages,
|
|
289
|
+
bySourcePath: new Map(sortedPages.map((page) => [page.sourcePath, page])),
|
|
290
|
+
byRelativePath: new Map(sortedPages.map((page) => [page.relativePath, page]))
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function readPage(input, sourcePath) {
|
|
294
|
+
const rawMarkdown = await readFile(sourcePath, "utf8");
|
|
295
|
+
return pageFromMarkdown(input, {
|
|
296
|
+
sourcePath,
|
|
297
|
+
relativePath: toPosixPath(path.relative(input.docsRoot, sourcePath)),
|
|
298
|
+
rawMarkdown
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function pageFromMarkdown(input, page) {
|
|
302
|
+
const parsed = matter(page.rawMarkdown);
|
|
303
|
+
const relativePath = toPosixPath(page.relativePath);
|
|
304
|
+
const route = page.sourcePath === input.homePage ? "index.html" : markdownPathToRoute(relativePath);
|
|
305
|
+
const headings = extractHeadings(parsed.content);
|
|
306
|
+
const title = inferTitle(parsed.data, parsed.content, page.sourcePath);
|
|
307
|
+
return {
|
|
308
|
+
sourcePath: page.sourcePath,
|
|
309
|
+
relativePath,
|
|
310
|
+
route,
|
|
311
|
+
outputPath: route,
|
|
312
|
+
title,
|
|
313
|
+
rawMarkdown: page.rawMarkdown,
|
|
314
|
+
markdown: parsed.content,
|
|
315
|
+
metadata: parsed.data,
|
|
316
|
+
headings
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function inferTitle(metadata, markdown, sourcePath) {
|
|
320
|
+
if (typeof metadata.title === "string" && metadata.title.trim()) return metadata.title.trim();
|
|
321
|
+
const h1 = markdown.match(/^#\s+(.+)$/m);
|
|
322
|
+
if (h1?.[1]) return stripMarkdownInline(h1[1]).trim();
|
|
323
|
+
return formatFilename(path.basename(sourcePath, path.extname(sourcePath)));
|
|
324
|
+
}
|
|
325
|
+
function slugify(value) {
|
|
326
|
+
return value.toLowerCase().trim().replace(/[`*_~[\]()]/g, "").replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
|
|
327
|
+
}
|
|
328
|
+
function extractHeadings(markdown) {
|
|
329
|
+
const seen = /* @__PURE__ */ new Map();
|
|
330
|
+
const headings = [];
|
|
331
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
332
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
333
|
+
if (!match?.[1] || !match[2]) continue;
|
|
334
|
+
const baseId = slugify(stripMarkdownInline(match[2]));
|
|
335
|
+
const count = seen.get(baseId) ?? 0;
|
|
336
|
+
seen.set(baseId, count + 1);
|
|
337
|
+
headings.push({
|
|
338
|
+
depth: match[1].length,
|
|
339
|
+
text: stripMarkdownInline(match[2]).trim(),
|
|
340
|
+
id: count === 0 ? baseId : `${baseId}-${count + 1}`
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return headings;
|
|
344
|
+
}
|
|
345
|
+
function stripMarkdownInline(value) {
|
|
346
|
+
return value.replace(/[`*_~]/g, "").replace(/\[(.*?)\]\(.*?\)/g, "$1");
|
|
347
|
+
}
|
|
348
|
+
function formatFilename(value) {
|
|
349
|
+
return value.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
350
|
+
}
|
|
351
|
+
function markdownPathToRoute(relativePath) {
|
|
352
|
+
const parsed = path.posix.parse(relativePath);
|
|
353
|
+
const slug = slugify(parsed.name);
|
|
354
|
+
return `${parsed.dir ? `${parsed.dir}/` : ""}${slug}.html`;
|
|
355
|
+
}
|
|
356
|
+
function routeRank(page, homePage) {
|
|
357
|
+
return page.sourcePath === homePage ? -1 : 0;
|
|
358
|
+
}
|
|
359
|
+
function entryPointLinkOrder(pages, homePage, navigationOrder) {
|
|
360
|
+
const home = pages.find((page) => page.sourcePath === homePage);
|
|
361
|
+
if (!home) return /* @__PURE__ */ new Map();
|
|
362
|
+
const byRelativePath = new Map(pages.map((page) => [page.relativePath, page]));
|
|
363
|
+
const ordered = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const href of markdownLinkHrefs(home.markdown)) {
|
|
365
|
+
const targetPath = resolveMarkdownDocumentPath(home.relativePath, href);
|
|
366
|
+
if (!targetPath) continue;
|
|
367
|
+
const target = byRelativePath.get(targetPath);
|
|
368
|
+
if (!target || target.sourcePath === homePage || navigationOrder.some((entry) => navigationOrderEntryMatchesPage(entry, target)) || ordered.has(target.relativePath)) continue;
|
|
369
|
+
ordered.set(target.relativePath, ordered.size);
|
|
370
|
+
}
|
|
371
|
+
return ordered;
|
|
372
|
+
}
|
|
373
|
+
function markdownLinkHrefs(markdown) {
|
|
374
|
+
const hrefs = [];
|
|
375
|
+
marked.walkTokens(marked.lexer(markdown), (token) => {
|
|
376
|
+
if (token.type === "link") hrefs.push(token.href);
|
|
377
|
+
});
|
|
378
|
+
return hrefs;
|
|
379
|
+
}
|
|
380
|
+
function entryPointLinkRank(page, order) {
|
|
381
|
+
return order.get(page.relativePath) ?? order.size;
|
|
382
|
+
}
|
|
383
|
+
function normalizeNavigationOrder(order) {
|
|
384
|
+
const seen = /* @__PURE__ */ new Map();
|
|
385
|
+
return order.map((value, index) => {
|
|
386
|
+
const normalized = normalizeNavigationOrderPath(value);
|
|
387
|
+
const previous = seen.get(normalized);
|
|
388
|
+
if (previous !== void 0) throw new LildocsError(`Docs config "navigation.order[${index}]" duplicates "navigation.order[${previous}]"`);
|
|
389
|
+
seen.set(normalized, index);
|
|
390
|
+
return {
|
|
391
|
+
value: normalized,
|
|
392
|
+
index,
|
|
393
|
+
kind: normalized.endsWith("/") ? "directory" : "file"
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function normalizeNavigationOrderPath(value) {
|
|
398
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
399
|
+
}
|
|
400
|
+
function navigationRank(page, order) {
|
|
401
|
+
return order.find((item) => navigationOrderEntryMatchesPage(item, page))?.index ?? order.length;
|
|
402
|
+
}
|
|
403
|
+
function validateNavigationOrder(order, pages) {
|
|
404
|
+
for (const entry of order) {
|
|
405
|
+
if (pages.some((page) => navigationOrderEntryMatchesPage(entry, page))) continue;
|
|
406
|
+
throw new LildocsError(`Docs config "navigation.order[${entry.index}]" does not match a page or folder`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function navigationOrderEntryMatchesPage(entry, page) {
|
|
410
|
+
if (entry.kind === "directory") return page.relativePath.startsWith(entry.value);
|
|
411
|
+
return page.relativePath === entry.value;
|
|
412
|
+
}
|
|
413
|
+
function isInsideOutput(relativePath, outDirName) {
|
|
414
|
+
return relativePath === outDirName || relativePath.startsWith(`${outDirName}/`);
|
|
415
|
+
}
|
|
416
|
+
function ensureUniqueRoutes(pages) {
|
|
417
|
+
const seen = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const page of pages) {
|
|
419
|
+
const count = seen.get(page.route) ?? 0;
|
|
420
|
+
seen.set(page.route, count + 1);
|
|
421
|
+
if (count === 0) continue;
|
|
422
|
+
const parsed = path.posix.parse(page.route);
|
|
423
|
+
page.route = `${parsed.dir ? `${parsed.dir}/` : ""}${parsed.name}-${count + 1}.html`;
|
|
424
|
+
page.outputPath = page.route;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/vendor/marked-alert/index.ts
|
|
429
|
+
const defaultAlertVariants = [
|
|
430
|
+
{
|
|
431
|
+
type: "note",
|
|
432
|
+
icon: materialSymbol("info")
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
type: "tip",
|
|
436
|
+
icon: materialSymbol("lightbulb")
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
type: "important",
|
|
440
|
+
icon: materialSymbol("feedback")
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
type: "warning",
|
|
444
|
+
icon: materialSymbol("warning")
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
type: "caution",
|
|
448
|
+
icon: materialSymbol("dangerous")
|
|
449
|
+
}
|
|
450
|
+
];
|
|
451
|
+
function markedAlert(options = {}) {
|
|
452
|
+
const { className = "markdown-alert", variants = [] } = options;
|
|
453
|
+
const resolvedVariants = resolveVariants(variants);
|
|
454
|
+
return {
|
|
455
|
+
walkTokens(token) {
|
|
456
|
+
if (token.type !== "blockquote") return;
|
|
457
|
+
const matchedVariant = resolvedVariants.find(({ type }) => new RegExp(createSyntaxPattern(type)).test(token.text));
|
|
458
|
+
if (!matchedVariant) return;
|
|
459
|
+
const { type: variant, icon, title = capitalize$1(matchedVariant.type), titleClassName = `${className}-title` } = matchedVariant;
|
|
460
|
+
const syntaxPattern = new RegExp(createSyntaxPattern(variant));
|
|
461
|
+
Object.assign(token, {
|
|
462
|
+
type: "alert",
|
|
463
|
+
meta: {
|
|
464
|
+
className,
|
|
465
|
+
variant,
|
|
466
|
+
icon,
|
|
467
|
+
title,
|
|
468
|
+
titleClassName
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
const firstLine = token.tokens?.[0];
|
|
472
|
+
const firstLineText = firstLine?.raw?.replace(syntaxPattern, "").trim();
|
|
473
|
+
if (!firstLine || !firstLineText) {
|
|
474
|
+
token.tokens?.shift();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const markerToken = firstLine.tokens[0];
|
|
478
|
+
if (markerToken) Object.assign(markerToken, {
|
|
479
|
+
raw: markerToken.raw.replace(syntaxPattern, ""),
|
|
480
|
+
text: markerToken.text.replace(syntaxPattern, "")
|
|
481
|
+
});
|
|
482
|
+
if (firstLine.tokens[1]?.type === "br") firstLine.tokens.splice(1, 1);
|
|
483
|
+
},
|
|
484
|
+
extensions: [{
|
|
485
|
+
name: "alert",
|
|
486
|
+
level: "block",
|
|
487
|
+
renderer(token) {
|
|
488
|
+
const { meta, tokens = [] } = token;
|
|
489
|
+
let html = `<div class="${meta.className} ${meta.className}-${meta.variant}">\n`;
|
|
490
|
+
html += `<p class="${meta.titleClassName}">`;
|
|
491
|
+
html += meta.icon;
|
|
492
|
+
html += meta.title;
|
|
493
|
+
html += "</p>\n";
|
|
494
|
+
html += this.parser.parse(tokens);
|
|
495
|
+
html += "</div>\n";
|
|
496
|
+
return html;
|
|
497
|
+
}
|
|
498
|
+
}]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function resolveVariants(variants) {
|
|
502
|
+
if (!variants.length) return defaultAlertVariants;
|
|
503
|
+
return Object.values([...defaultAlertVariants, ...variants].reduce((map, item) => {
|
|
504
|
+
map[item.type] = item;
|
|
505
|
+
return map;
|
|
506
|
+
}, {}));
|
|
507
|
+
}
|
|
508
|
+
function createSyntaxPattern(type) {
|
|
509
|
+
return `^(?:\\[!${escapeRegExp(type.toUpperCase())}]\\s*?)\\n*`;
|
|
510
|
+
}
|
|
511
|
+
function capitalize$1(value) {
|
|
512
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1).toLowerCase();
|
|
513
|
+
}
|
|
514
|
+
function materialSymbol(name) {
|
|
515
|
+
return `<span class="material-symbols-rounded markdown-alert-icon" aria-hidden="true">${name}</span>`;
|
|
516
|
+
}
|
|
517
|
+
function escapeRegExp(value) {
|
|
518
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/core/search.ts
|
|
522
|
+
function buildSearchIndex(pages) {
|
|
523
|
+
return pages.flatMap((page) => [{
|
|
524
|
+
kind: "page",
|
|
525
|
+
title: page.title,
|
|
526
|
+
pageTitle: page.title,
|
|
527
|
+
route: page.route,
|
|
528
|
+
headings: page.headings.map((heading) => heading.text),
|
|
529
|
+
text: normalizeSearchText(page.searchText ?? page.markdown)
|
|
530
|
+
}, ...buildSectionEntries(page)]);
|
|
531
|
+
}
|
|
532
|
+
function normalizeSearchText(value) {
|
|
533
|
+
return value.replace(/<[^>]+>/g, " ").replace(/[^\p{L}\p{N}]+/gu, " ").replace(/\s+/g, " ").trim();
|
|
534
|
+
}
|
|
535
|
+
function buildSectionEntries(page) {
|
|
536
|
+
const entries = [];
|
|
537
|
+
const headingQueue = [...page.headings];
|
|
538
|
+
const headingPath = [];
|
|
539
|
+
let activeSection;
|
|
540
|
+
const pushActiveSection = () => {
|
|
541
|
+
if (!activeSection) return;
|
|
542
|
+
entries.push({
|
|
543
|
+
kind: "section",
|
|
544
|
+
title: activeSection.heading.text,
|
|
545
|
+
pageTitle: page.title,
|
|
546
|
+
route: `${page.route}#${activeSection.heading.id}`,
|
|
547
|
+
headings: activeSection.path.map((heading) => heading.text),
|
|
548
|
+
text: normalizeSearchText(activeSection.lines.join("\n")),
|
|
549
|
+
depth: activeSection.heading.depth
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
for (const line of page.markdown.split(/\r?\n/)) {
|
|
553
|
+
if (!line.match(/^(#{1,6})\s+(.+)$/)?.[1]) {
|
|
554
|
+
activeSection?.lines.push(line);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const heading = headingQueue.shift();
|
|
558
|
+
if (!heading) {
|
|
559
|
+
activeSection?.lines.push(line);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
while (headingPath.at(-1) && headingPath.at(-1).depth >= heading.depth) headingPath.pop();
|
|
563
|
+
headingPath.push(heading);
|
|
564
|
+
if (heading.depth === 2 || heading.depth === 3) {
|
|
565
|
+
pushActiveSection();
|
|
566
|
+
activeSection = {
|
|
567
|
+
heading,
|
|
568
|
+
path: [...headingPath],
|
|
569
|
+
lines: []
|
|
570
|
+
};
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (activeSection && heading.depth <= activeSection.heading.depth) {
|
|
574
|
+
pushActiveSection();
|
|
575
|
+
activeSection = void 0;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
activeSection?.lines.push(line);
|
|
579
|
+
}
|
|
580
|
+
pushActiveSection();
|
|
581
|
+
return entries;
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/core/markdown.ts
|
|
585
|
+
async function renderMarkdownPage(model, page, outDir, options = {}) {
|
|
586
|
+
const assets = [];
|
|
587
|
+
const headingIds = [...page.headings];
|
|
588
|
+
let mermaidDiagramIndex = 0;
|
|
589
|
+
const renderer = new Renderer();
|
|
590
|
+
renderer.heading = ({ tokens, depth }) => {
|
|
591
|
+
const text = Parser.parseInline(tokens);
|
|
592
|
+
return `<h${depth} id="${nextHeading(headingIds, depth, stripHtml(text))?.id ?? ""}">${text}</h${depth}>`;
|
|
593
|
+
};
|
|
594
|
+
renderer.link = ({ href, title, tokens }) => {
|
|
595
|
+
const text = Parser.parseInline(tokens);
|
|
596
|
+
const rewritten = rewriteLink(model, page, href);
|
|
597
|
+
const titleAttribute = title ? ` title="${escapeHtml$1(title)}"` : "";
|
|
598
|
+
return `<a href="${escapeHtml$1(rewritten)}"${titleAttribute}>${text}</a>`;
|
|
599
|
+
};
|
|
600
|
+
renderer.image = ({ href, title, text }) => {
|
|
601
|
+
const rewritten = registerAsset(model, page, outDir, href, assets);
|
|
602
|
+
const titleAttribute = title ? ` title="${escapeHtml$1(title)}"` : "";
|
|
603
|
+
return `<img src="${escapeHtml$1(rewritten)}" alt="${escapeHtml$1(text)}"${titleAttribute}>`;
|
|
604
|
+
};
|
|
605
|
+
return {
|
|
606
|
+
html: await new Marked({
|
|
607
|
+
async: true,
|
|
608
|
+
gfm: true,
|
|
609
|
+
renderer
|
|
610
|
+
}).use(markedAlert(), markedShiki({ async highlight(code, lang, props) {
|
|
611
|
+
if (lang?.toLowerCase() === "mermaid") {
|
|
612
|
+
mermaidDiagramIndex += 1;
|
|
613
|
+
if (!options.mermaid) throw new LildocsError(`No Mermaid renderer configured for ${page.relativePath}`);
|
|
614
|
+
try {
|
|
615
|
+
return await options.mermaid.render(code, `mermaid-${stableIdPart(page.route)}-${mermaidDiagramIndex}`);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
throw new LildocsError(`Failed to render Mermaid diagram ${mermaidDiagramIndex} in ${page.relativePath}: ${errorMessage$2(error)}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return highlightCode(code, lang, props, options.shikiTheme);
|
|
621
|
+
} })).parse(page.markdown),
|
|
622
|
+
assets,
|
|
623
|
+
text: normalizeSearchText(page.markdown)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async function highlightCode(code, lang, props, theme = {}) {
|
|
627
|
+
const lightTheme = theme.light ?? "github-light";
|
|
628
|
+
const darkTheme = theme.dark;
|
|
629
|
+
const themeOptions = darkTheme ? {
|
|
630
|
+
themes: {
|
|
631
|
+
light: lightTheme,
|
|
632
|
+
dark: darkTheme
|
|
633
|
+
},
|
|
634
|
+
defaultColor: false
|
|
635
|
+
} : { theme: lightTheme };
|
|
636
|
+
try {
|
|
637
|
+
return normalizeShikiBackground(await codeToHtml(code, {
|
|
638
|
+
lang: lang || "text",
|
|
639
|
+
...themeOptions,
|
|
640
|
+
meta: { __raw: props.join(" ") }
|
|
641
|
+
}));
|
|
642
|
+
} catch {
|
|
643
|
+
return normalizeShikiBackground(await codeToHtml(code, {
|
|
644
|
+
lang: "text",
|
|
645
|
+
...themeOptions
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function normalizeShikiBackground(html) {
|
|
650
|
+
return html.replace(/background-color:[^;"]+;?/, "background-color:var(--ld-color-code-background);").replace(/--shiki-light-bg:[^;"]+/, "--shiki-light-bg:var(--ld-color-code-background)").replace(/--shiki-dark-bg:[^;"]+/, "--shiki-dark-bg:var(--ld-color-code-background)");
|
|
651
|
+
}
|
|
652
|
+
async function copyAssets(assets) {
|
|
653
|
+
const uniqueAssets = new Map(assets.map((asset) => [asset.to, asset]));
|
|
654
|
+
await Promise.all([...uniqueAssets.values()].map(async (asset) => {
|
|
655
|
+
let assetStat;
|
|
656
|
+
try {
|
|
657
|
+
assetStat = await stat(asset.from);
|
|
658
|
+
} catch {
|
|
659
|
+
throw new LildocsError(`Referenced asset does not exist: ${asset.from}`);
|
|
660
|
+
}
|
|
661
|
+
if (!assetStat.isFile()) throw new LildocsError(`Referenced asset is not a file: ${asset.from}`);
|
|
662
|
+
await mkdir(path.dirname(asset.to), { recursive: true });
|
|
663
|
+
await copyFile(asset.from, asset.to);
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
function nextHeading(headings, depth, text) {
|
|
667
|
+
const index = headings.findIndex((heading) => heading.depth === depth && heading.text === text);
|
|
668
|
+
if (index === -1) return;
|
|
669
|
+
return headings.splice(index, 1)[0];
|
|
670
|
+
}
|
|
671
|
+
function rewriteLink(model, page, href) {
|
|
672
|
+
const targetPath = resolveMarkdownDocumentPath(page.relativePath, href);
|
|
673
|
+
if (!targetPath) return href;
|
|
674
|
+
const targetPage = model.byRelativePath.get(targetPath);
|
|
675
|
+
if (!targetPage) return href;
|
|
676
|
+
const relative = path.posix.relative(path.posix.dirname(page.route), targetPage.route);
|
|
677
|
+
const url = relative.startsWith(".") ? relative : `./${relative}`;
|
|
678
|
+
const hash = href.includes("#") ? href.slice(href.indexOf("#") + 1) : void 0;
|
|
679
|
+
return hash ? `${url}#${hash}` : url;
|
|
680
|
+
}
|
|
681
|
+
function registerAsset(model, page, outDir, href, assets) {
|
|
682
|
+
if (isExternalOrAnchorUrl(href)) return href;
|
|
683
|
+
const source = path.resolve(model.docsRoot, path.dirname(page.relativePath), href);
|
|
684
|
+
const outputRelativePath = `assets/${toPosixPath(path.relative(model.docsRoot, source))}`;
|
|
685
|
+
assets.push({
|
|
686
|
+
from: source,
|
|
687
|
+
to: path.join(outDir, outputRelativePath)
|
|
688
|
+
});
|
|
689
|
+
return rootRelativeUrl(page.route, outputRelativePath);
|
|
690
|
+
}
|
|
691
|
+
function stripHtml(value) {
|
|
692
|
+
return value.replace(/<[^>]*>/g, "");
|
|
693
|
+
}
|
|
694
|
+
function stableIdPart(value) {
|
|
695
|
+
return value.replace(/\.[^.]+$/, "").replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
696
|
+
}
|
|
697
|
+
function errorMessage$2(error) {
|
|
698
|
+
if (error instanceof Error) return error.message;
|
|
699
|
+
return String(error);
|
|
700
|
+
}
|
|
701
|
+
function escapeHtml$1(value) {
|
|
702
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
703
|
+
}
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region src/core/mermaid.ts
|
|
706
|
+
async function createMermaidRenderer(options) {
|
|
707
|
+
return {
|
|
708
|
+
async render(source, idHint) {
|
|
709
|
+
try {
|
|
710
|
+
const svg = addImageRole(renderMermaidSVG(source, toRenderOptions(options.themeConfig)));
|
|
711
|
+
return `<figure class="mermaidDiagram" id="${escapeHtml(idHint)}">${svg}</figure>`;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
throw new LildocsError(`Failed to render Mermaid diagram ${idHint}: ${errorMessage$1(error)}`);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
async close() {}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function toRenderOptions(themeConfig) {
|
|
720
|
+
return {
|
|
721
|
+
bg: themeConfig.bg,
|
|
722
|
+
fg: themeConfig.fg,
|
|
723
|
+
accent: themeConfig.accent,
|
|
724
|
+
muted: themeConfig.muted,
|
|
725
|
+
surface: themeConfig.surface,
|
|
726
|
+
border: themeConfig.border,
|
|
727
|
+
font: themeConfig.fontFamily
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function addImageRole(svg) {
|
|
731
|
+
return svg.replace("<svg", "<svg role=\"img\"");
|
|
732
|
+
}
|
|
733
|
+
function errorMessage$1(error) {
|
|
734
|
+
if (error instanceof Error) return error.message;
|
|
735
|
+
return String(error);
|
|
736
|
+
}
|
|
737
|
+
function escapeHtml(value) {
|
|
738
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
739
|
+
}
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/core/nav.ts
|
|
742
|
+
function buildNavigation(model, activePage) {
|
|
743
|
+
return nestNavItems(model.pages.map((page) => pageToNavItem(page, activePage)));
|
|
744
|
+
}
|
|
745
|
+
function pageToNavItem(page, activePage) {
|
|
746
|
+
return {
|
|
747
|
+
title: page.title,
|
|
748
|
+
route: page.route,
|
|
749
|
+
active: page.route === activePage.route,
|
|
750
|
+
hasPage: true,
|
|
751
|
+
children: []
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function nestNavItems(items) {
|
|
755
|
+
const topLevel = [];
|
|
756
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
757
|
+
for (const item of items) {
|
|
758
|
+
const dir = path.posix.dirname(item.route);
|
|
759
|
+
if (dir === ".") {
|
|
760
|
+
topLevel.push(item);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const parts = dir.split("/");
|
|
764
|
+
let currentChildren = topLevel;
|
|
765
|
+
let currentPath = "";
|
|
766
|
+
for (const part of parts) {
|
|
767
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
768
|
+
let group = byDir.get(currentPath);
|
|
769
|
+
if (!group) {
|
|
770
|
+
group = {
|
|
771
|
+
title: titleFromDir(part),
|
|
772
|
+
route: `${currentPath}/index.html`,
|
|
773
|
+
active: false,
|
|
774
|
+
hasPage: false,
|
|
775
|
+
children: []
|
|
776
|
+
};
|
|
777
|
+
byDir.set(currentPath, group);
|
|
778
|
+
currentChildren.push(group);
|
|
779
|
+
}
|
|
780
|
+
currentChildren = group.children;
|
|
781
|
+
}
|
|
782
|
+
const group = byDir.get(dir);
|
|
783
|
+
if (group && item.route === group.route) {
|
|
784
|
+
group.active = item.active;
|
|
785
|
+
group.hasPage = true;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
currentChildren.push(item);
|
|
789
|
+
}
|
|
790
|
+
return topLevel;
|
|
791
|
+
}
|
|
792
|
+
function titleFromDir(dir) {
|
|
793
|
+
return dir.replace(/[-_]+/g, " ").replace(/\S+/g, (word) => `${word[0]?.toLocaleUpperCase() ?? ""}${word.slice(1)}`);
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/core/reference.ts
|
|
797
|
+
async function buildReferencePages(options) {
|
|
798
|
+
const packageJson = await resolveReferencePackageJson(options);
|
|
799
|
+
if (!packageJson) return [];
|
|
800
|
+
const packageJsonData = await readPackageJson(packageJson.packagePath);
|
|
801
|
+
if (!packageJson.configured && packageJsonData.exports === void 0) return [];
|
|
802
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "lildocs-reference-"));
|
|
803
|
+
try {
|
|
804
|
+
await generateMarkdownForModule(packageJson.packagePath, {
|
|
805
|
+
cwd: options.cwd,
|
|
806
|
+
outDir: tempDir
|
|
807
|
+
});
|
|
808
|
+
const markdownPaths = await collectMarkdownPaths(tempDir);
|
|
809
|
+
return Promise.all(markdownPaths.map(async (sourcePath) => {
|
|
810
|
+
const generatedPath = toPosixPath(path.relative(tempDir, sourcePath));
|
|
811
|
+
return {
|
|
812
|
+
sourcePath,
|
|
813
|
+
relativePath: toPosixPath(path.posix.join("reference", generatedPath)),
|
|
814
|
+
rawMarkdown: await readFile(sourcePath, "utf8")
|
|
815
|
+
};
|
|
816
|
+
}));
|
|
817
|
+
} catch (error) {
|
|
818
|
+
throw new LildocsError(`Failed to generate API reference: ${errorMessage(error)}`);
|
|
819
|
+
} finally {
|
|
820
|
+
await rm(tempDir, {
|
|
821
|
+
recursive: true,
|
|
822
|
+
force: true
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async function resolveReferencePackageJson(options) {
|
|
827
|
+
const configuredPath = options.reference?.packageJson;
|
|
828
|
+
if (configuredPath) {
|
|
829
|
+
const packagePath = path.resolve(options.docsRoot, configuredPath);
|
|
830
|
+
if (!existsSync(packagePath)) throw new LildocsError(`Configured reference package.json does not exist: ${packagePath}`);
|
|
831
|
+
return {
|
|
832
|
+
packagePath,
|
|
833
|
+
configured: true
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const siblingPackageJson = path.join(path.dirname(options.docsRoot), "package.json");
|
|
837
|
+
if (existsSync(siblingPackageJson)) return {
|
|
838
|
+
packagePath: siblingPackageJson,
|
|
839
|
+
configured: false
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function readPackageJson(packagePath) {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(await readFile(packagePath, "utf8"));
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (error instanceof SyntaxError) throw new LildocsError(`Invalid package.json for API reference: ${packagePath}`);
|
|
847
|
+
throw error;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function errorMessage(error) {
|
|
851
|
+
if (error instanceof Error) return error.message;
|
|
852
|
+
return String(error);
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/core/logo.ts
|
|
856
|
+
async function resolveLogoOptions(options) {
|
|
857
|
+
const assets = [];
|
|
858
|
+
const css = [];
|
|
859
|
+
const logo = {};
|
|
860
|
+
if (options.logo?.text) logo.text = options.logo.text;
|
|
861
|
+
else if (!options.logo?.image && options.defaultText) logo.text = options.defaultText;
|
|
862
|
+
if (options.logo?.image) logo.image = imageAssetValue(options.logo.image, options, assets);
|
|
863
|
+
if (options.logo?.font) {
|
|
864
|
+
const localFontPath = await resolveLocalFontPath$1(options.logo.font, options);
|
|
865
|
+
if (localFontPath) {
|
|
866
|
+
const family = "Lildocs Logo Font";
|
|
867
|
+
assets.push({
|
|
868
|
+
from: localFontPath,
|
|
869
|
+
to: path.join(options.outDir, "assets", "fonts", path.basename(localFontPath))
|
|
870
|
+
});
|
|
871
|
+
css.push(`@font-face {
|
|
872
|
+
font-family: ${quoteCssString$1(family)};
|
|
873
|
+
src: url("./fonts/${path.basename(localFontPath)}") format("${fontFormat$1(localFontPath)}");
|
|
874
|
+
font-display: swap;
|
|
875
|
+
}`);
|
|
876
|
+
css.push(`:root {
|
|
877
|
+
--ld-font-logo: ${quoteCssString$1(family)};
|
|
878
|
+
}`);
|
|
879
|
+
} else {
|
|
880
|
+
css.push(`@import url("${googleFontsCssUrl$1(options.logo.font, "400;500;600;700")}");`);
|
|
881
|
+
css.push(`:root {
|
|
882
|
+
--ld-font-logo: ${quoteCssString$1(options.logo.font)};
|
|
883
|
+
}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
assets,
|
|
888
|
+
css: css.length > 0 ? `${css.join("\n")}\n` : "",
|
|
889
|
+
logo,
|
|
890
|
+
favicon: options.favicon ? imageAssetValue(options.favicon, options, assets) : void 0
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function imageAssetValue(value, options, assets) {
|
|
894
|
+
const trimmed = value.trim();
|
|
895
|
+
if (isRemoteImage(trimmed)) return trimmed;
|
|
896
|
+
const source = path.resolve(options.docsRoot, trimmed);
|
|
897
|
+
const assetRelativePath = path.relative(options.docsRoot, source).split(path.sep).join("/");
|
|
898
|
+
assets.push({
|
|
899
|
+
from: source,
|
|
900
|
+
to: path.join(options.outDir, "assets", assetRelativePath)
|
|
901
|
+
});
|
|
902
|
+
return `assets/${assetRelativePath}`;
|
|
903
|
+
}
|
|
904
|
+
async function resolveLocalFontPath$1(value, options) {
|
|
905
|
+
const candidates = [path.resolve(options.cwd, value), path.resolve(options.docsRoot, value)];
|
|
906
|
+
const existingCandidate = (await Promise.all(candidates.map(async (candidate) => ({
|
|
907
|
+
candidate,
|
|
908
|
+
exists: isFontFile$1(candidate) && await exists$1(candidate)
|
|
909
|
+
})))).find((candidate) => candidate.exists);
|
|
910
|
+
if (existingCandidate) return existingCandidate.candidate;
|
|
911
|
+
if (isFontFile$1(value) && path.basename(value) !== value) throw new LildocsError(`Logo font file does not exist: ${value}`);
|
|
912
|
+
}
|
|
913
|
+
function isRemoteImage(value) {
|
|
914
|
+
return /^(?:[a-z]+:)?\/\//i.test(value) || value.startsWith("data:") || value.startsWith("/");
|
|
915
|
+
}
|
|
916
|
+
function isFontFile$1(value) {
|
|
917
|
+
return /\.(?:woff2?|ttf|otf)$/i.test(value);
|
|
918
|
+
}
|
|
919
|
+
function fontFormat$1(value) {
|
|
920
|
+
const extension = path.extname(value).toLowerCase();
|
|
921
|
+
if (extension === ".woff2") return "woff2";
|
|
922
|
+
if (extension === ".woff") return "woff";
|
|
923
|
+
if (extension === ".ttf") return "truetype";
|
|
924
|
+
if (extension === ".otf") return "opentype";
|
|
925
|
+
return "woff2";
|
|
926
|
+
}
|
|
927
|
+
function googleFontsCssUrl$1(fontName, weights) {
|
|
928
|
+
return `https://fonts.googleapis.com/css2?family=${fontName.trim().replace(/\s+/g, "+")}:wght@${weights}&display=swap`;
|
|
929
|
+
}
|
|
930
|
+
function quoteCssString$1(value) {
|
|
931
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
932
|
+
}
|
|
933
|
+
async function exists$1(filePath) {
|
|
934
|
+
try {
|
|
935
|
+
await access(filePath);
|
|
936
|
+
return true;
|
|
937
|
+
} catch {
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region src/core/theme.ts
|
|
943
|
+
const MIN_BACKGROUND_CONTRAST = 1.08;
|
|
944
|
+
const PREFERRED_BACKGROUND_CONTRAST = 1.14;
|
|
945
|
+
const MAX_BACKGROUND_CONTRAST_STEPS = 12;
|
|
946
|
+
const BACKGROUND_LIGHTNESS_STEP = .015;
|
|
947
|
+
const MAX_BACKGROUND_CHROMA = .03;
|
|
948
|
+
const toOklch = converter("oklch");
|
|
949
|
+
const clampToRgb = clampGamut("rgb");
|
|
950
|
+
const themes = {
|
|
951
|
+
default: {
|
|
952
|
+
color: {
|
|
953
|
+
background: "#ffffff",
|
|
954
|
+
text: "#171717",
|
|
955
|
+
mutedText: "#666666",
|
|
956
|
+
border: "#e5e5e5",
|
|
957
|
+
link: "#2563eb",
|
|
958
|
+
codeBackground: "#f6f8fa",
|
|
959
|
+
sidebarBackground: "#fafafa"
|
|
960
|
+
},
|
|
961
|
+
font: {
|
|
962
|
+
heading: "system-ui, sans-serif",
|
|
963
|
+
body: "system-ui, sans-serif",
|
|
964
|
+
code: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
965
|
+
},
|
|
966
|
+
shiki: { theme: "github-light" }
|
|
967
|
+
},
|
|
968
|
+
minimal: {
|
|
969
|
+
color: {
|
|
970
|
+
background: "#ffffff",
|
|
971
|
+
text: "#111111",
|
|
972
|
+
mutedText: "#707070",
|
|
973
|
+
border: "#dddddd",
|
|
974
|
+
link: "#0f766e",
|
|
975
|
+
codeBackground: "#f7f7f7",
|
|
976
|
+
sidebarBackground: "#ffffff"
|
|
977
|
+
},
|
|
978
|
+
font: {
|
|
979
|
+
heading: "Arial, sans-serif",
|
|
980
|
+
body: "Arial, sans-serif",
|
|
981
|
+
code: "Menlo, Consolas, monospace"
|
|
982
|
+
},
|
|
983
|
+
shiki: { theme: "github-light" }
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
async function resolveTheme(options) {
|
|
987
|
+
if (options.requestedTheme) {
|
|
988
|
+
if (typeof options.requestedTheme === "string") return { light: await resolveThemeName(options.requestedTheme) };
|
|
989
|
+
return {
|
|
990
|
+
light: await resolveThemeName(options.requestedTheme.light ?? "default"),
|
|
991
|
+
dark: await resolveThemeName(options.requestedTheme.dark ?? "github-dark")
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const localThemePath = path.join(options.docsRoot, "theme.ts");
|
|
995
|
+
if (await exists(localThemePath)) return { light: validateTheme(await createJiti(pathToFileURL(options.cwd).href).import(localThemePath, { default: true }), localThemePath) };
|
|
996
|
+
return {
|
|
997
|
+
light: themes.default,
|
|
998
|
+
dark: await resolveThemeName("github-dark")
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async function resolveThemeName(themeName) {
|
|
1002
|
+
const theme = themes[themeName];
|
|
1003
|
+
if (theme) return theme;
|
|
1004
|
+
return resolveShikiTheme(themeName);
|
|
1005
|
+
}
|
|
1006
|
+
async function resolveShikiTheme(themeName) {
|
|
1007
|
+
const themeLoader = bundledThemes[themeName];
|
|
1008
|
+
if (!themeLoader) throw new LildocsError(`Unknown theme "${themeName}". Use a built-in lildocs theme (${Object.keys(themes).join(", ")}) or a bundled Shiki theme.`);
|
|
1009
|
+
const themeModule = await themeLoader();
|
|
1010
|
+
return mapShikiThemeToTheme("default" in themeModule ? themeModule.default : themeModule, themeName);
|
|
1011
|
+
}
|
|
1012
|
+
async function resolveFontOverrides(options) {
|
|
1013
|
+
const css = [];
|
|
1014
|
+
const assets = [];
|
|
1015
|
+
const themeFonts = {};
|
|
1016
|
+
const resolved = await Promise.all([
|
|
1017
|
+
"heading",
|
|
1018
|
+
"body",
|
|
1019
|
+
"code"
|
|
1020
|
+
].map(async (target) => {
|
|
1021
|
+
const value = options.fonts?.[target];
|
|
1022
|
+
return {
|
|
1023
|
+
target,
|
|
1024
|
+
value,
|
|
1025
|
+
localFontPath: value ? await resolveLocalFontPath(value, options) : void 0
|
|
1026
|
+
};
|
|
1027
|
+
}));
|
|
1028
|
+
for (const { target, value, localFontPath } of resolved) {
|
|
1029
|
+
if (!value) continue;
|
|
1030
|
+
if (localFontPath) {
|
|
1031
|
+
const family = `Lildocs ${capitalize(target)} Font`;
|
|
1032
|
+
const outputRelativePath = `assets/fonts/${path.basename(localFontPath)}`;
|
|
1033
|
+
assets.push({
|
|
1034
|
+
from: localFontPath,
|
|
1035
|
+
to: path.join(options.outDir, outputRelativePath)
|
|
1036
|
+
});
|
|
1037
|
+
css.push(`@font-face {
|
|
1038
|
+
font-family: ${quoteCssString(family)};
|
|
1039
|
+
src: url("./fonts/${path.basename(localFontPath)}") format("${fontFormat(localFontPath)}");
|
|
1040
|
+
font-display: swap;
|
|
1041
|
+
}`);
|
|
1042
|
+
themeFonts[target] = quoteCssString(family);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
css.push(`@import url("${googleFontsCssUrl(value, target === "code" ? "400" : "400;500;600;700")}");`);
|
|
1046
|
+
themeFonts[target] = quoteCssString(value);
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
assets,
|
|
1050
|
+
css: css.length > 0 ? `${css.join("\n")}\n` : "",
|
|
1051
|
+
themeFonts
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function themeToCssVariables(theme, fontOverrides = {}, linkOptions = {}, navigationOptions = {}) {
|
|
1055
|
+
const linkDecoration = linkTextDecoration(linkOptions.underline);
|
|
1056
|
+
const navigationDuration = navigationOptions.duration ?? 180;
|
|
1057
|
+
const rootVariables = themeToCssVariableBlock(theme.light, fontOverrides);
|
|
1058
|
+
const darkVariables = theme.dark ? themeToCssVariableBlock(theme.dark, fontOverrides) : void 0;
|
|
1059
|
+
return `:root {
|
|
1060
|
+
${theme.dark ? " color-scheme: light dark;\n" : ""}${rootVariables}
|
|
1061
|
+
--ld-background-image: none;
|
|
1062
|
+
--ld-background-blend-mode: normal;
|
|
1063
|
+
--ld-font-logo: var(--ld-font-heading);
|
|
1064
|
+
--ld-link-text-decoration: ${linkDecoration.default};
|
|
1065
|
+
--ld-link-hover-text-decoration: ${linkDecoration.hover};
|
|
1066
|
+
--ld-navigation-duration: ${navigationDuration}ms;
|
|
1067
|
+
--ld-navigation-easing: ${navigationOptions.easing ?? "ease"};
|
|
1068
|
+
}${darkVariables ? `
|
|
1069
|
+
|
|
1070
|
+
@media (prefers-color-scheme: dark) {
|
|
1071
|
+
:root {
|
|
1072
|
+
${darkVariables} color-scheme: dark;
|
|
1073
|
+
}
|
|
1074
|
+
}` : ""}`;
|
|
1075
|
+
}
|
|
1076
|
+
function resolveBackgroundOptions(options) {
|
|
1077
|
+
const assets = [];
|
|
1078
|
+
const layers = [];
|
|
1079
|
+
if (options.background?.gradient) layers.push(options.background.gradient);
|
|
1080
|
+
if (options.background?.image) layers.push(backgroundImageValue(options.background.image, options, assets));
|
|
1081
|
+
if (layers.length === 0 && !options.background?.blendMode) return {
|
|
1082
|
+
assets,
|
|
1083
|
+
css: ""
|
|
1084
|
+
};
|
|
1085
|
+
return {
|
|
1086
|
+
assets,
|
|
1087
|
+
css: `:root {
|
|
1088
|
+
--ld-background-image: ${layers.length > 0 ? layers.join(", ") : "none"};
|
|
1089
|
+
--ld-background-blend-mode: ${options.background?.blendMode ?? "normal"};
|
|
1090
|
+
}
|
|
1091
|
+
`
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function themeToMermaidConfig(theme, fontOverrides = {}) {
|
|
1095
|
+
const fonts = resolveThemeFonts(theme.light, fontOverrides);
|
|
1096
|
+
return {
|
|
1097
|
+
bg: theme.light.color.background,
|
|
1098
|
+
fg: theme.light.color.text,
|
|
1099
|
+
accent: theme.light.color.link,
|
|
1100
|
+
muted: theme.light.color.mutedText,
|
|
1101
|
+
surface: theme.light.color.codeBackground,
|
|
1102
|
+
border: theme.light.color.border,
|
|
1103
|
+
fontFamily: fonts.body
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function themeToCssVariableBlock(theme, fontOverrides) {
|
|
1107
|
+
const fonts = resolveThemeFonts(theme, fontOverrides);
|
|
1108
|
+
const color = normalizeThemeColors(theme.color);
|
|
1109
|
+
return ` --ld-color-background: ${color.background};
|
|
1110
|
+
--ld-color-text: ${color.text};
|
|
1111
|
+
--ld-color-muted-text: ${color.mutedText};
|
|
1112
|
+
--ld-color-border: ${color.border};
|
|
1113
|
+
--ld-color-link: ${color.link};
|
|
1114
|
+
--ld-color-code-background: ${color.codeBackground};
|
|
1115
|
+
--ld-color-sidebar-background: ${color.sidebarBackground ?? color.background};
|
|
1116
|
+
--ld-font-heading: ${fonts.heading};
|
|
1117
|
+
--ld-font-body: ${fonts.body};
|
|
1118
|
+
--ld-font-code: ${fonts.code};
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
function normalizeThemeColors(color) {
|
|
1122
|
+
return {
|
|
1123
|
+
...color,
|
|
1124
|
+
border: ensureDarkBorderLightness({
|
|
1125
|
+
background: color.background,
|
|
1126
|
+
text: color.text,
|
|
1127
|
+
candidate: color.border
|
|
1128
|
+
})
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function validateTheme(value, themePath) {
|
|
1132
|
+
if (!value || typeof value !== "object") throw new LildocsError(`Local theme must export an object: ${themePath}`);
|
|
1133
|
+
const theme = value;
|
|
1134
|
+
if ([
|
|
1135
|
+
theme.color?.background,
|
|
1136
|
+
theme.color?.text,
|
|
1137
|
+
theme.color?.mutedText,
|
|
1138
|
+
theme.color?.border,
|
|
1139
|
+
theme.color?.link,
|
|
1140
|
+
theme.color?.codeBackground,
|
|
1141
|
+
theme.font?.body,
|
|
1142
|
+
theme.font?.code ?? theme.font?.mono
|
|
1143
|
+
].some((item) => typeof item !== "string" || item.length === 0)) throw new LildocsError(`Local theme is missing required color or font string values: ${themePath}`);
|
|
1144
|
+
return theme;
|
|
1145
|
+
}
|
|
1146
|
+
function mapShikiThemeToTheme(shikiTheme, themeName) {
|
|
1147
|
+
const colors = shikiTheme.colors ?? {};
|
|
1148
|
+
const tokenColors = shikiTheme.tokenColors ?? [];
|
|
1149
|
+
const background = pickColor(colors, [
|
|
1150
|
+
"editor.background",
|
|
1151
|
+
"background",
|
|
1152
|
+
"sideBar.background"
|
|
1153
|
+
], shikiTheme.bg ?? (shikiTheme.type === "dark" ? "#111111" : "#ffffff"));
|
|
1154
|
+
const text = pickColor(colors, [
|
|
1155
|
+
"editor.foreground",
|
|
1156
|
+
"foreground",
|
|
1157
|
+
"sideBar.foreground"
|
|
1158
|
+
], shikiTheme.fg ?? (shikiTheme.type === "dark" ? "#f5f5f5" : "#111111"));
|
|
1159
|
+
return {
|
|
1160
|
+
color: {
|
|
1161
|
+
background,
|
|
1162
|
+
text,
|
|
1163
|
+
mutedText: pickColor(colors, [
|
|
1164
|
+
"descriptionForeground",
|
|
1165
|
+
"breadcrumb.foreground",
|
|
1166
|
+
"editorLineNumber.foreground",
|
|
1167
|
+
"sideBar.foreground"
|
|
1168
|
+
], tokenForeground(tokenColors, "comment") ?? text),
|
|
1169
|
+
border: pickColor(colors, [
|
|
1170
|
+
"panel.border",
|
|
1171
|
+
"sideBar.border",
|
|
1172
|
+
"editorGroup.border",
|
|
1173
|
+
"tab.border"
|
|
1174
|
+
], shikiTheme.type === "dark" ? "#333333" : "#e5e5e5"),
|
|
1175
|
+
link: pickColor(colors, [
|
|
1176
|
+
"textLink.foreground",
|
|
1177
|
+
"terminal.ansiBlue",
|
|
1178
|
+
"activityBarBadge.background"
|
|
1179
|
+
], tokenForeground(tokenColors, "markup.inline.raw") ?? (shikiTheme.type === "dark" ? "#79b8ff" : "#2563eb")),
|
|
1180
|
+
codeBackground: ensureBackgroundContrast({
|
|
1181
|
+
background,
|
|
1182
|
+
candidate: pickColor(colors, [
|
|
1183
|
+
"textCodeBlock.background",
|
|
1184
|
+
"editorWidget.background",
|
|
1185
|
+
"editor.lineHighlightBackground",
|
|
1186
|
+
"editor.background"
|
|
1187
|
+
], background),
|
|
1188
|
+
isDark: shikiTheme.type === "dark"
|
|
1189
|
+
}),
|
|
1190
|
+
sidebarBackground: pickColor(colors, [
|
|
1191
|
+
"sideBar.background",
|
|
1192
|
+
"activityBar.background",
|
|
1193
|
+
"editorGroupHeader.tabsBackground"
|
|
1194
|
+
], background)
|
|
1195
|
+
},
|
|
1196
|
+
font: {
|
|
1197
|
+
heading: "system-ui, sans-serif",
|
|
1198
|
+
body: "system-ui, sans-serif",
|
|
1199
|
+
code: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
1200
|
+
},
|
|
1201
|
+
shiki: { theme: themeName }
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function pickColor(colors, keys, fallback) {
|
|
1205
|
+
for (const key of keys) {
|
|
1206
|
+
const value = colors[key];
|
|
1207
|
+
if (typeof value === "string" && value.trim()) return normalizeColor(value);
|
|
1208
|
+
}
|
|
1209
|
+
return normalizeColor(fallback);
|
|
1210
|
+
}
|
|
1211
|
+
function tokenForeground(tokenColors, scope) {
|
|
1212
|
+
return tokenColors.find((item) => {
|
|
1213
|
+
return (Array.isArray(item.scope) ? item.scope : [item.scope]).includes(scope);
|
|
1214
|
+
})?.settings?.foreground;
|
|
1215
|
+
}
|
|
1216
|
+
function normalizeColor(value) {
|
|
1217
|
+
if (/^#[0-9a-f]{3,8}$/i.test(value)) return value;
|
|
1218
|
+
return value;
|
|
1219
|
+
}
|
|
1220
|
+
function linkTextDecoration(underline = "always") {
|
|
1221
|
+
if (underline === "hover") return {
|
|
1222
|
+
default: "none",
|
|
1223
|
+
hover: "underline"
|
|
1224
|
+
};
|
|
1225
|
+
if (underline === "none") return {
|
|
1226
|
+
default: "none",
|
|
1227
|
+
hover: "none"
|
|
1228
|
+
};
|
|
1229
|
+
return {
|
|
1230
|
+
default: "underline",
|
|
1231
|
+
hover: "underline"
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function backgroundImageValue(value, options, assets) {
|
|
1235
|
+
const trimmed = value.trim();
|
|
1236
|
+
if (isCssImageFunction(trimmed)) return trimmed;
|
|
1237
|
+
if (/^(?:[a-z]+:)?\/\//i.test(trimmed) || trimmed.startsWith("data:") || trimmed.startsWith("/")) return `url(${quoteCssString(trimmed)})`;
|
|
1238
|
+
const source = path.resolve(options.docsRoot, trimmed);
|
|
1239
|
+
const assetRelativePath = path.relative(options.docsRoot, source).split(path.sep).join("/");
|
|
1240
|
+
assets.push({
|
|
1241
|
+
from: source,
|
|
1242
|
+
to: path.join(options.outDir, "assets", assetRelativePath)
|
|
1243
|
+
});
|
|
1244
|
+
return `url(${quoteCssString(`./${assetRelativePath}`)})`;
|
|
1245
|
+
}
|
|
1246
|
+
function isCssImageFunction(value) {
|
|
1247
|
+
return /^(?:url|linear-gradient|radial-gradient|conic-gradient|repeating-linear-gradient|repeating-radial-gradient|repeating-conic-gradient)\(/i.test(value);
|
|
1248
|
+
}
|
|
1249
|
+
function ensureBackgroundContrast(options) {
|
|
1250
|
+
const background = parse(options.background);
|
|
1251
|
+
const candidate = parse(options.candidate);
|
|
1252
|
+
if (!background || !candidate) return options.candidate;
|
|
1253
|
+
const currentContrast = wcagContrast(background, candidate);
|
|
1254
|
+
if (currentContrast >= MIN_BACKGROUND_CONTRAST) return options.candidate;
|
|
1255
|
+
const candidateOklch = toOklch(candidate);
|
|
1256
|
+
if (!candidateOklch || typeof candidateOklch.l !== "number") return options.candidate;
|
|
1257
|
+
const direction = options.isDark ? 1 : -1;
|
|
1258
|
+
let bestColor = options.candidate;
|
|
1259
|
+
let bestContrast = currentContrast;
|
|
1260
|
+
for (let step = 1; step <= MAX_BACKGROUND_CONTRAST_STEPS; step += 1) {
|
|
1261
|
+
const clampedColor = clampToRgb({
|
|
1262
|
+
...candidateOklch,
|
|
1263
|
+
l: clamp(candidateOklch.l + direction * BACKGROUND_LIGHTNESS_STEP * step, 0, 1),
|
|
1264
|
+
c: typeof candidateOklch.c === "number" ? Math.min(candidateOklch.c, MAX_BACKGROUND_CHROMA) : candidateOklch.c
|
|
1265
|
+
});
|
|
1266
|
+
if (!clampedColor) continue;
|
|
1267
|
+
const adjustedColor = formatHex(clampedColor);
|
|
1268
|
+
const adjustedParsedColor = parse(adjustedColor);
|
|
1269
|
+
if (!adjustedParsedColor) continue;
|
|
1270
|
+
const adjustedContrast = wcagContrast(background, adjustedParsedColor);
|
|
1271
|
+
if (adjustedContrast > bestContrast) {
|
|
1272
|
+
bestColor = adjustedColor;
|
|
1273
|
+
bestContrast = adjustedContrast;
|
|
1274
|
+
}
|
|
1275
|
+
if (adjustedContrast >= PREFERRED_BACKGROUND_CONTRAST) return adjustedColor;
|
|
1276
|
+
}
|
|
1277
|
+
return bestColor;
|
|
1278
|
+
}
|
|
1279
|
+
function ensureDarkBorderLightness(options) {
|
|
1280
|
+
const background = parse(options.background);
|
|
1281
|
+
const text = parse(options.text);
|
|
1282
|
+
const candidate = parse(options.candidate);
|
|
1283
|
+
if (!background || !text || !candidate) return options.candidate;
|
|
1284
|
+
const backgroundOklch = toOklch(background);
|
|
1285
|
+
const textOklch = toOklch(text);
|
|
1286
|
+
const candidateOklch = toOklch(candidate);
|
|
1287
|
+
if (!backgroundOklch || !textOklch || !candidateOklch || typeof backgroundOklch.l !== "number" || typeof textOklch.l !== "number" || typeof candidateOklch.l !== "number") return options.candidate;
|
|
1288
|
+
if (backgroundOklch.l >= textOklch.l || candidateOklch.l > backgroundOklch.l) return options.candidate;
|
|
1289
|
+
let bestColor = options.candidate;
|
|
1290
|
+
let bestLightness = candidateOklch.l;
|
|
1291
|
+
for (let step = 1; step <= MAX_BACKGROUND_CONTRAST_STEPS; step += 1) {
|
|
1292
|
+
const clampedColor = clampToRgb({
|
|
1293
|
+
...candidateOklch,
|
|
1294
|
+
l: clamp(candidateOklch.l + BACKGROUND_LIGHTNESS_STEP * step, 0, 1),
|
|
1295
|
+
c: typeof candidateOklch.c === "number" ? Math.min(candidateOklch.c, MAX_BACKGROUND_CHROMA) : candidateOklch.c
|
|
1296
|
+
});
|
|
1297
|
+
if (!clampedColor) continue;
|
|
1298
|
+
const adjustedColor = formatHex(clampedColor);
|
|
1299
|
+
const adjustedParsedColor = parse(adjustedColor);
|
|
1300
|
+
const adjustedOklch = adjustedParsedColor ? toOklch(adjustedParsedColor) : void 0;
|
|
1301
|
+
if (!adjustedOklch || typeof adjustedOklch.l !== "number") continue;
|
|
1302
|
+
if (adjustedOklch.l > bestLightness) {
|
|
1303
|
+
bestColor = adjustedColor;
|
|
1304
|
+
bestLightness = adjustedOklch.l;
|
|
1305
|
+
}
|
|
1306
|
+
if (adjustedOklch.l > backgroundOklch.l) return adjustedColor;
|
|
1307
|
+
}
|
|
1308
|
+
return bestColor;
|
|
1309
|
+
}
|
|
1310
|
+
function clamp(value, min, max) {
|
|
1311
|
+
return Math.min(Math.max(value, min), max);
|
|
1312
|
+
}
|
|
1313
|
+
function resolveThemeFonts(theme, overrides) {
|
|
1314
|
+
const body = overrides.body ?? theme.font.body;
|
|
1315
|
+
return {
|
|
1316
|
+
heading: overrides.heading ?? theme.font.heading ?? body,
|
|
1317
|
+
body,
|
|
1318
|
+
code: overrides.code ?? theme.font.code ?? theme.font.mono ?? "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
async function resolveLocalFontPath(value, options) {
|
|
1322
|
+
const candidates = [path.resolve(options.cwd, value), path.resolve(options.docsRoot, value)];
|
|
1323
|
+
const existingCandidate = (await Promise.all(candidates.map(async (candidate) => ({
|
|
1324
|
+
candidate,
|
|
1325
|
+
exists: isFontFile(candidate) && await exists(candidate)
|
|
1326
|
+
})))).find((candidate) => candidate.exists);
|
|
1327
|
+
if (existingCandidate) return existingCandidate.candidate;
|
|
1328
|
+
if (isFontFile(value) && path.basename(value) !== value) throw new LildocsError(`Font file does not exist: ${value}`);
|
|
1329
|
+
}
|
|
1330
|
+
function isFontFile(value) {
|
|
1331
|
+
return /\.(?:woff2?|ttf|otf)$/i.test(value);
|
|
1332
|
+
}
|
|
1333
|
+
function fontFormat(value) {
|
|
1334
|
+
const extension = path.extname(value).toLowerCase();
|
|
1335
|
+
if (extension === ".woff2") return "woff2";
|
|
1336
|
+
if (extension === ".woff") return "woff";
|
|
1337
|
+
if (extension === ".ttf") return "truetype";
|
|
1338
|
+
if (extension === ".otf") return "opentype";
|
|
1339
|
+
return "woff2";
|
|
1340
|
+
}
|
|
1341
|
+
function googleFontsCssUrl(fontName, weights) {
|
|
1342
|
+
return `https://fonts.googleapis.com/css2?family=${fontName.trim().replace(/\s+/g, "+")}:wght@${weights}&display=swap`;
|
|
1343
|
+
}
|
|
1344
|
+
function quoteCssString(value) {
|
|
1345
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
1346
|
+
}
|
|
1347
|
+
function capitalize(value) {
|
|
1348
|
+
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
1349
|
+
}
|
|
1350
|
+
async function exists(filePath) {
|
|
1351
|
+
try {
|
|
1352
|
+
await access(filePath);
|
|
1353
|
+
return true;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/render/Layout.tsx
|
|
1360
|
+
function Layout({ page, nav, pageNavigation, css, searchIndexJson, logo, favicon, repositoryUrl, projectName, navigation, dev }) {
|
|
1361
|
+
const transition = navigation?.transition ?? "fade";
|
|
1362
|
+
const documentTitle = projectName ? `${page.title} • ${projectName}` : page.title;
|
|
1363
|
+
const repoIconUrl = rootRelativeUrl(page.route, "assets/github-icon.svg");
|
|
1364
|
+
return /* @__PURE__ */ jsxs("html", {
|
|
1365
|
+
lang: "en",
|
|
1366
|
+
children: [/* @__PURE__ */ jsxs("head", { children: [
|
|
1367
|
+
/* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
|
|
1368
|
+
/* @__PURE__ */ jsx("meta", {
|
|
1369
|
+
name: "viewport",
|
|
1370
|
+
content: "width=device-width, initial-scale=1"
|
|
1371
|
+
}),
|
|
1372
|
+
/* @__PURE__ */ jsx("title", { children: documentTitle }),
|
|
1373
|
+
favicon ? /* @__PURE__ */ jsx("link", {
|
|
1374
|
+
rel: "icon",
|
|
1375
|
+
href: assetSrc(page.route, favicon)
|
|
1376
|
+
}) : null,
|
|
1377
|
+
/* @__PURE__ */ jsx("link", {
|
|
1378
|
+
rel: "stylesheet",
|
|
1379
|
+
href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20,400,0,0&display=block"
|
|
1380
|
+
}),
|
|
1381
|
+
/* @__PURE__ */ jsx("link", {
|
|
1382
|
+
rel: "stylesheet",
|
|
1383
|
+
href: rootRelativeUrl(page.route, "assets/lildocs.css")
|
|
1384
|
+
})
|
|
1385
|
+
] }), /* @__PURE__ */ jsxs("body", { children: [
|
|
1386
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1387
|
+
className: "pageShell",
|
|
1388
|
+
children: [/* @__PURE__ */ jsxs("header", {
|
|
1389
|
+
className: "siteHeader",
|
|
1390
|
+
children: [/* @__PURE__ */ jsxs("a", {
|
|
1391
|
+
className: "brand",
|
|
1392
|
+
href: relativeUrl(page.route, "index.html"),
|
|
1393
|
+
children: [logo.image ? /* @__PURE__ */ jsx("img", {
|
|
1394
|
+
className: "brandLogo",
|
|
1395
|
+
src: assetSrc(page.route, logo.image),
|
|
1396
|
+
alt: ""
|
|
1397
|
+
}) : null, logo.text ? /* @__PURE__ */ jsx("span", { children: logo.text }) : null]
|
|
1398
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1399
|
+
className: "headerActions",
|
|
1400
|
+
children: [repositoryUrl ? /* @__PURE__ */ jsx("a", {
|
|
1401
|
+
className: "repoButton",
|
|
1402
|
+
href: repositoryUrl,
|
|
1403
|
+
"aria-label": "View repository on GitHub",
|
|
1404
|
+
title: "View repository on GitHub",
|
|
1405
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1406
|
+
className: "repoIcon",
|
|
1407
|
+
"aria-hidden": "true",
|
|
1408
|
+
style: `--ld-repo-icon: url(${repoIconUrl})`
|
|
1409
|
+
})
|
|
1410
|
+
}) : null, /* @__PURE__ */ jsxs("div", {
|
|
1411
|
+
id: "lildocs-search-root",
|
|
1412
|
+
className: "searchBox",
|
|
1413
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1414
|
+
className: "searchIcon material-symbols-rounded",
|
|
1415
|
+
"aria-hidden": "true",
|
|
1416
|
+
children: "search"
|
|
1417
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1418
|
+
id: "lildocs-search-input",
|
|
1419
|
+
type: "search",
|
|
1420
|
+
placeholder: "Search docs",
|
|
1421
|
+
"aria-label": "Search docs"
|
|
1422
|
+
})]
|
|
1423
|
+
})]
|
|
1424
|
+
})]
|
|
1425
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1426
|
+
id: "swup",
|
|
1427
|
+
className: "contentGrid",
|
|
1428
|
+
children: [
|
|
1429
|
+
/* @__PURE__ */ jsx("aside", {
|
|
1430
|
+
className: "sidebar",
|
|
1431
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
1432
|
+
"aria-label": "Documentation navigation",
|
|
1433
|
+
children: /* @__PURE__ */ jsx(NavList, {
|
|
1434
|
+
items: nav,
|
|
1435
|
+
currentRoute: page.route,
|
|
1436
|
+
pageRoute: page.route
|
|
1437
|
+
})
|
|
1438
|
+
})
|
|
1439
|
+
}),
|
|
1440
|
+
/* @__PURE__ */ jsxs("main", {
|
|
1441
|
+
className: `content transition-${transition}`,
|
|
1442
|
+
children: [/* @__PURE__ */ jsx("article", { dangerouslySetInnerHTML: { __html: page.html ?? "" } }), /* @__PURE__ */ jsx(PageNav, { pageNavigation })]
|
|
1443
|
+
}),
|
|
1444
|
+
/* @__PURE__ */ jsx("aside", {
|
|
1445
|
+
className: `toc transition-${transition}`,
|
|
1446
|
+
children: /* @__PURE__ */ jsx(Toc, { headings: page.headings })
|
|
1447
|
+
})
|
|
1448
|
+
]
|
|
1449
|
+
})]
|
|
1450
|
+
}),
|
|
1451
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `window.lildocsSearchUrl = ${JSON.stringify(rootRelativeUrl(page.route, "search-index.json"))};` } }),
|
|
1452
|
+
/* @__PURE__ */ jsx("script", {
|
|
1453
|
+
type: "application/json",
|
|
1454
|
+
id: "lildocs-search-index",
|
|
1455
|
+
dangerouslySetInnerHTML: { __html: searchIndexJson }
|
|
1456
|
+
}),
|
|
1457
|
+
/* @__PURE__ */ jsx("div", { id: "lildocs-overlay-root" }),
|
|
1458
|
+
/* @__PURE__ */ jsx("script", {
|
|
1459
|
+
type: "module",
|
|
1460
|
+
src: rootRelativeUrl(page.route, "assets/search.js")
|
|
1461
|
+
}),
|
|
1462
|
+
/* @__PURE__ */ jsx("script", { src: rootRelativeUrl(page.route, "assets/copy-code.js") }),
|
|
1463
|
+
/* @__PURE__ */ jsx("script", { src: rootRelativeUrl(page.route, "assets/swup.umd.js") }),
|
|
1464
|
+
/* @__PURE__ */ jsx("script", { src: rootRelativeUrl(page.route, "assets/navigation.js") }),
|
|
1465
|
+
dev ? /* @__PURE__ */ jsx("script", {
|
|
1466
|
+
type: "module",
|
|
1467
|
+
src: dev.clientScriptPath
|
|
1468
|
+
}) : null,
|
|
1469
|
+
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } })
|
|
1470
|
+
] })]
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
function assetSrc(pageRoute, image) {
|
|
1474
|
+
if (/^(?:[a-z]+:)?\/\//i.test(image) || image.startsWith("data:") || image.startsWith("/")) return image;
|
|
1475
|
+
return rootRelativeUrl(pageRoute, image);
|
|
1476
|
+
}
|
|
1477
|
+
function PageNav({ pageNavigation }) {
|
|
1478
|
+
if (!pageNavigation?.previous && !pageNavigation?.next) return null;
|
|
1479
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
1480
|
+
className: "pageNav",
|
|
1481
|
+
"aria-label": "Page navigation",
|
|
1482
|
+
children: [pageNavigation.previous ? /* @__PURE__ */ jsxs("a", {
|
|
1483
|
+
className: "pageNavLink pageNavPrevious",
|
|
1484
|
+
rel: "prev",
|
|
1485
|
+
href: pageNavigation.previous.href,
|
|
1486
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Previous" }), pageNavigation.previous.title]
|
|
1487
|
+
}) : /* @__PURE__ */ jsx("span", {}), pageNavigation.next ? /* @__PURE__ */ jsxs("a", {
|
|
1488
|
+
className: "pageNavLink pageNavNext",
|
|
1489
|
+
rel: "next",
|
|
1490
|
+
href: pageNavigation.next.href,
|
|
1491
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Next" }), pageNavigation.next.title]
|
|
1492
|
+
}) : null]
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
function NavList({ items, currentRoute, pageRoute }) {
|
|
1496
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
1497
|
+
className: "navList",
|
|
1498
|
+
children: items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
1499
|
+
className: item.children.length > 0 ? "navGroup" : void 0,
|
|
1500
|
+
children: [item.children.length > 0 && item.hasPage ? /* @__PURE__ */ jsxs("details", {
|
|
1501
|
+
className: "navDisclosure",
|
|
1502
|
+
open: isActiveBranch(item, currentRoute),
|
|
1503
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
1504
|
+
className: item.route === currentRoute ? "active" : void 0,
|
|
1505
|
+
children: item.title
|
|
1506
|
+
}), /* @__PURE__ */ jsx(NavList, {
|
|
1507
|
+
items: item.children,
|
|
1508
|
+
currentRoute,
|
|
1509
|
+
pageRoute
|
|
1510
|
+
})]
|
|
1511
|
+
}) : item.children.length > 0 ? /* @__PURE__ */ jsx("span", {
|
|
1512
|
+
className: "navFolder",
|
|
1513
|
+
children: item.title
|
|
1514
|
+
}) : /* @__PURE__ */ jsx("a", {
|
|
1515
|
+
className: item.route === currentRoute ? "active" : void 0,
|
|
1516
|
+
href: relativeUrl(pageRoute, item.route),
|
|
1517
|
+
children: item.title
|
|
1518
|
+
}), item.children.length > 0 && !item.hasPage ? /* @__PURE__ */ jsx(NavList, {
|
|
1519
|
+
items: item.children,
|
|
1520
|
+
currentRoute,
|
|
1521
|
+
pageRoute
|
|
1522
|
+
}) : null]
|
|
1523
|
+
}))
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function isActiveBranch(item, currentRoute) {
|
|
1527
|
+
return item.route === currentRoute || item.children.some((child) => isActiveBranch(child, currentRoute));
|
|
1528
|
+
}
|
|
1529
|
+
function Toc({ headings }) {
|
|
1530
|
+
const tocHeadings = headings.filter((heading) => heading.depth > 1 && heading.depth < 4);
|
|
1531
|
+
if (tocHeadings.length === 0) return null;
|
|
1532
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
1533
|
+
"aria-label": "Table of contents",
|
|
1534
|
+
children: [/* @__PURE__ */ jsx("p", { children: "On this page" }), /* @__PURE__ */ jsx("ul", { children: tocHeadings.map((heading) => /* @__PURE__ */ jsx("li", {
|
|
1535
|
+
className: `tocDepth${heading.depth}`,
|
|
1536
|
+
children: /* @__PURE__ */ jsx("a", {
|
|
1537
|
+
href: `#${heading.id}`,
|
|
1538
|
+
children: heading.text
|
|
1539
|
+
})
|
|
1540
|
+
})) })]
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region src/render/renderPage.tsx
|
|
1545
|
+
function renderPage(page, nav, pageNavigation, css, searchIndexJson, logo, favicon, repositoryUrl, projectName, navigation, dev) {
|
|
1546
|
+
return `<!doctype html>${render(/* @__PURE__ */ jsx(Layout, {
|
|
1547
|
+
page,
|
|
1548
|
+
nav,
|
|
1549
|
+
pageNavigation,
|
|
1550
|
+
css,
|
|
1551
|
+
searchIndexJson,
|
|
1552
|
+
logo,
|
|
1553
|
+
favicon,
|
|
1554
|
+
repositoryUrl,
|
|
1555
|
+
projectName,
|
|
1556
|
+
navigation,
|
|
1557
|
+
dev
|
|
1558
|
+
}))}`;
|
|
1559
|
+
}
|
|
1560
|
+
//#endregion
|
|
1561
|
+
//#region src/core/build.ts
|
|
1562
|
+
const sourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1563
|
+
const require = createRequire(import.meta.url);
|
|
1564
|
+
async function buildSite(options) {
|
|
1565
|
+
const input = await resolveInput(options.input, options.cwd, { homePagePreference: options.homePagePreference });
|
|
1566
|
+
const configOptions = mergeConfigOptions({
|
|
1567
|
+
config: await readDocsConfig(input.docsRoot),
|
|
1568
|
+
theme: options.theme,
|
|
1569
|
+
fonts: options.fonts,
|
|
1570
|
+
favicon: options.favicon,
|
|
1571
|
+
logo: options.logo,
|
|
1572
|
+
background: options.background,
|
|
1573
|
+
link: options.link,
|
|
1574
|
+
navigation: options.navigation
|
|
1575
|
+
});
|
|
1576
|
+
const outDir = path.resolve(options.cwd, options.outDir);
|
|
1577
|
+
const outDirName = path.basename(outDir);
|
|
1578
|
+
const referencePages = await buildReferencePages({
|
|
1579
|
+
cwd: options.cwd,
|
|
1580
|
+
docsRoot: input.docsRoot,
|
|
1581
|
+
reference: configOptions.reference
|
|
1582
|
+
});
|
|
1583
|
+
const model = await buildContentModel(input, outDirName, configOptions.navigation.order, referencePages);
|
|
1584
|
+
const packagePath = await findNearestPackageJson(input.docsRoot, options.cwd);
|
|
1585
|
+
const repositoryUrl = await resolveRepositoryUrl(packagePath);
|
|
1586
|
+
const packageName = await resolvePackageName(packagePath);
|
|
1587
|
+
const projectName = configOptions.projectName ?? packageName;
|
|
1588
|
+
const theme = await resolveTheme({
|
|
1589
|
+
cwd: options.cwd,
|
|
1590
|
+
docsRoot: input.docsRoot,
|
|
1591
|
+
requestedTheme: configOptions.theme
|
|
1592
|
+
});
|
|
1593
|
+
const fontResolution = await resolveFontOverrides({
|
|
1594
|
+
cwd: options.cwd,
|
|
1595
|
+
docsRoot: input.docsRoot,
|
|
1596
|
+
outDir,
|
|
1597
|
+
fonts: configOptions.fonts
|
|
1598
|
+
});
|
|
1599
|
+
const baseCss = await readRenderAsset("styles.css");
|
|
1600
|
+
const searchScript = await readRenderAsset("search.js");
|
|
1601
|
+
const copyCodeScript = await readRenderAsset("copy-code.js");
|
|
1602
|
+
const navigationScript = await readRenderAsset("navigation.js");
|
|
1603
|
+
const githubIcon = await readRenderAsset("github-icon.svg");
|
|
1604
|
+
const swupScript = await readSwupAsset();
|
|
1605
|
+
const backgroundResolution = resolveBackgroundOptions({
|
|
1606
|
+
cwd: options.cwd,
|
|
1607
|
+
docsRoot: input.docsRoot,
|
|
1608
|
+
outDir,
|
|
1609
|
+
background: configOptions.background
|
|
1610
|
+
});
|
|
1611
|
+
const logoResolution = await resolveLogoOptions({
|
|
1612
|
+
cwd: options.cwd,
|
|
1613
|
+
docsRoot: input.docsRoot,
|
|
1614
|
+
outDir,
|
|
1615
|
+
logo: configOptions.logo,
|
|
1616
|
+
defaultText: packageName,
|
|
1617
|
+
favicon: configOptions.favicon
|
|
1618
|
+
});
|
|
1619
|
+
const css = `${fontResolution.css}${themeToCssVariables(theme, fontResolution.themeFonts, configOptions.link, configOptions.navigation)}\n${backgroundResolution.css}${logoResolution.css}${baseCss}`;
|
|
1620
|
+
const assets = [
|
|
1621
|
+
...fontResolution.assets,
|
|
1622
|
+
...backgroundResolution.assets,
|
|
1623
|
+
...logoResolution.assets
|
|
1624
|
+
];
|
|
1625
|
+
const mermaid = await createMermaidRenderer({ themeConfig: themeToMermaidConfig(theme, fontResolution.themeFonts) });
|
|
1626
|
+
await rm(outDir, {
|
|
1627
|
+
recursive: true,
|
|
1628
|
+
force: true
|
|
1629
|
+
});
|
|
1630
|
+
await mkdir(path.join(outDir, "assets"), { recursive: true });
|
|
1631
|
+
try {
|
|
1632
|
+
const renderedPages = await Promise.all(model.pages.map((page) => renderMarkdownPage(model, page, outDir, {
|
|
1633
|
+
mermaid,
|
|
1634
|
+
shikiTheme: {
|
|
1635
|
+
light: theme.light.shiki?.theme,
|
|
1636
|
+
dark: theme.dark?.shiki?.theme
|
|
1637
|
+
}
|
|
1638
|
+
})));
|
|
1639
|
+
for (const [index, rendered] of renderedPages.entries()) {
|
|
1640
|
+
const page = model.pages[index];
|
|
1641
|
+
if (!page) continue;
|
|
1642
|
+
page.html = rendered.html;
|
|
1643
|
+
page.searchText = rendered.text;
|
|
1644
|
+
assets.push(...rendered.assets);
|
|
1645
|
+
}
|
|
1646
|
+
const searchIndexJson = JSON.stringify(buildSearchIndex(model.pages), null, 2);
|
|
1647
|
+
const pageNavigation = buildPageNavigation(model.pages);
|
|
1648
|
+
await Promise.all(model.pages.map(async (page) => {
|
|
1649
|
+
const html = renderPage(page, buildNavigation(model, page), pageNavigation.get(page.route), css, searchIndexJson, logoResolution.logo, logoResolution.favicon, repositoryUrl, projectName, configOptions.navigation, options.dev);
|
|
1650
|
+
const outputPath = path.join(outDir, page.outputPath);
|
|
1651
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
1652
|
+
await writeFile(outputPath, html);
|
|
1653
|
+
}));
|
|
1654
|
+
await writeFile(path.join(outDir, "assets", "lildocs.css"), css);
|
|
1655
|
+
await writeFile(path.join(outDir, "assets", "search.js"), searchScript);
|
|
1656
|
+
await writeFile(path.join(outDir, "assets", "copy-code.js"), copyCodeScript);
|
|
1657
|
+
await writeFile(path.join(outDir, "assets", "swup.umd.js"), swupScript);
|
|
1658
|
+
await writeFile(path.join(outDir, "assets", "navigation.js"), navigationScript);
|
|
1659
|
+
await writeFile(path.join(outDir, "assets", "github-icon.svg"), githubIcon);
|
|
1660
|
+
await writeFile(path.join(outDir, "search-index.json"), searchIndexJson);
|
|
1661
|
+
await copyAssets(assets);
|
|
1662
|
+
} finally {
|
|
1663
|
+
await mermaid.close();
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
outDir,
|
|
1667
|
+
pages: model.pages
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
async function resolveRepositoryUrl(packagePath) {
|
|
1671
|
+
const envRepository = process.env.GITHUB_REPOSITORY?.trim();
|
|
1672
|
+
if (envRepository) return `https://github.com/${envRepository}`;
|
|
1673
|
+
if (!packagePath) return;
|
|
1674
|
+
return normalizePackageRepository(JSON.parse(await readFile(packagePath, "utf8")).repository);
|
|
1675
|
+
}
|
|
1676
|
+
async function resolvePackageName(packagePath) {
|
|
1677
|
+
if (!packagePath) return;
|
|
1678
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
1679
|
+
return typeof packageJson.name === "string" && packageJson.name.trim() ? packageJson.name.trim() : void 0;
|
|
1680
|
+
}
|
|
1681
|
+
async function findNearestPackageJson(start, stop) {
|
|
1682
|
+
let current = path.resolve(start);
|
|
1683
|
+
const root = path.parse(current).root;
|
|
1684
|
+
const stopDir = path.resolve(stop);
|
|
1685
|
+
while (true) {
|
|
1686
|
+
const packagePath = path.join(current, "package.json");
|
|
1687
|
+
if (existsSync(packagePath)) return packagePath;
|
|
1688
|
+
if (current === stopDir || current === root) return;
|
|
1689
|
+
current = path.dirname(current);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function normalizePackageRepository(repository) {
|
|
1693
|
+
if (typeof repository === "string") return normalizeGitHubRepository(repository);
|
|
1694
|
+
if (repository && typeof repository === "object" && "url" in repository) {
|
|
1695
|
+
const { url } = repository;
|
|
1696
|
+
if (typeof url === "string") return normalizeGitHubRepository(url);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function normalizeGitHubRepository(value) {
|
|
1700
|
+
const repository = value.trim();
|
|
1701
|
+
if (!repository) return;
|
|
1702
|
+
const shorthandMatch = /^(?:github:)?([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/.exec(repository);
|
|
1703
|
+
if (shorthandMatch?.[1]) return `https://github.com/${shorthandMatch[1].replace(/\.git$/, "")}`;
|
|
1704
|
+
const sshMatch = /^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/.exec(repository);
|
|
1705
|
+
if (sshMatch?.[1]) return `https://github.com/${sshMatch[1]}`;
|
|
1706
|
+
const urlMatch = /^(?:git\+)?https:\/\/github\.com\/([^/]+\/[^/#?]+?)(?:\.git)?(?:[#?].*)?$/.exec(repository);
|
|
1707
|
+
if (urlMatch?.[1]) return `https://github.com/${urlMatch[1]}`;
|
|
1708
|
+
}
|
|
1709
|
+
async function readRenderAsset(name) {
|
|
1710
|
+
const bundledPath = path.resolve(sourceDir, "render", name);
|
|
1711
|
+
try {
|
|
1712
|
+
return await readFile(bundledPath, "utf8");
|
|
1713
|
+
} catch {
|
|
1714
|
+
return readFile(path.resolve(sourceDir, "../render", name), "utf8");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
async function readSwupAsset() {
|
|
1718
|
+
return readFile(path.join(path.dirname(require.resolve("swup")), "Swup.umd.js"), "utf8");
|
|
1719
|
+
}
|
|
1720
|
+
function buildPageNavigation(pages) {
|
|
1721
|
+
const navigation = /* @__PURE__ */ new Map();
|
|
1722
|
+
if (pages.length < 2) return navigation;
|
|
1723
|
+
for (const [index, page] of pages.entries()) {
|
|
1724
|
+
const previous = pages[index - 1];
|
|
1725
|
+
const next = pages[index + 1];
|
|
1726
|
+
navigation.set(page.route, {
|
|
1727
|
+
previous: previous ? {
|
|
1728
|
+
title: previous.title,
|
|
1729
|
+
href: relativeUrl(page.route, previous.route)
|
|
1730
|
+
} : void 0,
|
|
1731
|
+
next: next ? {
|
|
1732
|
+
title: next.title,
|
|
1733
|
+
href: relativeUrl(page.route, next.route)
|
|
1734
|
+
} : void 0
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
return navigation;
|
|
1738
|
+
}
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/core/server.ts
|
|
1741
|
+
const MIME_TYPES = new Map([
|
|
1742
|
+
[".html", "text/html; charset=utf-8"],
|
|
1743
|
+
[".css", "text/css; charset=utf-8"],
|
|
1744
|
+
[".js", "text/javascript; charset=utf-8"],
|
|
1745
|
+
[".json", "application/json; charset=utf-8"],
|
|
1746
|
+
[".svg", "image/svg+xml"],
|
|
1747
|
+
[".png", "image/png"],
|
|
1748
|
+
[".jpg", "image/jpeg"],
|
|
1749
|
+
[".jpeg", "image/jpeg"],
|
|
1750
|
+
[".gif", "image/gif"],
|
|
1751
|
+
[".webp", "image/webp"],
|
|
1752
|
+
[".avif", "image/avif"],
|
|
1753
|
+
[".ico", "image/x-icon"],
|
|
1754
|
+
[".txt", "text/plain; charset=utf-8"],
|
|
1755
|
+
[".woff", "font/woff"],
|
|
1756
|
+
[".woff2", "font/woff2"],
|
|
1757
|
+
[".ttf", "font/ttf"],
|
|
1758
|
+
[".otf", "font/otf"],
|
|
1759
|
+
[".eot", "application/vnd.ms-fontobject"]
|
|
1760
|
+
]);
|
|
1761
|
+
async function serveStaticFile(req, res, outDir) {
|
|
1762
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1763
|
+
res.writeHead(405, { allow: "GET, HEAD" });
|
|
1764
|
+
res.end("Method Not Allowed");
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const pathname = requestPathname$1(req);
|
|
1768
|
+
if (!pathname) {
|
|
1769
|
+
res.writeHead(400);
|
|
1770
|
+
res.end("Bad Request");
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const relativePath = pathname === "/" ? "index.html" : pathname.slice(1);
|
|
1774
|
+
const filePath = path.resolve(outDir, relativePath);
|
|
1775
|
+
if (!isInside$1(outDir, filePath)) {
|
|
1776
|
+
res.writeHead(404);
|
|
1777
|
+
res.end("Not Found");
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
let fileStat;
|
|
1781
|
+
try {
|
|
1782
|
+
fileStat = await stat(filePath);
|
|
1783
|
+
} catch {
|
|
1784
|
+
res.writeHead(404);
|
|
1785
|
+
res.end("Not Found");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (!fileStat.isFile()) {
|
|
1789
|
+
res.writeHead(404);
|
|
1790
|
+
res.end("Not Found");
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
res.writeHead(200, {
|
|
1794
|
+
"content-length": fileStat.size,
|
|
1795
|
+
"content-type": MIME_TYPES.get(path.extname(filePath).toLowerCase()) ?? "application/octet-stream"
|
|
1796
|
+
});
|
|
1797
|
+
if (req.method === "HEAD") {
|
|
1798
|
+
res.end();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
createReadStream(filePath).pipe(res);
|
|
1802
|
+
}
|
|
1803
|
+
function requestPathname$1(req) {
|
|
1804
|
+
try {
|
|
1805
|
+
return decodeURIComponent(new URL(req.url ?? "/", "http://lildocs.local").pathname);
|
|
1806
|
+
} catch {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function isInside$1(root, candidate) {
|
|
1811
|
+
const relative = path.relative(root, candidate);
|
|
1812
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1813
|
+
}
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region src/core/dev.ts
|
|
1816
|
+
const DEV_CLIENT_PATH = "/__lildocs/client.js";
|
|
1817
|
+
const DEV_EVENTS_PATH = "/__lildocs/events";
|
|
1818
|
+
const DEV_CLIENT_SCRIPT = `
|
|
1819
|
+
const events = new EventSource("${DEV_EVENTS_PATH}");
|
|
1820
|
+
events.addEventListener("reload", () => location.reload());
|
|
1821
|
+
events.onerror = () => console.debug("[lildocs] live reload disconnected");
|
|
1822
|
+
`;
|
|
1823
|
+
async function startDevServer(options) {
|
|
1824
|
+
const input = await resolveInput(options.input, options.cwd, { homePagePreference: "readme-first" });
|
|
1825
|
+
const outDir = path.resolve(options.cwd, options.outDir);
|
|
1826
|
+
validateDevOutDir(options.cwd, input.docsRoot, outDir, options.outDir);
|
|
1827
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1828
|
+
let watchers = [];
|
|
1829
|
+
let rebuildTimer;
|
|
1830
|
+
let rebuilding = false;
|
|
1831
|
+
let pending = false;
|
|
1832
|
+
let lastSuccessfulBuild;
|
|
1833
|
+
async function rebuild(emitReload, throwOnFailure = false) {
|
|
1834
|
+
if (rebuilding) {
|
|
1835
|
+
pending = true;
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
rebuilding = true;
|
|
1839
|
+
const started = performance.now();
|
|
1840
|
+
try {
|
|
1841
|
+
lastSuccessfulBuild = await buildSite({
|
|
1842
|
+
input: options.input,
|
|
1843
|
+
outDir,
|
|
1844
|
+
cwd: options.cwd,
|
|
1845
|
+
theme: options.theme,
|
|
1846
|
+
fonts: options.fonts,
|
|
1847
|
+
background: options.background,
|
|
1848
|
+
link: options.link,
|
|
1849
|
+
homePagePreference: "readme-first",
|
|
1850
|
+
dev: { clientScriptPath: DEV_CLIENT_PATH }
|
|
1851
|
+
});
|
|
1852
|
+
await refreshWatchers();
|
|
1853
|
+
const elapsed = Math.round(performance.now() - started);
|
|
1854
|
+
console.log(`Rebuilt ${lastSuccessfulBuild.pages.length} page${lastSuccessfulBuild.pages.length === 1 ? "" : "s"} in ${elapsed}ms`);
|
|
1855
|
+
if (emitReload) sendReload(clients);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (throwOnFailure) throw error;
|
|
1858
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1859
|
+
console.error(`lildocs: rebuild failed: ${message}`);
|
|
1860
|
+
} finally {
|
|
1861
|
+
rebuilding = false;
|
|
1862
|
+
if (pending) {
|
|
1863
|
+
pending = false;
|
|
1864
|
+
await rebuild(true);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
function scheduleRebuild() {
|
|
1869
|
+
if (rebuildTimer) clearTimeout(rebuildTimer);
|
|
1870
|
+
rebuildTimer = setTimeout(() => {
|
|
1871
|
+
rebuild(true);
|
|
1872
|
+
}, 150);
|
|
1873
|
+
}
|
|
1874
|
+
async function refreshWatchers() {
|
|
1875
|
+
for (const watcher of watchers) watcher.close();
|
|
1876
|
+
watchers = [];
|
|
1877
|
+
const directories = await collectWatchDirectories(input.docsRoot, outDir);
|
|
1878
|
+
for (const directory of directories) watchers.push(watch(directory, { persistent: true }, (_event, filename) => {
|
|
1879
|
+
if (filename && shouldIgnore(path.join(directory, filename.toString()), input.docsRoot, outDir)) return;
|
|
1880
|
+
scheduleRebuild();
|
|
1881
|
+
}));
|
|
1882
|
+
}
|
|
1883
|
+
await rebuild(false, true);
|
|
1884
|
+
const server = http.createServer((req, res) => {
|
|
1885
|
+
const pathname = requestPathname(req.url);
|
|
1886
|
+
if (pathname === DEV_CLIENT_PATH) {
|
|
1887
|
+
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
|
|
1888
|
+
res.end(DEV_CLIENT_SCRIPT);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (pathname === DEV_EVENTS_PATH) {
|
|
1892
|
+
connectEvents(res, clients);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
serveStaticFile(req, res, outDir);
|
|
1896
|
+
});
|
|
1897
|
+
await new Promise((resolve, reject) => {
|
|
1898
|
+
server.once("error", reject);
|
|
1899
|
+
server.listen(options.port, options.host, () => {
|
|
1900
|
+
server.off("error", reject);
|
|
1901
|
+
resolve();
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
const address = server.address();
|
|
1905
|
+
const actualPort = typeof address === "object" && address ? address.port : options.port;
|
|
1906
|
+
const url = `http://${options.host}:${actualPort}/`;
|
|
1907
|
+
console.log(`lildocs dev server listening at ${url}`);
|
|
1908
|
+
return {
|
|
1909
|
+
url,
|
|
1910
|
+
close: async () => {
|
|
1911
|
+
if (rebuildTimer) clearTimeout(rebuildTimer);
|
|
1912
|
+
for (const watcher of watchers) watcher.close();
|
|
1913
|
+
for (const client of clients) client.end();
|
|
1914
|
+
await new Promise((resolve, reject) => {
|
|
1915
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function validateDevOutDir(cwd, docsRoot, outDir, requestedOutDir) {
|
|
1921
|
+
const root = path.resolve(cwd);
|
|
1922
|
+
if (!path.isAbsolute(requestedOutDir) && !isInside(root, outDir)) throw new LildocsError("Relative dev output directory must stay inside the current workspace.");
|
|
1923
|
+
if (outDir === root) throw new LildocsError("Dev output directory cannot be the repository root.");
|
|
1924
|
+
if (outDir === docsRoot) throw new LildocsError("Dev output directory cannot be the docs root.");
|
|
1925
|
+
if (isAncestor(outDir, docsRoot)) throw new LildocsError("Dev output directory cannot contain the docs root.");
|
|
1926
|
+
}
|
|
1927
|
+
async function collectWatchDirectories(docsRoot, outDir) {
|
|
1928
|
+
const directories = /* @__PURE__ */ new Set();
|
|
1929
|
+
async function walk(directory) {
|
|
1930
|
+
if (shouldIgnore(directory, docsRoot, outDir)) return;
|
|
1931
|
+
directories.add(directory);
|
|
1932
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1933
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1934
|
+
if (!entry.isDirectory()) return;
|
|
1935
|
+
await walk(path.join(directory, entry.name));
|
|
1936
|
+
}));
|
|
1937
|
+
}
|
|
1938
|
+
await walk(docsRoot);
|
|
1939
|
+
return [...directories];
|
|
1940
|
+
}
|
|
1941
|
+
function shouldIgnore(candidate, docsRoot, outDir) {
|
|
1942
|
+
const resolved = path.resolve(candidate);
|
|
1943
|
+
if (resolved === outDir || isAncestor(outDir, resolved)) return true;
|
|
1944
|
+
const relative = path.relative(docsRoot, resolved);
|
|
1945
|
+
return relative === "dist" || relative.startsWith(`dist${path.sep}`) || relative === "node_modules" || relative.startsWith(`node_modules${path.sep}`) || isHiddenOrSystemPath(relative);
|
|
1946
|
+
}
|
|
1947
|
+
function connectEvents(res, clients) {
|
|
1948
|
+
res.writeHead(200, {
|
|
1949
|
+
"cache-control": "no-cache",
|
|
1950
|
+
connection: "keep-alive",
|
|
1951
|
+
"content-type": "text/event-stream"
|
|
1952
|
+
});
|
|
1953
|
+
res.write(": connected\n\n");
|
|
1954
|
+
clients.add(res);
|
|
1955
|
+
res.on("close", () => clients.delete(res));
|
|
1956
|
+
}
|
|
1957
|
+
function sendReload(clients) {
|
|
1958
|
+
for (const client of clients) client.write("event: reload\ndata: {}\n\n");
|
|
1959
|
+
}
|
|
1960
|
+
function requestPathname(url) {
|
|
1961
|
+
try {
|
|
1962
|
+
return new URL(url ?? "/", "http://lildocs.local").pathname;
|
|
1963
|
+
} catch {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function isAncestor(parent, child) {
|
|
1968
|
+
const relative = path.relative(parent, child);
|
|
1969
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1970
|
+
}
|
|
1971
|
+
function isInside(parent, child) {
|
|
1972
|
+
const relative = path.relative(parent, child);
|
|
1973
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1974
|
+
}
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/core/deploy.ts
|
|
1977
|
+
async function deployGitHubPages(options) {
|
|
1978
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
1979
|
+
const result = await buildSite({
|
|
1980
|
+
...options,
|
|
1981
|
+
basePath
|
|
1982
|
+
});
|
|
1983
|
+
await writeFile(path.join(result.outDir, ".nojekyll"), "");
|
|
1984
|
+
await writeFile(path.join(result.outDir, "lildocs-deploy.json"), `${JSON.stringify({
|
|
1985
|
+
target: "github-pages",
|
|
1986
|
+
basePath,
|
|
1987
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1988
|
+
}, null, 2)}\n`);
|
|
1989
|
+
return {
|
|
1990
|
+
...result,
|
|
1991
|
+
basePath
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
async function initGitHubPagesWorkflow(options) {
|
|
1995
|
+
const workflow = await prepareWorkflow(options);
|
|
1996
|
+
await mkdir(path.dirname(workflow.path), { recursive: true });
|
|
1997
|
+
await writeFile(workflow.path, workflow.contents);
|
|
1998
|
+
return workflow.path;
|
|
1999
|
+
}
|
|
2000
|
+
async function prepareWorkflow(options) {
|
|
2001
|
+
const workflowPath = path.join(options.cwd, ".github", "workflows", "lildocs-pages.yml");
|
|
2002
|
+
if (await pathExists(workflowPath)) throw new Error(`Refusing to overwrite existing workflow at ${workflowPath}.`);
|
|
2003
|
+
return {
|
|
2004
|
+
path: workflowPath,
|
|
2005
|
+
contents: renderGitHubPagesWorkflow({
|
|
2006
|
+
input: repoRelativeInputPath(options.input, options.cwd),
|
|
2007
|
+
outDir: path.relative(options.cwd, path.resolve(options.cwd, options.outDir)) || ".",
|
|
2008
|
+
basePath: options.basePath
|
|
2009
|
+
})
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async function pathExists(filePath) {
|
|
2013
|
+
try {
|
|
2014
|
+
await access(filePath);
|
|
2015
|
+
return true;
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
2018
|
+
throw error;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
//#endregion
|
|
2022
|
+
//#region src/cli.ts
|
|
2023
|
+
const outOption = option({
|
|
2024
|
+
type: optional(string),
|
|
2025
|
+
long: "out",
|
|
2026
|
+
description: "Output directory for the generated site."
|
|
2027
|
+
});
|
|
2028
|
+
const themeOption = option({
|
|
2029
|
+
type: optional(string),
|
|
2030
|
+
long: "theme",
|
|
2031
|
+
description: "Built-in lildocs theme or bundled Shiki theme name to use."
|
|
2032
|
+
});
|
|
2033
|
+
const fontHeadingOption = option({
|
|
2034
|
+
type: optional(string),
|
|
2035
|
+
long: "font.heading",
|
|
2036
|
+
description: "Heading font name from Google Fonts or local font file path."
|
|
2037
|
+
});
|
|
2038
|
+
const fontBodyOption = option({
|
|
2039
|
+
type: optional(string),
|
|
2040
|
+
long: "font.body",
|
|
2041
|
+
description: "Body font name from Google Fonts or local font file path."
|
|
2042
|
+
});
|
|
2043
|
+
const fontCodeOption = option({
|
|
2044
|
+
type: optional(string),
|
|
2045
|
+
long: "font.code",
|
|
2046
|
+
description: "Code font name from Google Fonts or local font file path."
|
|
2047
|
+
});
|
|
2048
|
+
const backgroundImageOption = option({
|
|
2049
|
+
type: optional(string),
|
|
2050
|
+
long: "background.image",
|
|
2051
|
+
description: "Background image URL or docs-relative image path."
|
|
2052
|
+
});
|
|
2053
|
+
const backgroundGradientOption = option({
|
|
2054
|
+
type: optional(string),
|
|
2055
|
+
long: "background.gradient",
|
|
2056
|
+
description: "CSS background gradient, such as linear-gradient(...)."
|
|
2057
|
+
});
|
|
2058
|
+
const backgroundBlendModeOption = option({
|
|
2059
|
+
type: optional(string),
|
|
2060
|
+
long: "background.blendMode",
|
|
2061
|
+
description: "CSS background-blend-mode for the theme color and background layers."
|
|
2062
|
+
});
|
|
2063
|
+
const linkUnderlineOption = option({
|
|
2064
|
+
type: optional(string),
|
|
2065
|
+
long: "link.underline",
|
|
2066
|
+
description: "Link underline behavior: always, hover, or none."
|
|
2067
|
+
});
|
|
2068
|
+
const hostOption = option({
|
|
2069
|
+
type: optional(string),
|
|
2070
|
+
long: "host",
|
|
2071
|
+
description: "Host address for the local development server."
|
|
2072
|
+
});
|
|
2073
|
+
const portOption = option({
|
|
2074
|
+
type: optional(string),
|
|
2075
|
+
long: "port",
|
|
2076
|
+
description: "Port for the local development server."
|
|
2077
|
+
});
|
|
2078
|
+
const openOption = flag({
|
|
2079
|
+
long: "open",
|
|
2080
|
+
description: "Open the local development server in the default browser."
|
|
2081
|
+
});
|
|
2082
|
+
const baseOption = option({
|
|
2083
|
+
type: optional(string),
|
|
2084
|
+
long: "base",
|
|
2085
|
+
description: "GitHub Pages base path, such as /repo/ for project pages."
|
|
2086
|
+
});
|
|
2087
|
+
const inputArgument = positional({
|
|
2088
|
+
type: string,
|
|
2089
|
+
displayName: "path",
|
|
2090
|
+
description: "Markdown file or docs folder to build."
|
|
2091
|
+
});
|
|
2092
|
+
async function runBuild(options) {
|
|
2093
|
+
const result = await buildSite({
|
|
2094
|
+
input: options.input,
|
|
2095
|
+
outDir: options.out ?? "dist",
|
|
2096
|
+
theme: options.theme,
|
|
2097
|
+
fonts: {
|
|
2098
|
+
heading: options.fontHeading,
|
|
2099
|
+
body: options.fontBody,
|
|
2100
|
+
code: options.fontCode
|
|
2101
|
+
},
|
|
2102
|
+
background: {
|
|
2103
|
+
image: options.backgroundImage,
|
|
2104
|
+
gradient: options.backgroundGradient,
|
|
2105
|
+
blendMode: options.backgroundBlendMode
|
|
2106
|
+
},
|
|
2107
|
+
link: { underline: parseLinkUnderline(options.linkUnderline) },
|
|
2108
|
+
cwd: process.cwd()
|
|
2109
|
+
});
|
|
2110
|
+
console.log(`Built ${result.pages.length} page${result.pages.length === 1 ? "" : "s"} to ${result.outDir}`);
|
|
2111
|
+
}
|
|
2112
|
+
async function runDeploy(options) {
|
|
2113
|
+
const result = await deployGitHubPages({
|
|
2114
|
+
input: options.input,
|
|
2115
|
+
outDir: options.out ?? "dist",
|
|
2116
|
+
theme: options.theme,
|
|
2117
|
+
fonts: {
|
|
2118
|
+
heading: options.fontHeading,
|
|
2119
|
+
body: options.fontBody,
|
|
2120
|
+
code: options.fontCode
|
|
2121
|
+
},
|
|
2122
|
+
background: {
|
|
2123
|
+
image: options.backgroundImage,
|
|
2124
|
+
gradient: options.backgroundGradient,
|
|
2125
|
+
blendMode: options.backgroundBlendMode
|
|
2126
|
+
},
|
|
2127
|
+
link: { underline: parseLinkUnderline(options.linkUnderline) },
|
|
2128
|
+
basePath: options.base,
|
|
2129
|
+
cwd: process.cwd()
|
|
2130
|
+
});
|
|
2131
|
+
console.log(`Built GitHub Pages site to ${result.outDir}`);
|
|
2132
|
+
console.log(`Upload ${result.outDir} with actions/upload-pages-artifact or run init github-pages.`);
|
|
2133
|
+
}
|
|
2134
|
+
async function runInitGitHubPages(options) {
|
|
2135
|
+
const workflowPath = await initGitHubPagesWorkflow({
|
|
2136
|
+
input: options.input,
|
|
2137
|
+
outDir: options.out ?? "dist",
|
|
2138
|
+
basePath: options.base,
|
|
2139
|
+
cwd: process.cwd()
|
|
2140
|
+
});
|
|
2141
|
+
console.log(`Created GitHub Pages workflow at ${workflowPath}`);
|
|
2142
|
+
}
|
|
2143
|
+
const buildCommand = command({
|
|
2144
|
+
name: "build",
|
|
2145
|
+
description: "Build a static documentation site.",
|
|
2146
|
+
args: {
|
|
2147
|
+
input: inputArgument,
|
|
2148
|
+
out: outOption,
|
|
2149
|
+
theme: themeOption,
|
|
2150
|
+
fontHeading: fontHeadingOption,
|
|
2151
|
+
fontBody: fontBodyOption,
|
|
2152
|
+
fontCode: fontCodeOption,
|
|
2153
|
+
backgroundImage: backgroundImageOption,
|
|
2154
|
+
backgroundGradient: backgroundGradientOption,
|
|
2155
|
+
backgroundBlendMode: backgroundBlendModeOption,
|
|
2156
|
+
linkUnderline: linkUnderlineOption
|
|
2157
|
+
},
|
|
2158
|
+
handler: runBuild
|
|
2159
|
+
});
|
|
2160
|
+
const devCommand = command({
|
|
2161
|
+
name: "dev",
|
|
2162
|
+
description: "Start a local documentation development server.",
|
|
2163
|
+
args: {
|
|
2164
|
+
input: inputArgument,
|
|
2165
|
+
out: outOption,
|
|
2166
|
+
theme: themeOption,
|
|
2167
|
+
fontHeading: fontHeadingOption,
|
|
2168
|
+
fontBody: fontBodyOption,
|
|
2169
|
+
fontCode: fontCodeOption,
|
|
2170
|
+
backgroundImage: backgroundImageOption,
|
|
2171
|
+
backgroundGradient: backgroundGradientOption,
|
|
2172
|
+
backgroundBlendMode: backgroundBlendModeOption,
|
|
2173
|
+
linkUnderline: linkUnderlineOption,
|
|
2174
|
+
host: hostOption,
|
|
2175
|
+
port: portOption,
|
|
2176
|
+
open: openOption
|
|
2177
|
+
},
|
|
2178
|
+
handler: async (options) => {
|
|
2179
|
+
const port = parsePort(options.port ?? "3000");
|
|
2180
|
+
const server = await startDevServer({
|
|
2181
|
+
input: options.input,
|
|
2182
|
+
outDir: options.out ?? ".lildocs",
|
|
2183
|
+
theme: options.theme,
|
|
2184
|
+
fonts: {
|
|
2185
|
+
heading: options.fontHeading,
|
|
2186
|
+
body: options.fontBody,
|
|
2187
|
+
code: options.fontCode
|
|
2188
|
+
},
|
|
2189
|
+
background: {
|
|
2190
|
+
image: options.backgroundImage,
|
|
2191
|
+
gradient: options.backgroundGradient,
|
|
2192
|
+
blendMode: options.backgroundBlendMode
|
|
2193
|
+
},
|
|
2194
|
+
link: { underline: parseLinkUnderline(options.linkUnderline) },
|
|
2195
|
+
cwd: process.cwd(),
|
|
2196
|
+
host: options.host ?? "127.0.0.1",
|
|
2197
|
+
port
|
|
2198
|
+
});
|
|
2199
|
+
if (options.open) await openBrowser(server.url);
|
|
2200
|
+
await new Promise(() => {});
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
function parsePort(value) {
|
|
2204
|
+
const port = Number(value);
|
|
2205
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${value}`);
|
|
2206
|
+
return port;
|
|
2207
|
+
}
|
|
2208
|
+
function parseLinkUnderline(value) {
|
|
2209
|
+
if (value === void 0 || value === "always" || value === "hover" || value === "none") return value;
|
|
2210
|
+
throw new Error(`Invalid link underline style: ${value}`);
|
|
2211
|
+
}
|
|
2212
|
+
async function openBrowser(url) {
|
|
2213
|
+
const [opener, args] = browserOpenCommand(url);
|
|
2214
|
+
try {
|
|
2215
|
+
await new Promise((resolve, reject) => {
|
|
2216
|
+
const child = spawn(opener, args, {
|
|
2217
|
+
detached: true,
|
|
2218
|
+
stdio: "ignore"
|
|
2219
|
+
});
|
|
2220
|
+
child.once("error", reject);
|
|
2221
|
+
child.once("spawn", () => {
|
|
2222
|
+
child.unref();
|
|
2223
|
+
resolve();
|
|
2224
|
+
});
|
|
2225
|
+
});
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2228
|
+
console.error(`lildocs: failed to open browser: ${message}`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function browserOpenCommand(url) {
|
|
2232
|
+
if (process.platform === "darwin") return ["open", [url]];
|
|
2233
|
+
if (process.platform === "win32") return ["cmd", [
|
|
2234
|
+
"/c",
|
|
2235
|
+
"start",
|
|
2236
|
+
"",
|
|
2237
|
+
url
|
|
2238
|
+
]];
|
|
2239
|
+
return ["xdg-open", [url]];
|
|
2240
|
+
}
|
|
2241
|
+
const deployCommand = command({
|
|
2242
|
+
name: "deploy",
|
|
2243
|
+
description: "Build GitHub Pages-ready documentation output.",
|
|
2244
|
+
args: {
|
|
2245
|
+
input: inputArgument,
|
|
2246
|
+
out: outOption,
|
|
2247
|
+
theme: themeOption,
|
|
2248
|
+
fontHeading: fontHeadingOption,
|
|
2249
|
+
fontBody: fontBodyOption,
|
|
2250
|
+
fontCode: fontCodeOption,
|
|
2251
|
+
backgroundImage: backgroundImageOption,
|
|
2252
|
+
backgroundGradient: backgroundGradientOption,
|
|
2253
|
+
backgroundBlendMode: backgroundBlendModeOption,
|
|
2254
|
+
linkUnderline: linkUnderlineOption,
|
|
2255
|
+
base: baseOption
|
|
2256
|
+
},
|
|
2257
|
+
handler: runDeploy
|
|
2258
|
+
});
|
|
2259
|
+
const initCommand = subcommands({
|
|
2260
|
+
name: "init",
|
|
2261
|
+
description: "Initialize lildocs integration files.",
|
|
2262
|
+
cmds: { "github-pages": command({
|
|
2263
|
+
name: "github-pages",
|
|
2264
|
+
description: "Create a GitHub Actions workflow for GitHub Pages deployment.",
|
|
2265
|
+
args: {
|
|
2266
|
+
input: inputArgument,
|
|
2267
|
+
out: outOption,
|
|
2268
|
+
base: baseOption
|
|
2269
|
+
},
|
|
2270
|
+
handler: runInitGitHubPages
|
|
2271
|
+
}) }
|
|
2272
|
+
});
|
|
2273
|
+
const bareBuildCommand = command({
|
|
2274
|
+
name: "lildocs",
|
|
2275
|
+
description: "Build a static documentation site from Markdown files.",
|
|
2276
|
+
args: {
|
|
2277
|
+
input: inputArgument,
|
|
2278
|
+
out: outOption,
|
|
2279
|
+
theme: themeOption,
|
|
2280
|
+
fontHeading: fontHeadingOption,
|
|
2281
|
+
fontBody: fontBodyOption,
|
|
2282
|
+
fontCode: fontCodeOption,
|
|
2283
|
+
backgroundImage: backgroundImageOption,
|
|
2284
|
+
backgroundGradient: backgroundGradientOption,
|
|
2285
|
+
backgroundBlendMode: backgroundBlendModeOption,
|
|
2286
|
+
linkUnderline: linkUnderlineOption
|
|
2287
|
+
},
|
|
2288
|
+
handler: runBuild
|
|
2289
|
+
});
|
|
2290
|
+
const app = subcommands({
|
|
2291
|
+
name: "lildocs",
|
|
2292
|
+
description: "Turn Markdown docs into a static searchable documentation site.",
|
|
2293
|
+
cmds: {
|
|
2294
|
+
build: buildCommand,
|
|
2295
|
+
deploy: deployCommand,
|
|
2296
|
+
dev: devCommand,
|
|
2297
|
+
init: initCommand
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
function isKnownTopLevelArg(arg) {
|
|
2301
|
+
return arg === void 0 || arg === "build" || arg === "deploy" || arg === "dev" || arg === "init" || arg === "--help" || arg === "-h";
|
|
2302
|
+
}
|
|
2303
|
+
async function main() {
|
|
2304
|
+
const args = process.argv.slice(2);
|
|
2305
|
+
await run(isKnownTopLevelArg(args[0]) ? app : bareBuildCommand, args);
|
|
2306
|
+
}
|
|
2307
|
+
try {
|
|
2308
|
+
await main();
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2311
|
+
console.error(`lildocs: ${message}`);
|
|
2312
|
+
process.exitCode = 1;
|
|
2313
|
+
}
|
|
2314
|
+
//#endregion
|
|
2315
|
+
export {};
|