skrypt-ai 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.ts +0 -1
  3. package/dist/auth/index.js +3 -5
  4. package/dist/autofix/index.js +15 -3
  5. package/dist/cli.js +19 -4
  6. package/dist/commands/check-links.js +164 -174
  7. package/dist/commands/deploy.js +5 -2
  8. package/dist/commands/generate.js +206 -199
  9. package/dist/commands/i18n.js +3 -20
  10. package/dist/commands/init.js +47 -40
  11. package/dist/commands/lint.js +3 -20
  12. package/dist/commands/mcp.js +125 -122
  13. package/dist/commands/monitor.js +125 -108
  14. package/dist/commands/review-pr.js +1 -1
  15. package/dist/commands/sdk.js +1 -1
  16. package/dist/config/loader.js +21 -2
  17. package/dist/generator/organizer.d.ts +3 -0
  18. package/dist/generator/organizer.js +4 -9
  19. package/dist/generator/writer.js +2 -10
  20. package/dist/github/pr-comments.js +21 -8
  21. package/dist/plugins/index.js +1 -0
  22. package/dist/scanner/index.js +8 -2
  23. package/dist/template/docs.json +2 -1
  24. package/dist/template/next.config.mjs +3 -1
  25. package/dist/template/package.json +17 -14
  26. package/dist/template/public/favicon.svg +4 -0
  27. package/dist/template/public/search-index.json +1 -1
  28. package/dist/template/scripts/build-search-index.mjs +120 -25
  29. package/dist/template/src/app/api/chat/route.ts +11 -3
  30. package/dist/template/src/app/docs/README.md +28 -0
  31. package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
  32. package/dist/template/src/app/docs/auth/page.mdx +589 -0
  33. package/dist/template/src/app/docs/autofix/page.mdx +624 -0
  34. package/dist/template/src/app/docs/cli/page.mdx +217 -0
  35. package/dist/template/src/app/docs/config/page.mdx +428 -0
  36. package/dist/template/src/app/docs/configuration/page.mdx +86 -0
  37. package/dist/template/src/app/docs/deployment/page.mdx +112 -0
  38. package/dist/template/src/app/docs/error.tsx +20 -0
  39. package/dist/template/src/app/docs/generator/generator.md +504 -0
  40. package/dist/template/src/app/docs/generator/organizer.md +779 -0
  41. package/dist/template/src/app/docs/generator/page.mdx +613 -0
  42. package/dist/template/src/app/docs/github/page.mdx +502 -0
  43. package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
  44. package/dist/template/src/app/docs/llm/index.md +471 -0
  45. package/dist/template/src/app/docs/llm/page.mdx +428 -0
  46. package/dist/template/src/app/docs/llms-full.md +256 -0
  47. package/dist/template/src/app/docs/llms.txt +2971 -0
  48. package/dist/template/src/app/docs/not-found.tsx +23 -0
  49. package/dist/template/src/app/docs/page.mdx +0 -3
  50. package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
  51. package/dist/template/src/app/docs/pro/page.mdx +121 -0
  52. package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
  53. package/dist/template/src/app/docs/scanner/content-type.md +599 -0
  54. package/dist/template/src/app/docs/scanner/index.md +212 -0
  55. package/dist/template/src/app/docs/scanner/page.mdx +307 -0
  56. package/dist/template/src/app/docs/scanner/python.md +469 -0
  57. package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
  58. package/dist/template/src/app/docs/scanner/rust.md +325 -0
  59. package/dist/template/src/app/docs/scanner/typescript.md +201 -0
  60. package/dist/template/src/app/error.tsx +3 -3
  61. package/dist/template/src/app/icon.tsx +29 -0
  62. package/dist/template/src/app/layout.tsx +57 -7
  63. package/dist/template/src/app/not-found.tsx +35 -0
  64. package/dist/template/src/app/page.tsx +95 -11
  65. package/dist/template/src/components/ai-chat.tsx +26 -21
  66. package/dist/template/src/components/breadcrumbs.tsx +56 -12
  67. package/dist/template/src/components/copy-button.tsx +17 -3
  68. package/dist/template/src/components/docs-layout.tsx +202 -8
  69. package/dist/template/src/components/feedback.tsx +4 -2
  70. package/dist/template/src/components/footer.tsx +42 -0
  71. package/dist/template/src/components/header.tsx +56 -20
  72. package/dist/template/src/components/mdx/accordion.tsx +17 -13
  73. package/dist/template/src/components/mdx/callout.tsx +50 -37
  74. package/dist/template/src/components/mdx/card.tsx +24 -12
  75. package/dist/template/src/components/mdx/code-block.tsx +17 -3
  76. package/dist/template/src/components/mdx/code-group.tsx +78 -18
  77. package/dist/template/src/components/mdx/code-playground.tsx +3 -0
  78. package/dist/template/src/components/mdx/go-playground.tsx +3 -0
  79. package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
  80. package/dist/template/src/components/mdx/python-playground.tsx +2 -0
  81. package/dist/template/src/components/mdx/steps.tsx +6 -6
  82. package/dist/template/src/components/mdx/tabs.tsx +76 -8
  83. package/dist/template/src/components/page-header.tsx +19 -0
  84. package/dist/template/src/components/scroll-to-top.tsx +33 -0
  85. package/dist/template/src/components/search-dialog.tsx +251 -57
  86. package/dist/template/src/components/sidebar.tsx +137 -77
  87. package/dist/template/src/components/table-of-contents.tsx +29 -13
  88. package/dist/template/src/lib/highlight.ts +90 -31
  89. package/dist/template/src/lib/search.ts +14 -4
  90. package/dist/template/src/lib/theme-utils.ts +140 -0
  91. package/dist/template/src/styles/globals.css +397 -84
  92. package/dist/template/src/types/remark-gfm.d.ts +2 -0
  93. package/dist/utils/files.d.ts +9 -0
  94. package/dist/utils/files.js +33 -0
  95. package/dist/utils/validation.d.ts +4 -0
  96. package/dist/utils/validation.js +38 -0
  97. package/package.json +1 -4
