@ox-content/vite-plugin 0.0.1-alpha.0 → 0.3.0-alpha.12

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.d.ts CHANGED
@@ -1,522 +1,762 @@
1
- import { EnvironmentOptions, Plugin } from 'vite';
1
+ import { EnvironmentOptions, Plugin } from "vite";
2
2
 
3
+ //#region src/theme.d.ts
4
+
5
+ /**
6
+ * Theme API for ox-content SSG
7
+ *
8
+ * Provides VitePress-like theming with default theme + customization.
9
+ */
10
+ /**
11
+ * Theme color configuration.
12
+ */
13
+ interface ThemeColors {
14
+ /** Primary accent color */
15
+ primary?: string;
16
+ /** Primary color on hover */
17
+ primaryHover?: string;
18
+ /** Background color */
19
+ background?: string;
20
+ /** Alternative background color (sidebar, code blocks) */
21
+ backgroundAlt?: string;
22
+ /** Main text color */
23
+ text?: string;
24
+ /** Muted/secondary text color */
25
+ textMuted?: string;
26
+ /** Border color */
27
+ border?: string;
28
+ /** Code block background color */
29
+ codeBackground?: string;
30
+ /** Code block text color */
31
+ codeText?: string;
32
+ }
33
+ /**
34
+ * Theme layout configuration.
35
+ */
36
+ interface ThemeLayout {
37
+ /** Sidebar width (CSS value, e.g., "260px") */
38
+ sidebarWidth?: string;
39
+ /** Header height (CSS value, e.g., "60px") */
40
+ headerHeight?: string;
41
+ /** Maximum content width (CSS value, e.g., "960px") */
42
+ maxContentWidth?: string;
43
+ }
44
+ /**
45
+ * Theme font configuration.
46
+ */
47
+ interface ThemeFonts {
48
+ /** Sans-serif font stack */
49
+ sans?: string;
50
+ /** Monospace font stack */
51
+ mono?: string;
52
+ }
53
+ /**
54
+ * Theme header configuration.
55
+ */
56
+ interface ThemeHeader {
57
+ /** Logo image URL */
58
+ logo?: string;
59
+ /** Logo width in pixels */
60
+ logoWidth?: number;
61
+ /** Logo height in pixels */
62
+ logoHeight?: number;
63
+ }
64
+ /**
65
+ * Theme footer configuration.
66
+ */
67
+ interface ThemeFooter {
68
+ /** Footer message (supports HTML) */
69
+ message?: string;
70
+ /** Copyright text (supports HTML) */
71
+ copyright?: string;
72
+ }
3
73
  /**
4
- * Type definitions for vite-plugin-ox-content
74
+ * Social links configuration.
5
75
  */
76
+ interface SocialLinks {
77
+ /** GitHub URL */
78
+ github?: string;
79
+ /** Twitter/X URL */
80
+ twitter?: string;
81
+ /** Discord URL */
82
+ discord?: string;
83
+ }
84
+ /**
85
+ * Embedded HTML content for specific positions in the page layout.
86
+ */
87
+ interface ThemeEmbed {
88
+ /** Content to embed into <head> */
89
+ head?: string;
90
+ /** Content before header */
91
+ headerBefore?: string;
92
+ /** Content after header */
93
+ headerAfter?: string;
94
+ /** Content before sidebar navigation */
95
+ sidebarBefore?: string;
96
+ /** Content after sidebar navigation */
97
+ sidebarAfter?: string;
98
+ /** Content before main content */
99
+ contentBefore?: string;
100
+ /** Content after main content */
101
+ contentAfter?: string;
102
+ /** Content before footer */
103
+ footerBefore?: string;
104
+ /** Custom footer content (replaces default footer) */
105
+ footer?: string;
106
+ }
107
+ /**
108
+ * Complete theme configuration.
109
+ */
110
+ interface ThemeConfig {
111
+ /** Theme name for identification */
112
+ name?: string;
113
+ /** Base theme to extend */
114
+ extends?: ThemeConfig;
115
+ /** Light mode colors (maps to CSS variables) */
116
+ colors?: ThemeColors;
117
+ /** Dark mode colors (maps to CSS variables) */
118
+ darkColors?: ThemeColors;
119
+ /** Font configuration (maps to CSS variables) */
120
+ fonts?: ThemeFonts;
121
+ /** Layout configuration (maps to CSS variables) */
122
+ layout?: ThemeLayout;
123
+ /** Header configuration */
124
+ header?: ThemeHeader;
125
+ /** Footer configuration */
126
+ footer?: ThemeFooter;
127
+ /** Social links configuration */
128
+ socialLinks?: SocialLinks;
129
+ /** Embedded HTML content at specific positions */
130
+ embed?: ThemeEmbed;
131
+ /** Additional custom CSS */
132
+ css?: string;
133
+ /** Additional custom JavaScript */
134
+ js?: string;
135
+ }
136
+ /**
137
+ * Resolved theme configuration (after merging with defaults).
138
+ */
139
+ interface ResolvedThemeConfig {
140
+ name: string;
141
+ colors: ThemeColors;
142
+ darkColors: ThemeColors;
143
+ fonts: ThemeFonts;
144
+ layout: ThemeLayout;
145
+ header: ThemeHeader;
146
+ footer: ThemeFooter;
147
+ socialLinks: SocialLinks;
148
+ embed: ThemeEmbed;
149
+ css: string;
150
+ js: string;
151
+ }
152
+ /**
153
+ * Default theme configuration.
154
+ * Based on the current ox-content SSG styles.
155
+ */
156
+ declare const defaultTheme: ThemeConfig;
157
+ /**
158
+ * Defines a theme configuration with type checking.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const myTheme = defineTheme({
163
+ * extends: defaultTheme,
164
+ * colors: {
165
+ * primary: '#3498db',
166
+ * },
167
+ * footer: {
168
+ * copyright: '2025 My Company',
169
+ * },
170
+ * });
171
+ * ```
172
+ */
173
+ declare function defineTheme(config: ThemeConfig): ThemeConfig;
174
+ /**
175
+ * Merges multiple theme configurations.
176
+ * Later themes override earlier ones.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const merged = mergeThemes(defaultTheme, customTheme, overrides);
181
+ * ```
182
+ */
183
+ declare function mergeThemes(...themes: ThemeConfig[]): ThemeConfig;
184
+ /**
185
+ * Resolves a theme configuration by merging with its extends chain and defaults.
186
+ */
187
+ declare function resolveTheme(config?: ThemeConfig): ResolvedThemeConfig;
188
+ /**
189
+ * Converts resolved theme to the format expected by Rust NAPI.
190
+ */
191
+ //#endregion
192
+ //#region src/types.d.ts
193
+ /**
194
+ * Hero section action button.
195
+ */
196
+ interface HeroAction {
197
+ /** Button theme: 'brand' (primary) or 'alt' (secondary) */
198
+ theme?: "brand" | "alt";
199
+ /** Button text */
200
+ text: string;
201
+ /** Link URL */
202
+ link: string;
203
+ }
204
+ /**
205
+ * Hero section image configuration.
206
+ */
207
+ interface HeroImage {
208
+ /** Image source URL */
209
+ src: string;
210
+ /** Alt text */
211
+ alt?: string;
212
+ /** Image width */
213
+ width?: number;
214
+ /** Image height */
215
+ height?: number;
216
+ }
217
+ /**
218
+ * Hero section configuration for entry page.
219
+ */
220
+ interface HeroConfig {
221
+ /** Main title (large, gradient text) */
222
+ name?: string;
223
+ /** Secondary text (medium size) */
224
+ text?: string;
225
+ /** Tagline (smaller, muted) */
226
+ tagline?: string;
227
+ /** Hero image */
228
+ image?: HeroImage;
229
+ /** Action buttons */
230
+ actions?: HeroAction[];
231
+ }
232
+ /**
233
+ * Feature card for entry page.
234
+ */
235
+ interface FeatureConfig {
236
+ /** Icon - supports: "mdi:icon-name" (Iconify), image URL, or emoji */
237
+ icon?: string;
238
+ /** Feature title */
239
+ title: string;
240
+ /** Feature description */
241
+ details?: string;
242
+ /** Optional link */
243
+ link?: string;
244
+ /** Link text */
245
+ linkText?: string;
246
+ }
247
+ /**
248
+ * Entry page frontmatter configuration.
249
+ */
250
+ interface EntryPageConfig {
251
+ /** Layout type - set to 'entry' for entry page */
252
+ layout: "entry";
253
+ /** Hero section */
254
+ hero?: HeroConfig;
255
+ /** Feature cards */
256
+ features?: FeatureConfig[];
257
+ }
6
258
  /**
7
259
  * SSG (Static Site Generation) options.
8
260
  */
