@pruddiman/mdmirror 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.
Files changed (60) hide show
  1. package/README.md +1 -0
  2. package/dist/build/builder.d.ts +41 -0
  3. package/dist/build/builder.js +108 -0
  4. package/dist/build/builder.js.map +1 -0
  5. package/dist/cli/build.d.ts +14 -0
  6. package/dist/cli/build.js +116 -0
  7. package/dist/cli/build.js.map +1 -0
  8. package/dist/cli/index.d.ts +6 -0
  9. package/dist/cli/index.js +104 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/serve.d.ts +15 -0
  12. package/dist/cli/serve.js +233 -0
  13. package/dist/cli/serve.js.map +1 -0
  14. package/dist/core/discovery.d.ts +12 -0
  15. package/dist/core/discovery.js +91 -0
  16. package/dist/core/discovery.js.map +1 -0
  17. package/dist/core/navigation.d.ts +12 -0
  18. package/dist/core/navigation.js +155 -0
  19. package/dist/core/navigation.js.map +1 -0
  20. package/dist/core/slug.d.ts +47 -0
  21. package/dist/core/slug.js +132 -0
  22. package/dist/core/slug.js.map +1 -0
  23. package/dist/core/title.d.ts +22 -0
  24. package/dist/core/title.js +43 -0
  25. package/dist/core/title.js.map +1 -0
  26. package/dist/render/highlight.d.ts +23 -0
  27. package/dist/render/highlight.js +36 -0
  28. package/dist/render/highlight.js.map +1 -0
  29. package/dist/render/mermaid.d.ts +13 -0
  30. package/dist/render/mermaid.js +66 -0
  31. package/dist/render/mermaid.js.map +1 -0
  32. package/dist/render/pipeline.d.ts +32 -0
  33. package/dist/render/pipeline.js +141 -0
  34. package/dist/render/pipeline.js.map +1 -0
  35. package/dist/search/index.d.ts +19 -0
  36. package/dist/search/index.js +43 -0
  37. package/dist/search/index.js.map +1 -0
  38. package/dist/server/reload.d.ts +23 -0
  39. package/dist/server/reload.js +80 -0
  40. package/dist/server/reload.js.map +1 -0
  41. package/dist/server/server.d.ts +22 -0
  42. package/dist/server/server.js +137 -0
  43. package/dist/server/server.js.map +1 -0
  44. package/dist/server/watcher.d.ts +24 -0
  45. package/dist/server/watcher.js +62 -0
  46. package/dist/server/watcher.js.map +1 -0
  47. package/dist/theme/index.d.ts +8 -0
  48. package/dist/theme/index.js +19 -0
  49. package/dist/theme/index.js.map +1 -0
  50. package/dist/theme/layout.d.ts +37 -0
  51. package/dist/theme/layout.js +141 -0
  52. package/dist/theme/layout.js.map +1 -0
  53. package/dist/theme/scripts.d.ts +29 -0
  54. package/dist/theme/scripts.js +159 -0
  55. package/dist/theme/scripts.js.map +1 -0
  56. package/dist/theme/styles.css +462 -0
  57. package/dist/types.d.ts +99 -0
  58. package/dist/types.js +6 -0
  59. package/dist/types.js.map +1 -0
  60. package/package.json +76 -0
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # mdmirror
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Static site builder.
3
+ * Writes all rendered HTML pages and assets to an output directory.
4
+ */
5
+ import type { Document, NavigationTree } from "../types.js";
6
+ export interface BuildOptions {
7
+ /** All processed documents */
8
+ documents: Document[];
9
+ /** Built navigation tree */
10
+ navigationTree: NavigationTree;
11
+ /** CSS stylesheet content */
12
+ stylesheet: string;
13
+ /** Output directory path */
14
+ outputDir: string;
15
+ /** Navigation JS content */
16
+ navigationScript?: string;
17
+ /** URL path to the Mermaid UMD bundle */
18
+ mermaidBundleSrc?: string;
19
+ /** Inline script to initialize Mermaid after the bundle loads */
20
+ mermaidInitScript?: string;
21
+ /** Inline search script (injected per-page) */
22
+ searchScript?: string;
23
+ /** Base URL for canonical links and sitemap (e.g. "https://example.com") */
24
+ baseUrl?: string;
25
+ }
26
+ export interface BuildResult {
27
+ /** Number of pages written */
28
+ pageCount: number;
29
+ /** Total output size in bytes */
30
+ totalSize: number;
31
+ /** Absolute path of the output directory */
32
+ outputPath: string;
33
+ }
34
+ /**
35
+ * Build the static site output.
36
+ */
37
+ export declare function buildSite(options: BuildOptions): Promise<BuildResult>;
38
+ /**
39
+ * Format bytes into a human-readable string.
40
+ */
41
+ export declare function formatSize(bytes: number): string;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Static site builder.
3
+ * Writes all rendered HTML pages and assets to an output directory.
4
+ */
5
+ import { mkdir, writeFile, copyFile, stat as fsStat } from "node:fs/promises";
6
+ import { resolve, dirname, join } from "node:path";
7
+ import { renderLayout } from "../theme/layout.js";
8
+ import { buildSearchIndex } from "../search/index.js";
9
+ /**
10
+ * Build the static site output.
11
+ */
12
+ export async function buildSite(options) {
13
+ const { documents, navigationTree, stylesheet, outputDir, navigationScript, mermaidBundleSrc, mermaidInitScript, searchScript, baseUrl, } = options;
14
+ const outputPath = resolve(outputDir);
15
+ let totalSize = 0;
16
+ // Create output directory
17
+ try {
18
+ await mkdir(outputPath, { recursive: true });
19
+ }
20
+ catch (error) {
21
+ throw new Error(`Cannot create output directory: ${outputPath} (${error instanceof Error ? error.message : "permission denied"})`, { cause: error });
22
+ }
23
+ // Create assets directory
24
+ const assetsDir = join(outputPath, "assets");
25
+ await mkdir(assetsDir, { recursive: true });
26
+ const cssContent = stylesheet;
27
+ // Write search index
28
+ const searchIndex = buildSearchIndex(documents);
29
+ const searchJson = JSON.stringify(searchIndex);
30
+ await writeFile(join(assetsDir, "search.json"), searchJson, "utf-8");
31
+ totalSize += Buffer.byteLength(searchJson, "utf-8");
32
+ // Write navigation script if provided
33
+ if (navigationScript) {
34
+ await writeFile(join(assetsDir, "main.js"), navigationScript, "utf-8");
35
+ totalSize += Buffer.byteLength(navigationScript, "utf-8");
36
+ }
37
+ // Copy Mermaid JS bundle if available
38
+ try {
39
+ const mermaidSrc = resolve("node_modules/mermaid/dist/mermaid.min.js");
40
+ await fsStat(mermaidSrc);
41
+ await copyFile(mermaidSrc, join(assetsDir, "mermaid.min.js"));
42
+ const mermaidSize = (await fsStat(join(assetsDir, "mermaid.min.js"))).size;
43
+ totalSize += mermaidSize;
44
+ }
45
+ catch {
46
+ console.warn("Warning: Mermaid bundle not found — diagram pages may not render correctly");
47
+ }
48
+ // Generate sitemap.xml when a base URL is provided
49
+ if (baseUrl) {
50
+ const sitemapBase = baseUrl.replace(/\/$/, "");
51
+ const urls = documents
52
+ .map((doc) => {
53
+ const path = doc.slug === "" ? "/" : `/${doc.slug}/`;
54
+ return ` <url>\n <loc>${sitemapBase}${path}</loc>\n </url>`;
55
+ })
56
+ .join("\n");
57
+ const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>\n` +
58
+ `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` +
59
+ `${urls}\n` +
60
+ `</urlset>\n`;
61
+ await writeFile(join(outputPath, "sitemap.xml"), sitemapXml, "utf-8");
62
+ totalSize += Buffer.byteLength(sitemapXml, "utf-8");
63
+ }
64
+ // Write HTML pages
65
+ for (const doc of documents) {
66
+ const pagePath = doc.slug === "" ? "/" : `/${doc.slug}/`;
67
+ const canonicalUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${pagePath}` : undefined;
68
+ const html = renderLayout({
69
+ title: doc.title,
70
+ description: doc.description,
71
+ canonicalUrl,
72
+ content: doc.renderedHtml,
73
+ navigation: navigationTree,
74
+ currentSlug: doc.slug,
75
+ stylesheet: cssContent,
76
+ mode: "build",
77
+ mermaidBundleSrc,
78
+ mermaidInitScript,
79
+ navigationScript,
80
+ searchScript,
81
+ });
82
+ // Determine output file path
83
+ const htmlPath = doc.slug === ""
84
+ ? join(outputPath, "index.html")
85
+ : join(outputPath, doc.slug, "index.html");
86
+ // Create parent directories
87
+ await mkdir(dirname(htmlPath), { recursive: true });
88
+ // Write the HTML file
89
+ await writeFile(htmlPath, html, "utf-8");
90
+ totalSize += Buffer.byteLength(html, "utf-8");
91
+ }
92
+ return {
93
+ pageCount: documents.length,
94
+ totalSize,
95
+ outputPath,
96
+ };
97
+ }
98
+ /**
99
+ * Format bytes into a human-readable string.
100
+ */
101
+ export function formatSize(bytes) {
102
+ if (bytes < 1024)
103
+ return `${bytes} B`;
104
+ if (bytes < 1024 * 1024)
105
+ return `${(bytes / 1024).toFixed(1)} KB`;
106
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
107
+ }
108
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder.js","sourceRoot":"","sources":["../../src/build/builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build command handler.
3
+ * Orchestrates the full static build flow: validate, discover, render, write output.
4
+ */
5
+ export interface BuildCommandOptions {
6
+ path: string;
7
+ output: string;
8
+ baseUrl?: string;
9
+ }
10
+ /**
11
+ * Execute the build command.
12
+ * Discovers files, renders them, builds navigation, writes static output.
13
+ */
14
+ export declare function build(options: BuildCommandOptions): Promise<void>;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Build command handler.
3
+ * Orchestrates the full static build flow: validate, discover, render, write output.
4
+ */
5
+ import { readFile } from "node:fs/promises";
6
+ import { resolve } from "node:path";
7
+ import { discoverFiles } from "../core/discovery.js";
8
+ import { generateSlug, generateSortKey, detectSlugCollisions, disambiguateSlugs, } from "../core/slug.js";
9
+ import { getTitle } from "../core/title.js";
10
+ import { extractFrontMatter } from "../render/pipeline.js";
11
+ import { buildNavigationTree } from "../core/navigation.js";
12
+ import { renderContent } from "../render/pipeline.js";
13
+ import { buildSite, formatSize } from "../build/builder.js";
14
+ import { getStylesheet } from "../theme/index.js";
15
+ import { getMermaidBundleSrc, MERMAID_INIT_SCRIPT, NAVIGATION_SCRIPT, buildSearchScript, } from "../theme/scripts.js";
16
+ /**
17
+ * Execute the build command.
18
+ * Discovers files, renders them, builds navigation, writes static output.
19
+ */
20
+ export async function build(options) {
21
+ const sourcePath = resolve(options.path);
22
+ // Step 1: Discover files
23
+ const documents = await discoverFiles(sourcePath);
24
+ // Step 2: Read content, generate slugs, extract titles
25
+ await processDocuments(documents);
26
+ // Step 3: Check for slug collisions
27
+ checkSlugCollisions(documents);
28
+ // Step 4: Build navigation tree
29
+ const navigationTree = buildNavigationTree(documents);
30
+ // Step 5: Render all pages to HTML
31
+ await renderDocuments(documents);
32
+ // Step 6: Get theme assets
33
+ const stylesheet = getStylesheet();
34
+ // Step 7: Build static output
35
+ const result = await buildSite({
36
+ documents,
37
+ navigationTree,
38
+ stylesheet,
39
+ outputDir: options.output,
40
+ mermaidBundleSrc: getMermaidBundleSrc("build"),
41
+ mermaidInitScript: MERMAID_INIT_SCRIPT,
42
+ navigationScript: NAVIGATION_SCRIPT,
43
+ searchScript: buildSearchScript("assets/search.json"),
44
+ baseUrl: options.baseUrl,
45
+ });
46
+ // Print summary
47
+ console.log(`\nmdmirror build complete`);
48
+ console.log(` Source: ${sourcePath}`);
49
+ console.log(` Output: ${result.outputPath}`);
50
+ console.log(` Pages: ${result.pageCount}`);
51
+ console.log(` Size: ${formatSize(result.totalSize)}\n`);
52
+ }
53
+ /**
54
+ * Read file content, generate slugs, extract titles, and set sort keys.
55
+ * Skips files that can't be read (e.g. non-UTF-8 encoding) with a warning.
56
+ */
57
+ async function processDocuments(documents) {
58
+ const failed = [];
59
+ await Promise.all(documents.map(async (doc) => {
60
+ try {
61
+ doc.content = await readFile(doc.absolutePath, "utf-8");
62
+ // Detect non-UTF-8 content (replacement character indicates encoding issues)
63
+ if (doc.content.includes("\uFFFD")) {
64
+ console.error(`Warning: Skipping ${doc.sourcePath} (appears to be non-UTF-8 encoded)`);
65
+ failed.push(doc);
66
+ return;
67
+ }
68
+ const { frontMatter } = extractFrontMatter(doc.content);
69
+ doc.slug = generateSlug(doc.sourcePath);
70
+ doc.title =
71
+ frontMatter.title ||
72
+ getTitle(doc.content, doc.sourcePath);
73
+ doc.description = frontMatter.description || "";
74
+ const filename = doc.sourcePath.split(/[/\\]/).pop() || doc.sourcePath;
75
+ doc.sortKey = generateSortKey(filename.replace(/\.[^.]+$/, ""));
76
+ }
77
+ catch (error) {
78
+ console.error(`Warning: Skipping ${doc.sourcePath} (${error instanceof Error ? error.message : "read error"})`);
79
+ failed.push(doc);
80
+ }
81
+ }));
82
+ // Remove failed documents from the array
83
+ for (const doc of failed) {
84
+ const index = documents.indexOf(doc);
85
+ if (index !== -1)
86
+ documents.splice(index, 1);
87
+ }
88
+ }
89
+ /**
90
+ * Check for slug collisions, warn on stderr, and disambiguate with suffixes.
91
+ */
92
+ function checkSlugCollisions(documents) {
93
+ const slugMap = new Map();
94
+ for (const doc of documents) {
95
+ const existing = slugMap.get(doc.slug) || [];
96
+ existing.push(doc.sourcePath);
97
+ slugMap.set(doc.slug, existing);
98
+ }
99
+ const collisions = detectSlugCollisions(slugMap);
100
+ for (const [slug, paths] of collisions) {
101
+ console.error(`Warning: Slug collision detected: "${slug}" maps to ${paths.join(" and ")} — disambiguating with suffixes`);
102
+ }
103
+ // Apply disambiguation suffixes to colliding slugs
104
+ if (collisions.size > 0) {
105
+ disambiguateSlugs(documents);
106
+ }
107
+ }
108
+ /**
109
+ * Render all document content to HTML.
110
+ */
111
+ async function renderDocuments(documents) {
112
+ await Promise.all(documents.map(async (doc) => {
113
+ doc.renderedHtml = await renderContent(doc.content, doc.fileType);
114
+ }));
115
+ }
116
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for mdmirror.
4
+ * Handles manual subcommand routing since citty runs parent + subcommand.
5
+ */
6
+ export {};
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for mdmirror.
4
+ * Handles manual subcommand routing since citty runs parent + subcommand.
5
+ */
6
+ import { defineCommand, runMain } from "citty";
7
+ import { serve } from "./serve.js";
8
+ import { build } from "./build.js";
9
+ // Detect if "build" subcommand is being invoked
10
+ const isBuildCommand = process.argv[2] === "build";
11
+ if (isBuildCommand) {
12
+ // Remove "build" from argv so citty parses the remaining args correctly
13
+ process.argv.splice(2, 1);
14
+ const buildCommand = defineCommand({
15
+ meta: {
16
+ name: "mdmirror build",
17
+ description: "Generate a self-contained static site for deployment",
18
+ },
19
+ args: {
20
+ path: {
21
+ type: "positional",
22
+ description: "Path to the source folder containing documentation",
23
+ required: true,
24
+ },
25
+ output: {
26
+ type: "string",
27
+ alias: "o",
28
+ description: "Output directory for the generated site",
29
+ default: "./dist",
30
+ },
31
+ "base-url": {
32
+ type: "string",
33
+ description: "Base URL for canonical links and sitemap (e.g. https://example.com)",
34
+ default: "",
35
+ },
36
+ },
37
+ async run({ args }) {
38
+ try {
39
+ await build({
40
+ path: args.path,
41
+ output: args.output,
42
+ baseUrl: args["base-url"] || undefined,
43
+ });
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error) {
47
+ console.error(`Error: ${error.message}`);
48
+ }
49
+ else {
50
+ console.error("An unexpected error occurred");
51
+ }
52
+ process.exit(1);
53
+ }
54
+ },
55
+ });
56
+ runMain(buildCommand);
57
+ }
58
+ else {
59
+ const main = defineCommand({
60
+ meta: {
61
+ name: "mdmirror",
62
+ version: "1.0.0",
63
+ description: "Zero-config CLI tool that turns any folder of markdown files into a browsable documentation site",
64
+ },
65
+ args: {
66
+ path: {
67
+ type: "positional",
68
+ description: "Path to the source folder containing documentation",
69
+ required: true,
70
+ },
71
+ port: {
72
+ type: "string",
73
+ alias: "p",
74
+ description: "Port to serve on",
75
+ default: process.env["PORT"] || "3000",
76
+ },
77
+ host: {
78
+ type: "string",
79
+ description: "Bind address for the server",
80
+ default: "localhost",
81
+ },
82
+ },
83
+ async run({ args }) {
84
+ try {
85
+ await serve({
86
+ path: args.path,
87
+ port: parseInt(args.port, 10),
88
+ host: args.host,
89
+ });
90
+ }
91
+ catch (error) {
92
+ if (error instanceof Error) {
93
+ console.error(`Error: ${error.message}`);
94
+ }
95
+ else {
96
+ console.error("An unexpected error occurred");
97
+ }
98
+ process.exit(1);
99
+ }
100
+ },
101
+ });
102
+ runMain(main);
103
+ }
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Serve command handler.
3
+ * Orchestrates the full serve flow: validate, discover, render, serve, watch, live reload.
4
+ */
5
+ export interface ServeOptions {
6
+ path: string;
7
+ port: number;
8
+ host: string;
9
+ }
10
+ /**
11
+ * Execute the serve command.
12
+ * Discovers files, renders them, builds navigation, starts the server,
13
+ * then watches for changes and triggers live reload.
14
+ */
15
+ export declare function serve(options: ServeOptions): Promise<void>;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Serve command handler.
3
+ * Orchestrates the full serve flow: validate, discover, render, serve, watch, live reload.
4
+ */
5
+ import { readFile } from "node:fs/promises";
6
+ import { resolve, relative } from "node:path";
7
+ import { discoverFiles } from "../core/discovery.js";
8
+ import { generateSlug, generateSortKey, detectSlugCollisions, disambiguateSlugs, } from "../core/slug.js";
9
+ import { getTitle } from "../core/title.js";
10
+ import { extractFrontMatter } from "../render/pipeline.js";
11
+ import { buildNavigationTree } from "../core/navigation.js";
12
+ import { renderContent } from "../render/pipeline.js";
13
+ import { renderLayout } from "../theme/layout.js";
14
+ import { startServer } from "../server/server.js";
15
+ import { createReloadServer, LIVE_RELOAD_CLIENT_SCRIPT } from "../server/reload.js";
16
+ import { startWatcher } from "../server/watcher.js";
17
+ import { getStylesheet } from "../theme/index.js";
18
+ import { getMermaidBundleSrc, MERMAID_INIT_SCRIPT, NAVIGATION_SCRIPT, buildSearchScript, } from "../theme/scripts.js";
19
+ import { buildSearchIndex } from "../search/index.js";
20
+ /**
21
+ * Execute the serve command.
22
+ * Discovers files, renders them, builds navigation, starts the server,
23
+ * then watches for changes and triggers live reload.
24
+ */
25
+ export async function serve(options) {
26
+ const sourcePath = resolve(options.path);
27
+ // Step 1: Discover files
28
+ let documents = await discoverFiles(sourcePath);
29
+ // Step 2: Read content, generate slugs, extract titles
30
+ await processDocuments(documents);
31
+ // Step 3: Check for slug collisions
32
+ checkSlugCollisions(documents);
33
+ // Step 4: Build navigation tree
34
+ let navigationTree = buildNavigationTree(documents);
35
+ // Step 5: Render all pages to HTML
36
+ await renderDocuments(documents);
37
+ // Step 6: Generate full HTML pages
38
+ const stylesheet = getStylesheet();
39
+ let pages = generatePages(documents, navigationTree, stylesheet, "serve");
40
+ // Step 7: Start the server
41
+ const config = {
42
+ port: options.port,
43
+ host: options.host,
44
+ sourcePath,
45
+ liveReload: true,
46
+ };
47
+ const site = {
48
+ pages,
49
+ sourcePath,
50
+ };
51
+ const { server, updatePages } = await startServer(config, site);
52
+ // Step 8: Set up live reload WebSocket
53
+ const reloadServer = createReloadServer(server);
54
+ // Step 9: Start file watcher
55
+ let rebuilding = false;
56
+ const watcher = startWatcher({
57
+ sourcePath,
58
+ debounceMs: 300,
59
+ onChange: async (events) => {
60
+ if (rebuilding)
61
+ return;
62
+ rebuilding = true;
63
+ try {
64
+ const hasStructuralChange = events.some((e) => e.type === "add" || e.type === "unlink");
65
+ if (hasStructuralChange) {
66
+ // Full rebuild: re-discover, re-process, re-render everything
67
+ documents = await discoverFiles(sourcePath);
68
+ await processDocuments(documents);
69
+ checkSlugCollisions(documents);
70
+ navigationTree = buildNavigationTree(documents);
71
+ await renderDocuments(documents);
72
+ pages = generatePages(documents, navigationTree, stylesheet, "serve");
73
+ updatePages(pages);
74
+ const addedFiles = events.filter((e) => e.type === "add").length;
75
+ const removedFiles = events.filter((e) => e.type === "unlink").length;
76
+ if (addedFiles > 0)
77
+ console.log(` + ${addedFiles} file(s) added`);
78
+ if (removedFiles > 0)
79
+ console.log(` - ${removedFiles} file(s) removed`);
80
+ console.log(` Pages: ${navigationTree.totalPages}`);
81
+ }
82
+ else {
83
+ // Incremental update: only re-render changed files
84
+ for (const event of events) {
85
+ if (event.type === "change") {
86
+ const relativePath = relative(sourcePath, event.path);
87
+ const doc = documents.find((d) => d.sourcePath === relativePath);
88
+ if (doc) {
89
+ doc.content = await readFile(doc.absolutePath, "utf-8");
90
+ const { frontMatter: fm } = extractFrontMatter(doc.content);
91
+ doc.title =
92
+ fm.title ||
93
+ getTitle(doc.content, doc.sourcePath);
94
+ doc.description = fm.description || "";
95
+ doc.renderedHtml = await renderContent(doc.content, doc.fileType);
96
+ // Re-render the changed page (and update nav if title changed)
97
+ navigationTree = buildNavigationTree(documents);
98
+ pages = generatePages(documents, navigationTree, stylesheet, "serve");
99
+ updatePages(pages);
100
+ console.log(` ~ ${relativePath} updated`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ // Signal browsers to reload
106
+ reloadServer.reload();
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error) {
110
+ console.error(` Error during reload: ${error.message}`);
111
+ }
112
+ }
113
+ finally {
114
+ rebuilding = false;
115
+ }
116
+ },
117
+ });
118
+ // Print startup message
119
+ console.log(`\nmdmirror serving at http://${options.host}:${options.port}`);
120
+ console.log(` Source: ${sourcePath}`);
121
+ console.log(` Pages: ${navigationTree.totalPages}`);
122
+ console.log(` Live reload: enabled`);
123
+ console.log(`\n Press Ctrl+C to stop\n`);
124
+ // Handle graceful shutdown
125
+ const shutdown = () => {
126
+ console.log("\nShutting down...");
127
+ reloadServer.close();
128
+ watcher.close();
129
+ server.close(() => process.exit(0));
130
+ // Force exit if server doesn't close promptly
131
+ setTimeout(() => process.exit(0), 2000).unref();
132
+ };
133
+ process.on("SIGINT", shutdown);
134
+ process.on("SIGTERM", shutdown);
135
+ }
136
+ /**
137
+ * Read file content, generate slugs, extract titles, and set sort keys.
138
+ * Skips files that can't be read (e.g. non-UTF-8 encoding) with a warning.
139
+ */
140
+ async function processDocuments(documents) {
141
+ const failed = [];
142
+ await Promise.all(documents.map(async (doc) => {
143
+ try {
144
+ // Read content
145
+ doc.content = await readFile(doc.absolutePath, "utf-8");
146
+ // Detect non-UTF-8 content (replacement character indicates encoding issues)
147
+ if (doc.content.includes("\uFFFD")) {
148
+ console.error(`Warning: Skipping ${doc.sourcePath} (appears to be non-UTF-8 encoded)`);
149
+ failed.push(doc);
150
+ return;
151
+ }
152
+ // Extract front matter (title/description overrides)
153
+ const { frontMatter } = extractFrontMatter(doc.content);
154
+ // Generate slug
155
+ doc.slug = generateSlug(doc.sourcePath);
156
+ // Extract title (front matter wins, then heading, then humanized filename)
157
+ doc.title =
158
+ frontMatter.title ||
159
+ getTitle(doc.content, doc.sourcePath);
160
+ doc.description = frontMatter.description || "";
161
+ // Generate sort key from filename
162
+ const filename = doc.sourcePath.split(/[/\\]/).pop() || doc.sourcePath;
163
+ doc.sortKey = generateSortKey(filename.replace(/\.[^.]+$/, ""));
164
+ }
165
+ catch (error) {
166
+ console.error(`Warning: Skipping ${doc.sourcePath} (${error instanceof Error ? error.message : "read error"})`);
167
+ failed.push(doc);
168
+ }
169
+ }));
170
+ // Remove failed documents from the array
171
+ for (const doc of failed) {
172
+ const index = documents.indexOf(doc);
173
+ if (index !== -1)
174
+ documents.splice(index, 1);
175
+ }
176
+ }
177
+ /**
178
+ * Check for slug collisions, warn on stderr, and disambiguate with suffixes.
179
+ */
180
+ function checkSlugCollisions(documents) {
181
+ const slugMap = new Map();
182
+ for (const doc of documents) {
183
+ const existing = slugMap.get(doc.slug) || [];
184
+ existing.push(doc.sourcePath);
185
+ slugMap.set(doc.slug, existing);
186
+ }
187
+ const collisions = detectSlugCollisions(slugMap);
188
+ for (const [slug, paths] of collisions) {
189
+ console.error(`Warning: Slug collision detected: "${slug}" maps to ${paths.join(" and ")} — disambiguating with suffixes`);
190
+ }
191
+ // Apply disambiguation suffixes to colliding slugs
192
+ if (collisions.size > 0) {
193
+ disambiguateSlugs(documents);
194
+ }
195
+ }
196
+ /**
197
+ * Render all document content to HTML.
198
+ */
199
+ async function renderDocuments(documents) {
200
+ await Promise.all(documents.map(async (doc) => {
201
+ doc.renderedHtml = await renderContent(doc.content, doc.fileType);
202
+ }));
203
+ }
204
+ /**
205
+ * Generate full HTML pages for all documents.
206
+ * Returns a map of slug -> full HTML page.
207
+ */
208
+ function generatePages(documents, navigationTree, stylesheet, mode) {
209
+ const pages = new Map();
210
+ const mermaidBundleSrc = getMermaidBundleSrc(mode);
211
+ for (const doc of documents) {
212
+ const html = renderLayout({
213
+ title: doc.title,
214
+ description: doc.description,
215
+ content: doc.renderedHtml,
216
+ navigation: navigationTree,
217
+ currentSlug: doc.slug,
218
+ stylesheet,
219
+ mode,
220
+ liveReloadScript: mode === "serve" ? LIVE_RELOAD_CLIENT_SCRIPT : undefined,
221
+ mermaidBundleSrc,
222
+ mermaidInitScript: MERMAID_INIT_SCRIPT,
223
+ navigationScript: NAVIGATION_SCRIPT,
224
+ searchScript: buildSearchScript("/assets/search.json"),
225
+ });
226
+ pages.set(doc.slug, html);
227
+ }
228
+ // Also expose the search index at /assets/search.json
229
+ const searchIndex = buildSearchIndex(documents);
230
+ pages.set("assets/search.json", JSON.stringify(searchIndex));
231
+ return pages;
232
+ }
233
+ //# sourceMappingURL=serve.js.map