svelora 2.2.0 → 3.0.1
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 +2 -2
- package/dist/CodeBlock/CodeBlock.svelte +83 -0
- package/dist/CodeBlock/CodeBlock.svelte.d.ts +5 -0
- package/dist/CodeBlock/code-block.types.d.ts +15 -0
- package/dist/CodeBlock/code-block.types.js +1 -0
- package/dist/CodeBlock/code-block.variants.d.ts +288 -0
- package/dist/CodeBlock/code-block.variants.js +69 -0
- package/dist/CodeBlock/index.d.ts +2 -0
- package/dist/CodeBlock/index.js +1 -0
- package/dist/Drawer/Drawer.svelte +13 -3
- package/dist/Editor/Editor.svelte +3 -0
- package/dist/Fonts/Fonts.svelte +54 -0
- package/dist/Fonts/Fonts.svelte.d.ts +5 -0
- package/dist/Fonts/fonts.d.ts +12 -0
- package/dist/Fonts/fonts.js +128 -0
- package/dist/Fonts/fonts.types.d.ts +40 -0
- package/dist/Fonts/fonts.types.js +1 -0
- package/dist/Fonts/index.d.ts +3 -0
- package/dist/Fonts/index.js +2 -0
- package/dist/config.d.ts +12 -1
- package/dist/config.js +9 -0
- package/dist/docs/code-block.d.ts +13 -0
- package/dist/docs/code-block.js +214 -0
- package/dist/docs/navigation.d.ts +27 -0
- package/dist/docs/navigation.js +491 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/cursor.mcp.json +12 -0
- package/dist/mcp/install-template.mjs +55 -0
- package/dist/mcp/server.mjs +338 -0
- package/dist/mcp/svelora-docs.data.json +138 -0
- package/dist/theme.css +20 -1
- package/package.json +30 -9
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
9
|
+
import { codeToHtml } from 'shiki'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = path.dirname(__filename)
|
|
14
|
+
const repoRoot = path.resolve(__dirname, '../../')
|
|
15
|
+
const packagedDataPath = path.resolve(__dirname, './svelora-docs.data.json')
|
|
16
|
+
|
|
17
|
+
let packagedDataPromise
|
|
18
|
+
|
|
19
|
+
function resolveRepoPath(relativePath) {
|
|
20
|
+
const fullPath = path.resolve(repoRoot, relativePath)
|
|
21
|
+
if (!fullPath.startsWith(repoRoot)) {
|
|
22
|
+
throw new Error('Invalid path')
|
|
23
|
+
}
|
|
24
|
+
return fullPath
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readJsonIfExists(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
const source = await readFile(filePath, 'utf8')
|
|
30
|
+
return JSON.parse(source)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function getPackagedData() {
|
|
40
|
+
packagedDataPromise ??= readJsonIfExists(packagedDataPath)
|
|
41
|
+
return await packagedDataPromise
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeCode(code) {
|
|
45
|
+
const lines = code.replaceAll('\r\n', '\n').split('\n')
|
|
46
|
+
const firstNonEmpty = lines.findIndex((line) => line.trim().length > 0)
|
|
47
|
+
if (firstNonEmpty === -1) return ''
|
|
48
|
+
|
|
49
|
+
let lastNonEmptyFromEnd = 0
|
|
50
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
51
|
+
if (lines[index].trim().length > 0) break
|
|
52
|
+
lastNonEmptyFromEnd += 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trimmed = lines.slice(firstNonEmpty, lines.length - lastNonEmptyFromEnd)
|
|
56
|
+
const indents = trimmed
|
|
57
|
+
.filter((line) => line.trim().length > 0)
|
|
58
|
+
.map((line) => line.match(/^(\s*)/)?.[1].length ?? 0)
|
|
59
|
+
|
|
60
|
+
const minIndent = indents.length > 0 ? Math.min(...indents) : 0
|
|
61
|
+
return trimmed
|
|
62
|
+
.map((line) => line.slice(minIndent))
|
|
63
|
+
.join('\n')
|
|
64
|
+
.trim()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unwrapPreviewWrappers(source) {
|
|
68
|
+
let current = source.trim()
|
|
69
|
+
|
|
70
|
+
const stripPrefix = (token) => token.split(':').at(-1) ?? token
|
|
71
|
+
const isPresentationToken = (token) => {
|
|
72
|
+
const t = stripPrefix(token)
|
|
73
|
+
if (t.length === 0) return false
|
|
74
|
+
|
|
75
|
+
if (t.startsWith('max-w-') || t.startsWith('min-w-') || t.startsWith('w-')) return true
|
|
76
|
+
if (t.startsWith('max-h-') || t.startsWith('min-h-') || t.startsWith('h-')) return true
|
|
77
|
+
if (/^(m|p)[trblxyse]?-\d+$/.test(t)) return true
|
|
78
|
+
if (t.startsWith('gap-') || t.startsWith('space-')) return true
|
|
79
|
+
if (t.startsWith('overflow-')) return true
|
|
80
|
+
if (t === 'grid' || t.startsWith('grid-')) return true
|
|
81
|
+
if (t === 'flex' || t === 'inline-flex' || t.startsWith('flex-')) return true
|
|
82
|
+
if (t.startsWith('items-') || t.startsWith('justify-') || t.startsWith('content-'))
|
|
83
|
+
return true
|
|
84
|
+
if (t.startsWith('self-') || t.startsWith('place-')) return true
|
|
85
|
+
if (t.startsWith('rounded')) return true
|
|
86
|
+
if (t.startsWith('border') || t.startsWith('ring') || t.startsWith('shadow')) return true
|
|
87
|
+
if (t === 'bg-transparent') return true
|
|
88
|
+
if (t.startsWith('bg-surface')) return true
|
|
89
|
+
if (t.startsWith('bg-[')) return true
|
|
90
|
+
if (t.startsWith('text-') || t.startsWith('font-') || t.startsWith('leading-')) return true
|
|
91
|
+
if (t.startsWith('tracking-')) return true
|
|
92
|
+
if (t === 'uppercase' || t === 'lowercase' || t === 'capitalize') return true
|
|
93
|
+
if (t.startsWith('opacity-')) return true
|
|
94
|
+
if (t.startsWith('backdrop-blur') || t.startsWith('blur')) return true
|
|
95
|
+
if (t.startsWith('aspect-') || t.startsWith('object-')) return true
|
|
96
|
+
if (t.startsWith('size-')) return true
|
|
97
|
+
if (t.startsWith('[') && t.includes(']:')) return true
|
|
98
|
+
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
103
|
+
const match = current.match(/^<div\b([^>]*)>([\s\S]*)<\/div>$/)
|
|
104
|
+
if (!match) break
|
|
105
|
+
|
|
106
|
+
const attrs = match[1]
|
|
107
|
+
const attrsWithoutClass = attrs
|
|
108
|
+
.replace(/\s*\bclass=(?:"[^"]*"|'[^']*')/, '')
|
|
109
|
+
.replace(/\s+/g, ' ')
|
|
110
|
+
.trim()
|
|
111
|
+
if (attrsWithoutClass.length > 0) break
|
|
112
|
+
|
|
113
|
+
const classMatch = attrs.match(/\bclass=(?:"([^"]*)"|'([^']*)')/)
|
|
114
|
+
const classValue = (classMatch?.[1] ?? classMatch?.[2] ?? '').trim()
|
|
115
|
+
const tokens = classValue.length > 0 ? classValue.split(/\s+/) : []
|
|
116
|
+
|
|
117
|
+
const isPreviewBg = tokens.some((token) => token.startsWith('bg-surface-container'))
|
|
118
|
+
const isBordered = tokens.some((token) => token.startsWith('border-outline-variant'))
|
|
119
|
+
const isRounded = tokens.some((token) => token.startsWith('rounded'))
|
|
120
|
+
const hasPadding = tokens.some((token) => /^p[trblxy]?-\d+$/.test(token))
|
|
121
|
+
const isMaxWidth = tokens.some((token) => token.startsWith('max-w-'))
|
|
122
|
+
const isOverflow = tokens.some((token) => token.startsWith('overflow-'))
|
|
123
|
+
const isPresentationOnly = tokens.length > 0 && tokens.every(isPresentationToken)
|
|
124
|
+
|
|
125
|
+
const shouldUnwrap = ((isPreviewBg || isBordered || isRounded) && hasPadding) || isOverflow
|
|
126
|
+
const shouldUnwrapMaxWidth = isMaxWidth && isPresentationOnly
|
|
127
|
+
|
|
128
|
+
if (!shouldUnwrap && !shouldUnwrapMaxWidth) break
|
|
129
|
+
|
|
130
|
+
current = match[2].trim()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return current
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function decodeHeading(value) {
|
|
137
|
+
return value
|
|
138
|
+
.replaceAll('&', '&')
|
|
139
|
+
.replaceAll('×', '×')
|
|
140
|
+
.replaceAll(''', "'")
|
|
141
|
+
.replaceAll('"', '"')
|
|
142
|
+
.trim()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractSectionHeading(sectionSource) {
|
|
146
|
+
const match = sectionSource.match(/<h2\b[^>]*>([\s\S]*?)<\/h2>/)
|
|
147
|
+
if (!match) return ''
|
|
148
|
+
return decodeHeading(match[1].replace(/<[^>]+>/g, ' '))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractSectionSnippet(sectionSource) {
|
|
152
|
+
const openTagEnd = sectionSource.indexOf('>')
|
|
153
|
+
const closeTagStart = sectionSource.lastIndexOf('</section>')
|
|
154
|
+
|
|
155
|
+
if (openTagEnd < 0 || closeTagStart < 0) {
|
|
156
|
+
return normalizeCode(sectionSource)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let inner = sectionSource.slice(openTagEnd + 1, closeTagStart)
|
|
160
|
+
inner = inner.replace(/^\s*<h2\b[\s\S]*?<\/h2>\s*/, '')
|
|
161
|
+
inner = inner.replace(/^\s*<!--[\s\S]*?-->\s*/g, '')
|
|
162
|
+
|
|
163
|
+
while (/^\s*<p\b[\s\S]*?<\/p>\s*/.test(inner)) {
|
|
164
|
+
inner = inner.replace(/^\s*<p\b[\s\S]*?<\/p>\s*/, '')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
inner = inner.replace(/\s*<!--[\s\S]*?-->\s*$/g, '')
|
|
168
|
+
inner = unwrapPreviewWrappers(inner)
|
|
169
|
+
return normalizeCode(inner)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getSectionSnippets(source) {
|
|
173
|
+
return [...source.matchAll(/<section\b[\s\S]*?<\/section>/g)].map((match) => {
|
|
174
|
+
const sectionSource = match[0]
|
|
175
|
+
return {
|
|
176
|
+
heading: extractSectionHeading(sectionSource),
|
|
177
|
+
snippet: extractSectionSnippet(sectionSource)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function detectLanguage(code) {
|
|
183
|
+
const source = code.trim()
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
source.includes('<script lang="ts">') ||
|
|
187
|
+
source.includes('$state(') ||
|
|
188
|
+
source.includes('{#each') ||
|
|
189
|
+
source.includes('<Button') ||
|
|
190
|
+
source.includes('<FormField')
|
|
191
|
+
) {
|
|
192
|
+
return 'svelte'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (source.startsWith('{') || source.startsWith('[')) {
|
|
196
|
+
return 'json'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
source.startsWith('@import') ||
|
|
201
|
+
(source.includes('{') && source.includes(':') && source.includes(';'))
|
|
202
|
+
) {
|
|
203
|
+
return 'css'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (source.startsWith('#') || source.startsWith('bun ') || source.startsWith('npm ')) {
|
|
207
|
+
return 'bash'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (source.includes('export ') || source.includes('import ') || source.includes('interface ')) {
|
|
211
|
+
return 'ts'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return 'html'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function decorateHighlightedHtml(html) {
|
|
218
|
+
return html.replace(/<pre class="shiki[\s\S]*?"[^>]*>/, '<pre class="shiki">')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function renderHighlightedCode(code, isDarkMode = true) {
|
|
222
|
+
const html = await codeToHtml(code, {
|
|
223
|
+
lang: detectLanguage(code),
|
|
224
|
+
theme: isDarkMode ? 'github-dark' : 'github-light'
|
|
225
|
+
})
|
|
226
|
+
return decorateHighlightedHtml(html)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getPagePathFromSlug(slug) {
|
|
230
|
+
return `src/routes/${slug}/+page.svelte`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function readRouteSource(slug) {
|
|
234
|
+
const packagedData = await getPackagedData()
|
|
235
|
+
if (packagedData?.pages?.[slug]) {
|
|
236
|
+
return packagedData.pages[slug]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const filePath = resolveRepoPath(getPagePathFromSlug(slug))
|
|
240
|
+
return await readFile(filePath, 'utf8')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readDocsNavigationSource() {
|
|
244
|
+
const filePath = resolveRepoPath('src/lib/docs/navigation.ts')
|
|
245
|
+
return await readFile(filePath, 'utf8')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function listDocSlugs() {
|
|
249
|
+
const packagedData = await getPackagedData()
|
|
250
|
+
if (packagedData?.slugs) {
|
|
251
|
+
return packagedData.slugs
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const navigationSource = await readDocsNavigationSource()
|
|
255
|
+
return extractDocSlugs(navigationSource)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function extractDocSlugs(navigationSource) {
|
|
259
|
+
const componentSlugs = []
|
|
260
|
+
const hookSlugs = []
|
|
261
|
+
|
|
262
|
+
for (const match of navigationSource.matchAll(/href:\s*'\/docs\/components\/([^']+)'/g)) {
|
|
263
|
+
componentSlugs.push(match[1])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const match of navigationSource.matchAll(/href:\s*'\/docs\/hooks\/([^']+)'/g)) {
|
|
267
|
+
hookSlugs.push(match[1])
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
components: Array.from(new Set(componentSlugs)),
|
|
272
|
+
hooks: Array.from(new Set(hookSlugs))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const server = new McpServer({
|
|
277
|
+
name: 'svelora-docs',
|
|
278
|
+
version: '0.1.0'
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
server.tool(
|
|
282
|
+
'svelora_docs_list_slugs',
|
|
283
|
+
'List docs slugs from src/lib/docs/navigation.ts (components + hooks)',
|
|
284
|
+
{},
|
|
285
|
+
async () => {
|
|
286
|
+
const slugs = await listDocSlugs()
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'text',
|
|
292
|
+
text: JSON.stringify(slugs, null, 2)
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
server.tool(
|
|
300
|
+
'svelora_docs_get_page_source',
|
|
301
|
+
'Read src/routes/[slug]/+page.svelte and return as text',
|
|
302
|
+
{
|
|
303
|
+
slug: z.string().min(1).describe('Route slug, e.g. button, select-menu, use-form-field')
|
|
304
|
+
},
|
|
305
|
+
async ({ slug }) => {
|
|
306
|
+
const source = await readRouteSource(slug)
|
|
307
|
+
return { content: [{ type: 'text', text: source }] }
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
server.tool(
|
|
312
|
+
'svelora_docs_get_section_snippets',
|
|
313
|
+
'Extract all <section> headings and code snippets from a route page source',
|
|
314
|
+
{
|
|
315
|
+
slug: z.string().min(1).describe('Route slug, e.g. button, select-menu, use-form-field')
|
|
316
|
+
},
|
|
317
|
+
async ({ slug }) => {
|
|
318
|
+
const source = await readRouteSource(slug)
|
|
319
|
+
const sections = getSectionSnippets(source)
|
|
320
|
+
return { content: [{ type: 'text', text: JSON.stringify(sections, null, 2) }] }
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
server.tool(
|
|
325
|
+
'svelora_docs_render_shiki',
|
|
326
|
+
'Render highlighted HTML (Shiki) for a code snippet. Returns <pre class="shiki">...</pre> HTML',
|
|
327
|
+
{
|
|
328
|
+
code: z.string().min(1).describe('Code string'),
|
|
329
|
+
isDarkMode: z.boolean().optional().describe('Use dark theme when true')
|
|
330
|
+
},
|
|
331
|
+
async ({ code, isDarkMode }) => {
|
|
332
|
+
const html = await renderHighlightedCode(code, isDarkMode ?? true)
|
|
333
|
+
return { content: [{ type: 'text', text: html }] }
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const transport = new StdioServerTransport()
|
|
338
|
+
await server.connect(transport)
|