9
261
  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;
262
+ /**
263
+ * Enable SSG mode.
264
+ * @default true
265
+ */
266
+ enabled?: boolean;
267
+ /**
268
+ * Output file extension.
269
+ * @default '.html'
270
+ */
271
+ extension?: string;
272
+ /**
273
+ * Clean output directory before build.
274
+ * @default false
275
+ */
276
+ clean?: boolean;
277
+ /**
278
+ * Bare HTML output (no navigation, no styles).
279
+ * Useful for benchmarking or when using custom layouts.
280
+ * @default false
281
+ */
282
+ bare?: boolean;
283
+ /**
284
+ * Site name for header and title suffix.
285
+ */
286
+ siteName?: string;
287
+ /**
288
+ * OG image URL for social sharing (static URL).
289
+ * If generateOgImage is enabled, this serves as the fallback.
290
+ */
291
+ ogImage?: string;
292
+ /**
293
+ * Generate OG images per page using Rust-based generator.
294
+ * When enabled, each page will have a unique OG image.
295
+ * @default false
296
+ */
297
+ generateOgImage?: boolean;
298
+ /**
299
+ * Site URL for generating absolute OG image URLs.
300
+ * Required for proper SNS sharing.
301
+ * Example: 'https://example.com'
302
+ */
303
+ siteUrl?: string;
304
+ /**
305
+ * Theme configuration for customizing the SSG output.
306
+ * Use defineTheme() to create a theme configuration.
307
+ */
308
+ theme?: ThemeConfig;
52
309
  }
53
310
  /**
54
311
  * Resolved SSG options.
55
312
  */
56
313
  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;
314
+ enabled: boolean;
315
+ extension: string;
316
+ clean: boolean;
317
+ bare: boolean;
318
+ siteName?: string;
319
+ ogImage?: string;
320
+ generateOgImage: boolean;
321
+ siteUrl?: string;
322
+ theme?: ResolvedThemeConfig;
65
323
  }
66
324
  /**
67
325
  * Plugin options.
68
326
  */
69
327
  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;
328
+ /**
329
+ * Source directory for Markdown files.
330
+ * @default 'content'
331
+ */
332
+ srcDir?: string;
333
+ /**
334
+ * Output directory for built files.
335
+ * @default 'dist'
336
+ */
337
+ outDir?: string;
338
+ /**
339
+ * Base path for the site.
340
+ * @default '/'
341
+ */
342
+ base?: string;
343
+ /**
344
+ * SSG (Static Site Generation) options.
345
+ * Set to false to disable SSG completely.
346
+ * @default { enabled: true }
347
+ */
348
+ ssg?: SsgOptions | boolean;
349
+ /**
350
+ * Enable GitHub Flavored Markdown extensions.
351
+ * @default true
352
+ */
353
+ gfm?: boolean;
354
+ /**
355
+ * Enable footnotes.
356
+ * @default true
357
+ */
358
+ footnotes?: boolean;
359
+ /**
360
+ * Enable tables.
361
+ * @default true
362
+ */
363
+ tables?: boolean;
364
+ /**
365
+ * Enable task lists.
366
+ * @default true
367
+ */
368
+ taskLists?: boolean;
369
+ /**
370
+ * Enable strikethrough.
371
+ * @default true
372
+ */
373
+ strikethrough?: boolean;
374
+ /**
375
+ * Enable syntax highlighting for code blocks.
376
+ * @default false
377
+ */
378
+ highlight?: boolean;
379
+ /**
380
+ * Syntax highlighting theme.
381
+ * @default 'github-dark'
382
+ */
383
+ highlightTheme?: string;
384
+ /**
385
+ * Enable mermaid diagram rendering.
386
+ * @default false
387
+ */
388
+ mermaid?: boolean;
389
+ /**
390
+ * Parse YAML frontmatter.
391
+ * @default true
392
+ */
393
+ frontmatter?: boolean;
394
+ /**
395
+ * Generate table of contents.
396
+ * @default true
397
+ */
398
+ toc?: boolean;
399
+ /**
400
+ * Maximum heading depth for TOC.
401
+ * @default 3
402
+ */
403
+ tocMaxDepth?: number;
404
+ /**
405
+ * Enable OG image generation.
406
+ * @default false
407
+ */
408
+ ogImage?: boolean;
409
+ /**
410
+ * OG image generation options.
411
+ */
412
+ ogImageOptions?: OgImageOptions;
413
+ /**
414
+ * Custom AST transformers.
415
+ */
416
+ transformers?: MarkdownTransformer[];
417
+ /**
418
+ * Source documentation generation options.
419
+ * Set to false to disable (opt-out).
420
+ * @default { enabled: true }
421
+ */
422
+ docs?: DocsOptions | false;
423
+ /**
424
+ * Full-text search options.
425
+ * Set to false to disable search.
426
+ * @default { enabled: true }
427
+ */
428
+ search?: SearchOptions | boolean;
171
429
  }
172
430
  /**
173
431
  * Resolved options with all defaults applied.
174
432
  */
175
433
  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;
