@tayacrystals/lore 0.1.3 → 1.0.1

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/package.json +46 -36
  4. package/src/build.ts +149 -0
  5. package/src/config.ts +25 -0
  6. package/src/dev.ts +112 -0
  7. package/src/files.ts +193 -0
  8. package/src/i18n.ts +49 -0
  9. package/src/icons.ts +59 -0
  10. package/src/index.ts +28 -0
  11. package/src/mdx.ts +281 -0
  12. package/src/parse.ts +51 -0
  13. package/src/routing.ts +47 -0
  14. package/src/serve.ts +72 -0
  15. package/src/template.ts +753 -0
  16. package/src/types.ts +52 -0
  17. package/src/version.ts +33 -0
  18. package/components/docs/Breadcrumbs.astro +0 -41
  19. package/components/docs/PrevNext.astro +0 -50
  20. package/components/docs/Sidebar.astro +0 -28
  21. package/components/docs/SidebarGroup.astro +0 -72
  22. package/components/docs/SidebarItem.astro +0 -26
  23. package/components/docs/TableOfContents.astro +0 -82
  24. package/components/global/SearchModal.astro +0 -159
  25. package/components/mdx/Accordion.astro +0 -20
  26. package/components/mdx/Callout.astro +0 -53
  27. package/components/mdx/Card.astro +0 -26
  28. package/components/mdx/CardGrid.astro +0 -16
  29. package/components/mdx/CodeTabs.astro +0 -129
  30. package/components/mdx/FileTree.astro +0 -117
  31. package/components/mdx/Step.astro +0 -18
  32. package/components/mdx/Steps.astro +0 -6
  33. package/components/mdx/Tab.astro +0 -11
  34. package/components/mdx/Tabs.astro +0 -73
  35. package/components.ts +0 -11
  36. package/config.ts +0 -42
  37. package/index.ts +0 -2
  38. package/integration.ts +0 -68
  39. package/layouts/DocsLayout.astro +0 -277
  40. package/loaders.ts +0 -5
  41. package/routes/docs.astro +0 -211
  42. package/schema.ts +0 -13
  43. package/styles/global.css +0 -78
  44. package/styles/prose.css +0 -148
  45. package/utils/navigation.ts +0 -35
  46. package/utils/rehype-file-tree.ts +0 -229
  47. package/utils/sidebar.ts +0 -107
  48. package/utils/toc.ts +0 -28
  49. package/virtual.d.ts +0 -9
  50. package/vite-plugin.ts +0 -28
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 tayacrystals
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Lore
2
+
3
+ Zero-setup documentation static site generator.
4
+
5
+ ## Quick Start
6
+
7
+ Run lore in your docs directory:
8
+
9
+ ```bash
10
+ bunx @tayacrystals/lore
11
+ ```
12
+
13
+ This builds your documentation to `./build`. To start a dev server:
14
+
15
+ ```bash
16
+ bunx @tayacrystals/lore dev
17
+ ```
18
+
19
+ ## Documentation
20
+
21
+ For full documentation, visit [https://tayacrystals.github.io/lore/](https://tayacrystals.github.io/lore/)
22
+
23
+ ## Features
24
+
25
+ - Zero setup docs
26
+ - MDX support with custom components
27
+ - Versioning
28
+ - Internationalization
29
+ - Dark/Light mode with system preference detection
30
+ - Full-text search
package/package.json CHANGED
@@ -1,51 +1,61 @@
1
1
  {
2
2
  "name": "@tayacrystals/lore",
3
- "version": "0.1.3",
3
+ "version": "1.0.1",
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
- "*.ts",
14
- "*.d.ts",
15
- "components/",
16
- "layouts/",
17
- "routes/",
18
- "styles/",
19
- "utils/",
20
- "!**/*.test.ts"
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"
24
43
  },
25
44
  "peerDependencies": {
26
- "astro": "^5.0.0"
45
+ "typescript": "^5"
27
46
  },
28
47
  "dependencies": {
29
- "@astrojs/mdx": "^4.3.13",
30
- "@astrojs/sitemap": "^3.7.0",
31
- "@fontsource-variable/inter": "^5.2.8",
32
- "@fontsource/geist-mono": "^5.2.7",
33
- "@iconify-json/lucide": "^1.2.89",
34
- "@tailwindcss/vite": "^4.1.18",
35
- "astro-expressive-code": "^0.41.6",
36
- "astro-icon": "^1.1.5",
37
- "astro-pagefind": "^1.8.5",
38
- "hast-util-to-html": "^9.0.5",
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"
48
+ "lucide": "^0.577.0",
49
+ "rehype-autolink-headings": "^7.1.0",
50
+ "rehype-slug": "^6.0.0",
51
+ "rehype-stringify": "^10.0.1",
52
+ "remark-gfm": "^4.0.1",
53
+ "remark-parse": "^11.0.0",
54
+ "remark-rehype": "^11.1.2",
55
+ "shiki": "^4.0.2",
56
+ "unified": "^11.0.5"
44
57
  },
45
- "devDependencies": {
46
- "@astrojs/check": "^0.9.6",
47
- "@types/hast": "^3.0.4",
48
- "typescript": "^5.9.3",
49
- "vite": "6"
58
+ "engines": {
59
+ "bun": ">=1.0.0"
50
60
  }
51
61
  }
package/src/build.ts ADDED
@@ -0,0 +1,149 @@
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, baseUrl?: 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 `${baseUrl || ''}/${name}`.replace(/\/+/g, '/');
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, config.baseUrl);
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
+ baseUrl: config.baseUrl,
100
+ });
101
+
102
+ const urlParts: string[] = [];
103
+ if (currentLocale) urlParts.push(currentLocale);
104
+ if (currentVersion) urlParts.push(currentVersion);
105
+
106
+ const urlSegments = page.url.split("/").filter(Boolean);
107
+
108
+ let outputPath: string;
109
+ if (urlSegments.length === 0) {
110
+ outputPath = path.join(outDir, ...urlParts, "index.html");
111
+ } else {
112
+ outputPath = path.join(outDir, ...urlParts, ...urlSegments, "index.html");
113
+ }
114
+
115
+ await mkdir(path.dirname(outputPath), { recursive: true });
116
+ await Bun.write(outputPath, html);
117
+ console.log(` ${page.url} → ${path.relative(process.cwd(), outputPath)}`);
118
+ totalPages++;
119
+ }
120
+ }
121
+
122
+ const defaultLocale = config.internationalization
123
+ ? config.defaultLocale ?? "en"
124
+ : undefined;
125
+ const defaultVersion = config.versioning
126
+ ? config.defaultVersion ?? "latest"
127
+ : undefined;
128
+
129
+ const redirectPathParts: string[] = [];
130
+ if (defaultLocale) redirectPathParts.push(defaultLocale);
131
+ if (defaultVersion) redirectPathParts.push(defaultVersion);
132
+
133
+ if (redirectPathParts.length > 0 && totalPages > 0) {
134
+ const redirectPath = "/" + redirectPathParts.join("/") + "/";
135
+ const redirectHtml = `<!DOCTYPE html>
136
+ <html>
137
+ <head>
138
+ <meta http-equiv="refresh" content="0; url=${config.baseUrl || ''}${redirectPath}">
139
+ <script>window.location.href = "${config.baseUrl || ''}${redirectPath}";</script>
140
+ </head>
141
+ <body></body>
142
+ </html>`;
143
+ const outputPath = path.join(outDir, "index.html");
144
+ await Bun.write(outputPath, redirectHtml);
145
+ console.log(` / → ${path.relative(process.cwd(), outputPath)} (redirect to ${config.baseUrl || ''}${redirectPath})`);
146
+ }
147
+
148
+ console.log(`\nDone! Built ${totalPages} pages to ${outDir}`);
149
+ }
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Config } from "./types.ts";
2
+
3
+ export async function loadConfig(docsDir: string): Promise<Config> {
4
+ const file = Bun.file(`${docsDir}/lore.yml`);
5
+ if (!(await file.exists())) return {};
6
+ const text = await file.text();
7
+ return (Bun.YAML.parse(text) as Config) ?? {};
8
+ }
9
+
10
+ // WCAG AA compliant colors (≥4.5:1 contrast on white)
11
+ const NAMED_COLORS: Record<string, string> = {
12
+ red: "#b91c1c",
13
+ orange: "#c2410c",
14
+ yellow: "#a16207",
15
+ green: "#15803d",
16
+ blue: "#1d4ed8",
17
+ purple: "#6d28d9",
18
+ pink: "#be185d",
19
+ gray: "#374151",
20
+ };
21
+
22
+ export function resolveColor(color?: string): string {
23
+ if (!color) return "#1d4ed8";
24
+ return NAMED_COLORS[color] ?? color;
25
+ }
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
+ }