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.
- package/README.md +1 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +3 -5
- package/dist/autofix/index.js +15 -3
- package/dist/cli.js +19 -4
- package/dist/commands/check-links.js +164 -174
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +206 -199
- package/dist/commands/i18n.js +3 -20
- package/dist/commands/init.js +47 -40
- package/dist/commands/lint.js +3 -20
- package/dist/commands/mcp.js +125 -122
- package/dist/commands/monitor.js +125 -108
- package/dist/commands/review-pr.js +1 -1
- package/dist/commands/sdk.js +1 -1
- package/dist/config/loader.js +21 -2
- package/dist/generator/organizer.d.ts +3 -0
- package/dist/generator/organizer.js +4 -9
- package/dist/generator/writer.js +2 -10
- package/dist/github/pr-comments.js +21 -8
- package/dist/plugins/index.js +1 -0
- package/dist/scanner/index.js +8 -2
- package/dist/template/docs.json +2 -1
- package/dist/template/next.config.mjs +3 -1
- package/dist/template/package.json +17 -14
- package/dist/template/public/favicon.svg +4 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +120 -25
- package/dist/template/src/app/api/chat/route.ts +11 -3
- package/dist/template/src/app/docs/README.md +28 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
- package/dist/template/src/app/docs/auth/page.mdx +589 -0
- package/dist/template/src/app/docs/autofix/page.mdx +624 -0
- package/dist/template/src/app/docs/cli/page.mdx +217 -0
- package/dist/template/src/app/docs/config/page.mdx +428 -0
- package/dist/template/src/app/docs/configuration/page.mdx +86 -0
- package/dist/template/src/app/docs/deployment/page.mdx +112 -0
- package/dist/template/src/app/docs/error.tsx +20 -0
- package/dist/template/src/app/docs/generator/generator.md +504 -0
- package/dist/template/src/app/docs/generator/organizer.md +779 -0
- package/dist/template/src/app/docs/generator/page.mdx +613 -0
- package/dist/template/src/app/docs/github/page.mdx +502 -0
- package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
- package/dist/template/src/app/docs/llm/index.md +471 -0
- package/dist/template/src/app/docs/llm/page.mdx +428 -0
- package/dist/template/src/app/docs/llms-full.md +256 -0
- package/dist/template/src/app/docs/llms.txt +2971 -0
- package/dist/template/src/app/docs/not-found.tsx +23 -0
- package/dist/template/src/app/docs/page.mdx +0 -3
- package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
- package/dist/template/src/app/docs/pro/page.mdx +121 -0
- package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
- package/dist/template/src/app/docs/scanner/content-type.md +599 -0
- package/dist/template/src/app/docs/scanner/index.md +212 -0
- package/dist/template/src/app/docs/scanner/page.mdx +307 -0
- package/dist/template/src/app/docs/scanner/python.md +469 -0
- package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
- package/dist/template/src/app/docs/scanner/rust.md +325 -0
- package/dist/template/src/app/docs/scanner/typescript.md +201 -0
- package/dist/template/src/app/error.tsx +3 -3
- package/dist/template/src/app/icon.tsx +29 -0
- package/dist/template/src/app/layout.tsx +57 -7
- package/dist/template/src/app/not-found.tsx +35 -0
- package/dist/template/src/app/page.tsx +95 -11
- package/dist/template/src/components/ai-chat.tsx +26 -21
- package/dist/template/src/components/breadcrumbs.tsx +56 -12
- package/dist/template/src/components/copy-button.tsx +17 -3
- package/dist/template/src/components/docs-layout.tsx +202 -8
- package/dist/template/src/components/feedback.tsx +4 -2
- package/dist/template/src/components/footer.tsx +42 -0
- package/dist/template/src/components/header.tsx +56 -20
- package/dist/template/src/components/mdx/accordion.tsx +17 -13
- package/dist/template/src/components/mdx/callout.tsx +50 -37
- package/dist/template/src/components/mdx/card.tsx +24 -12
- package/dist/template/src/components/mdx/code-block.tsx +17 -3
- package/dist/template/src/components/mdx/code-group.tsx +78 -18
- package/dist/template/src/components/mdx/code-playground.tsx +3 -0
- package/dist/template/src/components/mdx/go-playground.tsx +3 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
- package/dist/template/src/components/mdx/python-playground.tsx +2 -0
- package/dist/template/src/components/mdx/steps.tsx +6 -6
- package/dist/template/src/components/mdx/tabs.tsx +76 -8
- package/dist/template/src/components/page-header.tsx +19 -0
- package/dist/template/src/components/scroll-to-top.tsx +33 -0
- package/dist/template/src/components/search-dialog.tsx +251 -57
- package/dist/template/src/components/sidebar.tsx +137 -77
- package/dist/template/src/components/table-of-contents.tsx +29 -13
- package/dist/template/src/lib/highlight.ts +90 -31
- package/dist/template/src/lib/search.ts +14 -4
- package/dist/template/src/lib/theme-utils.ts +140 -0
- package/dist/template/src/styles/globals.css +397 -84
- package/dist/template/src/types/remark-gfm.d.ts +2 -0
- package/dist/utils/files.d.ts +9 -0
- package/dist/utils/files.js +33 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.js +38 -0
- 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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
interface SidebarProps {
|
|
14
|
+
open?: boolean
|
|
15
|
+
onClose?: () => void
|
|
16
|
+
docsConfig: DocsConfig
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
30
|
-
return
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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-
|
|
63
|
-
'
|
|
64
|
-
open ? 'translate-x-0' : '-translate-x-full
|
|
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="
|
|
68
|
-
{navigation.map((
|
|
69
|
-
<
|
|
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
|
|
78
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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={
|
|
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="
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
61
|
-
<p className="text-[
|
|
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-
|
|
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 ===
|
|
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
|
-
|
|
3
|
+
// Cache highlighter instances per loaded theme set
|
|
4
|
+
const highlighterCache = new Map<string, Highlighter>()
|
|
4
5
|
|
|
5
|
-
export type ThemeName =
|
|
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 = '
|
|
29
|
+
export const DEFAULT_THEME: ThemeName = 'catppuccin-mocha'
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|