434
+ srcDir: string;
435
+ outDir: string;
436
+ base: string;
437
+ ssg: ResolvedSsgOptions;
438
+ gfm: boolean;
439
+ footnotes: boolean;
440
+ tables: boolean;
441
+ taskLists: boolean;
442
+ strikethrough: boolean;
443
+ highlight: boolean;
444
+ highlightTheme: string;
445
+ mermaid: boolean;
446
+ frontmatter: boolean;
447
+ toc: boolean;
448
+ tocMaxDepth: number;
449
+ ogImage: boolean;
450
+ ogImageOptions: OgImageOptions;
451
+ transformers: MarkdownTransformer[];
452
+ docs: ResolvedDocsOptions | false;
453
+ search: ResolvedSearchOptions;
196
454
  }
197
455
  /**
198
456
  * OG image generation options.
199
457
  */
200
458
  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;
459
+ /**
460
+ * Background color.
461
+ * @default '#1a1a2e'
462
+ */
463
+ background?: string;
464
+ /**
465
+ * Text color.
466
+ * @default '#ffffff'
467
+ */
468
+ textColor?: string;
469
+ /**
470
+ * Accent color.
471
+ * @default '#e94560'
472
+ */
473
+ accentColor?: string;
474
+ /**
475
+ * Font family.
476
+ */
477
+ fontFamily?: string;
478
+ /**
479
+ * Image width.
480
+ * @default 1200
481
+ */
482
+ width?: number;
483
+ /**
484
+ * Image height.
485
+ * @default 630
486
+ */
487
+ height?: number;
230
488
  }
231
489
  /**
232
490
  * Custom AST transformer.
233
491
  */
234
492
  interface MarkdownTransformer {
235
- /**
236
- * Transformer name.
237
- */
238
- name: string;
239
- /**
240
- * Transform function.
241
- */
242
- transform: (ast: MarkdownNode, context: TransformContext) => MarkdownNode | Promise<MarkdownNode>;
493
+ /**
494
+ * Transformer name.
495
+ */
496
+ name: string;
497
+ /**
498
+ * Transform function.
499
+ */
500
+ transform: (ast: MarkdownNode, context: TransformContext) => MarkdownNode | Promise<MarkdownNode>;
243
501
  }
244
502
  /**
245
503
  * Transform context passed to transformers.
246
504
  */
247
505
  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;
506
+ /**
507
+ * File path being processed.
508
+ */
509
+ filePath: string;
510
+ /**
511
+ * Frontmatter data.
512
+ */
513
+ frontmatter: Record<string, unknown>;
514
+ /**
515
+ * Resolved plugin options.
516
+ */
517
+ options: ResolvedOptions;
260
518
  }
261
519
  /**
262
520
  * Markdown AST node (simplified for TypeScript).
263
521
  */
264
522
  interface MarkdownNode {
265
- type: string;
266
- children?: MarkdownNode[];
267
- value?: string;
268
- [key: string]: unknown;
523
+ type: string;
524
+ children?: MarkdownNode[];
525
+ value?: string;
526
+ [key: string]: unknown;
269
527
  }
270
528
  /**
271
529
  * Transform result.
272
530
  */
273
531
  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[];
532
+ /**
533
+ * Generated JavaScript code.
534
+ */
535
+ code: string;
536
+ /**
537
+ * Source map (null means no source map).
538
+ */
539
+ map?: null;
540
+ /**
541
+ * Rendered HTML.
542
+ */
543
+ html: string;
544
+ /**
545
+ * Parsed frontmatter.
546
+ */
547
+ frontmatter: Record<string, unknown>;
548
+ /**
549
+ * Table of contents.
550
+ */
551
+ toc: TocEntry[];
294
552
  }
295
553
  /**
296
554
  * Table of contents entry.
297
555
  */
298
556
  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[];
557
+ /**
558
+ * Heading depth (1-6).
559
+ */
560
+ depth: number;
561
+ /**
562
+ * Heading text.
563
+ */
564
+ text: string;
565
+ /**
566
+ * Slug/ID for linking.
567
+ */
568
+ slug: string;
569
+ /**
570
+ * Child entries.
571
+ */
572
+ children: TocEntry[];
315
573
  }
316
574
  /**
317
575
  * Options for source documentation generation.
318
576
  */
319
577
  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;
578
+ /**
579
+ * Enable/disable docs generation.
580
+ * @default true (opt-out)
581
+ */
582
+ enabled?: boolean;
583
+ /**
584
+ * Source directories to scan for documentation.
585
+ * @default ['./src']
586
+ */
587
+ src?: string[];
588
+ /**
589
+ * Output directory for generated documentation.
590
+ * @default 'docs/api'
591
+ */
592
+ out?: string;
593
+ /**
594
+ * Glob patterns for files to include.
595
+ * @default ['**\/*.ts', '**\/*.tsx']
596
+ */
597
+ include?: string[];
598
+ /**
599
+ * Glob patterns for files to exclude.
600
+ * @default ['**\/*.test.*', '**\/*.spec.*', 'node_modules']
601
+ */
602
+ exclude?: string[];
603
+ /**
604
+ * Output format.
605
+ * @default 'markdown'
606
+ */
607
+ format?: "markdown" | "json" | "html";
608
+ /**
609
+ * Include private members in documentation.
610
+ * @default false
611
+ */
612
+ private?: boolean;
613
+ /**
614
+ * Generate table of contents for each file.
615
+ * @default true
616
+ */
617
+ toc?: boolean;
618
+ /**
619
+ * Group documentation by file or category.
620
+ * @default 'file'
621
+ */
622
+ groupBy?: "file" | "category";
623
+ /**
624
+ * GitHub repository URL for source code links.
625
+ * When provided, generated documentation will include links to source code.
626
+ * Example: 'https://github.com/ubugeeei/ox-content'
627
+ */
628
+ githubUrl?: string;
629
+ /**
630
+ * Generate navigation metadata file.
631
+ * @default true
632
+ */
633
+ generateNav?: boolean;
376
634
  }
377
635
  /**
378
636
  * Resolved docs options with all defaults applied.
379
637
  */
380
638
  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;
639
+ enabled: boolean;
640
+ src: string[];
641
+ out: string;
642
+ include: string[];
643
+ exclude: string[];
644
+ format: "markdown" | "json" | "html";
645
+ private: boolean;
646
+ toc: boolean;
647
+ groupBy: "file" | "category";
648
+ githubUrl?: string;
649
+ generateNav: boolean;
392
650
  }
393
651
  /**
394
652
  * A single documentation entry extracted from source.
395
653
  */
396
654
  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;
655
+ name: string;
656
+ kind: "function" | "class" | "interface" | "type" | "variable" | "module";
657
+ description: string;
658
+ params?: ParamDoc[];
659
+ returns?: ReturnDoc;
660
+ examples?: string[];
661
+ tags?: Record<string, string>;
662
+ private?: boolean;
663
+ file: string;
664
+ line: number;
665
+ signature?: string;
408
666
  }
409
667
  /**
410
668
  * Parameter documentation.
411
669
  */
412
670
  interface ParamDoc {
413
- name: string;
414
- type: string;
415
- description: string;
416
- optional?: boolean;
417
- default?: string;
671
+ name: string;
672
+ type: string;
673
+ description: string;
674
+ optional?: boolean;
675
+ default?: string;
418
676
  }
