@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.
@@ -0,0 +1,811 @@
1
+ import { EnvironmentOptions, Plugin } from 'vite';
2
+
3
+ /**
4
+ * Type definitions for vite-plugin-ox-content
5
+ */
6
+ /**
7
+ * SSG (Static Site Generation) options.
8
+ */
9
+ interface SsgOptions {
10
+ /**
11
+ * Enable SSG mode.
12
+ * @default true
13
+ */
14
+ enabled?: boolean;
15
+ /**
16
+ * Output file extension.
17
+ * @default '.html'
18
+ */
19
+ extension?: string;
20
+ /**
21
+ * Clean output directory before build.
22
+ * @default false
23
+ */
24
+ clean?: boolean;
25
+ /**
26
+ * Bare HTML output (no navigation, no styles).
27
+ * Useful for benchmarking or when using custom layouts.
28
+ * @default false
29
+ */
30
+ bare?: boolean;
31
+ /**
32
+ * Site name for header and title suffix.
33
+ */
34
+ siteName?: string;
35
+ /**
36
+ * OG image URL for social sharing (static URL).
37
+ * If generateOgImage is enabled, this serves as the fallback.
38
+ */
39
+ ogImage?: string;
40
+ /**
41
+ * Generate OG images per page using Rust-based generator.
42
+ * When enabled, each page will have a unique OG image.
43
+ * @default false
44
+ */
45
+ generateOgImage?: boolean;
46
+ /**
47
+ * Site URL for generating absolute OG image URLs.
48
+ * Required for proper SNS sharing.
49
+ * Example: 'https://example.com'
50
+ */
51
+ siteUrl?: string;
52
+ }
53
+ /**
54
+ * Resolved SSG options.
55
+ */
56
+ interface ResolvedSsgOptions {
57
+ enabled: boolean;
58
+ extension: string;
59
+ clean: boolean;
60
+ bare: boolean;
61
+ siteName?: string;
62
+ ogImage?: string;
63
+ generateOgImage: boolean;
64
+ siteUrl?: string;
65
+ }
66
+ /**
67
+ * Plugin options.
68
+ */
69
+ interface OxContentOptions {
70
+ /**
71
+ * Source directory for Markdown files.
72
+ * @default 'docs'
73
+ */
74
+ srcDir?: string;
75
+ /**
76
+ * Output directory for built files.
77
+ * @default 'dist'
78
+ */
79
+ outDir?: string;
80
+ /**
81
+ * Base path for the site.
82
+ * @default '/'
83
+ */
84
+ base?: string;
85
+ /**
86
+ * SSG (Static Site Generation) options.
87
+ * Set to false to disable SSG completely.
88
+ * @default { enabled: true }
89
+ */
90
+ ssg?: SsgOptions | boolean;
91
+ /**
92
+ * Enable GitHub Flavored Markdown extensions.
93
+ * @default true
94
+ */
95
+ gfm?: boolean;
96
+ /**
97
+ * Enable footnotes.
98
+ * @default true
99
+ */
100
+ footnotes?: boolean;
101
+ /**
102
+ * Enable tables.
103
+ * @default true
104
+ */
105
+ tables?: boolean;
106
+ /**
107
+ * Enable task lists.
108
+ * @default true
109
+ */
110
+ taskLists?: boolean;
111
+ /**
112
+ * Enable strikethrough.
113
+ * @default true
114
+ */
115
+ strikethrough?: boolean;
116
+ /**
117
+ * Enable syntax highlighting for code blocks.
118
+ * @default false
119
+ */
120
+ highlight?: boolean;
121
+ /**
122
+ * Syntax highlighting theme.
123
+ * @default 'github-dark'
124
+ */
125
+ highlightTheme?: string;
126
+ /**
127
+ * Enable mermaid diagram rendering.
128
+ * @default false
129
+ */
130
+ mermaid?: boolean;
131
+ /**
132
+ * Parse YAML frontmatter.
133
+ * @default true
134
+ */
135
+ frontmatter?: boolean;
136
+ /**
137
+ * Generate table of contents.
138
+ * @default true
139
+ */
140
+ toc?: boolean;
141
+ /**
142
+ * Maximum heading depth for TOC.
143
+ * @default 3
144
+ */
145
+ tocMaxDepth?: number;
146
+ /**
147
+ * Enable OG image generation.
148
+ * @default false
149
+ */
150
+ ogImage?: boolean;
151
+ /**
152
+ * OG image generation options.
153
+ */
154
+ ogImageOptions?: OgImageOptions;
155
+ /**
156
+ * Custom AST transformers.
157
+ */
158
+ transformers?: MarkdownTransformer[];
159
+ /**
160
+ * Source documentation generation options.
161
+ * Set to false to disable (opt-out).
162
+ * @default { enabled: true }
163
+ */
164
+ docs?: DocsOptions | false;
165
+ /**
166
+ * Full-text search options.
167
+ * Set to false to disable search.
168
+ * @default { enabled: true }
169
+ */
170
+ search?: SearchOptions | boolean;
171
+ }
172
+ /**
173
+ * Resolved options with all defaults applied.
174
+ */
175
+ interface ResolvedOptions {
176
+ srcDir: string;
177
+ outDir: string;
178
+ base: string;
179
+ ssg: ResolvedSsgOptions;
180
+ gfm: boolean;
181
+ footnotes: boolean;
182
+ tables: boolean;
183
+ taskLists: boolean;
184
+ strikethrough: boolean;
185
+ highlight: boolean;
186
+ highlightTheme: string;
187
+ mermaid: boolean;
188
+ frontmatter: boolean;
189
+ toc: boolean;
190
+ tocMaxDepth: number;
191
+ ogImage: boolean;
192
+ ogImageOptions: OgImageOptions;
193
+ transformers: MarkdownTransformer[];
194
+ docs: ResolvedDocsOptions | false;
195
+ search: ResolvedSearchOptions;
196
+ }
197
+ /**
198
+ * OG image generation options.
199
+ */
200
+ interface OgImageOptions {
201
+ /**
202
+ * Background color.
203
+ * @default '#1a1a2e'
204
+ */
205
+ background?: string;
206
+ /**
207
+ * Text color.
208
+ * @default '#ffffff'
209
+ */
210
+ textColor?: string;
211
+ /**
212
+ * Accent color.
213
+ * @default '#e94560'
214
+ */
215
+ accentColor?: string;
216
+ /**
217
+ * Font family.
218
+ */
219
+ fontFamily?: string;
220
+ /**
221
+ * Image width.
222
+ * @default 1200
223
+ */
224
+ width?: number;
225
+ /**
226
+ * Image height.
227
+ * @default 630
228
+ */
229
+ height?: number;
230
+ }
231
+ /**
232
+ * Custom AST transformer.
233
+ */
234
+ interface MarkdownTransformer {
235
+ /**
236
+ * Transformer name.
237
+ */
238
+ name: string;
239
+ /**
240
+ * Transform function.
241
+ */
242
+ transform: (ast: MarkdownNode, context: TransformContext) => MarkdownNode | Promise<MarkdownNode>;
243
+ }
244
+ /**
245
+ * Transform context passed to transformers.
246
+ */
247
+ interface TransformContext {
248
+ /**
249
+ * File path being processed.
250
+ */
251
+ filePath: string;
252
+ /**
253
+ * Frontmatter data.
254
+ */
255
+ frontmatter: Record<string, unknown>;
256
+ /**
257
+ * Resolved plugin options.
258
+ */
259
+ options: ResolvedOptions;
260
+ }
261
+ /**
262
+ * Markdown AST node (simplified for TypeScript).
263
+ */
264
+ interface MarkdownNode {
265
+ type: string;
266
+ children?: MarkdownNode[];
267
+ value?: string;
268
+ [key: string]: unknown;
269
+ }
270
+ /**
271
+ * Transform result.
272
+ */
273
+ interface TransformResult {
274
+ /**
275
+ * Generated JavaScript code.
276
+ */
277
+ code: string;
278
+ /**
279
+ * Source map (null means no source map).
280
+ */
281
+ map?: null;
282
+ /**
283
+ * Rendered HTML.
284
+ */
285
+ html: string;
286
+ /**
287
+ * Parsed frontmatter.
288
+ */
289
+ frontmatter: Record<string, unknown>;
290
+ /**
291
+ * Table of contents.
292
+ */
293
+ toc: TocEntry[];
294
+ }
295
+ /**
296
+ * Table of contents entry.
297
+ */
298
+ interface TocEntry {
299
+ /**
300
+ * Heading depth (1-6).
301
+ */
302
+ depth: number;
303
+ /**
304
+ * Heading text.
305
+ */
306
+ text: string;
307
+ /**
308
+ * Slug/ID for linking.
309
+ */
310
+ slug: string;
311
+ /**
312
+ * Child entries.
313
+ */
314
+ children: TocEntry[];
315
+ }
316
+ /**
317
+ * Options for source documentation generation.
318
+ */
319
+ interface DocsOptions {
320
+ /**
321
+ * Enable/disable docs generation.
322
+ * @default true (opt-out)
323
+ */
324
+ enabled?: boolean;
325
+ /**
326
+ * Source directories to scan for documentation.
327
+ * @default ['./src']
328
+ */
329
+ src?: string[];
330
+ /**
331
+ * Output directory for generated documentation.
332
+ * @default 'docs/api'
333
+ */
334
+ out?: string;
335
+ /**
336
+ * Glob patterns for files to include.
337
+ * @default ['**\/*.ts', '**\/*.tsx']
338
+ */
339
+ include?: string[];
340
+ /**
341
+ * Glob patterns for files to exclude.
342
+ * @default ['**\/*.test.*', '**\/*.spec.*', 'node_modules']
343
+ */
344
+ exclude?: string[];
345
+ /**
346
+ * Output format.
347
+ * @default 'markdown'
348
+ */
349
+ format?: 'markdown' | 'json' | 'html';
350
+ /**
351
+ * Include private members in documentation.
352
+ * @default false
353
+ */
354
+ private?: boolean;
355
+ /**
356
+ * Generate table of contents for each file.
357
+ * @default true
358
+ */
359
+ toc?: boolean;
360
+ /**
361
+ * Group documentation by file or category.
362
+ * @default 'file'
363
+ */
364
+ groupBy?: 'file' | 'category';
365
+ /**
366
+ * GitHub repository URL for source code links.
367
+ * When provided, generated documentation will include links to source code.
368
+ * Example: 'https://github.com/ubugeeei/ox-content'
369
+ */
370
+ githubUrl?: string;
371
+ /**
372
+ * Generate navigation metadata file.
373
+ * @default true
374
+ */
375
+ generateNav?: boolean;
376
+ }
377
+ /**
378
+ * Resolved docs options with all defaults applied.
379
+ */
380
+ interface ResolvedDocsOptions {
381
+ enabled: boolean;
382
+ src: string[];
383
+ out: string;
384
+ include: string[];
385
+ exclude: string[];
386
+ format: 'markdown' | 'json' | 'html';
387
+ private: boolean;
388
+ toc: boolean;
389
+ groupBy: 'file' | 'category';
390
+ githubUrl?: string;
391
+ generateNav: boolean;
392
+ }
393
+ /**
394
+ * A single documentation entry extracted from source.
395
+ */
396
+ interface DocEntry {
397
+ name: string;
398
+ kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'module';
399
+ description: string;
400
+ params?: ParamDoc[];
401
+ returns?: ReturnDoc;
402
+ examples?: string[];
403
+ tags?: Record<string, string>;
404
+ private?: boolean;
405
+ file: string;
406
+ line: number;
407
+ signature?: string;
408
+ }
409
+ /**
410
+ * Parameter documentation.
411
+ */
412
+ interface ParamDoc {
413
+ name: string;
414
+ type: string;
415
+ description: string;
416
+ optional?: boolean;
417
+ default?: string;
418
+ }
419
+ /**
420
+ * Return type documentation.
421
+ */
422
+ interface ReturnDoc {
423
+ type: string;
424
+ description: string;
425
+ }
426
+ /**
427
+ * Extracted documentation for a single file.
428
+ */
429
+ interface ExtractedDocs {
430
+ file: string;
431
+ entries: DocEntry[];
432
+ }
433
+ /**
434
+ * Navigation item for sidebar navigation.
435
+ */
436
+ interface NavItem {
437
+ /**
438
+ * Display title for the navigation item.
439
+ */
440
+ title: string;
441
+ /**
442
+ * Path to the documentation page.
443
+ */
444
+ path: string;
445
+ /**
446
+ * Child navigation items (optional).
447
+ */
448
+ children?: NavItem[];
449
+ }
450
+ /**
451
+ * Options for full-text search.
452
+ */
453
+ interface SearchOptions {
454
+ /**
455
+ * Enable search functionality.
456
+ * @default true
457
+ */
458
+ enabled?: boolean;
459
+ /**
460
+ * Maximum number of search results.
461
+ * @default 10
462
+ */
463
+ limit?: number;
464
+ /**
465
+ * Enable prefix matching for autocomplete.
466
+ * @default true
467
+ */
468
+ prefix?: boolean;
469
+ /**
470
+ * Placeholder text for the search input.
471
+ * @default 'Search documentation...'
472
+ */
473
+ placeholder?: string;
474
+ /**
475
+ * Keyboard shortcut to focus search (without modifier).
476
+ * @default '/'
477
+ */
478
+ hotkey?: string;
479
+ }
480
+ /**
481
+ * Resolved search options.
482
+ */
483
+ interface ResolvedSearchOptions {
484
+ enabled: boolean;
485
+ limit: number;
486
+ prefix: boolean;
487
+ placeholder: string;
488
+ hotkey: string;
489
+ }
490
+ /**
491
+ * Search document structure.
492
+ */
493
+ interface SearchDocument {
494
+ id: string;
495
+ title: string;
496
+ url: string;
497
+ body: string;
498
+ headings: string[];
499
+ code: string[];
500
+ }
501
+ /**
502
+ * Search result structure.
503
+ */
504
+ interface SearchResult {
505
+ id: string;
506
+ title: string;
507
+ url: string;
508
+ score: number;
509
+ matches: string[];
510
+ snippet: string;
511
+ }
512
+
513
+ /**
514
+ * Vite Environment API integration for Ox Content.
515
+ *
516
+ * Creates a dedicated environment for Markdown processing,
517
+ * enabling SSG-style rendering with separate client/server contexts.
518
+ */
519
+
520
+ /**
521
+ * Creates the Markdown processing environment configuration.
522
+ *
523
+ * This environment is used for:
524
+ * - Server-side rendering of Markdown files
525
+ * - Static site generation
526
+ * - Pre-rendering at build time
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * // In your vite.config.ts
531
+ * export default defineConfig({
532
+ * environments: {
533
+ * markdown: createMarkdownEnvironment({
534
+ * srcDir: 'docs',
535
+ * gfm: true,
536
+ * }),
537
+ * },
538
+ * });
539
+ * ```
540
+ */
541
+ declare function createMarkdownEnvironment(options: ResolvedOptions): EnvironmentOptions;
542
+
543
+ /**
544
+ * Markdown Transformation Engine
545
+ *
546
+ * This module handles the complete transformation pipeline for Markdown files,
547
+ * converting raw Markdown content into JavaScript modules that can be imported
548
+ * by web applications. The transformation process includes:
549
+ *
550
+ * 1. **Parsing**: Uses Rust-based parser via NAPI bindings for high performance
551
+ * 2. **Rendering**: Converts parsed AST to semantic HTML
552
+ * 3. **Enhancement**: Applies syntax highlighting, Mermaid diagram rendering, etc.
553
+ * 4. **Code Generation**: Generates JavaScript/TypeScript module code
554
+ *
555
+ * The generated modules export:
556
+ * - `html`: Rendered HTML content
557
+ * - `frontmatter`: Parsed YAML metadata
558
+ * - `toc`: Hierarchical table of contents
559
+ * - `render`: Client-side render function for dynamic updates
560
+ *
561
+ * @example
562
+ * ```typescript
563
+ * import { transformMarkdown } from './transform';
564
+ *
565
+ * const content = await transformMarkdown(
566
+ * '# Hello\n\nWorld',
567
+ * 'path/to/file.md',
568
+ * resolvedOptions
569
+ * );
570
+ *
571
+ * console.log(content.html); // '<h1>Hello</h1><p>World</p>'
572
+ * console.log(content.toc); // [{ depth: 1, text: 'Hello', slug: 'hello', children: [] }]
573
+ * ```
574
+ */
575
+
576
+ /**
577
+ * Transforms Markdown content into a JavaScript module.
578
+ *
579
+ * This is the primary entry point for transforming Markdown files. It handles
580
+ * the complete transformation pipeline including parsing, rendering, syntax
581
+ * highlighting, and code generation.
582
+ *
583
+ * ## Pipeline Steps
584
+ *
585
+ * 1. **Parse & Render**: Uses Rust-based parser via NAPI for high performance
586
+ * 2. **Extract Metadata**: Parses YAML frontmatter and generates table of contents
587
+ * 3. **Enhance HTML**: Applies syntax highlighting and Mermaid diagram rendering
588
+ * 4. **Generate Code**: Creates importable JavaScript module
589
+ *
590
+ * ## Generated Module Exports
591
+ *
592
+ * - `html` (string): Rendered HTML content with all enhancements applied
593
+ * - `frontmatter` (object): Parsed YAML frontmatter as JavaScript object
594
+ * - `toc` (array): Hierarchical table of contents entries
595
+ * - `render` (function): Client-side render function for dynamic updates
596
+ *
597
+ * ## Markdown Features Supported
598
+ *
599
+ * The supported features depend on parser options:
600
+ * - **Commonmark**: Headings, paragraphs, lists, code blocks, links, images
601
+ * - **GFM Extensions**: Tables, task lists, strikethrough, autolinks
602
+ * - **Enhancements**: Syntax highlighting, Mermaid diagrams, TOC generation
603
+ * - **Metadata**: YAML frontmatter parsing
604
+ *
605
+ * ## Performance
606
+ *
607
+ * Uses Rust-based parsing via NAPI bindings for optimal performance. Falls back
608
+ * gracefully if Rust bindings are unavailable.
609
+ *
610
+ * @param source - Raw Markdown source code (may include YAML frontmatter)
611
+ * @param filePath - File path for source attribution and relative link resolution
612
+ * @param options - Resolved plugin options controlling transformation behavior
613
+ *
614
+ * @returns Promise resolving to transformation result with HTML and metadata
615
+ *
616
+ * @throws Error if NAPI bindings are unavailable (can be handled gracefully)
617
+ *
618
+ * @example
619
+ * ```typescript
620
+ * import { transformMarkdown } from './transform';
621
+ * import { resolveOptions } from './index';
622
+ *
623
+ * // Transform a Markdown file with YAML frontmatter
624
+ * const markdown = `---
625
+ * title: Getting Started
626
+ * author: john
627
+ * ---
628
+ *
629
+ * # Getting Started
630
+ *
631
+ * Welcome! This guide explains [transformMarkdown] function.
632
+ *
633
+ * ## Installation
634
+ *
635
+ * \`\`\`bash
636
+ * npm install vite-plugin-ox-content
637
+ * \`\`\`
638
+ * `;
639
+ *
640
+ * const options = resolveOptions({
641
+ * highlight: true,
642
+ * highlightTheme: 'github-dark',
643
+ * toc: true,
644
+ * gfm: true,
645
+ * mermaid: true,
646
+ * });
647
+ *
648
+ * const result = await transformMarkdown(markdown, 'docs/getting-started.md', options);
649
+ *
650
+ * // Generated module exports
651
+ * console.log(result.html); // Rendered HTML with syntax highlighting
652
+ * console.log(result.frontmatter); // { title: 'Getting Started', author: 'john' }
653
+ * console.log(result.toc); // [{ depth: 1, text: 'Getting Started', ... }]
654
+ * console.log(result.code); // ES module export statement
655
+ * ```
656
+ */
657
+ /**
658
+ * SSG-specific transform options.
659
+ */
660
+ interface SsgTransformOptions {
661
+ /** Convert `.md` links to `.html` links */
662
+ convertMdLinks?: boolean;
663
+ /** Base URL for absolute link conversion */
664
+ baseUrl?: string;
665
+ }
666
+ declare function transformMarkdown(source: string, filePath: string, options: ResolvedOptions, ssgOptions?: SsgTransformOptions): Promise<TransformResult>;
667
+
668
+ /**
669
+ * Extracts JSDoc documentation from source files in specified directories.
670
+ *
671
+ * This function recursively searches directories for source files matching
672
+ * the include/exclude patterns, then extracts all documented items (functions,
673
+ * classes, interfaces, types) from those files.
674
+ *
675
+ * ## Process
676
+ *
677
+ * 1. **File Discovery**: Recursively walks directories, applying filters
678
+ * 2. **File Reading**: Loads each matching file's content
679
+ * 3. **JSDoc Extraction**: Parses JSDoc comments using regex patterns
680
+ * 4. **Declaration Matching**: Pairs JSDoc comments with source declarations
681
+ * 5. **Result Collection**: Aggregates extracted documentation by file
682
+ *
683
+ * ## Include/Exclude Patterns
684
+ *
685
+ * Patterns support:
686
+ * - `**` - Match any directory structure
687
+ * - `*` - Match any filename
688
+ * - Standard glob patterns (e.g., `**\/*.test.ts`)
689
+ *
690
+ * ## Performance Considerations
691
+ *
692
+ * - Uses filesystem I/O which can be slow for large codebases
693
+ * - Consider using more specific include patterns to reduce file scanning
694
+ * - Results are not cached; call once per build/dev session
695
+ *
696
+ * @param srcDirs - Array of source directory paths to scan
697
+ * @param options - Documentation extraction options (filters, grouping, etc.)
698
+ *
699
+ * @returns Promise resolving to array of extracted documentation by file.
700
+ * Each ExtractedDocs object contains file path and array of DocEntry items.
701
+ *
702
+ * @example
703
+ * ```typescript
704
+ * const docs = await extractDocs(
705
+ * ['./packages/vite-plugin/src'],
706
+ * {
707
+ * enabled: true,
708
+ * src: [],
709
+ * out: 'docs',
710
+ * include: ['**\/*.ts'],
711
+ * exclude: ['**\/*.test.ts', '**\/*.spec.ts'],
712
+ * format: 'markdown',
713
+ * private: false,
714
+ * toc: true,
715
+ * groupBy: 'file',
716
+ * generateNav: true,
717
+ * }
718
+ * );
719
+ *
720
+ * // Returns:
721
+ * // [
722
+ * // {
723
+ * // file: '/path/to/transform.ts',
724
+ * // entries: [
725
+ * // { name: 'transformMarkdown', kind: 'function', ... },
726
+ * // { name: 'loadNapiBindings', kind: 'function', ... },
727
+ * // ]
728
+ * // },
729
+ * // ...
730
+ * // ]
731
+ * ```
732
+ */
733
+ declare function extractDocs(srcDirs: string[], options: ResolvedDocsOptions): Promise<ExtractedDocs[]>;
734
+ /**
735
+ * Generates Markdown documentation from extracted docs.
736
+ */
737
+ declare function generateMarkdown(docs: ExtractedDocs[], options: ResolvedDocsOptions): Record<string, string>;
738
+ /**
739
+ * Writes generated documentation to the output directory.
740
+ */
741
+ declare function writeDocs(docs: Record<string, string>, outDir: string, extractedDocs?: ExtractedDocs[], options?: ResolvedDocsOptions): Promise<void>;
742
+ declare function resolveDocsOptions(options: DocsOptions | false | undefined): ResolvedDocsOptions | false;
743
+
744
+ /**
745
+ * SSG (Static Site Generation) module for ox-content
746
+ */
747
+
748
+ /**
749
+ * Default HTML template for SSG pages with navigation.
750
+ */
751
+ declare const DEFAULT_HTML_TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>{{title}}{{#siteName}} - {{siteName}}{{/siteName}}</title>\n {{#description}}<meta name=\"description\" content=\"{{description}}\">{{/description}}\n <!-- Open Graph -->\n <meta property=\"og:type\" content=\"website\">\n <meta property=\"og:title\" content=\"{{title}}{{#siteName}} - {{siteName}}{{/siteName}}\">\n {{#description}}<meta property=\"og:description\" content=\"{{description}}\">{{/description}}\n {{#ogImage}}<meta property=\"og:image\" content=\"{{ogImage}}\">{{/ogImage}}\n <!-- Twitter Card -->\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n <meta name=\"twitter:title\" content=\"{{title}}{{#siteName}} - {{siteName}}{{/siteName}}\">\n {{#description}}<meta name=\"twitter:description\" content=\"{{description}}\">{{/description}}\n {{#ogImage}}<meta name=\"twitter:image\" content=\"{{ogImage}}\">{{/ogImage}}\n <style>\n :root {\n --sidebar-width: 260px;\n --header-height: 60px;\n --max-content-width: 960px;\n --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n --color-bg: #ffffff;\n --color-bg-alt: #f8f9fa;\n --color-text: #1a1a1a;\n --color-text-muted: #666666;\n --color-border: #e5e7eb;\n --color-primary: #b7410e;\n --color-primary-hover: #ce5937;\n --color-code-bg: #1e293b;\n --color-code-text: #e2e8f0;\n }\n [data-theme=\"dark\"] {\n --color-bg: #0f172a;\n --color-bg-alt: #1e293b;\n --color-text: #e2e8f0;\n --color-text-muted: #94a3b8;\n --color-border: #334155;\n --color-primary: #e67e4d;\n --color-primary-hover: #f4a07a;\n --color-code-bg: #0f172a;\n --color-code-text: #e2e8f0;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n --color-bg: #0f172a;\n --color-bg-alt: #1e293b;\n --color-text: #e2e8f0;\n --color-text-muted: #94a3b8;\n --color-border: #334155;\n --color-primary: #e67e4d;\n --color-primary-hover: #f4a07a;\n --color-code-bg: #0f172a;\n --color-code-text: #e2e8f0;\n }\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n html { scroll-behavior: smooth; }\n body {\n font-family: var(--font-sans);\n line-height: 1.7;\n color: var(--color-text);\n background: var(--color-bg);\n }\n a { color: var(--color-primary); text-decoration: none; }\n a:hover { color: var(--color-primary-hover); text-decoration: underline; }\n\n /* Header */\n .header {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: var(--header-height);\n background: var(--color-bg);\n border-bottom: 1px solid var(--color-border);\n display: flex;\n align-items: center;\n padding: 0 1.5rem;\n z-index: 100;\n }\n .header-title {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--color-text);\n }\n .header-title:hover { text-decoration: none; }\n .menu-toggle {\n display: none;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0.5rem;\n margin-right: 0.75rem;\n }\n .menu-toggle svg { display: block; }\n .menu-toggle path { stroke: var(--color-text); }\n .header-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }\n .search-button {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 0.75rem;\n background: var(--color-bg-alt);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n color: var(--color-text-muted);\n cursor: pointer;\n font-size: 0.875rem;\n transition: border-color 0.15s, color 0.15s;\n }\n .search-button:hover { border-color: var(--color-primary); color: var(--color-text); }\n .search-button svg { width: 16px; height: 16px; }\n .search-button kbd {\n padding: 0.125rem 0.375rem;\n background: var(--color-bg);\n border: 1px solid var(--color-border);\n border-radius: 4px;\n font-family: var(--font-mono);\n font-size: 0.75rem;\n }\n @media (max-width: 640px) {\n .search-button span, .search-button kbd { display: none; }\n .search-button { padding: 0.5rem; }\n }\n .search-modal-overlay {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 200;\n background: rgba(0,0,0,0.6);\n backdrop-filter: blur(4px);\n justify-content: center;\n padding-top: 10vh;\n }\n .search-modal-overlay.open { display: flex; }\n .search-modal {\n width: 100%;\n max-width: 560px;\n margin: 0 1rem;\n background: var(--color-bg);\n border: 1px solid var(--color-border);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4);\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n }\n .search-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 1rem;\n border-bottom: 1px solid var(--color-border);\n }\n .search-header svg { flex-shrink: 0; color: var(--color-text-muted); }\n .search-input {\n flex: 1;\n background: none;\n border: none;\n outline: none;\n font-size: 1rem;\n color: var(--color-text);\n }\n .search-input::placeholder { color: var(--color-text-muted); }\n .search-close {\n padding: 0.25rem 0.5rem;\n background: var(--color-bg-alt);\n border: 1px solid var(--color-border);\n border-radius: 4px;\n color: var(--color-text-muted);\n font-family: var(--font-mono);\n font-size: 0.75rem;\n cursor: pointer;\n }\n .search-results {\n flex: 1;\n overflow-y: auto;\n padding: 0.5rem;\n }\n .search-result {\n display: block;\n padding: 0.75rem 1rem;\n border-radius: 8px;\n color: var(--color-text);\n text-decoration: none;\n }\n .search-result:hover, .search-result.selected { background: var(--color-bg-alt); text-decoration: none; }\n .search-result-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 0.25rem; }\n .search-result-snippet { font-size: 0.8125rem; color: var(--color-text-muted); }\n .search-empty { padding: 2rem 1rem; text-align: center; color: var(--color-text-muted); }\n .search-footer {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n padding: 0.75rem 1rem;\n border-top: 1px solid var(--color-border);\n background: var(--color-bg-alt);\n font-size: 0.75rem;\n color: var(--color-text-muted);\n }\n .search-footer kbd {\n padding: 0.125rem 0.375rem;\n background: var(--color-bg);\n border: 1px solid var(--color-border);\n border-radius: 4px;\n font-family: var(--font-mono);\n }\n .theme-toggle {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0.5rem;\n border-radius: 6px;\n color: var(--color-text-muted);\n transition: background 0.15s, color 0.15s;\n }\n .theme-toggle:hover { background: var(--color-bg-alt); color: var(--color-text); }\n .theme-toggle svg { display: block; width: 20px; height: 20px; }\n .theme-toggle .icon-sun { display: none; }\n .theme-toggle .icon-moon { display: block; }\n [data-theme=\"dark\"] .theme-toggle .icon-sun { display: block; }\n [data-theme=\"dark\"] .theme-toggle .icon-moon { display: none; }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) .theme-toggle .icon-sun { display: block; }\n :root:not([data-theme=\"light\"]) .theme-toggle .icon-moon { display: none; }\n }\n\n /* Layout */\n .layout {\n display: flex;\n padding-top: var(--header-height);\n min-height: 100vh;\n }\n\n /* Sidebar */\n .sidebar {\n position: fixed;\n top: var(--header-height);\n left: 0;\n bottom: 0;\n width: var(--sidebar-width);\n background: var(--color-bg-alt);\n border-right: 1px solid var(--color-border);\n overflow-y: auto;\n padding: 1.5rem 1rem;\n }\n .nav-section { margin-bottom: 1.5rem; }\n .nav-title {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--color-text-muted);\n margin-bottom: 0.5rem;\n padding: 0 0.75rem;\n }\n .nav-list { list-style: none; }\n .nav-item { margin: 0.125rem 0; }\n .nav-link {\n display: block;\n padding: 0.5rem 0.75rem;\n border-radius: 6px;\n color: var(--color-text);\n font-size: 0.875rem;\n transition: background 0.15s;\n }\n .nav-link:hover {\n background: var(--color-border);\n text-decoration: none;\n }\n .nav-link.active {\n background: var(--color-primary);\n color: white;\n }\n\n /* Main content */\n .main {\n flex: 1;\n margin-left: var(--sidebar-width);\n padding: 2rem;\n min-width: 0;\n overflow-x: hidden;\n }\n .content {\n max-width: var(--max-content-width);\n margin: 0 auto;\n overflow-wrap: break-word;\n word-wrap: break-word;\n word-break: break-word;\n }\n\n /* TOC (right sidebar) */\n .toc {\n position: fixed;\n top: calc(var(--header-height) + 2rem);\n right: 2rem;\n width: 200px;\n font-size: 0.8125rem;\n }\n .toc-title {\n font-weight: 600;\n margin-bottom: 0.75rem;\n color: var(--color-text-muted);\n }\n .toc-list { list-style: none; }\n .toc-item { margin: 0.375rem 0; }\n .toc-link {\n color: var(--color-text-muted);\n display: block;\n padding-left: calc((var(--depth, 1) - 1) * 0.75rem);\n }\n .toc-link:hover { color: var(--color-primary); }\n @media (max-width: 1200px) { .toc { display: none; } }\n\n /* Typography */\n .content h1 { font-size: 2.25rem; margin-bottom: 1rem; line-height: 1.2; }\n .content h2 { font-size: 1.5rem; margin-top: 2.5rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-border); }\n .content h3 { font-size: 1.25rem; margin-top: 2rem; margin-bottom: 0.75rem; }\n .content h4 { font-size: 1rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }\n .content p { margin-bottom: 1rem; }\n .content ul, .content ol { margin: 1rem 0; padding-left: 1.5rem; }\n .content li { margin: 0.375rem 0; }\n .content blockquote {\n border-left: 4px solid var(--color-primary);\n padding: 0.5rem 1rem;\n margin: 1rem 0;\n background: var(--color-bg-alt);\n border-radius: 0 6px 6px 0;\n }\n .content code {\n font-family: var(--font-mono);\n font-size: 0.875em;\n background: var(--color-bg-alt);\n padding: 0.2em 0.4em;\n border-radius: 4px;\n word-break: break-all;\n }\n .content pre {\n background: var(--color-code-bg);\n color: var(--color-code-text);\n padding: 1rem 1.25rem;\n border-radius: 8px;\n overflow-x: auto;\n margin: 1.5rem 0;\n line-height: 1.5;\n }\n .content pre code {\n background: transparent;\n padding: 0;\n font-size: 0.8125rem;\n }\n .content table {\n width: 100%;\n border-collapse: collapse;\n margin: 1.5rem 0;\n font-size: 0.875rem;\n }\n .content th, .content td {\n border: 1px solid var(--color-border);\n padding: 0.75rem 1rem;\n text-align: left;\n }\n .content th { background: var(--color-bg-alt); font-weight: 600; }\n .content img { max-width: 100%; height: auto; border-radius: 8px; display: block; }\n .content img[alt*=\"Logo\"] { max-width: 200px; display: block; margin: 1rem 0; }\n .content img[alt*=\"Architecture\"] { max-width: 600px; }\n .content img[alt*=\"Benchmark\"] { max-width: 680px; }\n .content hr { border: none; border-top: 1px solid var(--color-border); margin: 2rem 0; }\n\n /* Responsive */\n @media (max-width: 768px) {\n .menu-toggle { display: block; }\n .sidebar {\n transform: translateX(-100%);\n transition: transform 0.3s ease;\n z-index: 99;\n width: 280px;\n }\n .sidebar.open { transform: translateX(0); }\n .main { margin-left: 0; padding: 1rem 0.75rem; }\n .content { padding: 0 0.25rem; }\n .content h1 { font-size: 1.5rem; line-height: 1.3; margin-bottom: 0.75rem; }\n .content h2 { font-size: 1.2rem; margin-top: 2rem; }\n .content h3 { font-size: 1.1rem; }\n .content p { font-size: 0.9375rem; margin-bottom: 0.875rem; }\n .content ul, .content ol { padding-left: 1.25rem; font-size: 0.9375rem; }\n .content pre {\n padding: 0.75rem;\n font-size: 0.75rem;\n margin: 1rem -0.75rem;\n border-radius: 0;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .content code { font-size: 0.8125em; }\n .content table {\n display: block;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n font-size: 0.8125rem;\n margin: 1rem -0.75rem;\n width: calc(100% + 1.5rem);\n }\n .content th, .content td { padding: 0.5rem 0.75rem; white-space: nowrap; }\n .content img { margin: 1rem 0; }\n .content img[alt*=\"Logo\"] { max-width: 150px; }\n .content img[alt*=\"Architecture\"] { max-width: 100%; }\n .content img[alt*=\"Benchmark\"] { max-width: 100%; }\n .content blockquote { padding: 0.5rem 0.75rem; margin: 1rem 0; font-size: 0.9375rem; }\n .header { padding: 0 1rem; }\n .header-title { font-size: 1rem; }\n .header-title img { width: 24px; height: 24px; }\n .overlay {\n display: none;\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.5);\n z-index: 98;\n }\n .overlay.open { display: block; }\n }\n\n /* Extra small devices */\n @media (max-width: 480px) {\n .main { padding: 0.75rem 0.5rem; }\n .content h1 { font-size: 1.35rem; }\n .content pre { font-size: 0.6875rem; padding: 0.625rem; }\n .content table { font-size: 0.75rem; }\n .content th, .content td { padding: 0.375rem 0.5rem; }\n }\n </style>\n</head>\n<body>\n <header class=\"header\">\n <button class=\"menu-toggle\" aria-label=\"Toggle menu\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 12h18M3 6h18M3 18h18\"/>\n </svg>\n </button>\n <a href=\"{{base}}index.html\" class=\"header-title\">\n <img src=\"{{base}}logo.svg\" alt=\"\" width=\"28\" height=\"28\" style=\"margin-right: 8px; vertical-align: middle;\" />\n {{siteName}}\n </a>\n <div class=\"header-actions\">\n <button class=\"search-button\" aria-label=\"Search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>\n </svg>\n <span>Search</span>\n <kbd>/</kbd>\n </button>\n <button class=\"theme-toggle\" aria-label=\"Toggle theme\">\n <svg class=\"icon-sun\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <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\"/>\n </svg>\n <svg class=\"icon-moon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"/>\n </svg>\n </button>\n </div>\n </header>\n <div class=\"search-modal-overlay\">\n <div class=\"search-modal\">\n <div class=\"search-header\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>\n </svg>\n <input type=\"text\" class=\"search-input\" placeholder=\"Search documentation...\" />\n <button class=\"search-close\">Esc</button>\n </div>\n <div class=\"search-results\"></div>\n <div class=\"search-footer\">\n <span><kbd>\u2191</kbd><kbd>\u2193</kbd> to navigate</span>\n <span><kbd>Enter</kbd> to select</span>\n <span><kbd>Esc</kbd> to close</span>\n </div>\n </div>\n </div>\n <div class=\"overlay\"></div>\n <div class=\"layout\">\n <aside class=\"sidebar\">\n <nav>\n{{navigation}}\n </nav>\n </aside>\n <main class=\"main\">\n <article class=\"content\">\n{{content}}\n </article>\n </main>\n{{#hasToc}}\n <aside class=\"toc\">\n <div class=\"toc-title\">On this page</div>\n <ul class=\"toc-list\">\n{{toc}}\n </ul>\n </aside>\n{{/hasToc}}\n </div>\n <script>\n // Menu toggle\n const toggle = document.querySelector('.menu-toggle');\n const sidebar = document.querySelector('.sidebar');\n const overlay = document.querySelector('.overlay');\n if (toggle && sidebar && overlay) {\n const close = () => { sidebar.classList.remove('open'); overlay.classList.remove('open'); };\n toggle.addEventListener('click', () => {\n sidebar.classList.toggle('open');\n overlay.classList.toggle('open');\n });\n overlay.addEventListener('click', close);\n sidebar.querySelectorAll('a').forEach(a => a.addEventListener('click', close));\n }\n\n // Theme toggle\n const themeToggle = document.querySelector('.theme-toggle');\n const getPreferredTheme = () => {\n const stored = localStorage.getItem('theme');\n if (stored) return stored;\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n };\n const setTheme = (theme) => {\n document.documentElement.setAttribute('data-theme', theme);\n localStorage.setItem('theme', theme);\n };\n // Initialize theme\n setTheme(getPreferredTheme());\n if (themeToggle) {\n themeToggle.addEventListener('click', () => {\n const current = document.documentElement.getAttribute('data-theme') || getPreferredTheme();\n setTheme(current === 'dark' ? 'light' : 'dark');\n });\n }\n\n // Search functionality\n const searchButton = document.querySelector('.search-button');\n const searchOverlay = document.querySelector('.search-modal-overlay');\n const searchInput = document.querySelector('.search-input');\n const searchResults = document.querySelector('.search-results');\n const searchClose = document.querySelector('.search-close');\n let searchIndex = null;\n let selectedIndex = 0;\n let results = [];\n\n const openSearch = () => {\n searchOverlay.classList.add('open');\n searchInput.focus();\n };\n const closeSearch = () => {\n searchOverlay.classList.remove('open');\n searchInput.value = '';\n searchResults.innerHTML = '';\n selectedIndex = 0;\n results = [];\n };\n\n // Load search index\n const loadSearchIndex = async () => {\n if (searchIndex) return;\n try {\n const res = await fetch('{{base}}search-index.json');\n searchIndex = await res.json();\n } catch (e) {\n console.warn('Failed to load search index:', e);\n }\n };\n\n // Tokenize query\n const tokenize = (text) => {\n const tokens = [];\n let current = '';\n for (const char of text) {\n const isCjk = /[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\u3040-\\u309F\\u30A0-\\u30FF\\uAC00-\\uD7AF]/.test(char);\n if (isCjk) {\n if (current) { tokens.push(current.toLowerCase()); current = ''; }\n tokens.push(char);\n } else if (/[a-zA-Z0-9_]/.test(char)) {\n current += char;\n } else if (current) {\n tokens.push(current.toLowerCase());\n current = '';\n }\n }\n if (current) tokens.push(current.toLowerCase());\n return tokens;\n };\n\n // Perform search\n const performSearch = async (query) => {\n if (!query.trim()) {\n searchResults.innerHTML = '';\n results = [];\n return;\n }\n await loadSearchIndex();\n if (!searchIndex) {\n searchResults.innerHTML = '<div class=\"search-empty\">Search index not available</div>';\n return;\n }\n\n const tokens = tokenize(query);\n if (!tokens.length) {\n searchResults.innerHTML = '';\n results = [];\n return;\n }\n\n const k1 = 1.2, b = 0.75;\n const docScores = new Map();\n\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n const isLast = i === tokens.length - 1;\n let matchingTerms = [];\n if (isLast && token.length >= 2) {\n matchingTerms = Object.keys(searchIndex.index).filter(t => t.startsWith(token));\n } else if (searchIndex.index[token]) {\n matchingTerms = [token];\n }\n\n for (const term of matchingTerms) {\n const postings = searchIndex.index[term] || [];\n const df = searchIndex.df[term] || 1;\n const idf = Math.log((searchIndex.doc_count - df + 0.5) / (df + 0.5) + 1.0);\n\n for (const posting of postings) {\n const doc = searchIndex.documents[posting.doc_idx];\n if (!doc) continue;\n const boost = posting.field === 'Title' ? 10 : posting.field === 'Heading' ? 5 : 1;\n const tf = posting.tf;\n const docLen = doc.body.length;\n const score = idf * ((tf * (k1 + 1)) / (tf + k1 * (1 - b + b * docLen / searchIndex.avg_dl))) * boost;\n\n if (!docScores.has(posting.doc_idx)) {\n docScores.set(posting.doc_idx, { score: 0, matches: new Set() });\n }\n const entry = docScores.get(posting.doc_idx);\n entry.score += score;\n entry.matches.add(term);\n }\n }\n }\n\n results = Array.from(docScores.entries())\n .map(([docIdx, data]) => {\n const doc = searchIndex.documents[docIdx];\n let snippet = '';\n if (doc.body) {\n const bodyLower = doc.body.toLowerCase();\n let firstPos = -1;\n for (const match of data.matches) {\n const pos = bodyLower.indexOf(match);\n if (pos !== -1 && (firstPos === -1 || pos < firstPos)) firstPos = pos;\n }\n const start = Math.max(0, firstPos - 50);\n const end = Math.min(doc.body.length, start + 150);\n snippet = doc.body.slice(start, end);\n if (start > 0) snippet = '...' + snippet;\n if (end < doc.body.length) snippet += '...';\n }\n return { ...doc, score: data.score, snippet };\n })\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n\n selectedIndex = 0;\n renderResults();\n };\n\n const renderResults = () => {\n if (!results.length) {\n searchResults.innerHTML = '<div class=\"search-empty\">No results found</div>';\n return;\n }\n searchResults.innerHTML = results.map((r, i) =>\n '<a href=\"' + r.url + '\" class=\"search-result' + (i === selectedIndex ? ' selected' : '') + '\">' +\n '<div class=\"search-result-title\">' + r.title + '</div>' +\n (r.snippet ? '<div class=\"search-result-snippet\">' + r.snippet + '</div>' : '') +\n '</a>'\n ).join('');\n };\n\n // Event listeners\n if (searchButton) searchButton.addEventListener('click', openSearch);\n if (searchClose) searchClose.addEventListener('click', closeSearch);\n if (searchOverlay) searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });\n\n let searchTimeout = null;\n if (searchInput) {\n searchInput.addEventListener('input', () => {\n if (searchTimeout) clearTimeout(searchTimeout);\n searchTimeout = setTimeout(() => performSearch(searchInput.value), 150);\n });\n searchInput.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') closeSearch();\n else if (e.key === 'ArrowDown') {\n e.preventDefault();\n if (selectedIndex < results.length - 1) { selectedIndex++; renderResults(); }\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n if (selectedIndex > 0) { selectedIndex--; renderResults(); }\n } else if (e.key === 'Enter' && results[selectedIndex]) {\n e.preventDefault();\n window.location.href = results[selectedIndex].url;\n }\n });\n }\n\n // Global keyboard shortcut (/ or Cmd+K)\n document.addEventListener('keydown', (e) => {\n if ((e.key === '/' && !(e.target instanceof HTMLInputElement)) ||\n ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) {\n e.preventDefault();\n openSearch();\n }\n });\n </script>\n</body>\n</html>";
752
+ /**
753
+ * Resolves SSG options with defaults.
754
+ */
755
+ declare function resolveSsgOptions(ssg: SsgOptions | boolean | undefined): ResolvedSsgOptions;
756
+ /**
757
+ * Builds all markdown files to static HTML.
758
+ */
759
+ declare function buildSsg(options: ResolvedOptions, root: string): Promise<{
760
+ files: string[];
761
+ errors: string[];
762
+ }>;
763
+
764
+ /**
765
+ * Full-text search functionality for Ox Content.
766
+ *
767
+ * Generates search index at build time and provides client-side search.
768
+ */
769
+
770
+ /**
771
+ * Resolves search options with defaults.
772
+ */
773
+ declare function resolveSearchOptions(options: SearchOptions | boolean | undefined): ResolvedSearchOptions;
774
+ /**
775
+ * Builds the search index from Markdown files.
776
+ */
777
+ declare function buildSearchIndex(srcDir: string, base: string): Promise<string>;
778
+ /**
779
+ * Writes the search index to a file.
780
+ */
781
+ declare function writeSearchIndex(indexJson: string, outDir: string): Promise<void>;
782
+
783
+ /**
784
+ * Vite Plugin for Ox Content
785
+ *
786
+ * Uses Vite's Environment API for SSG-focused Markdown processing.
787
+ * Provides separate environments for client and server rendering.
788
+ */
789
+
790
+ /**
791
+ * Creates the Ox Content Vite plugin.
792
+ *
793
+ * @example
794
+ * ```ts
795
+ * // vite.config.ts
796
+ * import { defineConfig } from 'vite';
797
+ * import { oxContent } from 'vite-plugin-ox-content';
798
+ *
799
+ * export default defineConfig({
800
+ * plugins: [
801
+ * oxContent({
802
+ * srcDir: 'docs',
803
+ * gfm: true,
804
+ * }),
805
+ * ],
806
+ * });
807
+ * ```
808
+ */
809
+ declare function oxContent(options?: OxContentOptions): Plugin[];
810
+
811
+ export { DEFAULT_HTML_TEMPLATE, type DocEntry, type DocsOptions, type ExtractedDocs, type MarkdownNode, type MarkdownTransformer, type NavItem, type OgImageOptions, type OxContentOptions, type ParamDoc, type ResolvedDocsOptions, type ResolvedOptions, type ResolvedSearchOptions, type ResolvedSsgOptions, type ReturnDoc, type SearchDocument, type SearchOptions, type SearchResult, type SsgOptions, type TocEntry, type TransformContext, type TransformResult, buildSearchIndex, buildSsg, createMarkdownEnvironment, extractDocs, generateMarkdown, oxContent, resolveDocsOptions, resolveSearchOptions, resolveSsgOptions, transformMarkdown, writeDocs, writeSearchIndex };