@ox-content/vite-plugin 0.0.1-alpha.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/dist/index.js ADDED
@@ -0,0 +1,2468 @@
1
+ // src/index.ts
2
+ import * as path5 from "path";
3
+
4
+ // src/environment.ts
5
+ function createMarkdownEnvironment(options) {
6
+ return {
7
+ // Consumer type for this environment
8
+ consumer: "server",
9
+ // Build configuration
10
+ build: {
11
+ // Output to a separate directory
12
+ outDir: `${options.outDir}/.markdown`,
13
+ // Emit assets for SSG
14
+ emitAssets: true,
15
+ // Create manifest for asset tracking
16
+ manifest: true,
17
+ // SSR-like externalization
18
+ rollupOptions: {
19
+ external: [
20
+ // Externalize Node.js built-ins
21
+ /^node:/,
22
+ // Externalize native modules
23
+ /\.node$/
24
+ ]
25
+ }
26
+ },
27
+ // Resolve configuration
28
+ resolve: {
29
+ // Handle .md files
30
+ extensions: [".md", ".markdown"],
31
+ // Conditions for module resolution
32
+ conditions: ["markdown", "node", "import"],
33
+ // Don't dedupe - each environment gets its own modules
34
+ dedupe: []
35
+ },
36
+ // Optimize dependencies
37
+ optimizeDeps: {
38
+ // Include ox-content dependencies
39
+ include: [],
40
+ // Exclude native modules
41
+ exclude: ["@ox-content/napi"]
42
+ }
43
+ };
44
+ }
45
+
46
+ // src/highlight.ts
47
+ import { unified } from "unified";
48
+ import rehypeParse from "rehype-parse";
49
+ import rehypeStringify from "rehype-stringify";
50
+ import { createHighlighter } from "shiki";
51
+ var highlighterPromise = null;
52
+ async function getHighlighter(theme) {
53
+ if (!highlighterPromise) {
54
+ highlighterPromise = createHighlighter({
55
+ themes: [theme],
56
+ langs: [
57
+ "javascript",
58
+ "typescript",
59
+ "jsx",
60
+ "tsx",
61
+ "vue",
62
+ "svelte",
63
+ "html",
64
+ "css",
65
+ "scss",
66
+ "json",
67
+ "yaml",
68
+ "markdown",
69
+ "bash",
70
+ "shell",
71
+ "rust",
72
+ "python",
73
+ "go",
74
+ "java",
75
+ "c",
76
+ "cpp",
77
+ "sql",
78
+ "graphql",
79
+ "diff",
80
+ "toml"
81
+ ]
82
+ });
83
+ }
84
+ return highlighterPromise;
85
+ }
86
+ function rehypeShikiHighlight(options) {
87
+ const { theme } = options;
88
+ return async (tree) => {
89
+ const highlighter = await getHighlighter(theme);
90
+ const visit = async (node) => {
91
+ if ("children" in node) {
92
+ for (let i = 0; i < node.children.length; i++) {
93
+ const child = node.children[i];
94
+ if (child.type === "element" && child.tagName === "pre") {
95
+ const codeElement = child.children.find(
96
+ (c) => c.type === "element" && c.tagName === "code"
97
+ );
98
+ if (codeElement) {
99
+ const className = codeElement.properties?.className;
100
+ let lang = "text";
101
+ if (Array.isArray(className)) {
102
+ const langClass = className.find(
103
+ (c) => typeof c === "string" && c.startsWith("language-")
104
+ );
105
+ if (langClass && typeof langClass === "string") {
106
+ lang = langClass.replace("language-", "");
107
+ }
108
+ }
109
+ const codeText = getTextContent(codeElement);
110
+ try {
111
+ const highlighted = highlighter.codeToHtml(codeText, {
112
+ lang,
113
+ theme
114
+ });
115
+ const parsed = unified().use(rehypeParse, { fragment: true }).parse(highlighted);
116
+ if (parsed.children[0]) {
117
+ node.children[i] = parsed.children[0];
118
+ }
119
+ } catch {
120
+ }
121
+ }
122
+ } else if (child.type === "element") {
123
+ await visit(child);
124
+ }
125
+ }
126
+ }
127
+ };
128
+ await visit(tree);
129
+ };
130
+ }
131
+ function getTextContent(node) {
132
+ let text = "";
133
+ if ("children" in node) {
134
+ for (const child of node.children) {
135
+ if (child.type === "text") {
136
+ text += child.value;
137
+ } else if (child.type === "element") {
138
+ text += getTextContent(child);
139
+ }
140
+ }
141
+ }
142
+ return text;
143
+ }
144
+ async function highlightCode(html, theme = "github-dark") {
145
+ const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeShikiHighlight, { theme }).use(rehypeStringify).process(html);
146
+ return String(result);
147
+ }
148
+
149
+ // src/mermaid.ts
150
+ import { unified as unified2 } from "unified";
151
+ import rehypeParse2 from "rehype-parse";
152
+ import rehypeStringify2 from "rehype-stringify";
153
+ function getTextContent2(node) {
154
+ let text = "";
155
+ if ("children" in node) {
156
+ for (const child of node.children) {
157
+ if (child.type === "text") {
158
+ text += child.value;
159
+ } else if (child.type === "element") {
160
+ text += getTextContent2(child);
161
+ }
162
+ }
163
+ }
164
+ return text;
165
+ }
166
+ function rehypeMermaid() {
167
+ return (tree) => {
168
+ const visit = (node) => {
169
+ if ("children" in node) {
170
+ for (let i = 0; i < node.children.length; i++) {
171
+ const child = node.children[i];
172
+ if (child.type === "element" && child.tagName === "pre") {
173
+ const codeElement = child.children.find(
174
+ (c) => c.type === "element" && c.tagName === "code"
175
+ );
176
+ if (codeElement) {
177
+ const className = codeElement.properties?.className;
178
+ let isMermaid = false;
179
+ if (Array.isArray(className)) {
180
+ isMermaid = className.some(
181
+ (c) => typeof c === "string" && c.includes("mermaid")
182
+ );
183
+ }
184
+ if (isMermaid) {
185
+ const mermaidCode = getTextContent2(codeElement);
186
+ const wrapper = {
187
+ type: "element",
188
+ tagName: "div",
189
+ properties: {
190
+ className: ["ox-mermaid"],
191
+ "data-mermaid": mermaidCode
192
+ },
193
+ children: [
194
+ {
195
+ type: "element",
196
+ tagName: "pre",
197
+ properties: {
198
+ className: ["ox-mermaid-source"]
199
+ },
200
+ children: [
201
+ {
202
+ type: "text",
203
+ value: mermaidCode
204
+ }
205
+ ]
206
+ }
207
+ ]
208
+ };
209
+ node.children[i] = wrapper;
210
+ }
211
+ }
212
+ } else if (child.type === "element") {
213
+ visit(child);
214
+ }
215
+ }
216
+ }
217
+ };
218
+ visit(tree);
219
+ };
220
+ }
221
+ async function transformMermaid(html) {
222
+ const result = await unified2().use(rehypeParse2, { fragment: true }).use(rehypeMermaid).use(rehypeStringify2).process(html);
223
+ return String(result);
224
+ }
225
+
226
+ // src/transform.ts
227
+ var napiBindings;
228
+ var napiLoadAttempted = false;
229
+ async function loadNapiBindings() {
230
+ if (napiLoadAttempted) {
231
+ return napiBindings ?? null;
232
+ }
233
+ napiLoadAttempted = true;
234
+ try {
235
+ const mod = await import("@ox-content/napi");
236
+ napiBindings = mod;
237
+ return mod;
238
+ } catch (error) {
239
+ if (process.env.DEBUG) {
240
+ console.debug("[ox-content] NAPI bindings load failed:", error);
241
+ }
242
+ napiBindings = null;
243
+ return null;
244
+ }
245
+ }
246
+ async function transformMarkdown(source, filePath, options, ssgOptions) {
247
+ const napi = await loadNapiBindings();
248
+ if (!napi) {
249
+ throw new Error("[ox-content] NAPI bindings not available. Please ensure @ox-content/napi is built.");
250
+ }
251
+ const result = napi.transform(source, {
252
+ gfm: options.gfm,
253
+ footnotes: options.footnotes,
254
+ taskLists: options.taskLists,
255
+ tables: options.tables,
256
+ strikethrough: options.strikethrough,
257
+ tocMaxDepth: options.tocMaxDepth,
258
+ convertMdLinks: ssgOptions?.convertMdLinks,
259
+ baseUrl: ssgOptions?.baseUrl
260
+ });
261
+ if (result.errors.length > 0) {
262
+ console.warn("[ox-content] Transform warnings:", result.errors);
263
+ }
264
+ let html = result.html;
265
+ let frontmatter;
266
+ try {
267
+ frontmatter = JSON.parse(result.frontmatter);
268
+ } catch {
269
+ frontmatter = {};
270
+ }
271
+ const flatToc = result.toc.map((item) => ({
272
+ ...item,
273
+ children: []
274
+ }));
275
+ const toc = options.toc ? buildTocTree(flatToc) : [];
276
+ if (options.highlight) {
277
+ html = await highlightCode(html, options.highlightTheme);
278
+ }
279
+ if (options.mermaid) {
280
+ html = await transformMermaid(html);
281
+ }
282
+ const code = generateModuleCode(html, frontmatter, toc, filePath, options);
283
+ return {
284
+ code,
285
+ html,
286
+ frontmatter,
287
+ toc
288
+ };
289
+ }
290
+ function buildTocTree(entries) {
291
+ const root = [];
292
+ const stack = [];
293
+ for (const entry of entries) {
294
+ while (stack.length > 0 && stack[stack.length - 1].depth >= entry.depth) {
295
+ stack.pop();
296
+ }
297
+ if (stack.length === 0) {
298
+ root.push(entry);
299
+ } else {
300
+ stack[stack.length - 1].children.push(entry);
301
+ }
302
+ stack.push(entry);
303
+ }
304
+ return root;
305
+ }
306
+ function generateModuleCode(html, frontmatter, toc, filePath, _options) {
307
+ const htmlJson = JSON.stringify(html);
308
+ const frontmatterJson = JSON.stringify(frontmatter);
309
+ const tocJson = JSON.stringify(toc);
310
+ return `
311
+ // Generated by vite-plugin-ox-content
312
+ // Source: ${filePath}
313
+
314
+ /**
315
+ * Rendered HTML content.
316
+ */
317
+ export const html = ${htmlJson};
318
+
319
+ /**
320
+ * Parsed frontmatter.
321
+ */
322
+ export const frontmatter = ${frontmatterJson};
323
+
324
+ /**
325
+ * Table of contents.
326
+ */
327
+ export const toc = ${tocJson};
328
+
329
+ /**
330
+ * Default export with all data.
331
+ */
332
+ export default {
333
+ html,
334
+ frontmatter,
335
+ toc,
336
+ };
337
+
338
+ // HMR support
339
+ if (import.meta.hot) {
340
+ import.meta.hot.accept((newModule) => {
341
+ if (newModule) {
342
+ // Trigger re-render with new content
343
+ import.meta.hot.invalidate();
344
+ }
345
+ });
346
+ }
347
+ `;
348
+ }
349
+ async function generateOgImageSvg(data, config) {
350
+ const napi = await loadNapiBindings();
351
+ if (!napi) {
352
+ return null;
353
+ }
354
+ const napiConfig = config ? {
355
+ width: config.width,
356
+ height: config.height,
357
+ backgroundColor: config.backgroundColor,
358
+ textColor: config.textColor,
359
+ titleFontSize: config.titleFontSize,
360
+ descriptionFontSize: config.descriptionFontSize
361
+ } : void 0;
362
+ return napi.generateOgImageSvg(data, napiConfig);
363
+ }
364
+
365
+ // src/docs.ts
366
+ import * as fs from "fs";
367
+ import * as path2 from "path";
368
+
369
+ // src/nav-generator.ts
370
+ import path from "path";
371
+ function generateNavMetadata(docs, basePath = "/api") {
372
+ const sortedDocs = [...docs].sort((a, b) => {
373
+ const aName = getDocDisplayName(a.file);
374
+ const bName = getDocDisplayName(b.file);
375
+ return aName.localeCompare(bName);
376
+ });
377
+ return sortedDocs.map((doc) => ({
378
+ title: getDocDisplayName(doc.file),
379
+ path: `${basePath}/${getDocFileName(doc.file)}`
380
+ }));
381
+ }
382
+ function getDocDisplayName(filePath) {
383
+ const fileName = path.basename(filePath, path.extname(filePath));
384
+ if (fileName === "index" || fileName === "index-module") {
385
+ return "Overview";
386
+ }
387
+ return fileName.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
388
+ }
389
+ function getDocFileName(filePath) {
390
+ const fileName = path.basename(filePath, path.extname(filePath));
391
+ if (fileName === "index") {
392
+ return "index";
393
+ }
394
+ return fileName;
395
+ }
396
+ function generateNavCode(navItems, exportName = "apiNav") {
397
+ const json = JSON.stringify(navItems, null, 2);
398
+ return `/**
399
+ * Auto-generated API documentation navigation.
400
+ * This file is automatically generated by the docs plugin.
401
+ * Do not edit manually.
402
+ */
403
+
404
+ export interface NavItem {
405
+ title: string;
406
+ path: string;
407
+ children?: NavItem[];
408
+ }
409
+
410
+ export const ${exportName}: NavItem[] = ${json} as const;
411
+ `;
412
+ }
413
+
414
+ // src/docs.ts
415
+ var JSDOC_BLOCK = /^[ \t]*\/\*\*\s*([\s\S]*?)\s*\*\//gm;
416
+ async function extractDocs(srcDirs, options) {
417
+ const results = [];
418
+ for (const srcDir of srcDirs) {
419
+ const files = await findFiles(srcDir, options);
420
+ for (const file of files) {
421
+ const content = await fs.promises.readFile(file, "utf-8");
422
+ const entries = extractFromContent(content, file, options);
423
+ if (entries.length > 0) {
424
+ results.push({ file, entries });
425
+ }
426
+ }
427
+ }
428
+ return results;
429
+ }
430
+ async function findFiles(dir, options) {
431
+ const files = [];
432
+ async function walk(currentDir) {
433
+ let entries;
434
+ try {
435
+ entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
436
+ } catch {
437
+ return;
438
+ }
439
+ for (const entry of entries) {
440
+ const fullPath = path2.join(currentDir, entry.name);
441
+ if (entry.isDirectory()) {
442
+ if (!isExcluded(fullPath, options.exclude)) {
443
+ await walk(fullPath);
444
+ }
445
+ } else if (entry.isFile()) {
446
+ if (isIncluded(fullPath, options.include) && !isExcluded(fullPath, options.exclude)) {
447
+ files.push(fullPath);
448
+ }
449
+ }
450
+ }
451
+ }
452
+ await walk(dir);
453
+ return files;
454
+ }
455
+ function isIncluded(file, patterns) {
456
+ return patterns.some((pattern) => {
457
+ if (pattern.includes("**")) {
458
+ const ext = pattern.split(".").pop();
459
+ return file.endsWith(`.${ext}`);
460
+ }
461
+ return file.endsWith(pattern.replace("*", ""));
462
+ });
463
+ }
464
+ function isExcluded(file, patterns) {
465
+ return patterns.some((pattern) => {
466
+ if (pattern.includes("node_modules")) {
467
+ return file.includes("node_modules");
468
+ }
469
+ if (pattern.includes(".test.") || pattern.includes(".spec.")) {
470
+ return file.includes(".test.") || file.includes(".spec.");
471
+ }
472
+ return false;
473
+ });
474
+ }
475
+ function extractFromContent(content, file, options) {
476
+ const entries = [];
477
+ let match;
478
+ JSDOC_BLOCK.lastIndex = 0;
479
+ while ((match = JSDOC_BLOCK.exec(content)) !== null) {
480
+ const jsdocContent = match[1];
481
+ const jsdocEnd = match.index + match[0].length;
482
+ const afterJsdoc = content.slice(jsdocEnd).trim();
483
+ const lineNumber = content.slice(0, match.index).split("\n").length;
484
+ const entry = parseJsdocBlock(jsdocContent, afterJsdoc, file, lineNumber);
485
+ if (entry && (options.private || !entry.private)) {
486
+ entries.push(entry);
487
+ }
488
+ }
489
+ return entries;
490
+ }
491
+ function extractFunctionSignature(signature) {
492
+ const match = signature.match(
493
+ /(?:export\s+)?(?:async\s+)?(?:function\s+\w+|\w+\s*=\s*(?:async\s*)?\()\([^{]*?\)(?:\s*:\s*[^{;]+)?/s
494
+ );
495
+ if (match) {
496
+ let sig = match[0].trim();
497
+ sig = sig.split("\n").map((line) => line.trim()).filter((line) => line).join("\n ");
498
+ return sig;
499
+ }
500
+ return void 0;
501
+ }
502
+ function extractTypesFromSignature(signature, params) {
503
+ const paramTypes = [];
504
+ const paramListMatch = signature.match(/\(([^)]*)\)(?:\s*:\s*([^{=>]+))?/s);
505
+ if (paramListMatch && paramListMatch[1]) {
506
+ const paramListStr = paramListMatch[1];
507
+ const paramParts = splitParameters(paramListStr);
508
+ for (const part of paramParts) {
509
+ const trimmed = part.trim();
510
+ if (!trimmed) continue;
511
+ const typeMatch = /:\s*(.+?)(?:\s*=|$)/.exec(trimmed);
512
+ if (typeMatch) {
513
+ let typeStr = typeMatch[1].trim();
514
+ if (typeStr.includes("=")) {
515
+ typeStr = typeStr.split("=")[0].trim();
516
+ }
517
+ paramTypes.push(typeStr);
518
+ }
519
+ }
520
+ }
521
+ let returnType;
522
+ const returnTypeMatch = signature.match(/\)\s*:\s*(.+?)(?={|$)/);
523
+ if (returnTypeMatch) {
524
+ returnType = returnTypeMatch[1].trim();
525
+ }
526
+ return {
527
+ paramTypes,
528
+ returnType
529
+ };
530
+ }
531
+ function splitParameters(paramListStr) {
532
+ const parts = [];
533
+ let current = "";
534
+ let depth = 0;
535
+ for (const char of paramListStr) {
536
+ if (char === "<") {
537
+ depth++;
538
+ current += char;
539
+ } else if (char === ">") {
540
+ depth--;
541
+ current += char;
542
+ } else if (char === "," && depth === 0) {
543
+ parts.push(current);
544
+ current = "";
545
+ } else {
546
+ current += char;
547
+ }
548
+ }
549
+ if (current) {
550
+ parts.push(current);
551
+ }
552
+ return parts;
553
+ }
554
+ function parseJsdocBlock(jsdoc, declaration, file, line) {
555
+ const params = [];
556
+ const examples = [];
557
+ const tags = {};
558
+ let description = "";
559
+ let returns;
560
+ let isPrivate = false;
561
+ const rawLines = jsdoc.split("\n").map((l) => l.replace(/^\s*\*\s?/, ""));
562
+ const cleanedLines = rawLines.map((l) => l.trim()).filter((l) => l);
563
+ let currentExample = "";
564
+ let inExample = false;
565
+ let rawLineIndex = 0;
566
+ for (const lineText of cleanedLines) {
567
+ while (rawLineIndex < rawLines.length && rawLines[rawLineIndex].trim() !== lineText) {
568
+ rawLineIndex++;
569
+ }
570
+ const rawLine = rawLineIndex < rawLines.length ? rawLines[rawLineIndex] : lineText;
571
+ rawLineIndex++;
572
+ if (lineText.startsWith("@")) {
573
+ if (inExample) {
574
+ examples.push(currentExample.trim());
575
+ currentExample = "";
576
+ inExample = false;
577
+ }
578
+ const tagMatch = /@(\w+)\s*(?:\{([^}]*)\})?(.*)/.exec(lineText);
579
+ if (tagMatch) {
580
+ const [, tagName, tagType, tagRest] = tagMatch;
581
+ switch (tagName) {
582
+ case "param":
583
+ const paramMatch = /(\w+)\s*-?\s*(.*)/.exec(tagRest.trim());
584
+ if (paramMatch) {
585
+ params.push({
586
+ name: paramMatch[1],
587
+ type: tagType || "unknown",
588
+ description: paramMatch[2]
589
+ });
590
+ }
591
+ break;
592
+ case "returns":
593
+ case "return":
594
+ returns = {
595
+ type: tagType || "unknown",
596
+ description: tagRest.trim()
597
+ };
598
+ break;
599
+ case "example":
600
+ inExample = true;
601
+ break;
602
+ case "private":
603
+ isPrivate = true;
604
+ break;
605
+ default:
606
+ tags[tagName] = tagRest.trim();
607
+ }
608
+ }
609
+ } else if (inExample) {
610
+ currentExample += rawLine + "\n";
611
+ } else if (!description) {
612
+ description = lineText;
613
+ } else {
614
+ description += "\n" + lineText;
615
+ }
616
+ }
617
+ if (inExample && currentExample) {
618
+ examples.push(currentExample.trim());
619
+ }
620
+ const firstFewLines = declaration.split("\n").slice(0, 5).join("\n");
621
+ let name = "";
622
+ let kind = "function";
623
+ const ANCHORED_FUNCTION = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
624
+ const ANCHORED_CONST_FUNC = /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/;
625
+ const ANCHORED_CLASS = /^(?:export\s+)?class\s+(\w+)/;
626
+ const ANCHORED_INTERFACE = /^(?:export\s+)?interface\s+(\w+)/;
627
+ const ANCHORED_TYPE = /^(?:export\s+)?type\s+(\w+)/;
628
+ let declMatch;
629
+ if (declMatch = ANCHORED_FUNCTION.exec(firstFewLines)) {
630
+ name = declMatch[1];
631
+ kind = "function";
632
+ } else if (declMatch = ANCHORED_CONST_FUNC.exec(firstFewLines)) {
633
+ name = declMatch[1];
634
+ kind = "function";
635
+ } else if (declMatch = ANCHORED_CLASS.exec(firstFewLines)) {
636
+ name = declMatch[1];
637
+ kind = "class";
638
+ } else if (declMatch = ANCHORED_INTERFACE.exec(firstFewLines)) {
639
+ name = declMatch[1];
640
+ kind = "interface";
641
+ } else if (declMatch = ANCHORED_TYPE.exec(firstFewLines)) {
642
+ name = declMatch[1];
643
+ kind = "type";
644
+ }
645
+ if (!name) return null;
646
+ let signature;
647
+ if (kind === "function") {
648
+ const signatureTypes = extractTypesFromSignature(firstFewLines, params);
649
+ if (signatureTypes.paramTypes.length > 0) {
650
+ for (let i = 0; i < params.length && i < signatureTypes.paramTypes.length; i++) {
651
+ if (params[i].type === "unknown") {
652
+ params[i].type = signatureTypes.paramTypes[i];
653
+ }
654
+ }
655
+ }
656
+ if (signatureTypes.returnType && (!returns || returns.type === "unknown")) {
657
+ if (returns) {
658
+ returns.type = signatureTypes.returnType;
659
+ } else {
660
+ returns = {
661
+ type: signatureTypes.returnType,
662
+ description: ""
663
+ };
664
+ }
665
+ }
666
+ signature = extractFunctionSignature(firstFewLines);
667
+ }
668
+ return {
669
+ name,
670
+ kind,
671
+ description,
672
+ params: params.length > 0 ? params : void 0,
673
+ returns,
674
+ examples: examples.length > 0 ? examples : void 0,
675
+ tags: Object.keys(tags).length > 0 ? tags : void 0,
676
+ private: isPrivate,
677
+ file,
678
+ line,
679
+ signature
680
+ };
681
+ }
682
+ function generateMarkdown(docs, options) {
683
+ const result = {};
684
+ const symbolMap = buildSymbolMap(docs);
685
+ if (options.groupBy === "file") {
686
+ const docToFile = /* @__PURE__ */ new Map();
687
+ for (const doc of docs) {
688
+ let fileName = path2.basename(doc.file, path2.extname(doc.file));
689
+ if (fileName === "index") {
690
+ fileName = "index-module";
691
+ }
692
+ docToFile.set(doc, fileName);
693
+ const markdown = generateFileMarkdown(doc, options, fileName, symbolMap);
694
+ result[`${fileName}.md`] = markdown;
695
+ }
696
+ result["index.md"] = generateIndex(docs, docToFile);
697
+ } else {
698
+ const byKind = /* @__PURE__ */ new Map();
699
+ for (const doc of docs) {
700
+ for (const entry of doc.entries) {
701
+ const existing = byKind.get(entry.kind) || [];
702
+ existing.push(entry);
703
+ byKind.set(entry.kind, existing);
704
+ }
705
+ }
706
+ for (const [kind, entries] of byKind) {
707
+ result[`${kind}s.md`] = generateCategoryMarkdown(kind, entries, options, symbolMap);
708
+ }
709
+ result["index.md"] = generateCategoryIndex(byKind);
710
+ }
711
+ return result;
712
+ }
713
+ function generateFileMarkdown(doc, options, currentFileName, symbolMap) {
714
+ const displayName = path2.basename(doc.file);
715
+ let md = `# ${displayName}
716
+
717
+ `;
718
+ if (options.githubUrl) {
719
+ const sourceLink = generateSourceLink(doc.file, options.githubUrl);
720
+ if (sourceLink) {
721
+ md += sourceLink + "\n\n";
722
+ }
723
+ }
724
+ for (const entry of doc.entries) {
725
+ md += generateEntryMarkdown(entry, options, currentFileName, symbolMap);
726
+ }
727
+ return md;
728
+ }
729
+ function generateEntryMarkdown(entry, options, currentFileName, symbolMap) {
730
+ let md = `## ${entry.name}
731
+
732
+ `;
733
+ md += `\`${entry.kind}\`
734
+
735
+ `;
736
+ if (entry.description) {
737
+ const processedDescription = currentFileName && symbolMap ? convertSymbolLinks(entry.description, currentFileName, symbolMap) : entry.description;
738
+ md += `${processedDescription}
739
+
740
+ `;
741
+ }
742
+ if (options?.githubUrl) {
743
+ const sourceLink = generateSourceLink(entry.file, options.githubUrl, entry.line);
744
+ if (sourceLink) {
745
+ md += sourceLink + "\n\n";
746
+ }
747
+ }
748
+ if (entry.signature && entry.kind === "function") {
749
+ md += "```typescript\n";
750
+ md += entry.signature + "\n";
751
+ md += "```\n\n";
752
+ }
753
+ if (entry.params && entry.params.length > 0) {
754
+ md += "### Parameters\n\n";
755
+ md += "| Name | Type | Description |\n";
756
+ md += "|------|------|-------------|\n";
757
+ for (const param of entry.params) {
758
+ md += `| \`${param.name}\` | \`${param.type}\` | ${param.description} |
759
+ `;
760
+ }
761
+ md += "\n";
762
+ }
763
+ if (entry.returns) {
764
+ md += "### Returns\n\n";
765
+ md += `\`${entry.returns.type}\` - ${entry.returns.description}
766
+
767
+ `;
768
+ }
769
+ if (entry.examples && entry.examples.length > 0) {
770
+ md += "### Examples\n\n";
771
+ for (const example of entry.examples) {
772
+ md += "```ts\n";
773
+ md += example.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
774
+ md += "\n```\n\n";
775
+ }
776
+ }
777
+ md += "---\n\n";
778
+ return md;
779
+ }
780
+ function generateIndex(docs, docToFile) {
781
+ let md = "# API Documentation\n\n";
782
+ md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
783
+ md += "## Modules\n\n";
784
+ for (const doc of docs) {
785
+ const displayName = path2.basename(doc.file, path2.extname(doc.file));
786
+ let fileName = displayName;
787
+ if (docToFile && docToFile.has(doc)) {
788
+ fileName = docToFile.get(doc);
789
+ } else if (fileName === "index") {
790
+ fileName = "index-module";
791
+ }
792
+ md += `### [${displayName}](./${fileName}.md)
793
+
794
+ `;
795
+ for (const entry of doc.entries) {
796
+ const desc = entry.description?.slice(0, 80) || "";
797
+ const ellipsis = entry.description && entry.description.length > 80 ? "..." : "";
798
+ md += `- \`${entry.kind}\` **${entry.name}** - ${desc}${ellipsis}
799
+ `;
800
+ }
801
+ md += "\n";
802
+ }
803
+ return md;
804
+ }
805
+ function generateCategoryMarkdown(kind, entries, options, symbolMap) {
806
+ const categoryFileName = `${kind}s`;
807
+ let md = `# ${kind.charAt(0).toUpperCase() + kind.slice(1)}s
808
+
809
+ `;
810
+ for (const entry of entries) {
811
+ md += generateEntryMarkdown(entry, options, categoryFileName, symbolMap);
812
+ }
813
+ return md;
814
+ }
815
+ function generateCategoryIndex(byKind) {
816
+ let md = "# API Documentation\n\n";
817
+ md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
818
+ for (const [kind, entries] of byKind) {
819
+ const kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1) + "s";
820
+ md += `## [${kindTitle}](./${kind}s.md)
821
+
822
+ `;
823
+ for (const entry of entries) {
824
+ const desc = entry.description?.slice(0, 60) || "";
825
+ md += `- **${entry.name}** - ${desc}...
826
+ `;
827
+ }
828
+ md += "\n";
829
+ }
830
+ return md;
831
+ }
832
+ function convertSymbolLinks(text, currentFileName, symbolMap) {
833
+ return text.replace(/\[([A-Z_]\w*)\](?!\()/g, (match, symbolName) => {
834
+ const location = symbolMap.get(symbolName);
835
+ if (!location) {
836
+ return match;
837
+ }
838
+ if (location.fileName === currentFileName) {
839
+ return `[${symbolName}](#${symbolName.toLowerCase()})`;
840
+ } else {
841
+ return `[${symbolName}](./${location.fileName}.md#${symbolName.toLowerCase()})`;
842
+ }
843
+ });
844
+ }
845
+ function buildSymbolMap(docs) {
846
+ const map = /* @__PURE__ */ new Map();
847
+ for (const doc of docs) {
848
+ let fileName = path2.basename(doc.file, path2.extname(doc.file));
849
+ if (fileName === "index") {
850
+ fileName = "index-module";
851
+ }
852
+ for (const entry of doc.entries) {
853
+ map.set(entry.name, {
854
+ name: entry.name,
855
+ file: doc.file,
856
+ fileName
857
+ });
858
+ }
859
+ }
860
+ return map;
861
+ }
862
+ async function writeDocs(docs, outDir, extractedDocs, options) {
863
+ await fs.promises.mkdir(outDir, { recursive: true });
864
+ for (const [fileName, content] of Object.entries(docs)) {
865
+ const filePath = path2.join(outDir, fileName);
866
+ await fs.promises.writeFile(filePath, content, "utf-8");
867
+ }
868
+ if (extractedDocs && options?.generateNav && options.groupBy === "file") {
869
+ const navItems = generateNavMetadata(extractedDocs, "/api");
870
+ const navCode = generateNavCode(navItems, "apiNav");
871
+ const navFilePath = path2.join(outDir, "nav.ts");
872
+ await fs.promises.writeFile(navFilePath, navCode, "utf-8");
873
+ }
874
+ }
875
+ function generateSourceLink(filePath, githubUrl, lineNumber) {
876
+ const relativePath = filePath.replace(/^.*?\/(npm|packages|crates|src)\//, "$1/");
877
+ const fragment = lineNumber ? `#L${lineNumber}` : "";
878
+ const link = `${githubUrl}/blob/main/${relativePath}${fragment}`;
879
+ return `**[Source](${link})**`;
880
+ }
881
+ function resolveDocsOptions(options) {
882
+ if (options === false) {
883
+ return false;
884
+ }
885
+ const opts = options || {};
886
+ return {
887
+ enabled: opts.enabled ?? true,
888
+ src: opts.src ?? ["./src"],
889
+ out: opts.out ?? "docs/api",
890
+ include: opts.include ?? ["**/*.ts", "**/*.tsx"],
891
+ exclude: opts.exclude ?? ["**/*.test.*", "**/*.spec.*", "node_modules"],
892
+ format: opts.format ?? "markdown",
893
+ private: opts.private ?? false,
894
+ toc: false,
895
+ groupBy: opts.groupBy ?? "file",
896
+ githubUrl: opts.githubUrl,
897
+ generateNav: opts.generateNav ?? true
898
+ };
899
+ }
900
+
901
+ // src/ssg.ts
902
+ import * as fs2 from "fs/promises";
903
+ import * as path3 from "path";
904
+ import { glob } from "glob";
905
+ var DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
906
+ <html lang="en">
907
+ <head>
908
+ <meta charset="UTF-8">
909
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
910
+ <title>{{title}}{{#siteName}} - {{siteName}}{{/siteName}}</title>
911
+ {{#description}}<meta name="description" content="{{description}}">{{/description}}
912
+ <!-- Open Graph -->
913
+ <meta property="og:type" content="website">
914
+ <meta property="og:title" content="{{title}}{{#siteName}} - {{siteName}}{{/siteName}}">
915
+ {{#description}}<meta property="og:description" content="{{description}}">{{/description}}
916
+ {{#ogImage}}<meta property="og:image" content="{{ogImage}}">{{/ogImage}}
917
+ <!-- Twitter Card -->
918
+ <meta name="twitter:card" content="summary_large_image">
919
+ <meta name="twitter:title" content="{{title}}{{#siteName}} - {{siteName}}{{/siteName}}">
920
+ {{#description}}<meta name="twitter:description" content="{{description}}">{{/description}}
921
+ {{#ogImage}}<meta name="twitter:image" content="{{ogImage}}">{{/ogImage}}
922
+ <style>
923
+ :root {
924
+ --sidebar-width: 260px;
925
+ --header-height: 60px;
926
+ --max-content-width: 960px;
927
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
928
+ --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
929
+ --color-bg: #ffffff;
930
+ --color-bg-alt: #f8f9fa;
931
+ --color-text: #1a1a1a;
932
+ --color-text-muted: #666666;
933
+ --color-border: #e5e7eb;
934
+ --color-primary: #b7410e;
935
+ --color-primary-hover: #ce5937;
936
+ --color-code-bg: #1e293b;
937
+ --color-code-text: #e2e8f0;
938
+ }
939
+ [data-theme="dark"] {
940
+ --color-bg: #0f172a;
941
+ --color-bg-alt: #1e293b;
942
+ --color-text: #e2e8f0;
943
+ --color-text-muted: #94a3b8;
944
+ --color-border: #334155;
945
+ --color-primary: #e67e4d;
946
+ --color-primary-hover: #f4a07a;
947
+ --color-code-bg: #0f172a;
948
+ --color-code-text: #e2e8f0;
949
+ }
950
+ @media (prefers-color-scheme: dark) {
951
+ :root:not([data-theme="light"]) {
952
+ --color-bg: #0f172a;
953
+ --color-bg-alt: #1e293b;
954
+ --color-text: #e2e8f0;
955
+ --color-text-muted: #94a3b8;
956
+ --color-border: #334155;
957
+ --color-primary: #e67e4d;
958
+ --color-primary-hover: #f4a07a;
959
+ --color-code-bg: #0f172a;
960
+ --color-code-text: #e2e8f0;
961
+ }
962
+ }
963
+ * { box-sizing: border-box; margin: 0; padding: 0; }
964
+ html { scroll-behavior: smooth; }
965
+ body {
966
+ font-family: var(--font-sans);
967
+ line-height: 1.7;
968
+ color: var(--color-text);
969
+ background: var(--color-bg);
970
+ }
971
+ a { color: var(--color-primary); text-decoration: none; }
972
+ a:hover { color: var(--color-primary-hover); text-decoration: underline; }
973
+
974
+ /* Header */
975
+ .header {
976
+ position: fixed;
977
+ top: 0;
978
+ left: 0;
979
+ right: 0;
980
+ height: var(--header-height);
981
+ background: var(--color-bg);
982
+ border-bottom: 1px solid var(--color-border);
983
+ display: flex;
984
+ align-items: center;
985
+ padding: 0 1.5rem;
986
+ z-index: 100;
987
+ }
988
+ .header-title {
989
+ font-size: 1.25rem;
990
+ font-weight: 600;
991
+ color: var(--color-text);
992
+ }
993
+ .header-title:hover { text-decoration: none; }
994
+ .menu-toggle {
995
+ display: none;
996
+ background: none;
997
+ border: none;
998
+ cursor: pointer;
999
+ padding: 0.5rem;
1000
+ margin-right: 0.75rem;
1001
+ }
1002
+ .menu-toggle svg { display: block; }
1003
+ .menu-toggle path { stroke: var(--color-text); }
1004
+ .header-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }
1005
+ .search-button {
1006
+ display: flex;
1007
+ align-items: center;
1008
+ gap: 0.5rem;
1009
+ padding: 0.5rem 0.75rem;
1010
+ background: var(--color-bg-alt);
1011
+ border: 1px solid var(--color-border);
1012
+ border-radius: 6px;
1013
+ color: var(--color-text-muted);
1014
+ cursor: pointer;
1015
+ font-size: 0.875rem;
1016
+ transition: border-color 0.15s, color 0.15s;
1017
+ }
1018
+ .search-button:hover { border-color: var(--color-primary); color: var(--color-text); }
1019
+ .search-button svg { width: 16px; height: 16px; }
1020
+ .search-button kbd {
1021
+ padding: 0.125rem 0.375rem;
1022
+ background: var(--color-bg);
1023
+ border: 1px solid var(--color-border);
1024
+ border-radius: 4px;
1025
+ font-family: var(--font-mono);
1026
+ font-size: 0.75rem;
1027
+ }
1028
+ @media (max-width: 640px) {
1029
+ .search-button span, .search-button kbd { display: none; }
1030
+ .search-button { padding: 0.5rem; }
1031
+ }
1032
+ .search-modal-overlay {
1033
+ display: none;
1034
+ position: fixed;
1035
+ inset: 0;
1036
+ z-index: 200;
1037
+ background: rgba(0,0,0,0.6);
1038
+ backdrop-filter: blur(4px);
1039
+ justify-content: center;
1040
+ padding-top: 10vh;
1041
+ }
1042
+ .search-modal-overlay.open { display: flex; }
1043
+ .search-modal {
1044
+ width: 100%;
1045
+ max-width: 560px;
1046
+ margin: 0 1rem;
1047
+ background: var(--color-bg);
1048
+ border: 1px solid var(--color-border);
1049
+ border-radius: 12px;
1050
+ overflow: hidden;
1051
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4);
1052
+ max-height: 70vh;
1053
+ display: flex;
1054
+ flex-direction: column;
1055
+ }
1056
+ .search-header {
1057
+ display: flex;
1058
+ align-items: center;
1059
+ gap: 0.75rem;
1060
+ padding: 1rem;
1061
+ border-bottom: 1px solid var(--color-border);
1062
+ }
1063
+ .search-header svg { flex-shrink: 0; color: var(--color-text-muted); }
1064
+ .search-input {
1065
+ flex: 1;
1066
+ background: none;
1067
+ border: none;
1068
+ outline: none;
1069
+ font-size: 1rem;
1070
+ color: var(--color-text);
1071
+ }
1072
+ .search-input::placeholder { color: var(--color-text-muted); }
1073
+ .search-close {
1074
+ padding: 0.25rem 0.5rem;
1075
+ background: var(--color-bg-alt);
1076
+ border: 1px solid var(--color-border);
1077
+ border-radius: 4px;
1078
+ color: var(--color-text-muted);
1079
+ font-family: var(--font-mono);
1080
+ font-size: 0.75rem;
1081
+ cursor: pointer;
1082
+ }
1083
+ .search-results {
1084
+ flex: 1;
1085
+ overflow-y: auto;
1086
+ padding: 0.5rem;
1087
+ }
1088
+ .search-result {
1089
+ display: block;
1090
+ padding: 0.75rem 1rem;
1091
+ border-radius: 8px;
1092
+ color: var(--color-text);
1093
+ text-decoration: none;
1094
+ }
1095
+ .search-result:hover, .search-result.selected { background: var(--color-bg-alt); text-decoration: none; }
1096
+ .search-result-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 0.25rem; }
1097
+ .search-result-snippet { font-size: 0.8125rem; color: var(--color-text-muted); }
1098
+ .search-empty { padding: 2rem 1rem; text-align: center; color: var(--color-text-muted); }
1099
+ .search-footer {
1100
+ display: flex;
1101
+ align-items: center;
1102
+ justify-content: center;
1103
+ gap: 1rem;
1104
+ padding: 0.75rem 1rem;
1105
+ border-top: 1px solid var(--color-border);
1106
+ background: var(--color-bg-alt);
1107
+ font-size: 0.75rem;
1108
+ color: var(--color-text-muted);
1109
+ }
1110
+ .search-footer kbd {
1111
+ padding: 0.125rem 0.375rem;
1112
+ background: var(--color-bg);
1113
+ border: 1px solid var(--color-border);
1114
+ border-radius: 4px;
1115
+ font-family: var(--font-mono);
1116
+ }
1117
+ .theme-toggle {
1118
+ background: none;
1119
+ border: none;
1120
+ cursor: pointer;
1121
+ padding: 0.5rem;
1122
+ border-radius: 6px;
1123
+ color: var(--color-text-muted);
1124
+ transition: background 0.15s, color 0.15s;
1125
+ }
1126
+ .theme-toggle:hover { background: var(--color-bg-alt); color: var(--color-text); }
1127
+ .theme-toggle svg { display: block; width: 20px; height: 20px; }
1128
+ .theme-toggle .icon-sun { display: none; }
1129
+ .theme-toggle .icon-moon { display: block; }
1130
+ [data-theme="dark"] .theme-toggle .icon-sun { display: block; }
1131
+ [data-theme="dark"] .theme-toggle .icon-moon { display: none; }
1132
+ @media (prefers-color-scheme: dark) {
1133
+ :root:not([data-theme="light"]) .theme-toggle .icon-sun { display: block; }
1134
+ :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: none; }
1135
+ }
1136
+
1137
+ /* Layout */
1138
+ .layout {
1139
+ display: flex;
1140
+ padding-top: var(--header-height);
1141
+ min-height: 100vh;
1142
+ }
1143
+
1144
+ /* Sidebar */
1145
+ .sidebar {
1146
+ position: fixed;
1147
+ top: var(--header-height);
1148
+ left: 0;
1149
+ bottom: 0;
1150
+ width: var(--sidebar-width);
1151
+ background: var(--color-bg-alt);
1152
+ border-right: 1px solid var(--color-border);
1153
+ overflow-y: auto;
1154
+ padding: 1.5rem 1rem;
1155
+ }
1156
+ .nav-section { margin-bottom: 1.5rem; }
1157
+ .nav-title {
1158
+ font-size: 0.75rem;
1159
+ font-weight: 600;
1160
+ text-transform: uppercase;
1161
+ letter-spacing: 0.05em;
1162
+ color: var(--color-text-muted);
1163
+ margin-bottom: 0.5rem;
1164
+ padding: 0 0.75rem;
1165
+ }
1166
+ .nav-list { list-style: none; }
1167
+ .nav-item { margin: 0.125rem 0; }
1168
+ .nav-link {
1169
+ display: block;
1170
+ padding: 0.5rem 0.75rem;
1171
+ border-radius: 6px;
1172
+ color: var(--color-text);
1173
+ font-size: 0.875rem;
1174
+ transition: background 0.15s;
1175
+ }
1176
+ .nav-link:hover {
1177
+ background: var(--color-border);
1178
+ text-decoration: none;
1179
+ }
1180
+ .nav-link.active {
1181
+ background: var(--color-primary);
1182
+ color: white;
1183
+ }
1184
+
1185
+ /* Main content */
1186
+ .main {
1187
+ flex: 1;
1188
+ margin-left: var(--sidebar-width);
1189
+ padding: 2rem;
1190
+ min-width: 0;
1191
+ overflow-x: hidden;
1192
+ }
1193
+ .content {
1194
+ max-width: var(--max-content-width);
1195
+ margin: 0 auto;
1196
+ overflow-wrap: break-word;
1197
+ word-wrap: break-word;
1198
+ word-break: break-word;
1199
+ }
1200
+
1201
+ /* TOC (right sidebar) */
1202
+ .toc {
1203
+ position: fixed;
1204
+ top: calc(var(--header-height) + 2rem);
1205
+ right: 2rem;
1206
+ width: 200px;
1207
+ font-size: 0.8125rem;
1208
+ }
1209
+ .toc-title {
1210
+ font-weight: 600;
1211
+ margin-bottom: 0.75rem;
1212
+ color: var(--color-text-muted);
1213
+ }
1214
+ .toc-list { list-style: none; }
1215
+ .toc-item { margin: 0.375rem 0; }
1216
+ .toc-link {
1217
+ color: var(--color-text-muted);
1218
+ display: block;
1219
+ padding-left: calc((var(--depth, 1) - 1) * 0.75rem);
1220
+ }
1221
+ .toc-link:hover { color: var(--color-primary); }
1222
+ @media (max-width: 1200px) { .toc { display: none; } }
1223
+
1224
+ /* Typography */
1225
+ .content h1 { font-size: 2.25rem; margin-bottom: 1rem; line-height: 1.2; }
1226
+ .content h2 { font-size: 1.5rem; margin-top: 2.5rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-border); }
1227
+ .content h3 { font-size: 1.25rem; margin-top: 2rem; margin-bottom: 0.75rem; }
1228
+ .content h4 { font-size: 1rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
1229
+ .content p { margin-bottom: 1rem; }
1230
+ .content ul, .content ol { margin: 1rem 0; padding-left: 1.5rem; }
1231
+ .content li { margin: 0.375rem 0; }
1232
+ .content blockquote {
1233
+ border-left: 4px solid var(--color-primary);
1234
+ padding: 0.5rem 1rem;
1235
+ margin: 1rem 0;
1236
+ background: var(--color-bg-alt);
1237
+ border-radius: 0 6px 6px 0;
1238
+ }
1239
+ .content code {
1240
+ font-family: var(--font-mono);
1241
+ font-size: 0.875em;
1242
+ background: var(--color-bg-alt);
1243
+ padding: 0.2em 0.4em;
1244
+ border-radius: 4px;
1245
+ word-break: break-all;
1246
+ }
1247
+ .content pre {
1248
+ background: var(--color-code-bg);
1249
+ color: var(--color-code-text);
1250
+ padding: 1rem 1.25rem;
1251
+ border-radius: 8px;
1252
+ overflow-x: auto;
1253
+ margin: 1.5rem 0;
1254
+ line-height: 1.5;
1255
+ }
1256
+ .content pre code {
1257
+ background: transparent;
1258
+ padding: 0;
1259
+ font-size: 0.8125rem;
1260
+ }
1261
+ .content table {
1262
+ width: 100%;
1263
+ border-collapse: collapse;
1264
+ margin: 1.5rem 0;
1265
+ font-size: 0.875rem;
1266
+ }
1267
+ .content th, .content td {
1268
+ border: 1px solid var(--color-border);
1269
+ padding: 0.75rem 1rem;
1270
+ text-align: left;
1271
+ }
1272
+ .content th { background: var(--color-bg-alt); font-weight: 600; }
1273
+ .content img { max-width: 100%; height: auto; border-radius: 8px; display: block; }
1274
+ .content img[alt*="Logo"] { max-width: 200px; display: block; margin: 1rem 0; }
1275
+ .content img[alt*="Architecture"] { max-width: 600px; }
1276
+ .content img[alt*="Benchmark"] { max-width: 680px; }
1277
+ .content hr { border: none; border-top: 1px solid var(--color-border); margin: 2rem 0; }
1278
+
1279
+ /* Responsive */
1280
+ @media (max-width: 768px) {
1281
+ .menu-toggle { display: block; }
1282
+ .sidebar {
1283
+ transform: translateX(-100%);
1284
+ transition: transform 0.3s ease;
1285
+ z-index: 99;
1286
+ width: 280px;
1287
+ }
1288
+ .sidebar.open { transform: translateX(0); }
1289
+ .main { margin-left: 0; padding: 1rem 0.75rem; }
1290
+ .content { padding: 0 0.25rem; }
1291
+ .content h1 { font-size: 1.5rem; line-height: 1.3; margin-bottom: 0.75rem; }
1292
+ .content h2 { font-size: 1.2rem; margin-top: 2rem; }
1293
+ .content h3 { font-size: 1.1rem; }
1294
+ .content p { font-size: 0.9375rem; margin-bottom: 0.875rem; }
1295
+ .content ul, .content ol { padding-left: 1.25rem; font-size: 0.9375rem; }
1296
+ .content pre {
1297
+ padding: 0.75rem;
1298
+ font-size: 0.75rem;
1299
+ margin: 1rem -0.75rem;
1300
+ border-radius: 0;
1301
+ overflow-x: auto;
1302
+ -webkit-overflow-scrolling: touch;
1303
+ }
1304
+ .content code { font-size: 0.8125em; }
1305
+ .content table {
1306
+ display: block;
1307
+ overflow-x: auto;
1308
+ -webkit-overflow-scrolling: touch;
1309
+ font-size: 0.8125rem;
1310
+ margin: 1rem -0.75rem;
1311
+ width: calc(100% + 1.5rem);
1312
+ }
1313
+ .content th, .content td { padding: 0.5rem 0.75rem; white-space: nowrap; }
1314
+ .content img { margin: 1rem 0; }
1315
+ .content img[alt*="Logo"] { max-width: 150px; }
1316
+ .content img[alt*="Architecture"] { max-width: 100%; }
1317
+ .content img[alt*="Benchmark"] { max-width: 100%; }
1318
+ .content blockquote { padding: 0.5rem 0.75rem; margin: 1rem 0; font-size: 0.9375rem; }
1319
+ .header { padding: 0 1rem; }
1320
+ .header-title { font-size: 1rem; }
1321
+ .header-title img { width: 24px; height: 24px; }
1322
+ .overlay {
1323
+ display: none;
1324
+ position: fixed;
1325
+ inset: 0;
1326
+ background: rgba(0,0,0,0.5);
1327
+ z-index: 98;
1328
+ }
1329
+ .overlay.open { display: block; }
1330
+ }
1331
+
1332
+ /* Extra small devices */
1333
+ @media (max-width: 480px) {
1334
+ .main { padding: 0.75rem 0.5rem; }
1335
+ .content h1 { font-size: 1.35rem; }
1336
+ .content pre { font-size: 0.6875rem; padding: 0.625rem; }
1337
+ .content table { font-size: 0.75rem; }
1338
+ .content th, .content td { padding: 0.375rem 0.5rem; }
1339
+ }
1340
+ </style>
1341
+ </head>
1342
+ <body>
1343
+ <header class="header">
1344
+ <button class="menu-toggle" aria-label="Toggle menu">
1345
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round">
1346
+ <path d="M3 12h18M3 6h18M3 18h18"/>
1347
+ </svg>
1348
+ </button>
1349
+ <a href="{{base}}index.html" class="header-title">
1350
+ <img src="{{base}}logo.svg" alt="" width="28" height="28" style="margin-right: 8px; vertical-align: middle;" />
1351
+ {{siteName}}
1352
+ </a>
1353
+ <div class="header-actions">
1354
+ <button class="search-button" aria-label="Search">
1355
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1356
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
1357
+ </svg>
1358
+ <span>Search</span>
1359
+ <kbd>/</kbd>
1360
+ </button>
1361
+ <button class="theme-toggle" aria-label="Toggle theme">
1362
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1363
+ <circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
1364
+ </svg>
1365
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1366
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
1367
+ </svg>
1368
+ </button>
1369
+ </div>
1370
+ </header>
1371
+ <div class="search-modal-overlay">
1372
+ <div class="search-modal">
1373
+ <div class="search-header">
1374
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1375
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
1376
+ </svg>
1377
+ <input type="text" class="search-input" placeholder="Search documentation..." />
1378
+ <button class="search-close">Esc</button>
1379
+ </div>
1380
+ <div class="search-results"></div>
1381
+ <div class="search-footer">
1382
+ <span><kbd>\u2191</kbd><kbd>\u2193</kbd> to navigate</span>
1383
+ <span><kbd>Enter</kbd> to select</span>
1384
+ <span><kbd>Esc</kbd> to close</span>
1385
+ </div>
1386
+ </div>
1387
+ </div>
1388
+ <div class="overlay"></div>
1389
+ <div class="layout">
1390
+ <aside class="sidebar">
1391
+ <nav>
1392
+ {{navigation}}
1393
+ </nav>
1394
+ </aside>
1395
+ <main class="main">
1396
+ <article class="content">
1397
+ {{content}}
1398
+ </article>
1399
+ </main>
1400
+ {{#hasToc}}
1401
+ <aside class="toc">
1402
+ <div class="toc-title">On this page</div>
1403
+ <ul class="toc-list">
1404
+ {{toc}}
1405
+ </ul>
1406
+ </aside>
1407
+ {{/hasToc}}
1408
+ </div>
1409
+ <script>
1410
+ // Menu toggle
1411
+ const toggle = document.querySelector('.menu-toggle');
1412
+ const sidebar = document.querySelector('.sidebar');
1413
+ const overlay = document.querySelector('.overlay');
1414
+ if (toggle && sidebar && overlay) {
1415
+ const close = () => { sidebar.classList.remove('open'); overlay.classList.remove('open'); };
1416
+ toggle.addEventListener('click', () => {
1417
+ sidebar.classList.toggle('open');
1418
+ overlay.classList.toggle('open');
1419
+ });
1420
+ overlay.addEventListener('click', close);
1421
+ sidebar.querySelectorAll('a').forEach(a => a.addEventListener('click', close));
1422
+ }
1423
+
1424
+ // Theme toggle
1425
+ const themeToggle = document.querySelector('.theme-toggle');
1426
+ const getPreferredTheme = () => {
1427
+ const stored = localStorage.getItem('theme');
1428
+ if (stored) return stored;
1429
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1430
+ };
1431
+ const setTheme = (theme) => {
1432
+ document.documentElement.setAttribute('data-theme', theme);
1433
+ localStorage.setItem('theme', theme);
1434
+ };
1435
+ // Initialize theme
1436
+ setTheme(getPreferredTheme());
1437
+ if (themeToggle) {
1438
+ themeToggle.addEventListener('click', () => {
1439
+ const current = document.documentElement.getAttribute('data-theme') || getPreferredTheme();
1440
+ setTheme(current === 'dark' ? 'light' : 'dark');
1441
+ });
1442
+ }
1443
+
1444
+ // Search functionality
1445
+ const searchButton = document.querySelector('.search-button');
1446
+ const searchOverlay = document.querySelector('.search-modal-overlay');
1447
+ const searchInput = document.querySelector('.search-input');
1448
+ const searchResults = document.querySelector('.search-results');
1449
+ const searchClose = document.querySelector('.search-close');
1450
+ let searchIndex = null;
1451
+ let selectedIndex = 0;
1452
+ let results = [];
1453
+
1454
+ const openSearch = () => {
1455
+ searchOverlay.classList.add('open');
1456
+ searchInput.focus();
1457
+ };
1458
+ const closeSearch = () => {
1459
+ searchOverlay.classList.remove('open');
1460
+ searchInput.value = '';
1461
+ searchResults.innerHTML = '';
1462
+ selectedIndex = 0;
1463
+ results = [];
1464
+ };
1465
+
1466
+ // Load search index
1467
+ const loadSearchIndex = async () => {
1468
+ if (searchIndex) return;
1469
+ try {
1470
+ const res = await fetch('{{base}}search-index.json');
1471
+ searchIndex = await res.json();
1472
+ } catch (e) {
1473
+ console.warn('Failed to load search index:', e);
1474
+ }
1475
+ };
1476
+
1477
+ // Tokenize query
1478
+ const tokenize = (text) => {
1479
+ const tokens = [];
1480
+ let current = '';
1481
+ for (const char of text) {
1482
+ const isCjk = /[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\u3040-\\u309F\\u30A0-\\u30FF\\uAC00-\\uD7AF]/.test(char);
1483
+ if (isCjk) {
1484
+ if (current) { tokens.push(current.toLowerCase()); current = ''; }
1485
+ tokens.push(char);
1486
+ } else if (/[a-zA-Z0-9_]/.test(char)) {
1487
+ current += char;
1488
+ } else if (current) {
1489
+ tokens.push(current.toLowerCase());
1490
+ current = '';
1491
+ }
1492
+ }
1493
+ if (current) tokens.push(current.toLowerCase());
1494
+ return tokens;
1495
+ };
1496
+
1497
+ // Perform search
1498
+ const performSearch = async (query) => {
1499
+ if (!query.trim()) {
1500
+ searchResults.innerHTML = '';
1501
+ results = [];
1502
+ return;
1503
+ }
1504
+ await loadSearchIndex();
1505
+ if (!searchIndex) {
1506
+ searchResults.innerHTML = '<div class="search-empty">Search index not available</div>';
1507
+ return;
1508
+ }
1509
+
1510
+ const tokens = tokenize(query);
1511
+ if (!tokens.length) {
1512
+ searchResults.innerHTML = '';
1513
+ results = [];
1514
+ return;
1515
+ }
1516
+
1517
+ const k1 = 1.2, b = 0.75;
1518
+ const docScores = new Map();
1519
+
1520
+ for (let i = 0; i < tokens.length; i++) {
1521
+ const token = tokens[i];
1522
+ const isLast = i === tokens.length - 1;
1523
+ let matchingTerms = [];
1524
+ if (isLast && token.length >= 2) {
1525
+ matchingTerms = Object.keys(searchIndex.index).filter(t => t.startsWith(token));
1526
+ } else if (searchIndex.index[token]) {
1527
+ matchingTerms = [token];
1528
+ }
1529
+
1530
+ for (const term of matchingTerms) {
1531
+ const postings = searchIndex.index[term] || [];
1532
+ const df = searchIndex.df[term] || 1;
1533
+ const idf = Math.log((searchIndex.doc_count - df + 0.5) / (df + 0.5) + 1.0);
1534
+
1535
+ for (const posting of postings) {
1536
+ const doc = searchIndex.documents[posting.doc_idx];
1537
+ if (!doc) continue;
1538
+ const boost = posting.field === 'Title' ? 10 : posting.field === 'Heading' ? 5 : 1;
1539
+ const tf = posting.tf;
1540
+ const docLen = doc.body.length;
1541
+ const score = idf * ((tf * (k1 + 1)) / (tf + k1 * (1 - b + b * docLen / searchIndex.avg_dl))) * boost;
1542
+
1543
+ if (!docScores.has(posting.doc_idx)) {
1544
+ docScores.set(posting.doc_idx, { score: 0, matches: new Set() });
1545
+ }
1546
+ const entry = docScores.get(posting.doc_idx);
1547
+ entry.score += score;
1548
+ entry.matches.add(term);
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ results = Array.from(docScores.entries())
1554
+ .map(([docIdx, data]) => {
1555
+ const doc = searchIndex.documents[docIdx];
1556
+ let snippet = '';
1557
+ if (doc.body) {
1558
+ const bodyLower = doc.body.toLowerCase();
1559
+ let firstPos = -1;
1560
+ for (const match of data.matches) {
1561
+ const pos = bodyLower.indexOf(match);
1562
+ if (pos !== -1 && (firstPos === -1 || pos < firstPos)) firstPos = pos;
1563
+ }
1564
+ const start = Math.max(0, firstPos - 50);
1565
+ const end = Math.min(doc.body.length, start + 150);
1566
+ snippet = doc.body.slice(start, end);
1567
+ if (start > 0) snippet = '...' + snippet;
1568
+ if (end < doc.body.length) snippet += '...';
1569
+ }
1570
+ return { ...doc, score: data.score, snippet };
1571
+ })
1572
+ .sort((a, b) => b.score - a.score)
1573
+ .slice(0, 10);
1574
+
1575
+ selectedIndex = 0;
1576
+ renderResults();
1577
+ };
1578
+
1579
+ const renderResults = () => {
1580
+ if (!results.length) {
1581
+ searchResults.innerHTML = '<div class="search-empty">No results found</div>';
1582
+ return;
1583
+ }
1584
+ searchResults.innerHTML = results.map((r, i) =>
1585
+ '<a href="' + r.url + '" class="search-result' + (i === selectedIndex ? ' selected' : '') + '">' +
1586
+ '<div class="search-result-title">' + r.title + '</div>' +
1587
+ (r.snippet ? '<div class="search-result-snippet">' + r.snippet + '</div>' : '') +
1588
+ '</a>'
1589
+ ).join('');
1590
+ };
1591
+
1592
+ // Event listeners
1593
+ if (searchButton) searchButton.addEventListener('click', openSearch);
1594
+ if (searchClose) searchClose.addEventListener('click', closeSearch);
1595
+ if (searchOverlay) searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
1596
+
1597
+ let searchTimeout = null;
1598
+ if (searchInput) {
1599
+ searchInput.addEventListener('input', () => {
1600
+ if (searchTimeout) clearTimeout(searchTimeout);
1601
+ searchTimeout = setTimeout(() => performSearch(searchInput.value), 150);
1602
+ });
1603
+ searchInput.addEventListener('keydown', (e) => {
1604
+ if (e.key === 'Escape') closeSearch();
1605
+ else if (e.key === 'ArrowDown') {
1606
+ e.preventDefault();
1607
+ if (selectedIndex < results.length - 1) { selectedIndex++; renderResults(); }
1608
+ } else if (e.key === 'ArrowUp') {
1609
+ e.preventDefault();
1610
+ if (selectedIndex > 0) { selectedIndex--; renderResults(); }
1611
+ } else if (e.key === 'Enter' && results[selectedIndex]) {
1612
+ e.preventDefault();
1613
+ window.location.href = results[selectedIndex].url;
1614
+ }
1615
+ });
1616
+ }
1617
+
1618
+ // Global keyboard shortcut (/ or Cmd+K)
1619
+ document.addEventListener('keydown', (e) => {
1620
+ if ((e.key === '/' && !(e.target instanceof HTMLInputElement)) ||
1621
+ ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) {
1622
+ e.preventDefault();
1623
+ openSearch();
1624
+ }
1625
+ });
1626
+ </script>
1627
+ </body>
1628
+ </html>`;
1629
+ var BARE_HTML_TEMPLATE = `<!DOCTYPE html>
1630
+ <html lang="en">
1631
+ <head>
1632
+ <meta charset="UTF-8">
1633
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1634
+ <title>{{title}}</title>
1635
+ </head>
1636
+ <body>
1637
+ {{content}}
1638
+ </body>
1639
+ </html>`;
1640
+ function resolveSsgOptions(ssg) {
1641
+ if (ssg === false) {
1642
+ return {
1643
+ enabled: false,
1644
+ extension: ".html",
1645
+ clean: false,
1646
+ bare: false,
1647
+ generateOgImage: false
1648
+ };
1649
+ }
1650
+ if (ssg === true || ssg === void 0) {
1651
+ return {
1652
+ enabled: true,
1653
+ extension: ".html",
1654
+ clean: false,
1655
+ bare: false,
1656
+ generateOgImage: false
1657
+ };
1658
+ }
1659
+ return {
1660
+ enabled: ssg.enabled ?? true,
1661
+ extension: ssg.extension ?? ".html",
1662
+ clean: ssg.clean ?? false,
1663
+ bare: ssg.bare ?? false,
1664
+ siteName: ssg.siteName,
1665
+ ogImage: ssg.ogImage,
1666
+ generateOgImage: ssg.generateOgImage ?? false,
1667
+ siteUrl: ssg.siteUrl
1668
+ };
1669
+ }
1670
+ function renderTemplate(template, data) {
1671
+ let result = template;
1672
+ result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
1673
+ return data[key] ? content : "";
1674
+ });
1675
+ result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
1676
+ const value = data[key];
1677
+ return value !== void 0 && value !== null ? String(value) : "";
1678
+ });
1679
+ return result;
1680
+ }
1681
+ function extractTitle(content, frontmatter) {
1682
+ if (frontmatter.title && typeof frontmatter.title === "string") {
1683
+ return frontmatter.title;
1684
+ }
1685
+ const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
1686
+ if (h1Match) {
1687
+ return h1Match[1].trim();
1688
+ }
1689
+ return "Untitled";
1690
+ }
1691
+ function generateBareHtmlPage(content, title) {
1692
+ return renderTemplate(BARE_HTML_TEMPLATE, {
1693
+ title,
1694
+ content
1695
+ });
1696
+ }
1697
+ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage) {
1698
+ const mod = await import("@ox-content/napi");
1699
+ const tocForRust = pageData.toc.map((entry) => ({
1700
+ depth: entry.depth,
1701
+ text: entry.text,
1702
+ slug: entry.slug
1703
+ }));
1704
+ const navGroupsForRust = navGroups.map((group) => ({
1705
+ title: group.title,
1706
+ items: group.items.map((item) => ({
1707
+ title: item.title,
1708
+ path: item.path,
1709
+ href: item.href
1710
+ }))
1711
+ }));
1712
+ return mod.generateSsgHtml(
1713
+ {
1714
+ title: pageData.title,
1715
+ description: pageData.description,
1716
+ content: pageData.content,
1717
+ toc: tocForRust,
1718
+ path: pageData.path
1719
+ },
1720
+ navGroupsForRust,
1721
+ {
1722
+ siteName,
1723
+ base,
1724
+ ogImage
1725
+ }
1726
+ );
1727
+ }
1728
+ function getOutputPath(inputPath, srcDir, outDir, extension) {
1729
+ const relativePath = path3.relative(srcDir, inputPath);
1730
+ const baseName = relativePath.replace(/\.(?:md|markdown)$/i, extension);
1731
+ if (baseName.endsWith(`index${extension}`)) {
1732
+ return path3.join(outDir, baseName);
1733
+ }
1734
+ const dirName = baseName.replace(new RegExp(`\\${extension}$`), "");
1735
+ return path3.join(outDir, dirName, `index${extension}`);
1736
+ }
1737
+ function getUrlPath(inputPath, srcDir) {
1738
+ const relativePath = path3.relative(srcDir, inputPath);
1739
+ const baseName = relativePath.replace(/\.(?:md|markdown)$/i, "");
1740
+ if (baseName === "index" || baseName.endsWith("/index")) {
1741
+ return baseName.replace(/\/?index$/, "") || "/";
1742
+ }
1743
+ return baseName;
1744
+ }
1745
+ function getHref(inputPath, srcDir, base, extension) {
1746
+ const urlPath = getUrlPath(inputPath, srcDir);
1747
+ if (urlPath === "/" || urlPath === "") {
1748
+ return `${base}index${extension}`;
1749
+ }
1750
+ return `${base}${urlPath}/index${extension}`;
1751
+ }
1752
+ function getOgImagePath(inputPath, srcDir, outDir) {
1753
+ const relativePath = path3.relative(srcDir, inputPath);
1754
+ const baseName = relativePath.replace(/\.(?:md|markdown)$/i, "");
1755
+ if (baseName === "index" || baseName.endsWith("/index")) {
1756
+ const dirPath = baseName.replace(/\/?index$/, "") || "";
1757
+ return path3.join(outDir, dirPath, "og-image.svg");
1758
+ }
1759
+ return path3.join(outDir, baseName, "og-image.svg");
1760
+ }
1761
+ function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
1762
+ const urlPath = getUrlPath(inputPath, srcDir);
1763
+ let relativePath;
1764
+ if (urlPath === "/" || urlPath === "") {
1765
+ relativePath = `${base}og-image.svg`;
1766
+ } else {
1767
+ relativePath = `${base}${urlPath}/og-image.svg`;
1768
+ }
1769
+ if (siteUrl) {
1770
+ const cleanSiteUrl = siteUrl.replace(/\/$/, "");
1771
+ return `${cleanSiteUrl}${relativePath}`;
1772
+ }
1773
+ return relativePath;
1774
+ }
1775
+ function getDisplayTitle(filePath) {
1776
+ const fileName = path3.basename(filePath, path3.extname(filePath));
1777
+ if (fileName === "index") {
1778
+ const dirName = path3.basename(path3.dirname(filePath));
1779
+ if (dirName && dirName !== ".") {
1780
+ return formatTitle(dirName);
1781
+ }
1782
+ return "Home";
1783
+ }
1784
+ return formatTitle(fileName);
1785
+ }
1786
+ function formatTitle(name) {
1787
+ return name.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
1788
+ }
1789
+ async function collectMarkdownFiles(srcDir) {
1790
+ const pattern = path3.join(srcDir, "**/*.{md,markdown}");
1791
+ const files = await glob(pattern, {
1792
+ nodir: true,
1793
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
1794
+ });
1795
+ return files.sort();
1796
+ }
1797
+ function buildNavItems(markdownFiles, srcDir, base, extension) {
1798
+ const groups = /* @__PURE__ */ new Map();
1799
+ const groupOrder = ["", "examples", "packages", "api"];
1800
+ for (const file of markdownFiles) {
1801
+ const relativePath = path3.relative(srcDir, file);
1802
+ const parts = relativePath.split(path3.sep);
1803
+ let groupKey = "";
1804
+ if (parts.length > 1) {
1805
+ groupKey = parts[0];
1806
+ }
1807
+ if (!groups.has(groupKey)) {
1808
+ groups.set(groupKey, []);
1809
+ }
1810
+ const urlPath = getUrlPath(file, srcDir);
1811
+ let title;
1812
+ if (urlPath === "/" || urlPath === "") {
1813
+ title = "Overview";
1814
+ } else {
1815
+ title = getDisplayTitle(file);
1816
+ }
1817
+ groups.get(groupKey).push({
1818
+ title,
1819
+ path: urlPath,
1820
+ href: getHref(file, srcDir, base, extension)
1821
+ });
1822
+ }
1823
+ const sortItems = (items) => {
1824
+ return items.sort((a, b) => {
1825
+ const aIsRoot = a.path === "/" || a.path === "";
1826
+ const bIsRoot = b.path === "/" || b.path === "";
1827
+ if (aIsRoot && !bIsRoot) return -1;
1828
+ if (!aIsRoot && bIsRoot) return 1;
1829
+ return a.title.localeCompare(b.title);
1830
+ });
1831
+ };
1832
+ const result = [];
1833
+ for (const key of groupOrder) {
1834
+ const items = groups.get(key);
1835
+ if (items && items.length > 0) {
1836
+ result.push({
1837
+ title: key === "" ? "Guide" : formatTitle(key),
1838
+ items: sortItems(items)
1839
+ });
1840
+ groups.delete(key);
1841
+ }
1842
+ }
1843
+ for (const [key, items] of groups) {
1844
+ if (items.length > 0) {
1845
+ result.push({
1846
+ title: formatTitle(key),
1847
+ items: sortItems(items)
1848
+ });
1849
+ }
1850
+ }
1851
+ return result;
1852
+ }
1853
+ async function buildSsg(options, root) {
1854
+ const ssgOptions = options.ssg;
1855
+ if (!ssgOptions.enabled) {
1856
+ return { files: [], errors: [] };
1857
+ }
1858
+ const srcDir = path3.resolve(root, options.srcDir);
1859
+ const outDir = path3.resolve(root, options.outDir);
1860
+ const base = options.base.endsWith("/") ? options.base : options.base + "/";
1861
+ const generatedFiles = [];
1862
+ const errors = [];
1863
+ if (ssgOptions.clean) {
1864
+ try {
1865
+ await fs2.rm(outDir, { recursive: true, force: true });
1866
+ } catch {
1867
+ }
1868
+ }
1869
+ const markdownFiles = await collectMarkdownFiles(srcDir);
1870
+ const navItems = buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension);
1871
+ let siteName = ssgOptions.siteName ?? "Documentation";
1872
+ if (!ssgOptions.siteName) {
1873
+ try {
1874
+ const pkgPath = path3.join(root, "package.json");
1875
+ const pkg = JSON.parse(await fs2.readFile(pkgPath, "utf-8"));
1876
+ if (pkg.name) {
1877
+ siteName = formatTitle(pkg.name);
1878
+ }
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ for (const inputPath of markdownFiles) {
1883
+ try {
1884
+ const content = await fs2.readFile(inputPath, "utf-8");
1885
+ const result = await transformMarkdown(content, inputPath, options, {
1886
+ convertMdLinks: true,
1887
+ baseUrl: base
1888
+ });
1889
+ const title = extractTitle(result.html, result.frontmatter);
1890
+ const description = result.frontmatter.description;
1891
+ let pageOgImage = ssgOptions.ogImage;
1892
+ if (ssgOptions.generateOgImage && !ssgOptions.bare) {
1893
+ const ogImageData = {
1894
+ title,
1895
+ description,
1896
+ siteName,
1897
+ author: result.frontmatter.author
1898
+ };
1899
+ const ogImageConfig = options.ogImageOptions ? {
1900
+ width: options.ogImageOptions.width,
1901
+ height: options.ogImageOptions.height,
1902
+ backgroundColor: options.ogImageOptions.background,
1903
+ textColor: options.ogImageOptions.textColor
1904
+ } : void 0;
1905
+ const svg = await generateOgImageSvg(ogImageData, ogImageConfig);
1906
+ if (svg) {
1907
+ const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
1908
+ await fs2.mkdir(path3.dirname(ogImageOutputPath), { recursive: true });
1909
+ await fs2.writeFile(ogImageOutputPath, svg, "utf-8");
1910
+ generatedFiles.push(ogImageOutputPath);
1911
+ pageOgImage = getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl);
1912
+ }
1913
+ }
1914
+ let html;
1915
+ if (ssgOptions.bare) {
1916
+ html = generateBareHtmlPage(result.html, title);
1917
+ } else {
1918
+ const pageData = {
1919
+ title,
1920
+ description,
1921
+ content: result.html,
1922
+ toc: result.toc,
1923
+ frontmatter: result.frontmatter,
1924
+ path: getUrlPath(inputPath, srcDir),
1925
+ href: getHref(inputPath, srcDir, base, ssgOptions.extension)
1926
+ };
1927
+ html = await generateHtmlPage(pageData, navItems, siteName, base, pageOgImage);
1928
+ }
1929
+ const outputPath = getOutputPath(
1930
+ inputPath,
1931
+ srcDir,
1932
+ outDir,
1933
+ ssgOptions.extension
1934
+ );
1935
+ await fs2.mkdir(path3.dirname(outputPath), { recursive: true });
1936
+ await fs2.writeFile(outputPath, html, "utf-8");
1937
+ generatedFiles.push(outputPath);
1938
+ } catch (err) {
1939
+ const errorMessage = err instanceof Error ? err.message : String(err);
1940
+ errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
1941
+ }
1942
+ }
1943
+ return { files: generatedFiles, errors };
1944
+ }
1945
+
1946
+ // src/search.ts
1947
+ import * as fs3 from "fs/promises";
1948
+ import * as path4 from "path";
1949
+ var oxContent = null;
1950
+ async function getOxContent() {
1951
+ if (!oxContent) {
1952
+ try {
1953
+ oxContent = await import("@ox-content/napi");
1954
+ } catch {
1955
+ console.warn("[ox-content] Native bindings not available, search disabled");
1956
+ return null;
1957
+ }
1958
+ }
1959
+ return oxContent;
1960
+ }
1961
+ function resolveSearchOptions(options) {
1962
+ if (options === false) {
1963
+ return {
1964
+ enabled: false,
1965
+ limit: 10,
1966
+ prefix: true,
1967
+ placeholder: "Search documentation...",
1968
+ hotkey: "/"
1969
+ };
1970
+ }
1971
+ const opts = typeof options === "object" ? options : {};
1972
+ return {
1973
+ enabled: opts.enabled ?? true,
1974
+ limit: opts.limit ?? 10,
1975
+ prefix: opts.prefix ?? true,
1976
+ placeholder: opts.placeholder ?? "Search documentation...",
1977
+ hotkey: opts.hotkey ?? "/"
1978
+ };
1979
+ }
1980
+ async function collectMarkdownFiles2(dir) {
1981
+ const files = [];
1982
+ async function walk(currentDir) {
1983
+ try {
1984
+ const entries = await fs3.readdir(currentDir, { withFileTypes: true });
1985
+ for (const entry of entries) {
1986
+ const fullPath = path4.join(currentDir, entry.name);
1987
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1988
+ await walk(fullPath);
1989
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1990
+ files.push(fullPath);
1991
+ }
1992
+ }
1993
+ } catch {
1994
+ }
1995
+ }
1996
+ await walk(dir);
1997
+ return files;
1998
+ }
1999
+ async function buildSearchIndex(srcDir, base) {
2000
+ const napi = await getOxContent();
2001
+ if (!napi) {
2002
+ return JSON.stringify({ documents: [], index: {}, df: {}, avg_dl: 0, doc_count: 0 });
2003
+ }
2004
+ const files = await collectMarkdownFiles2(srcDir);
2005
+ const documents = [];
2006
+ for (const file of files) {
2007
+ try {
2008
+ const content = await fs3.readFile(file, "utf-8");
2009
+ const relativePath = path4.relative(srcDir, file);
2010
+ const url = base + relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
2011
+ const id = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
2012
+ const extractSearchContent = napi.extractSearchContent;
2013
+ if (!extractSearchContent) {
2014
+ console.warn("[ox-content] Search not available: extractSearchContent not implemented");
2015
+ return "[]";
2016
+ }
2017
+ const doc = extractSearchContent(content, id, url, { gfm: true });
2018
+ documents.push({
2019
+ id: doc.id,
2020
+ title: doc.title,
2021
+ url: doc.url,
2022
+ body: doc.body,
2023
+ headings: doc.headings,
2024
+ code: doc.code
2025
+ });
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ const buildSearchIndex2 = napi.buildSearchIndex;
2030
+ if (!buildSearchIndex2) {
2031
+ console.warn("[ox-content] Search not available: buildSearchIndex not implemented");
2032
+ return JSON.stringify(documents);
2033
+ }
2034
+ return buildSearchIndex2(documents);
2035
+ }
2036
+ async function writeSearchIndex(indexJson, outDir) {
2037
+ const indexPath = path4.join(outDir, "search-index.json");
2038
+ await fs3.mkdir(outDir, { recursive: true });
2039
+ await fs3.writeFile(indexPath, indexJson, "utf-8");
2040
+ }
2041
+ function generateSearchModule(options, indexPath) {
2042
+ return `
2043
+ // Search module generated by ox-content
2044
+ const searchOptions = ${JSON.stringify(options)};
2045
+
2046
+ let searchIndex = null;
2047
+ let indexPromise = null;
2048
+
2049
+ // Tokenizer for queries
2050
+ function tokenizeQuery(text) {
2051
+ const tokens = [];
2052
+ let current = '';
2053
+
2054
+ for (const char of text) {
2055
+ const isCjk = /[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\u3040-\\u309F\\u30A0-\\u30FF\\uAC00-\\uD7AF]/.test(char);
2056
+
2057
+ if (isCjk) {
2058
+ if (current) {
2059
+ tokens.push(current.toLowerCase());
2060
+ current = '';
2061
+ }
2062
+ tokens.push(char);
2063
+ } else if (/[a-zA-Z0-9_]/.test(char)) {
2064
+ current += char;
2065
+ } else if (current) {
2066
+ tokens.push(current.toLowerCase());
2067
+ current = '';
2068
+ }
2069
+ }
2070
+
2071
+ if (current) {
2072
+ tokens.push(current.toLowerCase());
2073
+ }
2074
+
2075
+ return tokens;
2076
+ }
2077
+
2078
+ // BM25 scoring
2079
+ function computeIdf(df, docCount) {
2080
+ return Math.log((docCount - df + 0.5) / (df + 0.5) + 1.0);
2081
+ }
2082
+
2083
+ function getFieldBoost(field) {
2084
+ switch (field) {
2085
+ case 'Title': return 10.0;
2086
+ case 'Heading': return 5.0;
2087
+ case 'Body': return 1.0;
2088
+ case 'Code': return 0.5;
2089
+ default: return 1.0;
2090
+ }
2091
+ }
2092
+
2093
+ // Load the index
2094
+ async function loadIndex() {
2095
+ if (searchIndex) return searchIndex;
2096
+ if (indexPromise) return indexPromise;
2097
+
2098
+ indexPromise = fetch('${indexPath}')
2099
+ .then(res => res.json())
2100
+ .then(data => {
2101
+ searchIndex = data;
2102
+ return data;
2103
+ })
2104
+ .catch(err => {
2105
+ console.error('[ox-content] Failed to load search index:', err);
2106
+ return null;
2107
+ });
2108
+
2109
+ return indexPromise;
2110
+ }
2111
+
2112
+ // Search function
2113
+ export async function search(query, options = {}) {
2114
+ const index = await loadIndex();
2115
+
2116
+ if (!index || !query.trim()) {
2117
+ return [];
2118
+ }
2119
+
2120
+ const limit = options.limit ?? searchOptions.limit;
2121
+ const prefix = options.prefix ?? searchOptions.prefix;
2122
+ const tokens = tokenizeQuery(query);
2123
+
2124
+ if (tokens.length === 0) {
2125
+ return [];
2126
+ }
2127
+
2128
+ const k1 = 1.2;
2129
+ const b = 0.75;
2130
+ const docScores = new Map();
2131
+
2132
+ for (let i = 0; i < tokens.length; i++) {
2133
+ const token = tokens[i];
2134
+ const isLast = i === tokens.length - 1;
2135
+
2136
+ // Find matching terms
2137
+ let matchingTerms = [];
2138
+ if (prefix && isLast && token.length >= 2) {
2139
+ matchingTerms = Object.keys(index.index).filter(term => term.startsWith(token));
2140
+ } else if (index.index[token]) {
2141
+ matchingTerms = [token];
2142
+ }
2143
+
2144
+ for (const term of matchingTerms) {
2145
+ const postings = index.index[term] || [];
2146
+ const df = index.df[term] || 1;
2147
+ const idf = computeIdf(df, index.doc_count);
2148
+
2149
+ for (const posting of postings) {
2150
+ const doc = index.documents[posting.doc_idx];
2151
+ if (!doc) continue;
2152
+
2153
+ const docLen = doc.body.length;
2154
+ const tf = posting.tf;
2155
+ const boost = getFieldBoost(posting.field);
2156
+
2157
+ const score = idf * ((tf * (k1 + 1.0)) / (tf + k1 * (1.0 - b + b * docLen / index.avg_dl))) * boost;
2158
+
2159
+ if (!docScores.has(posting.doc_idx)) {
2160
+ docScores.set(posting.doc_idx, { score: 0, matches: new Set() });
2161
+ }
2162
+ const entry = docScores.get(posting.doc_idx);
2163
+ entry.score += score;
2164
+ entry.matches.add(term);
2165
+ }
2166
+ }
2167
+ }
2168
+
2169
+ // Convert to results
2170
+ const results = Array.from(docScores.entries())
2171
+ .map(([docIdx, data]) => {
2172
+ const doc = index.documents[docIdx];
2173
+ const matches = Array.from(data.matches);
2174
+
2175
+ // Generate snippet
2176
+ let snippet = '';
2177
+ if (doc.body) {
2178
+ const bodyLower = doc.body.toLowerCase();
2179
+ let firstPos = -1;
2180
+ for (const match of matches) {
2181
+ const pos = bodyLower.indexOf(match);
2182
+ if (pos !== -1 && (firstPos === -1 || pos < firstPos)) {
2183
+ firstPos = pos;
2184
+ }
2185
+ }
2186
+
2187
+ const start = Math.max(0, firstPos - 50);
2188
+ const end = Math.min(doc.body.length, start + 150);
2189
+ snippet = doc.body.slice(start, end);
2190
+ if (start > 0) snippet = '...' + snippet;
2191
+ if (end < doc.body.length) snippet = snippet + '...';
2192
+ }
2193
+
2194
+ return {
2195
+ id: doc.id,
2196
+ title: doc.title,
2197
+ url: doc.url,
2198
+ score: data.score,
2199
+ matches,
2200
+ snippet,
2201
+ };
2202
+ })
2203
+ .sort((a, b) => b.score - a.score)
2204
+ .slice(0, limit);
2205
+
2206
+ return results;
2207
+ }
2208
+
2209
+ export { searchOptions };
2210
+ export default { search, searchOptions, loadIndex };
2211
+ `;
2212
+ }
2213
+
2214
+ // src/index.ts
2215
+ function oxContent2(options = {}) {
2216
+ const resolvedOptions = resolveOptions(options);
2217
+ let config;
2218
+ let _server;
2219
+ const mainPlugin = {
2220
+ name: "ox-content",
2221
+ configResolved(resolvedConfig) {
2222
+ config = resolvedConfig;
2223
+ },
2224
+ configureServer(devServer) {
2225
+ _server = devServer;
2226
+ devServer.middlewares.use(async (req, res, next) => {
2227
+ const url = req.url;
2228
+ if (!url || !url.endsWith(".md")) {
2229
+ return next();
2230
+ }
2231
+ next();
2232
+ });
2233
+ },
2234
+ resolveId(id) {
2235
+ if (id.startsWith("virtual:ox-content/")) {
2236
+ return "\0" + id;
2237
+ }
2238
+ if (id.endsWith(".md")) {
2239
+ return id;
2240
+ }
2241
+ return null;
2242
+ },
2243
+ async load(id) {
2244
+ if (id.startsWith("\0virtual:ox-content/")) {
2245
+ const path6 = id.slice("\0virtual:ox-content/".length);
2246
+ return generateVirtualModule(path6, resolvedOptions);
2247
+ }
2248
+ return null;
2249
+ },
2250
+ async transform(code, id) {
2251
+ if (!id.endsWith(".md")) {
2252
+ return null;
2253
+ }
2254
+ const result = await transformMarkdown(code, id, resolvedOptions);
2255
+ return {
2256
+ code: result.code,
2257
+ map: null
2258
+ };
2259
+ },
2260
+ // Hot Module Replacement support
2261
+ async handleHotUpdate({ file, server }) {
2262
+ if (file.endsWith(".md")) {
2263
+ server.ws.send({
2264
+ type: "custom",
2265
+ event: "ox-content:update",
2266
+ data: { file }
2267
+ });
2268
+ const modules = server.moduleGraph.getModulesByFile(file);
2269
+ return modules ? Array.from(modules) : [];
2270
+ }
2271
+ }
2272
+ };
2273
+ const environmentPlugin = {
2274
+ name: "ox-content:environment",
2275
+ config() {
2276
+ return {
2277
+ environments: {
2278
+ // Markdown processing environment
2279
+ markdown: createMarkdownEnvironment(resolvedOptions)
2280
+ }
2281
+ };
2282
+ }
2283
+ };
2284
+ const docsPlugin = {
2285
+ name: "ox-content:docs",
2286
+ async buildStart() {
2287
+ const docsOptions = resolvedOptions.docs;
2288
+ if (!docsOptions || !docsOptions.enabled) {
2289
+ return;
2290
+ }
2291
+ const root = config?.root || process.cwd();
2292
+ const srcDirs = docsOptions.src.map((src) => path5.resolve(root, src));
2293
+ const outDir = path5.resolve(root, docsOptions.out);
2294
+ try {
2295
+ const extracted = await extractDocs(srcDirs, docsOptions);
2296
+ if (extracted.length > 0) {
2297
+ const generated = generateMarkdown(extracted, docsOptions);
2298
+ await writeDocs(generated, outDir, extracted, docsOptions);
2299
+ console.log(
2300
+ `[ox-content] Generated ${Object.keys(generated).length} documentation files to ${docsOptions.out}`
2301
+ );
2302
+ }
2303
+ } catch (err) {
2304
+ console.warn("[ox-content] Failed to generate documentation:", err);
2305
+ }
2306
+ },
2307
+ configureServer(devServer) {
2308
+ const docsOptions = resolvedOptions.docs;
2309
+ if (!docsOptions || !docsOptions.enabled) {
2310
+ return;
2311
+ }
2312
+ const root = config?.root || process.cwd();
2313
+ const srcDirs = docsOptions.src.map((src) => path5.resolve(root, src));
2314
+ for (const srcDir of srcDirs) {
2315
+ devServer.watcher.add(srcDir);
2316
+ }
2317
+ devServer.watcher.on("change", async (file) => {
2318
+ const isSourceFile = srcDirs.some(
2319
+ (srcDir) => file.startsWith(srcDir) && (file.endsWith(".ts") || file.endsWith(".tsx"))
2320
+ );
2321
+ if (isSourceFile) {
2322
+ const outDir = path5.resolve(root, docsOptions.out);
2323
+ try {
2324
+ const extracted = await extractDocs(srcDirs, docsOptions);
2325
+ if (extracted.length > 0) {
2326
+ const generated = generateMarkdown(extracted, docsOptions);
2327
+ await writeDocs(generated, outDir, extracted, docsOptions);
2328
+ }
2329
+ } catch {
2330
+ }
2331
+ }
2332
+ });
2333
+ }
2334
+ };
2335
+ const ssgPlugin = {
2336
+ name: "ox-content:ssg",
2337
+ async closeBundle() {
2338
+ const ssgOptions = resolvedOptions.ssg;
2339
+ if (!ssgOptions.enabled) {
2340
+ return;
2341
+ }
2342
+ const root = config?.root || process.cwd();
2343
+ try {
2344
+ const result = await buildSsg(resolvedOptions, root);
2345
+ if (result.files.length > 0) {
2346
+ console.log(
2347
+ `[ox-content] Generated ${result.files.length} HTML files`
2348
+ );
2349
+ }
2350
+ if (result.errors.length > 0) {
2351
+ for (const error of result.errors) {
2352
+ console.warn(`[ox-content] ${error}`);
2353
+ }
2354
+ }
2355
+ } catch (err) {
2356
+ console.error("[ox-content] SSG build failed:", err);
2357
+ }
2358
+ }
2359
+ };
2360
+ let searchIndexJson = "";
2361
+ const searchPlugin = {
2362
+ name: "ox-content:search",
2363
+ resolveId(id) {
2364
+ if (id === "virtual:ox-content/search") {
2365
+ return "\0virtual:ox-content/search";
2366
+ }
2367
+ return null;
2368
+ },
2369
+ async load(id) {
2370
+ if (id === "\0virtual:ox-content/search") {
2371
+ const searchOptions = resolvedOptions.search;
2372
+ if (!searchOptions.enabled) {
2373
+ return "export const search = () => []; export const searchOptions = { enabled: false }; export default { search, searchOptions };";
2374
+ }
2375
+ const indexPath = resolvedOptions.base + "search-index.json";
2376
+ return generateSearchModule(searchOptions, indexPath);
2377
+ }
2378
+ return null;
2379
+ },
2380
+ async buildStart() {
2381
+ const searchOptions = resolvedOptions.search;
2382
+ if (!searchOptions.enabled) {
2383
+ return;
2384
+ }
2385
+ const root = config?.root || process.cwd();
2386
+ const srcDir = path5.resolve(root, resolvedOptions.srcDir);
2387
+ try {
2388
+ searchIndexJson = await buildSearchIndex(srcDir, resolvedOptions.base);
2389
+ console.log("[ox-content] Search index built");
2390
+ } catch (err) {
2391
+ console.warn("[ox-content] Failed to build search index:", err);
2392
+ }
2393
+ },
2394
+ async closeBundle() {
2395
+ const searchOptions = resolvedOptions.search;
2396
+ if (!searchOptions.enabled || !searchIndexJson) {
2397
+ return;
2398
+ }
2399
+ const root = config?.root || process.cwd();
2400
+ const outDir = path5.resolve(root, resolvedOptions.outDir);
2401
+ try {
2402
+ await writeSearchIndex(searchIndexJson, outDir);
2403
+ console.log("[ox-content] Search index written to", path5.join(outDir, "search-index.json"));
2404
+ } catch (err) {
2405
+ console.warn("[ox-content] Failed to write search index:", err);
2406
+ }
2407
+ }
2408
+ };
2409
+ return [mainPlugin, environmentPlugin, docsPlugin, ssgPlugin, searchPlugin];
2410
+ }
2411
+ function resolveOptions(options) {
2412
+ return {
2413
+ srcDir: options.srcDir ?? "docs",
2414
+ outDir: options.outDir ?? "dist",
2415
+ base: options.base ?? "/",
2416
+ ssg: resolveSsgOptions(options.ssg),
2417
+ gfm: options.gfm ?? true,
2418
+ footnotes: options.footnotes ?? true,
2419
+ tables: options.tables ?? true,
2420
+ taskLists: options.taskLists ?? true,
2421
+ strikethrough: options.strikethrough ?? true,
2422
+ highlight: options.highlight ?? false,
2423
+ highlightTheme: options.highlightTheme ?? "github-dark",
2424
+ mermaid: options.mermaid ?? false,
2425
+ frontmatter: options.frontmatter ?? true,
2426
+ toc: options.toc ?? true,
2427
+ tocMaxDepth: options.tocMaxDepth ?? 3,
2428
+ ogImage: options.ogImage ?? false,
2429
+ ogImageOptions: options.ogImageOptions ?? {},
2430
+ transformers: options.transformers ?? [],
2431
+ docs: resolveDocsOptions(options.docs),
2432
+ search: resolveSearchOptions(options.search)
2433
+ };
2434
+ }
2435
+ function generateVirtualModule(path6, options) {
2436
+ if (path6 === "config") {
2437
+ return `export default ${JSON.stringify(options)};`;
2438
+ }
2439
+ if (path6 === "runtime") {
2440
+ return `
2441
+ export function useMarkdown() {
2442
+ return {
2443
+ render: (content) => {
2444
+ // Client-side rendering if needed
2445
+ return content;
2446
+ },
2447
+ };
2448
+ }
2449
+ `;
2450
+ }
2451
+ return "export default {};";
2452
+ }
2453
+ export {
2454
+ DEFAULT_HTML_TEMPLATE,
2455
+ buildSearchIndex,
2456
+ buildSsg,
2457
+ createMarkdownEnvironment,
2458
+ extractDocs,
2459
+ generateMarkdown,
2460
+ oxContent2 as oxContent,
2461
+ resolveDocsOptions,
2462
+ resolveSearchOptions,
2463
+ resolveSsgOptions,
2464
+ transformMarkdown,
2465
+ writeDocs,
2466
+ writeSearchIndex
2467
+ };
2468
+ //# sourceMappingURL=index.js.map