419
677
  /**
420
678
  * Return type documentation.
421
679
  */
422
680
  interface ReturnDoc {
423
- type: string;
424
- description: string;
681
+ type: string;
682
+ description: string;
425
683
  }
426
684
  /**
427
685
  * Extracted documentation for a single file.
428
686
  */
429
687
  interface ExtractedDocs {
430
- file: string;
431
- entries: DocEntry[];
688
+ file: string;
689
+ entries: DocEntry[];
432
690
  }
433
691
  /**
434
692
  * Navigation item for sidebar navigation.
435
693
  */
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
- }
694
+
450
695
  /**
451
696
  * Options for full-text search.
452
697
  */
453
698
  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;
699
+ /**
700
+ * Enable search functionality.
701
+ * @default true
702
+ */
703
+ enabled?: boolean;
704
+ /**
705
+ * Maximum number of search results.
706
+ * @default 10
707
+ */
708
+ limit?: number;
709
+ /**
710
+ * Enable prefix matching for autocomplete.
711
+ * @default true
712
+ */
713
+ prefix?: boolean;
714
+ /**
715
+ * Placeholder text for the search input.
716
+ * @default 'Search documentation...'
717
+ */
718
+ placeholder?: string;
719
+ /**
720
+ * Keyboard shortcut to focus search (without modifier).
721
+ * @default '/'
722
+ */
723
+ hotkey?: string;
479
724
  }
480
725
  /**
481
726
  * Resolved search options.
482
727
  */
483
728
  interface ResolvedSearchOptions {
484
- enabled: boolean;
485
- limit: number;
486
- prefix: boolean;
487
- placeholder: string;
488
- hotkey: string;
729
+ enabled: boolean;
730
+ limit: number;
731
+ prefix: boolean;
732
+ placeholder: string;
733
+ hotkey: string;
489
734
  }
490
735
  /**
491
736
  * Search document structure.
492
737
  */
493
738
  interface SearchDocument {
494
- id: string;
495
- title: string;
496
- url: string;
497
- body: string;
498
- headings: string[];
499
- code: string[];
739
+ id: string;
740
+ title: string;
741
+ url: string;
742
+ body: string;
743
+ headings: string[];
744
+ code: string[];
500
745
  }
501
746
  /**
502
747
  * Search result structure.
503
748
  */
504
749
  interface SearchResult {
505
- id: string;
506
- title: string;
507
- url: string;
508
- score: number;
509
- matches: string[];
510
- snippet: string;
750
+ id: string;
751
+ title: string;
752
+ url: string;
753
+ score: number;
754
+ matches: string[];
755
+ snippet: string;
511
756
  }
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
-
757
+ //# sourceMappingURL=types.d.ts.map
758
+ //#endregion
759
+ //#region src/environment.d.ts
520
760
  /**
521
761
  * Creates the Markdown processing environment configuration.
522
762
  *
@@ -531,7 +771,7 @@ interface SearchResult {
531
771
  * export default defineConfig({
532
772
  * environments: {
533
773
  * markdown: createMarkdownEnvironment({
534
- * srcDir: 'docs',
774
+ * srcDir: 'content',
535
775
  * gfm: true,
536
776
  * }),
537
777
  * },
@@ -539,40 +779,15 @@ interface SearchResult {
539
779
  * ```
540
780
  */
541
781
  declare function createMarkdownEnvironment(options: ResolvedOptions): EnvironmentOptions;
542
-
543
782
  /**
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:
783
+ * Environment-specific module transformer.
549
784
  *
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
- * ```
785
+ * This is called during the transform phase to process
786
+ * Markdown files within the environment context.
574
787
  */
575
788
 
789
+ //#endregion
790
+ //#region src/transform.d.ts
576
791
  /**
577
792
  * Transforms Markdown content into a JavaScript module.
578
793
  *
@@ -633,7 +848,7 @@ declare function createMarkdownEnvironment(options: ResolvedOptions): Environmen
633
848
  * ## Installation
634
849
  *
635
850
  * \`\`\`bash
636
- * npm install vite-plugin-ox-content
851
+ * npm install @ox-content/vite-plugin
637
852
  * \`\`\`
638
853
  * `;
639
854
  *
@@ -658,13 +873,21 @@ declare function createMarkdownEnvironment(options: ResolvedOptions): Environmen
658
873
  * SSG-specific transform options.
659
874
  */
660
875
  interface SsgTransformOptions {
661
- /** Convert `.md` links to `.html` links */
662
- convertMdLinks?: boolean;
663
- /** Base URL for absolute link conversion */
664
- baseUrl?: string;
876
+ /** Convert `.md` links to `.html` links */
877
+ convertMdLinks?: boolean;
878
+ /** Base URL for absolute link conversion */
879
+ baseUrl?: string;
880
+ /** Source file path for relative link resolution */
881
+ sourcePath?: string;
665
882
  }
666
883
  declare function transformMarkdown(source: string, filePath: string, options: ResolvedOptions, ssgOptions?: SsgTransformOptions): Promise<TransformResult>;
667
-
884
+ /**
885
+ * Extracts imports from Markdown content.
886
+ *
887
+ * Supports importing components for interactive islands.
888
+ */
889
+ //#endregion
890
+ //#region src/docs.d.ts
668
891
  /**
669
892
  * Extracts JSDoc documentation from source files in specified directories.
670
893
  *
@@ -740,33 +963,34 @@ declare function generateMarkdown(docs: ExtractedDocs[], options: ResolvedDocsOp
740
963
  */
741
964
  declare function writeDocs(docs: Record<string, string>, outDir: string, extractedDocs?: ExtractedDocs[], options?: ResolvedDocsOptions): Promise<void>;
742
965
  declare function resolveDocsOptions(options: DocsOptions | false | undefined): ResolvedDocsOptions | false;
743
-
966
+ //# sourceMappingURL=docs.d.ts.map
967
+ //#endregion
968
+ //#region src/ssg.d.ts
744
969
  /**
745
- * SSG (Static Site Generation) module for ox-content
970
+ * Default HTML template for SSG pages with navigation.
746
971
  */
747
-
972
+ 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: #141414;\n --color-bg-alt: #141414;\n --color-text: #e5e5e5;\n --color-text-muted: #a3a3a3;\n --color-border: #2a2a2a;\n --color-primary: #c9714a;\n --color-primary-hover: #d4845f;\n --color-code-bg: #1a1a1a;\n --color-code-text: #e5e5e5;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n --color-bg: #141414;\n --color-bg-alt: #141414;\n --color-text: #e5e5e5;\n --color-text-muted: #a3a3a3;\n --color-border: #2a2a2a;\n --color-primary: #c9714a;\n --color-primary-hover: #d4845f;\n --color-code-bg: #1a1a1a;\n --color-code-text: #e5e5e5;\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>";
748
973
  /**
749
- * Default HTML template for SSG pages with navigation.
974
+ * Bare HTML template (no navigation, no styles).
750
975
  */
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>";
976
+
752
977
  /**
753
978
  * Resolves SSG options with defaults.
754
979
  */