@@ -2,71 +2,68 @@
2
2
 
3
3
  import Link from 'next/link'
4
4
  import { usePathname } from 'next/navigation'
5
- import { ChevronRight } from 'lucide-react'
6
- import { useState } from 'react'
5
+ import { ChevronRight, BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe, Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart, Package, Puzzle, GitBranch, Cpu } from 'lucide-react'
6
+ import { useState, useMemo } from 'react'
7
7
  import { cn } from '@/lib/utils'
8
8
 
9
- interface NavItem {
10
- title: string
11
- href?: string
12
- items?: NavItem[]
13
- }
14
-
15
- interface NavPage {
16
- title: string
17
- path: string
18
- }
9
+ interface NavPage { title: string; path: string; pages?: NavPage[] }
10
+ interface NavGroup { group: string; icon?: string; pages: (NavPage | NavGroup)[] }
11
+ interface DocsConfig { navigation: NavGroup[] }
19
12
 
20
- interface NavGroup {
21
- group: string
22
- pages: NavPage[]
13
+ interface SidebarProps {
14
+ open?: boolean
15
+ onClose?: () => void
16
+ docsConfig: DocsConfig
23
17
  }
24
18
 
25
- interface DocsConfig {
26
- navigation: NavGroup[]
19
+ const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
20
+ BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe,
21
+ Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart,
22
+ Package, Puzzle, GitBranch, Cpu,
27
23
  }
28
24
 
29
- function buildNavigation(config: DocsConfig): NavItem[] {
30
- return config.navigation.map((group) => ({
31
- title: group.group,
32
- items: group.pages.map((page) => ({
33
- title: page.title,
34
- href: page.path,
35
- })),
36
- }))
25
+ function isNavGroup(item: NavPage | NavGroup): item is NavGroup {
26
+ return 'group' in item && !('path' in item)
37
27
  }
38
28
 
39
- interface SidebarProps {
40
- open?: boolean
41
- onClose?: () => void
42
- docsConfig: DocsConfig
29
+ /** Check if any descendant page matches the current pathname */
30
+ function hasActivePage(pages: (NavPage | NavGroup)[], pathname: string): boolean {
31
+ for (const item of pages) {
32
+ if (isNavGroup(item)) {
33
+ if (hasActivePage(item.pages, pathname)) return true
34
+ } else {
35
+ if (item.path === pathname) return true
36
+ if (item.pages && hasActivePage(item.pages, pathname)) return true
37
+ }
38
+ }
39
+ return false
43
40
  }
44
41
 
45
42
  export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
46
43
  const pathname = usePathname()
47
- const navigation = buildNavigation(docsConfig)
48
44
 
49
45
  return (
50
46
  <>
51
- {/* Mobile overlay */}
52
47
  {open && (
53
48
  <div
54
- className="fixed inset-0 z-40 bg-black/50 md:hidden"
49
+ className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
50
+ role="button"
51
+ tabIndex={0}
52
+ aria-label="Close sidebar"
55
53
  onClick={onClose}
54
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClose?.() } }}
56
55
  />
57
56
  )}
58
-
59
- {/* Sidebar */}
60
57
  <aside
61
58
  className={cn(
62
- 'fixed left-0 top-16 bottom-0 w-[var(--sidebar-width)] border-r border-[var(--color-border)] bg-[var(--color-bg)] overflow-y-auto z-50 transition-transform duration-200',
63
- 'md:translate-x-0',
64
- open ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
59
+ 'fixed left-0 top-[var(--header-height)] bottom-0 w-[var(--sidebar-width)] bg-[var(--color-bg)] overflow-y-auto z-50 transition-transform duration-100 pl-2 sidebar-scroll-shadow',
60
+ 'lg:translate-x-0',
61
+ open ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
65
62
  )}
66
63
  >
67
- <nav className="p-4 space-y-6">
68
- {navigation.map((section) => (
69
- <NavSection key={section.title} section={section} pathname={pathname} onNavigate={onClose} />
64
+ <nav className="py-4 pr-2 space-y-6 divide-y divide-[var(--color-border)]">
65
+ {docsConfig.navigation.map((group) => (
66
+ <SidebarGroup key={group.group} group={group} pathname={pathname} onNavigate={onClose} level={0} />
70
67
  ))}
71
68
  </nav>
72
69
  </aside>
@@ -74,54 +71,117 @@ export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
74
71
  )
75
72
  }
76
73
 
77
- function NavSection({ section, pathname, onNavigate }: { section: NavItem; pathname: string; onNavigate?: () => void }) {
78
- const [expanded, setExpanded] = useState(true)
74
+ function SidebarGroup({ group, pathname, onNavigate, level }: { group: NavGroup; pathname: string; onNavigate?: () => void; level: number }) {
75
+ const isActive = useMemo(() => hasActivePage(group.pages, pathname), [group.pages, pathname])
76
+ const [expanded, setExpanded] = useState(isActive)
79
77
 
80
- if (!section.items) {
81
- return (
82
- <Link
83
- href={section.href || '#'}
84
- onClick={onNavigate}
85
- className={cn(
86
- 'block px-3 py-2 text-sm rounded-md transition-colors',
87
- pathname === section.href
88
- ? 'bg-[var(--color-primary)]/10 text-[var(--color-primary)] font-medium'
89
- : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
90
- )}
91
- >
92
- {section.title}
93
- </Link>
94
- )
95
- }
78
+ const Icon = group.icon ? iconMap[group.icon] : null
79
+
80
+ // Limit nesting to 3 levels
81
+ if (level > 2) return null
82
+
83
+ const paddingLeft = level === 0 ? 'pl-4' : level === 1 ? 'pl-10' : 'pl-14'
96
84
 
97
85
  return (
98
86
  <div>
99
87
  <button
100
88
  onClick={() => setExpanded(!expanded)}
101
- className="flex items-center justify-between w-full px-3 py-2 text-sm font-medium text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] rounded-md transition-colors"
89
+ aria-expanded={expanded}
90
+ className={cn(
91
+ 'flex items-center gap-2.5 w-full mb-2 text-[0.8125rem] font-medium text-[var(--color-text)] hover:text-[var(--color-text)] transition-colors',
92
+ paddingLeft
93
+ )}
102
94
  >
103
- {section.title}
104
95
  <ChevronRight
105
- size={16}
106
- className={cn('transition-transform', expanded && 'rotate-90')}
96
+ size={14}
97
+ className={cn('text-[var(--color-text-tertiary)] transition-transform duration-100', expanded && 'rotate-90')}
107
98
  />
99
+ {Icon && <Icon size={14} className="text-[var(--color-text-tertiary)]" />}
100
+ {group.group}
108
101
  </button>
109
102
  {expanded && (
110
- <div className="mt-1 ml-3 space-y-1 border-l border-[var(--color-border)] pl-3">
111
- {section.items.map((item) => (
112
- <Link
113
- key={item.href}
114
- href={item.href || '#'}
115
- onClick={onNavigate}
116
- className={cn(
117
- 'block px-3 py-1.5 text-sm rounded-md transition-colors',
118
- pathname === item.href
119
- ? 'bg-[var(--color-primary)]/10 text-[var(--color-primary)] font-medium'
120
- : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text)]'
121
- )}
122
- >
123
- {item.title}
124
- </Link>
103
+ <div className="space-y-px">
104
+ {group.pages.map((item) => {
105
+ if (isNavGroup(item)) {
106
+ return (
107
+ <SidebarGroup
108
+ key={item.group}
109
+ group={item}
110
+ pathname={pathname}
111
+ onNavigate={onNavigate}
112
+ level={level + 1}
113
+ />
114
+ )
115
+ }
116
+
117
+ return (
118
+ <SidebarPageItem
119
+ key={item.path}
120
+ page={item}
121
+ pathname={pathname}
122
+ onNavigate={onNavigate}
123
+ level={level}
124
+ />
125
+ )
126
+ })}
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }
132
+
133
+ function SidebarPageItem({ page, pathname, onNavigate, level }: { page: NavPage; pathname: string; onNavigate?: () => void; level: number }) {
134
+ // Limit nesting depth to prevent infinite recursion
135
+ if (level > 3) return null
136
+ const hasChildren = page.pages && page.pages.length > 0
137
+ const isActive = useMemo(() => {
138
+ if (page.path === pathname) return true
139
+ if (page.pages && hasActivePage(page.pages, pathname)) return true
140
+ return false
141
+ }, [page, pathname])
142
+ const [expanded, setExpanded] = useState(isActive)
143
+
144
+ const paddingClass = level === 0 ? 'pl-10' : level === 1 ? 'pl-14' : 'pl-18'
145
+
146
+ return (
147
+ <div>
148
+ <div className="flex items-center">
149
+ <Link
150
+ href={page.path}
151
+ onClick={onNavigate}
152
+ className={cn(
153
+ 'block flex-1 pr-3 py-1.5 text-[0.8125rem] rounded-lg transition-colors',
154
+ paddingClass,
155
+ pathname === page.path
156
+ ? 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
157
+ : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-primary)]/10 hover:text-[var(--color-primary)]'
158
+ )}
159
+ >
160
+ {page.title}
161
+ </Link>
162
+ {hasChildren && (
163
+ <button
164
+ onClick={() => setExpanded(!expanded)}
165
+ aria-expanded={expanded}
166
+ className="p-1 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)]"
167
+ >
168
+ <ChevronRight
169
+ size={12}
170
+ className={cn('transition-transform duration-100', expanded && 'rotate-90')}
171
+ />
172
+ </button>
173
+ )}
174
+ </div>
175
+ {hasChildren && expanded && (
176
+ <div className="space-y-px">
177
+ {page.pages!.map((child) => (
178
+ <SidebarPageItem
179
+ key={child.path}
180
+ page={child}
181
+ pathname={pathname}
182
+ onNavigate={onNavigate}
183
+ level={level + 1}
184
+ />
125
185
  ))}
126
186
  </div>
127
187
  )}
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
+ import { usePathname } from 'next/navigation'
4
5
  import { cn } from '@/lib/utils'
5
6
 
6
7
  interface Heading {
@@ -12,6 +13,7 @@ interface Heading {
12
13
  export function TableOfContents() {
13
14
  const [headings, setHeadings] = useState<Heading[]>([])
14
15
  const [activeId, setActiveId] = useState('')
16
+ const pathname = usePathname()
15
17
 
16
18
  useEffect(() => {
17
19
  const article = document.querySelector('article')
@@ -20,19 +22,33 @@ export function TableOfContents() {
20
22
  const elements = article.querySelectorAll('h2, h3')
21
23
  const items: Heading[] = []
22
24
 
25
+ // Generic headings that repeat in API reference pages — skip them in TOC
26
+ const genericHeadings = new Set([
27
+ 'parameters', 'returns', 'return value', 'return type',
28
+ 'returned validator function', 'requirements',
29
+ 'when results are returned', 'when each value is returned',
30
+ 'validationresult shape',
31
+ ])
32
+
23
33
  elements.forEach((el) => {
24
- const id = el.id || el.textContent?.toLowerCase().replace(/\s+/g, '-') || ''
34
+ const text = el.textContent || ''
35
+ const normalized = text.toLowerCase().trim()
36
+
37
+ // Skip generic/repeated headings
38
+ if (genericHeadings.has(normalized)) return
39
+
40
+ const id = el.id || normalized.replace(/\s+/g, '-')
25
41
  if (!el.id) el.id = id
26
42
 
27
43
  items.push({
28
44
  id,
29
- text: el.textContent || '',
45
+ text,
30
46
  level: parseInt(el.tagName[1]),
31
47
  })
32
48
  })
33
49
 
34
50
  setHeadings(items)
35
- }, [])
51
+ }, [pathname])
36
52
 
37
53
  useEffect(() => {
38
54
  const observer = new IntersectionObserver(
@@ -43,7 +59,7 @@ export function TableOfContents() {
43
59
  }
44
60
  })
45
61
  },
46
- { rootMargin: '-80px 0px -80% 0px' }
62
+ { rootMargin: '-80px 0px -60% 0px' }
47
63
  )
48
64
 
49
65
  headings.forEach(({ id }) => {
@@ -57,21 +73,21 @@ export function TableOfContents() {
57
73
  if (headings.length === 0) return null
58
74
 
59
75
  return (
60
- <nav className="hidden xl:block fixed right-8 top-24 w-[var(--toc-width)] max-h-[calc(100vh-8rem)] overflow-y-auto">
61
- <p className="text-[11px] font-semibold uppercase tracking-wider text-[var(--color-text-tertiary)] mb-3">
76
+ <nav className="hidden lg:block fixed right-8 top-[calc(var(--header-height)+2rem)] w-[var(--toc-width)] max-h-[calc(100vh-var(--header-height)-4rem)] overflow-y-auto">
77
+ <p className="text-[0.6875rem] font-semibold uppercase tracking-widest text-[var(--color-text-tertiary)] mb-3">
62
78
  On this page
63
79
  </p>
64
- <ul className="space-y-2 text-[13px]">
65
- {headings.map((heading) => (
66
- <li key={heading.id}>
80
+ <ul className="space-y-0.5 border-l border-[var(--color-border)]">
81
+ {headings.map((heading, index) => (
82
+ <li key={`${heading.id}-${index}`}>
67
83
  <a
68
84
  href={`#${heading.id}`}
69
85
  className={cn(
70
- 'block transition-colors hover:text-[var(--color-text)]',
71
- heading.level === 3 && 'pl-3',
86
+ 'block py-1 text-[0.8125rem] leading-snug transition-colors border-l-2 -ml-px hover:text-[var(--color-text)] hover:no-underline',
87
+ heading.level === 2 ? 'pl-4' : 'pl-7',
72
88
  activeId === heading.id
73
- ? 'text-[var(--color-primary)] font-medium'
74
- : 'text-[var(--color-text-tertiary)]'
89
+ ? 'border-[var(--color-primary)] text-[var(--color-primary)] font-medium'
90
+ : 'border-transparent text-[var(--color-text-tertiary)]'
75
91
  )}
76
92
  >
77
93
  {heading.text}
@@ -1,10 +1,22 @@
1
1
  import { createHighlighter, type Highlighter, type BundledTheme } from 'shiki'
2
2
 
3
- let highlighter: Highlighter | null = null
3
+ // Cache highlighter instances per loaded theme set
4
+ const highlighterCache = new Map<string, Highlighter>()
4
5
 
5
- export type ThemeName = 'github-dark' | 'github-light' | 'dracula' | 'nord' | 'one-dark-pro' | 'vitesse-dark' | 'vitesse-light'
6
+ export type ThemeName =
7
+ | 'catppuccin-mocha'
8
+ | 'catppuccin-latte'
9
+ | 'github-dark'
10
+ | 'github-light'
11
+ | 'dracula'
12
+ | 'nord'
13
+ | 'one-dark-pro'
14
+ | 'vitesse-dark'
15
+ | 'vitesse-light'
6
16
 
7
17
  export const AVAILABLE_THEMES: { name: ThemeName; label: string; isDark: boolean }[] = [
18
+ { name: 'catppuccin-mocha', label: 'Catppuccin', isDark: true },
19
+ { name: 'catppuccin-latte', label: 'Catppuccin Light', isDark: false },
8
20
  { name: 'github-dark', label: 'GitHub Dark', isDark: true },
9
21
  { name: 'github-light', label: 'GitHub Light', isDark: false },
10
22
  { name: 'dracula', label: 'Dracula', isDark: true },
@@ -14,34 +26,79 @@ export const AVAILABLE_THEMES: { name: ThemeName; label: string; isDark: boolean
14
26
  { name: 'vitesse-light', label: 'Vitesse Light', isDark: false },
15
27
  ]
16
28
 
17
- export const DEFAULT_THEME: ThemeName = 'github-dark'
29
+ export const DEFAULT_THEME: ThemeName = 'catppuccin-mocha'
18
30
 
19
- export async function getHighlighter(): Promise<Highlighter> {
20
- if (!highlighter) {
21
- highlighter = await createHighlighter({
22
- themes: AVAILABLE_THEMES.map(t => t.name) as BundledTheme[],
23
- langs: [
24
- 'typescript',
25
- 'javascript',
26
- 'python',
27
- 'go',
28
- 'rust',
29
- 'json',
30
- 'yaml',
31
- 'bash',
32
- 'shell',
33
- 'markdown',
34
- 'html',
35
- 'css',
36
- 'jsx',
37
- 'tsx',
38
- 'sql',
39
- 'graphql',
40
- 'diff',
41
- ],
42
- })
31
+ const SUPPORTED_LANGS = [
32
+ 'typescript',
33
+ 'javascript',
34
+ 'python',
35
+ 'go',
36
+ 'rust',
37
+ 'json',
38
+ 'yaml',
39
+ 'bash',
40
+ 'shell',
41
+ 'markdown',
42
+ 'html',
43
+ 'css',
44
+ 'jsx',
45
+ 'tsx',
46
+ 'sql',
47
+ 'graphql',
48
+ 'diff',
49
+ ]
50
+
51
+ // Start with just the default theme; load others on demand
52
+ let defaultHighlighter: Highlighter | null = null
53
+ let defaultHighlighterPromise: Promise<Highlighter> | null = null
54
+
55
+ async function getDefaultHighlighter(): Promise<Highlighter> {
56
+ if (defaultHighlighter) return defaultHighlighter
57
+ if (defaultHighlighterPromise) return defaultHighlighterPromise
58
+
59
+ defaultHighlighterPromise = createHighlighter({
60
+ themes: [DEFAULT_THEME] as BundledTheme[],
61
+ langs: SUPPORTED_LANGS,
62
+ }).then(hl => {
63
+ defaultHighlighter = hl
64
+ highlighterCache.set(DEFAULT_THEME, hl)
65
+ return hl
66
+ })
67
+
68
+ return defaultHighlighterPromise
69
+ }
70
+
71
+ async function getHighlighterForTheme(theme: ThemeName): Promise<Highlighter> {
72
+ if (theme === DEFAULT_THEME || highlighterCache.has(DEFAULT_THEME)) {
73
+ // Try to load the theme into the existing default highlighter
74
+ const hl = await getDefaultHighlighter()
75
+ try {
76
+ // Check if the theme is already loaded by attempting to use it
77
+ const loadedThemes = hl.getLoadedThemes()
78
+ if (!loadedThemes.includes(theme as BundledTheme)) {
79
+ await hl.loadTheme(theme as BundledTheme)
80
+ }
81
+ return hl
82
+ } catch {
83
+ // If loading into existing highlighter fails, create a new one
84
+ }
43
85
  }
44
- return highlighter
86
+
87
+ // Check cache
88
+ const cached = highlighterCache.get(theme)
89
+ if (cached) return cached
90
+
91
+ // Create a new highlighter with this theme
92
+ const hl = await createHighlighter({
93
+ themes: [theme] as BundledTheme[],
94
+ langs: SUPPORTED_LANGS,
95
+ })
96
+ highlighterCache.set(theme, hl)
97
+ return hl
98
+ }
99
+
100
+ export async function getHighlighter(): Promise<Highlighter> {
101
+ return getDefaultHighlighter()
45
102
  }
46
103
 
47
104
  export async function highlight(
@@ -49,9 +106,8 @@ export async function highlight(
49
106
  language: string,
50
107
  theme: ThemeName = DEFAULT_THEME
51
108
  ): Promise<string> {
52
- const hl = await getHighlighter()
109
+ const hl = await getHighlighterForTheme(theme)
53
110
 
54
- // Normalize language names
55
111
  const lang = normalizeLanguage(language)
56
112
 
57
113
  try {
@@ -60,7 +116,6 @@ export async function highlight(
60
116
  theme: theme as BundledTheme,
61
117
  })
62
118
  } catch {
63
- // Fallback to plain text if language not supported
64
119
  return hl.codeToHtml(code, {
65
120
  lang: 'text',
66
121
  theme: theme as BundledTheme,
@@ -68,6 +123,10 @@ export async function highlight(
68
123
  }
69
124
  }
70
125
 
126
+ export function isLightTheme(theme: ThemeName): boolean {
127
+ return AVAILABLE_THEMES.find(t => t.name === theme)?.isDark === false
128
+ }
129
+
71
130
  function normalizeLanguage(lang: string): string {
72
131
  const aliases: Record<string, string> = {
73
132
  js: 'javascript',
@@ -1,4 +1,4 @@
1
- import { create, load, search as oramaSearch } from '@orama/orama'
1
+ import { create, load, search as oramaSearch, type RawData } from '@orama/orama'
2
2
  import type { SearchDatabase, SearchHit } from './search-types'
3
3
 
4
4
  let db: SearchDatabase | null = null
@@ -27,12 +27,22 @@ async function loadSearchIndex(): Promise<void> {
27
27
  try {
28
28
  const response = await fetch('/search-index.json')
29
29
  if (!response.ok) {
30
- throw new Error('Search index not found')
30
+ console.warn('Search index not available (HTTP ' + response.status + '). Search will be disabled.')
31
+ db = null
32
+ return
33
+ }
34
+
35
+ let data: unknown
36
+ try {
37
+ data = await response.json()
38
+ } catch (parseErr) {
39
+ console.error('Search index contains invalid JSON. Search will be disabled.')
40
+ db = null
41
+ return
31
42
  }
32
- const data = await response.json()
33
43
 
34
44
  const newDb = await create({ schema }) as SearchDatabase
35
- await load(newDb, data)
45
+ await load(newDb, data as RawData)
36
46
  db = newDb
37
47
  } catch (err) {
38
48
  const message = err instanceof Error ? err.message : String(err)