@tayacrystals/lore 0.1.2 → 1.0.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 +7 -0
- package/package.json +48 -36
- package/src/build.ts +148 -0
- package/src/config.ts +26 -0
- package/src/dev.ts +112 -0
- package/src/files.ts +193 -0
- package/src/i18n.ts +49 -0
- package/src/icons.ts +59 -0
- package/src/index.ts +28 -0
- package/src/mdx.ts +281 -0
- package/src/parse.ts +53 -0
- package/src/routing.ts +46 -0
- package/src/serve.ts +72 -0
- package/src/template.ts +747 -0
- package/src/types.ts +51 -0
- package/src/version.ts +33 -0
- package/components/docs/Breadcrumbs.astro +0 -41
- package/components/docs/PrevNext.astro +0 -50
- package/components/docs/Sidebar.astro +0 -28
- package/components/docs/SidebarGroup.astro +0 -55
- package/components/docs/SidebarItem.astro +0 -26
- package/components/docs/TableOfContents.astro +0 -82
- package/components/global/SearchModal.astro +0 -159
- package/components/mdx/Accordion.astro +0 -20
- package/components/mdx/Callout.astro +0 -53
- package/components/mdx/Card.astro +0 -26
- package/components/mdx/CardGrid.astro +0 -16
- package/components/mdx/CodeTabs.astro +0 -129
- package/components/mdx/FileTree.astro +0 -117
- package/components/mdx/Step.astro +0 -18
- package/components/mdx/Steps.astro +0 -6
- package/components/mdx/Tab.astro +0 -11
- package/components/mdx/Tabs.astro +0 -73
- package/components.ts +0 -11
- package/config.ts +0 -42
- package/index.ts +0 -2
- package/integration.ts +0 -68
- package/layouts/DocsLayout.astro +0 -277
- package/loaders.ts +0 -5
- package/routes/docs.astro +0 -201
- package/schema.ts +0 -13
- package/styles/global.css +0 -78
- package/styles/prose.css +0 -148
- package/utils/navigation.ts +0 -32
- package/utils/rehype-file-tree.ts +0 -229
- package/utils/sidebar.ts +0 -97
- package/utils/toc.ts +0 -28
- package/virtual.d.ts +0 -9
- package/vite-plugin.ts +0 -28
package/README.md
ADDED
package/package.json
CHANGED
|
@@ -1,51 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tayacrystals/lore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple documentation site generator with versioning and internationalization support",
|
|
4
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lore": "src/index.ts"
|
|
8
|
+
},
|
|
5
9
|
"exports": {
|
|
6
|
-
".": "./index.ts"
|
|
7
|
-
"./schema": "./schema.ts",
|
|
8
|
-
"./loaders": "./loaders.ts",
|
|
9
|
-
"./components": "./components.ts",
|
|
10
|
-
"./styles/*": "./styles/*"
|
|
10
|
+
".": "./src/index.ts"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "bun run src/index.ts build",
|
|
17
|
+
"dev": "bun run src/index.ts dev",
|
|
18
|
+
"serve": "bun run src/index.ts serve"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"documentation",
|
|
22
|
+
"static-site-generator",
|
|
23
|
+
"docs",
|
|
24
|
+
"markdown",
|
|
25
|
+
"mdx",
|
|
26
|
+
"cli"
|
|
21
27
|
],
|
|
22
28
|
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/tayacrystals/lore.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/tayacrystals/lore#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/tayacrystals/lore/issues"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@types/js-yaml": "^4.0.9"
|
|
24
44
|
},
|
|
25
45
|
"peerDependencies": {
|
|
26
|
-
"
|
|
46
|
+
"typescript": "^5"
|
|
27
47
|
},
|
|
28
48
|
"dependencies": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"hastscript": "^9.0.1",
|
|
40
|
-
"rehype": "^13.0.2",
|
|
41
|
-
"tailwindcss": "^4.1.18",
|
|
42
|
-
"unist-util-visit": "^5.1.0",
|
|
43
|
-
"zod": "^3.24.0"
|
|
49
|
+
"js-yaml": "^4.1.1",
|
|
50
|
+
"lucide": "^0.577.0",
|
|
51
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
52
|
+
"rehype-slug": "^6.0.0",
|
|
53
|
+
"rehype-stringify": "^10.0.1",
|
|
54
|
+
"remark-gfm": "^4.0.1",
|
|
55
|
+
"remark-parse": "^11.0.0",
|
|
56
|
+
"remark-rehype": "^11.1.2",
|
|
57
|
+
"shiki": "^4.0.2",
|
|
58
|
+
"unified": "^11.0.5"
|
|
44
59
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"@types/hast": "^3.0.4",
|
|
48
|
-
"typescript": "^5.9.3",
|
|
49
|
-
"vite": "6"
|
|
60
|
+
"engines": {
|
|
61
|
+
"bun": ">=1.0.0"
|
|
50
62
|
}
|
|
51
63
|
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderMdx } from "./mdx.ts";
|
|
4
|
+
import { loadConfig } from "./config.ts";
|
|
5
|
+
import { buildDocTree } from "./files.ts";
|
|
6
|
+
import { renderPage } from "./template.ts";
|
|
7
|
+
import { detectVersions, getDefaultVersion } from "./version.ts";
|
|
8
|
+
import { detectLocales, getDefaultLocale } from "./i18n.ts";
|
|
9
|
+
|
|
10
|
+
async function findLogo(docsDir: string, outDir: string): Promise<string | undefined> {
|
|
11
|
+
for (const name of ["logo.svg", "logo.png"]) {
|
|
12
|
+
const src = Bun.file(`${docsDir}/${name}`);
|
|
13
|
+
if (await src.exists()) {
|
|
14
|
+
await Bun.write(`${outDir}/${name}`, src);
|
|
15
|
+
return `/${name}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function build(
|
|
21
|
+
docsDir: string,
|
|
22
|
+
outDir: string,
|
|
23
|
+
opts: { devMode?: boolean } = {}
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
console.log(`Building docs from ${docsDir} → ${outDir}`);
|
|
26
|
+
|
|
27
|
+
// Clear build directory first to avoid merging old and new artifacts
|
|
28
|
+
try {
|
|
29
|
+
await rm(outDir, { recursive: true, force: true });
|
|
30
|
+
} catch {
|
|
31
|
+
// Directory doesn't exist, which is fine
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = await loadConfig(docsDir);
|
|
35
|
+
const logoSrc = await findLogo(docsDir, outDir);
|
|
36
|
+
|
|
37
|
+
const hasVersioning = config.versioning;
|
|
38
|
+
const hasI18n = config.internationalization;
|
|
39
|
+
|
|
40
|
+
const builds: Array<{ version?: string; locale?: string }> = [];
|
|
41
|
+
|
|
42
|
+
if (hasI18n) {
|
|
43
|
+
const locales = await detectLocales(docsDir);
|
|
44
|
+
for (const locale of locales) {
|
|
45
|
+
if (hasVersioning) {
|
|
46
|
+
const localeDir = path.join(docsDir, locale.code);
|
|
47
|
+
const versions = await detectVersions(localeDir);
|
|
48
|
+
for (const version of versions) {
|
|
49
|
+
builds.push({ locale: locale.code, version: version.name });
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
builds.push({ locale: locale.code });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else if (hasVersioning) {
|
|
56
|
+
const versions = await detectVersions(docsDir);
|
|
57
|
+
for (const version of versions) {
|
|
58
|
+
builds.push({ version: version.name });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (builds.length === 0) {
|
|
63
|
+
builds.push({});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let totalPages = 0;
|
|
67
|
+
|
|
68
|
+
for (const buildOpts of builds) {
|
|
69
|
+
const { pages, sidebar, versions, locales, currentVersion, currentLocale } =
|
|
70
|
+
await buildDocTree(docsDir, buildOpts);
|
|
71
|
+
|
|
72
|
+
const buildLabel = [
|
|
73
|
+
currentLocale,
|
|
74
|
+
currentVersion,
|
|
75
|
+
].filter(Boolean).join("/") || "default";
|
|
76
|
+
|
|
77
|
+
console.log(`Building for ${buildLabel}: found ${pages.length} pages`);
|
|
78
|
+
|
|
79
|
+
for (const page of pages) {
|
|
80
|
+
const contentHtml = await renderMdx(page.content, {
|
|
81
|
+
locale: currentLocale,
|
|
82
|
+
version: currentVersion,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const html = renderPage({
|
|
86
|
+
config,
|
|
87
|
+
title: page.title,
|
|
88
|
+
description: page.description,
|
|
89
|
+
contentHtml,
|
|
90
|
+
sidebar,
|
|
91
|
+
currentUrl: page.url,
|
|
92
|
+
logoSrc,
|
|
93
|
+
devMode: opts.devMode,
|
|
94
|
+
versions,
|
|
95
|
+
locales,
|
|
96
|
+
currentVersion,
|
|
97
|
+
currentLocale,
|
|
98
|
+
translationOf: page.context.translationOf,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const urlParts: string[] = [];
|
|
102
|
+
if (currentLocale) urlParts.push(currentLocale);
|
|
103
|
+
if (currentVersion) urlParts.push(currentVersion);
|
|
104
|
+
|
|
105
|
+
const urlSegments = page.url.split("/").filter(Boolean);
|
|
106
|
+
|
|
107
|
+
let outputPath: string;
|
|
108
|
+
if (urlSegments.length === 0) {
|
|
109
|
+
outputPath = path.join(outDir, ...urlParts, "index.html");
|
|
110
|
+
} else {
|
|
111
|
+
outputPath = path.join(outDir, ...urlParts, ...urlSegments, "index.html");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
115
|
+
await Bun.write(outputPath, html);
|
|
116
|
+
console.log(` ${page.url} → ${path.relative(process.cwd(), outputPath)}`);
|
|
117
|
+
totalPages++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const defaultLocale = config.internationalization
|
|
122
|
+
? config.defaultLocale ?? "en"
|
|
123
|
+
: undefined;
|
|
124
|
+
const defaultVersion = config.versioning
|
|
125
|
+
? config.defaultVersion ?? "latest"
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
128
|
+
const redirectPathParts: string[] = [];
|
|
129
|
+
if (defaultLocale) redirectPathParts.push(defaultLocale);
|
|
130
|
+
if (defaultVersion) redirectPathParts.push(defaultVersion);
|
|
131
|
+
|
|
132
|
+
if (redirectPathParts.length > 0 && totalPages > 0) {
|
|
133
|
+
const redirectPath = "/" + redirectPathParts.join("/") + "/";
|
|
134
|
+
const redirectHtml = `<!DOCTYPE html>
|
|
135
|
+
<html>
|
|
136
|
+
<head>
|
|
137
|
+
<meta http-equiv="refresh" content="0; url=${redirectPath}">
|
|
138
|
+
<script>window.location.href = "${redirectPath}";</script>
|
|
139
|
+
</head>
|
|
140
|
+
<body></body>
|
|
141
|
+
</html>`;
|
|
142
|
+
const outputPath = path.join(outDir, "index.html");
|
|
143
|
+
await Bun.write(outputPath, redirectHtml);
|
|
144
|
+
console.log(` / → ${path.relative(process.cwd(), outputPath)} (redirect to ${redirectPath})`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\nDone! Built ${totalPages} pages to ${outDir}`);
|
|
148
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
import type { Config } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function loadConfig(docsDir: string): Promise<Config> {
|
|
5
|
+
const file = Bun.file(`${docsDir}/lore.yml`);
|
|
6
|
+
if (!(await file.exists())) return {};
|
|
7
|
+
const text = await file.text();
|
|
8
|
+
return (yaml.load(text) as Config) ?? {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// WCAG AA compliant colors (≥4.5:1 contrast on white)
|
|
12
|
+
const NAMED_COLORS: Record<string, string> = {
|
|
13
|
+
red: "#b91c1c",
|
|
14
|
+
orange: "#c2410c",
|
|
15
|
+
yellow: "#a16207",
|
|
16
|
+
green: "#15803d",
|
|
17
|
+
blue: "#1d4ed8",
|
|
18
|
+
purple: "#6d28d9",
|
|
19
|
+
pink: "#be185d",
|
|
20
|
+
gray: "#374151",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function resolveColor(color?: string): string {
|
|
24
|
+
if (!color) return "#1d4ed8";
|
|
25
|
+
return NAMED_COLORS[color] ?? color;
|
|
26
|
+
}
|
package/src/dev.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { build } from "./build.ts";
|
|
5
|
+
import { loadConfig } from "./config.ts";
|
|
6
|
+
import { getDefaultVersion } from "./version.ts";
|
|
7
|
+
import { getDefaultLocale } from "./i18n.ts";
|
|
8
|
+
|
|
9
|
+
export async function dev(docsDir: string, outDir: string): Promise<void> {
|
|
10
|
+
const port = 3000;
|
|
11
|
+
|
|
12
|
+
// Load config for redirect logic
|
|
13
|
+
const config = await loadConfig(docsDir);
|
|
14
|
+
|
|
15
|
+
// Initial build
|
|
16
|
+
await build(docsDir, outDir, { devMode: true });
|
|
17
|
+
|
|
18
|
+
// SSE clients waiting for reload signals
|
|
19
|
+
const sseClients = new Set<ReadableStreamDefaultController<string>>();
|
|
20
|
+
|
|
21
|
+
function notifyReload() {
|
|
22
|
+
for (const ctrl of sseClients) {
|
|
23
|
+
try {
|
|
24
|
+
ctrl.enqueue("data: reload\n\n");
|
|
25
|
+
} catch {
|
|
26
|
+
sseClients.delete(ctrl);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Watch docs dir and rebuild on changes
|
|
32
|
+
let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
33
|
+
watch(docsDir, { recursive: true }, (_event, filename) => {
|
|
34
|
+
if (rebuildTimer) clearTimeout(rebuildTimer);
|
|
35
|
+
rebuildTimer = setTimeout(async () => {
|
|
36
|
+
console.log(`\n${filename} changed — rebuilding...`);
|
|
37
|
+
try {
|
|
38
|
+
await build(docsDir, outDir, { devMode: true });
|
|
39
|
+
notifyReload();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error("Build error:", err);
|
|
42
|
+
}
|
|
43
|
+
}, 100);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Bun.serve({
|
|
47
|
+
port,
|
|
48
|
+
idleTimeout: 0,
|
|
49
|
+
async fetch(req) {
|
|
50
|
+
const url = new URL(req.url);
|
|
51
|
+
|
|
52
|
+
// Redirect from root to default version/locale
|
|
53
|
+
if (url.pathname === "/") {
|
|
54
|
+
const defaultLocale = config.internationalization
|
|
55
|
+
? getDefaultLocale(config)
|
|
56
|
+
: undefined;
|
|
57
|
+
const defaultVersion = config.versioning
|
|
58
|
+
? getDefaultVersion(config)
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
const redirectPathParts: string[] = [];
|
|
62
|
+
if (defaultLocale) redirectPathParts.push(defaultLocale);
|
|
63
|
+
if (defaultVersion) redirectPathParts.push(defaultVersion);
|
|
64
|
+
|
|
65
|
+
if (redirectPathParts.length > 0) {
|
|
66
|
+
const redirectPath = "/" + redirectPathParts.join("/") + "/";
|
|
67
|
+
return Response.redirect(url.origin + redirectPath, 302);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// SSE endpoint for live reload
|
|
72
|
+
if (url.pathname === "/_lore/reload") {
|
|
73
|
+
let ctrl!: ReadableStreamDefaultController<string>;
|
|
74
|
+
const stream = new ReadableStream<string>({
|
|
75
|
+
start(c) {
|
|
76
|
+
ctrl = c;
|
|
77
|
+
sseClients.add(ctrl);
|
|
78
|
+
ctrl.enqueue(": connected\n\n");
|
|
79
|
+
},
|
|
80
|
+
cancel() {
|
|
81
|
+
sseClients.delete(ctrl);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
return new Response(stream, {
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "text/event-stream",
|
|
87
|
+
"Cache-Control": "no-cache",
|
|
88
|
+
"Connection": "keep-alive",
|
|
89
|
+
"X-Accel-Buffering": "no",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Serve files from outDir
|
|
95
|
+
let filePath = path.join(outDir, url.pathname);
|
|
96
|
+
try {
|
|
97
|
+
const s = await stat(filePath);
|
|
98
|
+
if (s.isDirectory()) filePath = path.join(filePath, "index.html");
|
|
99
|
+
} catch {
|
|
100
|
+
filePath = path.join(outDir, url.pathname, "index.html");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const file = Bun.file(filePath);
|
|
104
|
+
if (await file.exists()) return new Response(file);
|
|
105
|
+
|
|
106
|
+
return new Response("Not found", { status: 404 });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(`\nDev server running at http://localhost:${port}`);
|
|
111
|
+
console.log("Watching for changes...\n");
|
|
112
|
+
}
|
package/src/files.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parsePage, filenameToTitle } from "./parse.ts";
|
|
4
|
+
import type { PageInfo, SidebarItem, VersionInfo, LocaleInfo } from "./types.ts";
|
|
5
|
+
import { loadConfig } from "./config.ts";
|
|
6
|
+
import { detectVersions, getDefaultVersion } from "./version.ts";
|
|
7
|
+
import { detectLocales, getDefaultLocale } from "./i18n.ts";
|
|
8
|
+
|
|
9
|
+
export interface DocTree {
|
|
10
|
+
pages: PageInfo[];
|
|
11
|
+
sidebar: SidebarItem[];
|
|
12
|
+
versions?: VersionInfo[];
|
|
13
|
+
locales?: LocaleInfo[];
|
|
14
|
+
currentVersion?: string;
|
|
15
|
+
currentLocale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Strip numeric prefix: "01-configuration" → "configuration" */
|
|
19
|
+
function stripPrefix(name: string): string {
|
|
20
|
+
return name.replace(/^\d+-/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Convert a filename (with .mdx) to a URL slug */
|
|
24
|
+
function toSlug(filename: string): string {
|
|
25
|
+
return stripPrefix(filename.replace(/\.mdx$/, ""));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Build the URL from a list of path parts relative to docs dir */
|
|
29
|
+
function buildUrl(parts: string[]): string {
|
|
30
|
+
const segments = parts.map((p) => toSlug(p));
|
|
31
|
+
const filtered = segments.filter((s) => s !== "index");
|
|
32
|
+
if (filtered.length === 0) return "/";
|
|
33
|
+
return "/" + filtered.join("/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateListing(title: string, url: string, items: SidebarItem[]): PageInfo {
|
|
37
|
+
const lines = [`# ${title}`, ""];
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
const href = item.type === "page" ? item.url : (item.url ?? "#");
|
|
40
|
+
lines.push(`- [${item.title}](${href})`);
|
|
41
|
+
}
|
|
42
|
+
return { filePath: url, url, title, content: lines.join("\n"), context: {} };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Sort entries: index.mdx first, then alphabetically */
|
|
46
|
+
function sortEntries(entries: string[]): string[] {
|
|
47
|
+
return entries.sort((a, b) => {
|
|
48
|
+
const aIsIndex = a === "index.mdx";
|
|
49
|
+
const bIsIndex = b === "index.mdx";
|
|
50
|
+
if (aIsIndex && !bIsIndex) return -1;
|
|
51
|
+
if (!aIsIndex && bIsIndex) return 1;
|
|
52
|
+
return a.localeCompare(b);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function buildDocTree(
|
|
57
|
+
docsDir: string,
|
|
58
|
+
opts: {
|
|
59
|
+
version?: string;
|
|
60
|
+
locale?: string;
|
|
61
|
+
} = {}
|
|
62
|
+
): Promise<DocTree> {
|
|
63
|
+
const config = await loadConfig(docsDir);
|
|
64
|
+
const hasVersioning = config.versioning;
|
|
65
|
+
const hasI18n = config.internationalization;
|
|
66
|
+
|
|
67
|
+
let versions: VersionInfo[] = [];
|
|
68
|
+
let locales: LocaleInfo[] = [];
|
|
69
|
+
|
|
70
|
+
if (hasI18n) {
|
|
71
|
+
locales = await detectLocales(docsDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasVersioning) {
|
|
75
|
+
if (hasI18n && locales.length > 0) {
|
|
76
|
+
const locale = opts.locale ?? getDefaultLocale(config);
|
|
77
|
+
const localeDir = path.join(docsDir, locale);
|
|
78
|
+
versions = await detectVersions(localeDir);
|
|
79
|
+
} else {
|
|
80
|
+
versions = await detectVersions(docsDir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let contentDir = docsDir;
|
|
85
|
+
let currentVersion: string | undefined;
|
|
86
|
+
let currentLocale: string | undefined;
|
|
87
|
+
|
|
88
|
+
if (hasI18n) {
|
|
89
|
+
currentLocale = opts.locale ?? getDefaultLocale(config);
|
|
90
|
+
contentDir = path.join(contentDir, currentLocale);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasVersioning) {
|
|
94
|
+
currentVersion = opts.version ?? getDefaultVersion(config);
|
|
95
|
+
contentDir = path.join(contentDir, currentVersion);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pages: PageInfo[] = [];
|
|
99
|
+
const sidebar: SidebarItem[] = [];
|
|
100
|
+
|
|
101
|
+
await walkDir(
|
|
102
|
+
docsDir,
|
|
103
|
+
contentDir,
|
|
104
|
+
[],
|
|
105
|
+
pages,
|
|
106
|
+
sidebar,
|
|
107
|
+
true,
|
|
108
|
+
currentLocale,
|
|
109
|
+
currentVersion
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
pages,
|
|
114
|
+
sidebar,
|
|
115
|
+
versions: versions.length > 0 ? versions : undefined,
|
|
116
|
+
locales: locales.length > 0 ? locales : undefined,
|
|
117
|
+
currentVersion,
|
|
118
|
+
currentLocale,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function walkDir(
|
|
123
|
+
docsDir: string,
|
|
124
|
+
dir: string,
|
|
125
|
+
pathParts: string[],
|
|
126
|
+
pages: PageInfo[],
|
|
127
|
+
sidebar: SidebarItem[],
|
|
128
|
+
isRoot: boolean,
|
|
129
|
+
currentLocale?: string,
|
|
130
|
+
currentVersion?: string
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const entries = sortEntries(await readdir(dir));
|
|
133
|
+
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry === "lore.yml" || entry.startsWith(".")) continue;
|
|
136
|
+
|
|
137
|
+
const fullPath = path.join(dir, entry);
|
|
138
|
+
const s = await stat(fullPath);
|
|
139
|
+
|
|
140
|
+
if (s.isDirectory()) {
|
|
141
|
+
const sectionTitle = filenameToTitle(entry);
|
|
142
|
+
const sectionItems: SidebarItem[] = [];
|
|
143
|
+
const sectionPages: PageInfo[] = [];
|
|
144
|
+
|
|
145
|
+
await walkDir(docsDir, fullPath, [...pathParts, entry], sectionPages, sectionItems, false, currentLocale, currentVersion);
|
|
146
|
+
|
|
147
|
+
const sectionUrl = buildUrl([...pathParts, entry]);
|
|
148
|
+
const hasIndex = sectionPages.some((p) => p.url === sectionUrl);
|
|
149
|
+
|
|
150
|
+
// Auto-generate a directory listing page if there's no explicit index.mdx
|
|
151
|
+
if (!hasIndex) {
|
|
152
|
+
sectionPages.push(generateListing(sectionTitle, sectionUrl, sectionItems));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pages.push(...sectionPages);
|
|
156
|
+
|
|
157
|
+
sidebar.push({
|
|
158
|
+
type: "section",
|
|
159
|
+
title: sectionTitle,
|
|
160
|
+
url: sectionUrl,
|
|
161
|
+
items: sectionItems,
|
|
162
|
+
});
|
|
163
|
+
} else if (entry.endsWith(".mdx")) {
|
|
164
|
+
const url = buildUrl([...pathParts, entry]);
|
|
165
|
+
const raw = await Bun.file(fullPath).text();
|
|
166
|
+
const parsed = parsePage(raw);
|
|
167
|
+
const baseName = entry.replace(/\.mdx$/, "");
|
|
168
|
+
const title = parsed.h1Title ?? filenameToTitle(baseName);
|
|
169
|
+
|
|
170
|
+
const page: PageInfo = {
|
|
171
|
+
filePath: fullPath,
|
|
172
|
+
url,
|
|
173
|
+
title,
|
|
174
|
+
description: parsed.description,
|
|
175
|
+
content: parsed.content,
|
|
176
|
+
context: {
|
|
177
|
+
version: currentVersion,
|
|
178
|
+
locale: currentLocale,
|
|
179
|
+
translationOf: parsed.frontmatter["translation-of"] as string | undefined,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
pages.push(page);
|
|
183
|
+
|
|
184
|
+
const isIndex = toSlug(entry) === "index";
|
|
185
|
+
// Root index.mdx goes into sidebar; subdirectory index.mdx is shown
|
|
186
|
+
// via the section header link and not as a separate sidebar item.
|
|
187
|
+
if (!isIndex || isRoot) {
|
|
188
|
+
const sidebarTitle = isRoot && isIndex ? "Home" : title;
|
|
189
|
+
sidebar.push({ type: "page", title: sidebarTitle, url });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { LocaleInfo } from "./types.ts";
|
|
4
|
+
import type { Config } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export async function detectLocales(docsDir: string): Promise<LocaleInfo[]> {
|
|
7
|
+
const entries = await readdir(docsDir);
|
|
8
|
+
const locales: LocaleInfo[] = [];
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = path.join(docsDir, entry);
|
|
12
|
+
const s = await stat(fullPath);
|
|
13
|
+
|
|
14
|
+
if (s.isDirectory() && /^[a-z]{2}(-[a-z]{2})?$/.test(entry)) {
|
|
15
|
+
locales.push({ code: entry });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return locales.sort((a, b) => a.code.localeCompare(b.code));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getDefaultLocale(config: Config): string {
|
|
23
|
+
return config.defaultLocale ?? "en";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLocaleLabel(code: string): string {
|
|
27
|
+
const labels: Record<string, string> = {
|
|
28
|
+
en: "English",
|
|
29
|
+
es: "Español",
|
|
30
|
+
fr: "Français",
|
|
31
|
+
de: "Deutsch",
|
|
32
|
+
ja: "日本語",
|
|
33
|
+
zh: "中文",
|
|
34
|
+
ko: "한국어",
|
|
35
|
+
pt: "Português",
|
|
36
|
+
ru: "Русский",
|
|
37
|
+
it: "Italiano",
|
|
38
|
+
nl: "Nederlands",
|
|
39
|
+
pl: "Polski",
|
|
40
|
+
tr: "Türkçe",
|
|
41
|
+
ar: "العربية",
|
|
42
|
+
hi: "हिन्दी",
|
|
43
|
+
vi: "Tiếng Việt",
|
|
44
|
+
th: "ไทย",
|
|
45
|
+
id: "Bahasa Indonesia",
|
|
46
|
+
ms: "Bahasa Melayu",
|
|
47
|
+
};
|
|
48
|
+
return labels[code] ?? code.toUpperCase();
|
|
49
|
+
}
|