755
980
  declare function resolveSsgOptions(ssg: SsgOptions | boolean | undefined): ResolvedSsgOptions;
756
981
  /**
757
- * Builds all markdown files to static HTML.
982
+ * Generates bare HTML page (no navigation, no styles).
758
983
  */
759
- declare function buildSsg(options: ResolvedOptions, root: string): Promise<{
760
- files: string[];
761
- errors: string[];
762
- }>;
763
984
 
764
985
  /**
765
- * Full-text search functionality for Ox Content.
766
- *
767
- * Generates search index at build time and provides client-side search.
986
+ * Builds all markdown files to static HTML.
768
987
  */
769
-
988
+ declare function buildSsg(options: ResolvedOptions, root: string): Promise<{
989
+ files: string[];
990
+ errors: string[];
991
+ }>;
992
+ //#endregion
993
+ //#region src/search.d.ts
770
994
  /**
771
995
  * Resolves search options with defaults.
772
996
  */
@@ -779,14 +1003,633 @@ declare function buildSearchIndex(srcDir: string, base: string): Promise<string>
779
1003
  * Writes the search index to a file.
780
1004
  */
781
1005
  declare function writeSearchIndex(indexJson: string, outDir: string): Promise<void>;
1006
+ /**
1007
+ * Client-side search module code.
1008
+ * This is injected into the bundle as a virtual module.
1009
+ */
782
1010
 
1011
+ //#endregion
1012
+ //#region src/jsx-runtime.d.ts
783
1013
  /**
784
- * Vite Plugin for Ox Content
1014
+ * Custom JSX Runtime for Static HTML Generation
1015
+ *
1016
+ * This module provides a JSX runtime that outputs static HTML strings.
1017
+ * No React, no hydration, no client-side JavaScript - just pure HTML.
785
1018
  *
786
- * Uses Vite's Environment API for SSG-focused Markdown processing.
787
- * Provides separate environments for client and server rendering.
1019
+ * @example
1020
+ * ```tsx
1021
+ * // tsconfig.json or vite.config.ts
1022
+ * {
1023
+ * "compilerOptions": {
1024
+ * "jsx": "react-jsx",
1025
+ * "jsxImportSource": "@ox-content/vite-plugin"
1026
+ * }
1027
+ * }
1028
+ *
1029
+ * // MyComponent.tsx
1030
+ * export function Hero({ title }: { title: string }) {
1031
+ * return (
1032
+ * <section class="hero">
1033
+ * <h1>{title}</h1>
1034
+ * </section>
1035
+ * );
1036
+ * }
1037
+ * ```
1038
+ */
1039
+ /**
1040
+ * JSX element type - either a string (intrinsic) or a function component.
1041
+ */
1042
+ type JSXElementType = string | ((props: Record<string, unknown>) => JSXNode);
1043
+ /**
1044
+ * Valid JSX child types.
1045
+ */
1046
+ type JSXChild = string | number | boolean | null | undefined | JSXNode | JSXChild[];
1047
+ /**
1048
+ * JSX node - the result of JSX expressions.
1049
+ */
1050
+ interface JSXNode {
1051
+ __html: string;
1052
+ }
1053
+ /**
1054
+ * Props with children.
1055
+ */
1056
+ interface JSXProps {
1057
+ children?: JSXChild;
1058
+ [key: string]: unknown;
1059
+ }
1060
+ /**
1061
+ * Creates a JSX element.
1062
+ * This is the core function called by the JSX transform.
1063
+ */
1064
+ declare function jsx(type: JSXElementType, props: JSXProps, _key?: string): JSXNode;
1065
+ /**
1066
+ * Creates a JSX element with static children.
1067
+ * Called by the JSX transform for elements with multiple children.
1068
+ */
1069
+ declare function jsxs(type: JSXElementType, props: JSXProps, key?: string): JSXNode;
1070
+ /**
1071
+ * Fragment component - renders children without a wrapper element.
1072
+ */
1073
+ declare function Fragment({
1074
+ children
1075
+ }: {
1076
+ children?: JSXChild;
1077
+ }): JSXNode;
1078
+ /**
1079
+ * Renders a JSX node to an HTML string.
1080
+ */
1081
+ declare function renderToString(node: JSXNode): string;
1082
+ /**
1083
+ * Creates raw HTML without escaping.
1084
+ * Use with caution - only for trusted content.
1085
+ *
1086
+ * @example
1087
+ * ```tsx
1088
+ * <div>{raw('<strong>Bold</strong>')}</div>
1089
+ * ```
1090
+ */
1091
+ declare function raw(html: string): JSXNode;
1092
+ /**
1093
+ * Conditionally renders content.
1094
+ *
1095
+ * @example
1096
+ * ```tsx
1097
+ * {when(isLoggedIn, <UserMenu />)}
1098
+ * ```
1099
+ */
1100
+ declare function when(condition: boolean, content: JSXNode): JSXNode;
1101
+ /**
1102
+ * Maps over an array and renders each item.
1103
+ *
1104
+ * @example
1105
+ * ```tsx
1106
+ * {each(items, (item) => <li>{item.name}</li>)}
1107
+ * ```
1108
+ */
1109
+ declare function each<T>(items: T[], render: (item: T, index: number) => JSXNode): JSXNode;
1110
+ //#endregion
1111
+ //#region src/page-context.d.ts
1112
+ /**
1113
+ * Base page props available for all pages.
1114
+ */
1115
+ interface BasePageProps {
1116
+ /** Page title from frontmatter or first heading */
1117
+ title: string;
1118
+ /** Page description from frontmatter */
1119
+ description?: string;
1120
+ /** Rendered HTML content */
1121
+ html: string;
1122
+ /** Table of contents entries */
1123
+ toc: TocEntry[];
1124
+ /** Source file path (relative to docs root) */
1125
+ path: string;
1126
+ /** Output URL path */
1127
+ url: string;
1128
+ /** Raw frontmatter object */
1129
+ frontmatter: Record<string, unknown>;
1130
+ /** Layout name from frontmatter */
1131
+ layout?: string;
1132
+ }
1133
+ /**
1134
+ * Extended page props with custom frontmatter.
1135
+ */
1136
+ type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = BasePageProps & {
1137
+ /** Custom frontmatter fields */
1138
+ frontmatter: T & Record<string, unknown>;
1139
+ };
1140
+ /**
1141
+ * Site-wide configuration available in context.
1142
+ */
1143
+ interface SiteConfig {
1144
+ /** Site name */
1145
+ name: string;
1146
+ /** Base URL path */
1147
+ base: string;
1148
+ /** All pages in the site */
1149
+ pages: BasePageProps[];
1150
+ /** Navigation groups */
1151
+ nav: NavGroup[];
1152
+ }
1153
+ /**
1154
+ * Navigation group.
1155
+ */
1156
+ interface NavGroup {
1157
+ title: string;
1158
+ items: NavItem[];
1159
+ }
1160
+ /**
1161
+ * Navigation item.
1162
+ */
1163
+ interface NavItem {
1164
+ title: string;
1165
+ path: string;
1166
+ href: string;
1167
+ }
1168
+ /**
1169
+ * Complete render context.
1170
+ */
1171
+ interface RenderContext<T extends Record<string, unknown> = Record<string, unknown>> {
1172
+ /** Current page props */
1173
+ page: PageProps<T>;
1174
+ /** Site configuration */
1175
+ site: SiteConfig;
1176
+ }
1177
+ /**
1178
+ * Sets the current render context.
1179
+ * Called internally during page rendering.
1180
+ * @internal
1181
+ */
1182
+ declare function setRenderContext(ctx: RenderContext): void;
1183
+ /**
1184
+ * Clears the current render context.
1185
+ * Called internally after page rendering.
1186
+ * @internal
1187
+ */
1188
+ declare function clearRenderContext(): void;
1189
+ /**
1190
+ * Gets the current page props.
1191
+ *
1192
+ * @returns The current page props
1193
+ * @throws Error if called outside of a render context
1194
+ *
1195
+ * @example
1196
+ * ```tsx
1197
+ * function PageTitle() {
1198
+ * const page = usePageProps();
1199
+ * return <h1>{page.title}</h1>;
1200
+ * }
1201
+ * ```
1202
+ */
1203
+ declare function usePageProps<T extends Record<string, unknown> = Record<string, unknown>>(): PageProps<T>;
1204
+ /**
1205
+ * Gets the site configuration.
1206
+ *
1207
+ * @returns The site configuration
1208
+ * @throws Error if called outside of a render context
1209
+ *
1210
+ * @example
1211
+ * ```tsx
1212
+ * function SiteHeader() {
1213
+ * const site = useSiteConfig();
1214
+ * return <header>{site.name}</header>;
1215
+ * }
1216
+ * ```
1217
+ */
1218
+ declare function useSiteConfig(): SiteConfig;
1219
+ /**
1220
+ * Gets the full render context.
1221
+ *
1222
+ * @returns The complete render context
1223
+ * @throws Error if called outside of a render context
1224
+ *
1225
+ * @example
1226
+ * ```tsx
1227
+ * function Layout({ children }) {
1228
+ * const ctx = useRenderContext();
1229
+ * return (
1230
+ * <html>
1231
+ * <head><title>{ctx.page.title} - {ctx.site.name}</title></head>
1232
+ * <body>{children}</body>
1233
+ * </html>
1234
+ * );
1235
+ * }
1236
+ * ```
1237
+ */
1238
+ declare function useRenderContext<T extends Record<string, unknown> = Record<string, unknown>>(): RenderContext<T>;
1239
+ /**
1240
+ * Gets the navigation groups.
1241
+ *
1242
+ * @example
1243
+ * ```tsx
1244
+ * function Sidebar() {
1245
+ * const nav = useNav();
1246
+ * return (
1247
+ * <nav>
1248
+ * {each(nav, (group) => (
1249
+ * <div>
1250
+ * <h3>{group.title}</h3>
1251
+ * <ul>
1252
+ * {each(group.items, (item) => (
1253
+ * <li><a href={item.href}>{item.title}</a></li>
1254
+ * ))}
1255
+ * </ul>
1256
+ * </div>
1257
+ * ))}
1258
+ * </nav>
1259
+ * );
1260
+ * }
1261
+ * ```
1262
+ */
1263
+ declare function useNav(): NavGroup[];
1264
+ /**
1265
+ * Checks if the given path is the current page.
1266
+ *
1267
+ * @example
1268
+ * ```tsx
1269
+ * function NavLink({ href, children }) {
1270
+ * const isActive = useIsActive(href);
1271
+ * return <a href={href} class={isActive ? 'active' : ''}>{children}</a>;
1272
+ * }
1273
+ * ```
1274
+ */
1275
+ declare function useIsActive(path: string): boolean;
1276
+ /**
1277
+ * Schema for frontmatter type generation.
1278
+ */
1279
+ interface FrontmatterSchema {
1280
+ /** Field name */
1281
+ name: string;
1282
+ /** TypeScript type */
1283
+ type: string;
1284
+ /** Whether the field is optional */
1285
+ optional: boolean;
1286
+ /** JSDoc description */
1287
+ description?: string;
1288
+ }
1289
+ /**
1290
+ * Infers TypeScript types from frontmatter values.
1291
+ */
1292
+ declare function inferType(value: unknown): string;
1293
+ /**
1294
+ * Generates TypeScript interface from frontmatter samples.
1295
+ */
1296
+ declare function generateFrontmatterTypes(samples: Record<string, unknown>[], interfaceName?: string): string;
1297
+ //# sourceMappingURL=page-context.d.ts.map
1298
+ //#endregion
1299
+ //#region src/theme-renderer.d.ts
1300
+ /**
1301
+ * Theme component type.
1302
+ */
1303
+ type ThemeComponent = (props: ThemeProps) => JSXNode;
1304
+ /**
1305
+ * Props passed to the theme component.
1306
+ */
1307
+ interface ThemeProps {
1308
+ /** Rendered page content as JSX */
1309
+ children: JSXNode;
1310
+ }
1311
+ /**
1312
+ * Page data for rendering.
1313
+ */
1314
+ interface PageData {
1315
+ /** Page title */
1316
+ title: string;
1317
+ /** Page description */
1318
+ description?: string;
1319
+ /** Rendered HTML content */
1320
+ html: string;
1321
+ /** Table of contents */
1322
+ toc: TocEntry[];
1323
+ /** Source file path */
1324
+ path: string;
1325
+ /** Output URL path */
1326
+ url: string;
1327
+ /** Frontmatter */
1328
+ frontmatter: Record<string, unknown>;
1329
+ /** Layout name */
1330
+ layout?: string;
1331
+ }
1332
+ /**
1333
+ * Theme render options.
1334
+ */
1335
+ interface ThemeRenderOptions {
1336
+ /** Theme component to use */
1337
+ theme: ThemeComponent;
1338
+ /** Site name */
1339
+ siteName: string;
1340
+ /** Base URL path */
1341
+ base: string;
1342
+ /** Navigation groups */
1343
+ nav: NavGroup[];
1344
+ /** All pages (for site context) */
1345
+ pages: PageData[];
1346
+ /** Output directory for type definitions */
1347
+ typesOutDir?: string;
1348
+ }
1349
+ /**
1350
+ * Renders a page using the theme component.
1351
+ *
1352
+ * @param page - Page data to render
1353
+ * @param options - Theme render options
1354
+ * @returns Rendered HTML string
1355
+ */
1356
+ declare function renderPage(page: PageData, options: ThemeRenderOptions): string;
1357
+ /**
1358
+ * Renders all pages and generates type definitions.
1359
+ *
1360
+ * @param pages - All pages to render
1361
+ * @param options - Theme render options
1362
+ * @returns Map of output paths to rendered HTML
1363
+ */
1364
+ declare function renderAllPages(pages: PageData[], options: ThemeRenderOptions): Promise<Map<string, string>>;
1365
+ /**
1366
+ * Generates TypeScript type definitions from page frontmatter.
1367
+ *
1368
+ * @param pages - All pages
1369
+ * @param outDir - Output directory for types
1370
+ */
1371
+ declare function generateTypes(pages: PageData[], outDir: string): Promise<void>;
1372
+ /**
1373
+ * Default theme component.
1374
+ * A minimal theme that renders page content with basic styling.
1375
+ */
1376
+ declare function DefaultTheme({
1377
+ children
1378
+ }: ThemeProps): JSXNode;
1379
+ /**
1380
+ * Creates a theme with layout switching support.
1381
+ *
1382
+ * @example
1383
+ * ```tsx
1384
+ * import { createTheme } from '@ox-content/vite-plugin';
1385
+ * import { DefaultLayout } from './layouts/Default';
1386
+ * import { EntryLayout } from './layouts/Entry';
1387
+ *
1388
+ * export default createTheme({
1389
+ * layouts: {
1390
+ * default: DefaultLayout,
1391
+ * entry: EntryLayout,
1392
+ * },
1393
+ * });
1394
+ * ```
1395
+ */
1396
+ declare function createTheme(config: {
1397
+ layouts: Record<string, ThemeComponent>;
1398
+ defaultLayout?: string;
1399
+ }): ThemeComponent;
1400
+ //# sourceMappingURL=theme-renderer.d.ts.map
1401
+ //#endregion
1402
+ //#region src/plugins/tabs.d.ts
1403
+ /**
1404
+ * Transform Tabs components in HTML.
1405
+ */
1406
+ declare function transformTabs(html: string): Promise<string>;
1407
+ /**
1408
+ * Generate dynamic CSS for :has() based tab switching.
1409
+ * This is needed because :has() selectors need unique IDs.
1410
+ */
1411
+ declare function generateTabsCSS(groupCount: number): string;
1412
+ //# sourceMappingURL=tabs.d.ts.map
1413
+ //#endregion
1414
+ //#region src/plugins/youtube.d.ts
1415
+ /**
1416
+ * YouTube Plugin - Privacy-enhanced iframe embedding
1417
+ *
1418
+ * Transforms <YouTube> components into responsive iframe embeds
1419
+ * using youtube-nocookie.com for enhanced privacy.
1420
+ */
1421
+ interface YouTubeOptions {
1422
+ /** Use privacy-enhanced mode (youtube-nocookie.com). Default: true */
1423
+ privacyEnhanced?: boolean;
1424
+ /** Default aspect ratio. Default: "16/9" */
1425
+ aspectRatio?: string;
1426
+ /** Allow fullscreen. Default: true */
1427
+ allowFullscreen?: boolean;
1428
+ /** Lazy load iframe. Default: true */
1429
+ lazyLoad?: boolean;
1430
+ }
1431
+ /**
1432
+ * Extract YouTube video ID from various URL formats.
1433
+ */
1434
+ declare function extractVideoId(input: string): string | null;
1435
+ /**
1436
+ * Transform YouTube components in HTML.
1437
+ */
1438
+ declare function transformYouTube(html: string, options?: YouTubeOptions): Promise<string>;
1439
+ //# sourceMappingURL=youtube.d.ts.map
1440
+ //#endregion
1441
+ //#region src/plugins/github.d.ts
1442
+ /**
1443
+ * GitHub Plugin - Repository card embedding
1444
+ *
1445
+ * Transforms <GitHub> components into static repository cards
1446
+ * by fetching data from GitHub API at build time.
1447
+ */
1448
+ interface GitHubRepoData {
1449
+ name: string;
1450
+ full_name: string;
1451
+ description: string | null;
1452
+ html_url: string;
1453
+ stargazers_count: number;
1454
+ forks_count: number;
1455
+ language: string | null;
1456
+ owner: {
1457
+ login: string;
1458
+ avatar_url: string;
1459
+ };
1460
+ }
1461
+ interface GitHubOptions {
1462
+ /** GitHub API token for higher rate limits. */
1463
+ token?: string;
1464
+ /** Cache fetched data. Default: true */
1465
+ cache?: boolean;
1466
+ /** Cache TTL in milliseconds. Default: 3600000 (1 hour) */
1467
+ cacheTTL?: number;
1468
+ }
1469
+ /**
1470
+ * Fetch repository data from GitHub API.
1471
+ */
1472
+ declare function fetchRepoData(repo: string, options: Required<GitHubOptions>): Promise<GitHubRepoData | null>;
1473
+ /**
1474
+ * Collect all GitHub repos from HTML for pre-fetching.
1475
+ */
1476
+ declare function collectGitHubRepos(html: string): Promise<string[]>;
1477
+ /**
1478
+ * Pre-fetch all GitHub repos data.
1479
+ */
1480
+ declare function prefetchGitHubRepos(repos: string[], options?: GitHubOptions): Promise<Map<string, GitHubRepoData | null>>;
1481
+ /**
1482
+ * Transform GitHub components in HTML.
1483
+ */
1484
+ declare function transformGitHub(html: string, repoDataMap?: Map<string, GitHubRepoData | null>, options?: GitHubOptions): Promise<string>;
1485
+ //# sourceMappingURL=github.d.ts.map
1486
+ //#endregion
1487
+ //#region src/plugins/ogp.d.ts
1488
+ /**
1489
+ * OGP Card Plugin - Link card embedding
1490
+ *
1491
+ * Transforms <OgCard> components into static link preview cards
1492
+ * by fetching OGP metadata at build time.
1493
+ */
1494
+ interface OgpData {
1495
+ url: string;
1496
+ title: string;
1497
+ description?: string;
1498
+ image?: string;
1499
+ siteName?: string;
1500
+ favicon?: string;
1501
+ }
1502
+ interface OgpOptions {
1503
+ /** Request timeout in milliseconds. Default: 10000 */
1504
+ timeout?: number;
1505
+ /** Cache fetched data. Default: true */
1506
+ cache?: boolean;
1507
+ /** Cache TTL in milliseconds. Default: 3600000 (1 hour) */
1508
+ cacheTTL?: number;
1509
+ /** User agent for requests */
1510
+ userAgent?: string;
1511
+ }
1512
+ /**
1513
+ * Fetch OGP data for a URL.
1514
+ */
1515
+ declare function fetchOgpData(url: string, options: Required<OgpOptions>): Promise<OgpData | null>;
1516
+ /**
1517
+ * Collect all OGP URLs from HTML for pre-fetching.
1518
+ */
1519
+ declare function collectOgpUrls(html: string): Promise<string[]>;
1520
+ /**
1521
+ * Pre-fetch all OGP data.
1522
+ */
1523
+ declare function prefetchOgpData(urls: string[], options?: OgpOptions): Promise<Map<string, OgpData | null>>;
1524
+ /**
1525
+ * Transform OgCard components in HTML.
1526
+ */
1527
+ declare function transformOgp(html: string, ogpDataMap?: Map<string, OgpData | null>, options?: OgpOptions): Promise<string>;
1528
+ //# sourceMappingURL=ogp.d.ts.map
1529
+ //#endregion
1530
+ //#region src/plugins/mermaid.d.ts
1531
+ /**
1532
+ * Mermaid Plugin - Native Rust renderer via NAPI
1533
+ *
1534
+ * Renders mermaid code blocks to SVG using the native Rust renderer
1535
+ * via NAPI. Delegates to the NAPI `transformMermaid` function which
1536
+ * extracts mermaid code blocks from HTML and renders them using mmdc.
1537
+ */
1538
+ interface MermaidOptions {
1539
+ /** Mermaid theme. Default: "neutral" */
1540
+ theme?: "default" | "dark" | "forest" | "neutral" | "base";
1541
+ }
1542
+ /**
1543
+ * Transforms mermaid code blocks in HTML to rendered SVG diagrams.
1544
+ * Uses the native Rust NAPI transformMermaid function.
1545
+ */
1546
+ declare function transformMermaidStatic(html: string, _options?: MermaidOptions): Promise<string>;
1547
+ /**
1548
+ * @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.
1549
+ */
1550
+ declare const mermaidClientScript = "";
1551
+ //# sourceMappingURL=mermaid.d.ts.map
1552
+ //#endregion
1553
+ //#region src/plugins/index.d.ts
1554
+ /**
1555
+ * Transform all plugin components in HTML.
1556
+ * Call this during SSG build to process all plugins at once.
1557
+ */
1558
+ interface TransformAllOptions {
1559
+ tabs?: boolean;
1560
+ youtube?: boolean;
1561
+ github?: boolean;
1562
+ ogp?: boolean;
1563
+ mermaid?: boolean;
1564
+ githubToken?: string;
1565
+ }
1566
+ /**
1567
+ * Transform all enabled plugins in HTML content.
1568
+ */
1569
+ declare function transformAllPlugins(html: string, options?: TransformAllOptions): Promise<string>;
1570
+ //# sourceMappingURL=index.d.ts.map
1571
+ //#endregion
1572
+ //#region src/island/parse.d.ts
1573
+ /**
1574
+ * Island Parser
1575
+ *
1576
+ * Detects <Island> components in HTML and transforms them
1577
+ * into hydration-ready elements with data attributes.
1578
+ */
1579
+ type LoadStrategy = "eager" | "idle" | "visible" | "media";
1580
+ interface IslandInfo {
1581
+ component: string;
1582
+ load: LoadStrategy;
1583
+ mediaQuery?: string;
1584
+ props: Record<string, unknown>;
1585
+ }
1586
+ interface ParseIslandsResult {
1587
+ html: string;
1588
+ islands: IslandInfo[];
1589
+ }
1590
+ /**
1591
+ * Reset island counter (for testing).
788
1592
  */
789
1593
 
1594
+ /**
1595
+ * Transform Island components in HTML.
1596
+ *
1597
+ * Converts:
1598
+ * ```html
1599
+ * <Island load="visible">
1600
+ * <Counter initial={0} />
1601
+ * </Island>
1602
+ * ```
1603
+ *
1604
+ * To:
1605
+ * ```html
1606
+ * <div id="ox-island-0"
1607
+ * data-ox-island="Counter"
1608
+ * data-ox-load="visible"
1609
+ * data-ox-props='{"initial":0}'
1610
+ * class="ox-island">
1611
+ * <!-- fallback content -->
1612
+ * </div>
1613
+ * ```
1614
+ */
1615
+ declare function transformIslands(html: string): Promise<ParseIslandsResult>;
1616
+ /**
1617
+ * Check if HTML contains any Island components.
1618
+ */
1619
+ declare function hasIslands(html: string): boolean;
1620
+ /**
1621
+ * Extract island info without transforming HTML.
1622
+ * Useful for analysis/bundling purposes.
1623
+ */
1624
+ declare function extractIslandInfo(html: string): Promise<IslandInfo[]>;
1625
+ /**
1626
+ * Generate client-side hydration script.
1627
+ * This is a minimal script that imports and initializes islands.
1628
+ */
1629
+ declare function generateHydrationScript(components: string[]): string;
1630
+ //# sourceMappingURL=parse.d.ts.map
1631
+ //#endregion
1632
+ //#region src/index.d.ts
790
1633
  /**
791
1634
  * Creates the Ox Content Vite plugin.
792
1635
  *
@@ -794,12 +1637,12 @@ declare function writeSearchIndex(indexJson: string, outDir: string): Promise<vo
794
1637
  * ```ts
795
1638
  * // vite.config.ts
796
1639
  * import { defineConfig } from 'vite';
797
- * import { oxContent } from 'vite-plugin-ox-content';
1640
+ * import { oxContent } from '@ox-content/vite-plugin';
798
1641
  *
799
1642
  * export default defineConfig({
800
1643
  * plugins: [
801
1644
  * oxContent({
802
- * srcDir: 'docs',
1645
+ * srcDir: 'content',
803
1646
  * gfm: true,
804
1647
  * }),
805
1648
  * ],
@@ -807,5 +1650,6 @@ declare function writeSearchIndex(indexJson: string, outDir: string): Promise<vo
807
1650
  * ```
808
1651
  */
809
1652
  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 };
1653
+ //#endregion
1654
+ export { type BasePageProps, DEFAULT_HTML_TEMPLATE, DefaultTheme, type DocEntry, type DocsOptions, type EntryPageConfig, type ExtractedDocs, type FeatureConfig, Fragment, type FrontmatterSchema, type GitHubOptions, type GitHubRepoData, type HeroAction, type HeroConfig, type HeroImage, type IslandInfo, type JSXChild, type JSXElementType, type JSXNode, type JSXProps, type LoadStrategy, MarkdownNode, MarkdownTransformer, type MermaidOptions, type NavGroup, type NavItem, OgImageOptions, type OgpData, type OgpOptions, type OxContentOptions, type PageData, type PageProps, type ParamDoc, type ParseIslandsResult, type RenderContext, type ResolvedDocsOptions, ResolvedOptions, type ResolvedSearchOptions, type ResolvedSsgOptions, type ResolvedThemeConfig, type ReturnDoc, type SearchDocument, type SearchOptions, type SearchResult, type SiteConfig, type SocialLinks, type SsgOptions, type ThemeColors, type ThemeComponent, type ThemeConfig, type ThemeEmbed, type ThemeFonts, type ThemeFooter, type ThemeHeader, type ThemeLayout, type ThemeProps, type ThemeRenderOptions, TocEntry, type TransformAllOptions, TransformContext, TransformResult, type YouTubeOptions, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectOgpUrls, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractDocs, extractIslandInfo, extractVideoId, fetchOgpData, fetchRepoData, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateTabsCSS, generateTypes, hasIslands, inferType, jsx, jsxs, mergeThemes, mermaidClientScript, oxContent, prefetchGitHubRepos, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveDocsOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
1655
+ //# sourceMappingURL=index.d.ts.map