@abraca/mcp 1.8.1 → 2.3.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.
@@ -1,915 +1,13 @@
1
- /**
2
- * Markdown Y.js converter.
3
- * Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
4
- */
5
- import * as Y from 'yjs'
6
- import type { PageMeta } from './types.ts'
7
-
8
- // ── Filename → readable label ────────────────────────────────────────────────
9
-
10
- export function filenameToLabel(raw: string): string {
11
- const base = raw.replace(/\.[^.]+$/, '')
12
- const spaced = base.replace(/([a-z])([A-Z])/g, '$1 $2')
13
- const clean = spaced.replace(/[-_.]+/g, ' ').replace(/\s+/g, ' ').trim()
14
- return clean.charAt(0).toUpperCase() + clean.slice(1)
15
- }
16
-
17
- // ── YAML frontmatter parser ──────────────────────────────────────────────────
18
-
19
- export interface FrontmatterResult {
20
- title?: string
21
- meta: Partial<PageMeta>
22
- body: string
23
- }
24
-
25
- function coerceScalar(raw: string): string | number | boolean {
26
- const trimmed = raw.trim()
27
- if (trimmed === 'true') return true
28
- if (trimmed === 'false') return false
29
- const num = Number(trimmed)
30
- if (!Number.isNaN(num) && trimmed !== '') return num
31
- if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
32
- || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
33
- return trimmed.slice(1, -1)
34
- }
35
- return trimmed
36
- }
37
-
38
- function parseInlineArray(raw: string): string[] {
39
- return raw.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean)
40
- }
41
-
42
- export function parseFrontmatter(markdown: string): FrontmatterResult {
43
- const noResult: FrontmatterResult = { meta: {}, body: markdown }
44
-
45
- const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
46
- if (!match) return noResult
47
-
48
- const yamlBlock = match[1]!
49
- const body = markdown.slice(match[0].length)
50
-
51
- const raw: Record<string, string | string[]> = {}
52
- const lines = yamlBlock.split('\n')
53
- let i = 0
54
- while (i < lines.length) {
55
- const line = lines[i]!
56
- const blockSeqKey = line.match(/^(\w[\w-]*):\s*$/)
57
- if (blockSeqKey && i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1]!)) {
58
- const key = blockSeqKey[1]!
59
- const items: string[] = []
60
- i++
61
- while (i < lines.length && /^\s+-\s/.test(lines[i]!)) {
62
- items.push(lines[i]!.replace(/^\s+-\s/, '').trim())
63
- i++
64
- }
65
- raw[key] = items
66
- continue
67
- }
68
- const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/)
69
- if (kvMatch) {
70
- const key = kvMatch[1]!
71
- const val = kvMatch[2]!.trim()
72
- if (val.startsWith('[') && val.endsWith(']')) {
73
- raw[key] = parseInlineArray(val)
74
- } else {
75
- raw[key] = val
76
- }
77
- }
78
- i++
79
- }
80
-
81
- const meta: Partial<PageMeta> = {}
82
-
83
- const getStr = (keys: string[]): string | undefined => {
84
- for (const k of keys) {
85
- const v = raw[k]
86
- if (typeof v === 'string' && v) return v
87
- }
88
- }
89
-
90
- if (raw['tags']) meta.tags = Array.isArray(raw['tags']) ? raw['tags'] : [raw['tags'] as string]
91
- const color = getStr(['color'])
92
- if (color) meta.color = color
93
- const icon = getStr(['icon'])
94
- if (icon) meta.icon = icon
95
- const status = getStr(['status'])
96
- if (status) meta.status = status
97
-
98
- const priorityRaw = getStr(['priority'])
99
- if (priorityRaw !== undefined) {
100
- const map: Record<string, number> = { low: 1, medium: 2, high: 3, urgent: 4 }
101
- meta.priority = map[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0)
102
- }
103
-
104
- const checkedRaw = raw['checked'] ?? raw['done']
105
- if (checkedRaw !== undefined) meta.checked = checkedRaw === 'true' || checkedRaw === true
106
-
107
- const dateStart = getStr(['date', 'created'])
108
- if (dateStart) meta.dateStart = dateStart
109
- const dateEnd = getStr(['due'])
110
- if (dateEnd) meta.dateEnd = dateEnd
111
-
112
- const subtitle = getStr(['description', 'subtitle'])
113
- if (subtitle) meta.subtitle = subtitle
114
- const url = getStr(['url'])
115
- if (url) meta.url = url
116
- const email = getStr(['email'])
117
- if (email) meta.email = email
118
- const phone = getStr(['phone'])
119
- if (phone) meta.phone = phone
120
-
121
- const ratingRaw = getStr(['rating'])
122
- if (ratingRaw !== undefined) {
123
- const n = Number(ratingRaw)
124
- if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n))
125
- }
126
-
127
- // Datetime fields
128
- const datetimeStart = getStr(['datetimeStart'])
129
- if (datetimeStart) meta.datetimeStart = datetimeStart
130
- const datetimeEnd = getStr(['datetimeEnd'])
131
- if (datetimeEnd) meta.datetimeEnd = datetimeEnd
132
- const allDayRaw = raw['allDay']
133
- if (allDayRaw !== undefined) meta.allDay = allDayRaw === 'true' || allDayRaw === true
134
-
135
- // Geo fields
136
- const geoLatRaw = getStr(['geoLat'])
137
- if (geoLatRaw !== undefined) { const n = Number(geoLatRaw); if (!Number.isNaN(n)) meta.geoLat = n }
138
- const geoLngRaw = getStr(['geoLng'])
139
- if (geoLngRaw !== undefined) { const n = Number(geoLngRaw); if (!Number.isNaN(n)) meta.geoLng = n }
140
- const geoType = getStr(['geoType'])
141
- if (geoType && (geoType === 'marker' || geoType === 'line' || geoType === 'measure')) {
142
- meta.geoType = geoType
143
- }
144
- const geoDescription = getStr(['geoDescription'])
145
- if (geoDescription) meta.geoDescription = geoDescription
146
-
147
- // Numeric fields
148
- const numberRaw = getStr(['number'])
149
- if (numberRaw !== undefined) { const n = Number(numberRaw); if (!Number.isNaN(n)) meta.number = n }
150
- const unit = getStr(['unit'])
151
- if (unit) meta.unit = unit
152
-
153
- // Note
154
- const note = getStr(['note'])
155
- if (note) meta.note = note
156
-
157
- const title = typeof raw['title'] === 'string' ? raw['title'] : undefined
158
-
159
- return { title, meta, body }
160
- }
161
-
162
- // ── Inline token parsing ─────────────────────────────────────────────────────
163
-
164
- interface InlineToken {
165
- text: string
166
- attrs?: Record<string, unknown>
167
- }
168
-
169
- function parseInline(text: string): InlineToken[] {
170
- const stripped = text.replace(/\{lang="[^"]*"\}/g, '')
171
- .replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, '$2')
172
- .replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, '')
173
-
174
- const tokens: InlineToken[] = []
175
- const re = /:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|!?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g
176
- let lastIndex = 0
177
- let match: RegExpExecArray | null
178
-
179
- while ((match = re.exec(stripped)) !== null) {
180
- if (match.index > lastIndex) {
181
- tokens.push({ text: stripped.slice(lastIndex, match.index) })
182
- }
183
- if (match[1] !== undefined) {
184
- const badgeProps = parseMdcProps(match[2])
185
- tokens.push({ text: match[1] || 'Badge', attrs: { badge: { label: match[1] || 'Badge', color: badgeProps['color'] || 'neutral', variant: badgeProps['variant'] || 'subtle' } } })
186
- } else if (match[3] !== undefined) {
187
- const iconProps = parseMdcProps(`{${match[3]}}`)
188
- tokens.push({ text: '\u200B', attrs: { proseIcon: { name: iconProps['name'] || 'i-lucide-star' } } })
189
- } else if (match[4] !== undefined) {
190
- const kbdProps = parseMdcProps(`{${match[4]}}`)
191
- tokens.push({ text: kbdProps['value'] || '', attrs: { kbd: { value: kbdProps['value'] || '' } } })
192
- } else if (match[5] !== undefined) {
193
- // Inline wikilink [[docId]] or [[docId|label]] → link to /doc/docId
194
- const docId = match[5]
195
- const displayText = match[6] ?? docId
196
- tokens.push({ text: displayText!, attrs: { link: { href: `/doc/${docId}` } } })
197
- } else if (match[7] !== undefined) {
198
- tokens.push({ text: match[7], attrs: { strike: true } })
199
- } else if (match[8] !== undefined) {
200
- tokens.push({ text: match[8], attrs: { bold: true } })
201
- } else if (match[9] !== undefined) {
202
- tokens.push({ text: match[9], attrs: { italic: true } })
203
- } else if (match[10] !== undefined) {
204
- tokens.push({ text: match[10], attrs: { italic: true } })
205
- } else if (match[11] !== undefined) {
206
- tokens.push({ text: match[11], attrs: { code: true } })
207
- } else if (match[12] !== undefined && match[13] !== undefined) {
208
- tokens.push({ text: match[12], attrs: { link: { href: match[13] } } })
209
- }
210
- lastIndex = match.index + match[0].length
211
- }
212
-
213
- if (lastIndex < stripped.length) {
214
- tokens.push({ text: stripped.slice(lastIndex) })
215
- }
216
- return tokens.filter(t => t.text.length > 0)
217
- }
218
-
219
- // ── Block-level parser ───────────────────────────────────────────────────────
220
-
221
- interface TaskItem {
222
- text: string
223
- checked: boolean
224
- }
225
-
226
- type Block =
227
- | { type: 'heading'; level: number; text: string }
228
- | { type: 'paragraph'; text: string }
229
- | { type: 'bulletList'; items: string[] }
230
- | { type: 'orderedList'; items: string[] }
231
- | { type: 'taskList'; items: TaskItem[] }
232
- | { type: 'codeBlock'; lang: string; code: string }
233
- | { type: 'blockquote'; lines: string[] }
234
- | { type: 'table'; headerRow: string[]; dataRows: string[][] }
235
- | { type: 'hr' }
236
- | { type: 'callout'; calloutType: string; innerBlocks: Block[] }
237
- | { type: 'collapsible'; label: string; open: boolean; innerBlocks: Block[] }
238
- | { type: 'steps'; innerBlocks: Block[] }
239
- | { type: 'card'; title: string; icon: string; to: string; innerBlocks: Block[] }
240
- | { type: 'cardGroup'; cards: Block[] }
241
- | { type: 'codeCollapse'; codeBlocks: Block[] }
242
- | { type: 'codeGroup'; codeBlocks: Block[] }
243
- | { type: 'codePreview'; innerBlocks: Block[]; codeBlocks: Block[] }
244
- | { type: 'codeTree'; files: string }
245
- | { type: 'accordion'; items: { label: string; icon: string; innerBlocks: Block[] }[] }
246
- | { type: 'tabs'; items: { label: string; icon: string; innerBlocks: Block[] }[] }
247
- | { type: 'field'; name: string; fieldType: string; required: boolean; innerBlocks: Block[] }
248
- | { type: 'fieldGroup'; fields: Block[] }
249
- | { type: 'image'; src: string; alt: string; width?: string; height?: string }
250
- | { type: 'docEmbed'; docId: string }
251
- | { type: 'svgEmbed'; svg: string; title: string }
252
-
253
- function parseTableRow(line: string): string[] {
254
- const parts = line.split('|')
255
- return parts.slice(1, parts.length - 1).map(c => c.trim())
256
- }
257
-
258
- function isTableSeparator(line: string): boolean {
259
- return /^\|[\s|:-]+\|$/.test(line.trim())
260
- }
261
-
262
- function extractFencedCode(lines: string[]): Block[] {
263
- const result: Block[] = []
264
- let i = 0
265
- while (i < lines.length) {
266
- const line = lines[i]!
267
- const fenceMatch = line.match(/^(`{3,})(\w*)/)
268
- if (fenceMatch) {
269
- const fence = fenceMatch[1]!
270
- const lang = fenceMatch[2] ?? ''
271
- const codeLines: string[] = []
272
- i++
273
- while (i < lines.length && !lines[i]!.startsWith(fence)) {
274
- codeLines.push(lines[i]!)
275
- i++
276
- }
277
- i++
278
- result.push({ type: 'codeBlock', lang, code: codeLines.join('\n') })
279
- continue
280
- }
281
- i++
282
- }
283
- return result
284
- }
285
-
286
- function parseMdcProps(propsStr: string | undefined): Record<string, string> {
287
- if (!propsStr) return {}
288
- const result: Record<string, string> = {}
289
- const re = /(\w[\w-]*)="([^"]*)"/g
290
- let m: RegExpExecArray | null
291
- while ((m = re.exec(propsStr)) !== null) {
292
- result[m[1]!] = m[2]!
293
- }
294
- return result
295
- }
296
-
297
- function parseMdcChildren(innerLines: string[], slotPrefix: string): { label: string; icon: string; innerBlocks: Block[] }[] {
298
- const items: { label: string; icon: string; lines: string[] }[] = []
299
- let current: { label: string; icon: string; lines: string[] } | null = null
300
- const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`)
301
-
302
- for (const line of innerLines) {
303
- const slotMatch = line.match(slotRe)
304
- if (slotMatch) {
305
- if (current) items.push(current)
306
- const props = parseMdcProps(slotMatch[1])
307
- current = { label: props['label'] || props['title'] || `Item ${items.length + 1}`, icon: props['icon'] || '', lines: [] }
308
- continue
309
- }
310
- if (current) {
311
- current.lines.push(line)
312
- } else {
313
- if (!items.length && !current) {
314
- current = { label: `Item 1`, icon: '', lines: [line] }
315
- }
316
- }
317
- }
318
- if (current) items.push(current)
319
-
320
- return items.map(item => ({
321
- label: item.label,
322
- icon: item.icon,
323
- innerBlocks: parseBlocks(item.lines.join('\n')),
324
- }))
325
- }
326
-
327
- const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/
328
-
329
- function parseBlocks(markdown: string): Block[] {
330
- const rawLines = markdown.split('\n')
331
- let firstContentLine = 0
332
- while (firstContentLine < rawLines.length) {
333
- const l = rawLines[firstContentLine]!
334
- if (l.trim() === '' || /^import\s/.test(l) || /^export\s/.test(l)) {
335
- firstContentLine++
336
- } else {
337
- break
338
- }
339
- }
340
- const stripped = rawLines.slice(firstContentLine).join('\n')
341
-
342
- const blocks: Block[] = []
343
- const lines = stripped.split('\n')
344
- let i = 0
345
-
346
- while (i < lines.length) {
347
- const line = lines[i]!
348
-
349
- const fenceBlockMatch = line.match(/^(`{3,})(.*)$/)
350
- if (fenceBlockMatch) {
351
- const fence = fenceBlockMatch[1]!
352
- const lang = fenceBlockMatch[2]!.trim()
353
- .replace(/\{[^}]*\}$/, '')
354
- .replace(/\s*\[.*\]$/, '')
355
- .trim()
356
- const codeLines: string[] = []
357
- i++
358
- while (i < lines.length && !lines[i]!.startsWith(fence)) {
359
- codeLines.push(lines[i]!)
360
- i++
361
- }
362
- i++
363
- if (lang === 'svg' || lang.startsWith('svg ')) {
364
- const svgTitle = lang === 'svg' ? '' : lang.slice(4).trim()
365
- blocks.push({ type: 'svgEmbed', svg: codeLines.join('\n'), title: svgTitle })
366
- } else {
367
- blocks.push({ type: 'codeBlock', lang, code: codeLines.join('\n') })
368
- }
369
- continue
370
- }
371
-
372
- const headingMatch = line.match(/^(#{1,6})\s+(.*)/)
373
- if (headingMatch) {
374
- blocks.push({ type: 'heading', level: headingMatch[1]!.length, text: headingMatch[2]!.trim() })
375
- i++
376
- continue
377
- }
378
-
379
- if (/^[-*_]{3,}\s*$/.test(line)) {
380
- blocks.push({ type: 'hr' })
381
- i++
382
- continue
383
- }
384
-
385
- const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/)
386
- if (docEmbedMatch) {
387
- blocks.push({ type: 'docEmbed', docId: docEmbedMatch[1]! })
388
- i++
389
- continue
390
- }
391
-
392
- const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/)
393
- if (imgMatch) {
394
- const alt = imgMatch[1] ?? ''
395
- const src = imgMatch[2] ?? ''
396
- const attrs = parseMdcProps(imgMatch[3])
397
- blocks.push({ type: 'image', src, alt, width: attrs['width'], height: attrs['height'] })
398
- i++
399
- continue
400
- }
401
-
402
- if (line.startsWith('> ') || line === '>') {
403
- const bqLines: string[] = []
404
- while (i < lines.length && (lines[i]!.startsWith('> ') || lines[i] === '>')) {
405
- bqLines.push(lines[i]!.replace(/^>\s?/, ''))
406
- i++
407
- }
408
- blocks.push({ type: 'blockquote', lines: bqLines })
409
- continue
410
- }
411
-
412
- if (/^\s*\|/.test(line)) {
413
- const tableLines: string[] = []
414
- while (i < lines.length && /^\s*\|/.test(lines[i]!)) {
415
- tableLines.push(lines[i]!)
416
- i++
417
- }
418
- if (tableLines.length >= 2 && isTableSeparator(tableLines[1]!)) {
419
- const headerRow = parseTableRow(tableLines[0]!)
420
- const dataRows = tableLines.slice(2)
421
- .filter(l => !isTableSeparator(l))
422
- .map(parseTableRow)
423
- blocks.push({ type: 'table', headerRow, dataRows })
424
- } else {
425
- for (const l of tableLines) blocks.push({ type: 'paragraph', text: l })
426
- }
427
- continue
428
- }
429
-
430
- const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/
431
- if (MDC_OPEN.test(line)) {
432
- const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2
433
- const componentName = line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? ''
434
- const innerLines: string[] = []
435
- i++
436
- while (i < lines.length) {
437
- const l = lines[i]!
438
- if (new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)) { i++; break }
439
- const innerFence = l.match(/^(\s*`{3,})/)
440
- if (innerFence) {
441
- const fenceStr = innerFence[1]!.trimStart()
442
- innerLines.push(l)
443
- i++
444
- while (i < lines.length && !lines[i]!.trimStart().startsWith(fenceStr)) {
445
- innerLines.push(lines[i]!)
446
- i++
447
- }
448
- if (i < lines.length) { innerLines.push(lines[i]!); i++ }
449
- continue
450
- }
451
- innerLines.push(l)
452
- i++
453
- }
454
-
455
- const nonBlank = innerLines.filter(l => l.trim().length > 0)
456
- if (nonBlank.length) {
457
- const minIndent = Math.min(...nonBlank.map(l => l.match(/^(\s*)/)?.[1]?.length ?? 0))
458
- if (minIndent > 0) {
459
- for (let j = 0; j < innerLines.length; j++) {
460
- innerLines[j] = innerLines[j]!.slice(Math.min(minIndent, innerLines[j]!.length))
461
- }
462
- }
463
- }
464
-
465
- let contentStart = 0
466
- if (innerLines[0]?.trim() === '---') {
467
- const fmEnd = innerLines.findIndex((l, idx) => idx > 0 && l.trim() === '---')
468
- if (fmEnd !== -1) contentStart = fmEnd + 1
469
- }
470
- const contentLines = innerLines.slice(contentStart)
471
-
472
- const defaultSlotLines: string[] = []
473
- const codeSlotLines: string[] = []
474
- let currentSlot: 'default' | 'code' | 'other' = 'default'
475
- for (const l of contentLines) {
476
- if (/^#code\s*$/.test(l)) { currentSlot = 'code'; continue }
477
- if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) { currentSlot = 'other'; continue }
478
- if (currentSlot === 'default') defaultSlotLines.push(l)
479
- else if (currentSlot === 'code') codeSlotLines.push(l)
480
- }
481
- const innerBlocks = parseBlocks(defaultSlotLines.join('\n'))
482
-
483
- const codeBlocks = extractFencedCode(codeSlotLines)
484
-
485
- const CALLOUT_NAMES = new Set(['tip', 'note', 'info', 'warning', 'caution', 'danger', 'callout', 'alert'])
486
- if (CALLOUT_NAMES.has(componentName.toLowerCase())) {
487
- blocks.push({ type: 'callout', calloutType: componentName.toLowerCase(), innerBlocks })
488
- } else {
489
- const mdcProps = parseMdcProps(line.match(MDC_OPEN)?.[3])
490
- const lc = componentName.toLowerCase()
491
-
492
- if (lc === 'collapsible') {
493
- blocks.push({ type: 'collapsible', label: mdcProps['label'] || 'Details', open: mdcProps['open'] === 'true', innerBlocks })
494
- } else if (lc === 'steps') {
495
- blocks.push({ type: 'steps', innerBlocks })
496
- } else if (lc === 'card') {
497
- blocks.push({ type: 'card', title: mdcProps['title'] || '', icon: mdcProps['icon'] || '', to: mdcProps['to'] || '', innerBlocks })
498
- } else if (lc === 'card-group') {
499
- const cards = innerBlocks.filter(b => b.type === 'card')
500
- if (cards.length) {
501
- blocks.push({ type: 'cardGroup', cards })
502
- } else {
503
- blocks.push(...innerBlocks)
504
- }
505
- } else if (lc === 'code-collapse') {
506
- blocks.push({ type: 'codeCollapse', codeBlocks: codeBlocks.length ? codeBlocks : innerBlocks.filter(b => b.type === 'codeBlock') })
507
- } else if (lc === 'code-group') {
508
- const allCode = [...innerBlocks.filter(b => b.type === 'codeBlock'), ...codeBlocks]
509
- blocks.push({ type: 'codeGroup', codeBlocks: allCode })
510
- } else if (lc === 'code-preview') {
511
- blocks.push({ type: 'codePreview', innerBlocks, codeBlocks })
512
- } else if (lc === 'code-tree') {
513
- blocks.push({ type: 'codeTree', files: mdcProps['files'] || '[]' })
514
- } else if (lc === 'accordion') {
515
- const items = parseMdcChildren(contentLines, 'item')
516
- if (items.length) {
517
- blocks.push({ type: 'accordion', items })
518
- } else {
519
- blocks.push({ type: 'accordion', items: [{ label: 'Item 1', icon: '', innerBlocks }] })
520
- }
521
- } else if (lc === 'tabs') {
522
- const items = parseMdcChildren(contentLines, 'tab')
523
- if (items.length) {
524
- blocks.push({ type: 'tabs', items })
525
- } else {
526
- blocks.push({ type: 'tabs', items: [{ label: 'Tab 1', icon: '', innerBlocks }] })
527
- }
528
- } else if (lc === 'field') {
529
- blocks.push({ type: 'field', name: mdcProps['name'] || '', fieldType: mdcProps['type'] || 'string', required: mdcProps['required'] === 'true', innerBlocks })
530
- } else if (lc === 'field-group') {
531
- const fields = innerBlocks.filter(b => b.type === 'field')
532
- if (fields.length) {
533
- blocks.push({ type: 'fieldGroup', fields })
534
- } else {
535
- blocks.push(...innerBlocks)
536
- }
537
- } else {
538
- blocks.push(...innerBlocks)
539
- blocks.push(...codeBlocks)
540
- }
541
- }
542
- continue
543
- }
544
-
545
- if (TASK_RE.test(line)) {
546
- const items: TaskItem[] = []
547
- while (i < lines.length && TASK_RE.test(lines[i]!)) {
548
- const m = lines[i]!.match(TASK_RE)!
549
- items.push({ checked: m[1]!.toLowerCase() === 'x', text: m[2]! })
550
- i++
551
- }
552
- blocks.push({ type: 'taskList', items })
553
- continue
554
- }
555
-
556
- if (/^[-*+]\s+/.test(line)) {
557
- const items: string[] = []
558
- while (i < lines.length && /^[-*+]\s+/.test(lines[i]!) && !TASK_RE.test(lines[i]!)) {
559
- items.push(lines[i]!.replace(/^[-*+]\s+/, ''))
560
- i++
561
- }
562
- if (items.length) {
563
- blocks.push({ type: 'bulletList', items })
564
- continue
565
- }
566
- }
567
-
568
- if (/^\d+\.\s+/.test(line)) {
569
- const items: string[] = []
570
- while (i < lines.length && /^\d+\.\s+/.test(lines[i]!)) {
571
- items.push(lines[i]!.replace(/^\d+\.\s+/, ''))
572
- i++
573
- }
574
- blocks.push({ type: 'orderedList', items })
575
- continue
576
- }
577
-
578
- if (line.trim() === '') {
579
- i++
580
- continue
581
- }
582
-
583
- const paraLines: string[] = []
584
- while (
585
- i < lines.length
586
- && lines[i]!.trim() !== ''
587
- && !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(lines[i]!)
588
- ) {
589
- paraLines.push(lines[i]!)
590
- i++
591
- }
592
- if (paraLines.length) {
593
- blocks.push({ type: 'paragraph', text: paraLines.join(' ') })
594
- }
595
- }
596
-
597
- return blocks
598
- }
599
-
600
- // ── Y.js content population ──────────────────────────────────────────────────
601
-
602
- function fillTextInto(el: Y.XmlElement, tokens: InlineToken[]): void {
603
- const filtered = tokens.filter(t => t.text.length > 0)
604
- if (!filtered.length) return
605
-
606
- const xtNodes = filtered.map(() => new Y.XmlText())
607
- el.insert(0, xtNodes)
608
-
609
- filtered.forEach((tok, i) => {
610
- if (tok.attrs) {
611
- xtNodes[i]!.insert(0, tok.text, tok.attrs as Record<string, boolean | object>)
612
- } else {
613
- xtNodes[i]!.insert(0, tok.text)
614
- }
615
- })
616
- }
617
-
618
- function blockElName(b: Block): string {
619
- switch (b.type) {
620
- case 'heading': return 'heading'
621
- case 'paragraph': return 'paragraph'
622
- case 'bulletList': return 'bulletList'
623
- case 'orderedList': return 'orderedList'
624
- case 'taskList': return 'taskList'
625
- case 'codeBlock': return 'codeBlock'
626
- case 'blockquote': return 'blockquote'
627
- case 'table': return 'table'
628
- case 'hr': return 'horizontalRule'
629
- case 'callout': return 'callout'
630
- case 'collapsible': return 'collapsible'
631
- case 'steps': return 'steps'
632
- case 'card': return 'card'
633
- case 'cardGroup': return 'cardGroup'
634
- case 'codeCollapse': return 'codeCollapse'
635
- case 'codeGroup': return 'codeGroup'
636
- case 'codePreview': return 'codePreview'
637
- case 'codeTree': return 'codeTree'
638
- case 'accordion': return 'accordion'
639
- case 'tabs': return 'tabs'
640
- case 'field': return 'field'
641
- case 'fieldGroup': return 'fieldGroup'
642
- case 'image': return 'image'
643
- case 'docEmbed': return 'docEmbed'
644
- case 'svgEmbed': return 'svgEmbed'
645
- }
646
- }
647
-
648
- function fillBlock(el: Y.XmlElement, block: Block): void {
649
- switch (block.type) {
650
- case 'heading': {
651
- el.setAttribute('level', block.level as any)
652
- fillTextInto(el, parseInline(block.text))
653
- break
654
- }
655
- case 'paragraph': {
656
- fillTextInto(el, parseInline(block.text))
657
- break
658
- }
659
- case 'bulletList':
660
- case 'orderedList': {
661
- const listItemEls = block.items.map(() => new Y.XmlElement('listItem'))
662
- el.insert(0, listItemEls)
663
- block.items.forEach((text, i) => {
664
- const paraEl = new Y.XmlElement('paragraph')
665
- listItemEls[i]!.insert(0, [paraEl])
666
- fillTextInto(paraEl, parseInline(text))
667
- })
668
- break
669
- }
670
- case 'taskList': {
671
- const taskItemEls = block.items.map(() => new Y.XmlElement('taskItem'))
672
- el.insert(0, taskItemEls)
673
- block.items.forEach((item, i) => {
674
- taskItemEls[i]!.setAttribute('checked', item.checked as any)
675
- const paraEl = new Y.XmlElement('paragraph')
676
- taskItemEls[i]!.insert(0, [paraEl])
677
- fillTextInto(paraEl, parseInline(item.text))
678
- })
679
- break
680
- }
681
- case 'codeBlock': {
682
- if (block.lang) el.setAttribute('language', block.lang)
683
- const xt = new Y.XmlText()
684
- el.insert(0, [xt])
685
- xt.insert(0, block.code)
686
- break
687
- }
688
- case 'blockquote': {
689
- const paraEls = block.lines.map(() => new Y.XmlElement('paragraph'))
690
- el.insert(0, paraEls)
691
- block.lines.forEach((line, i) => fillTextInto(paraEls[i]!, parseInline(line)))
692
- break
693
- }
694
- case 'table': {
695
- const headerRowEl = new Y.XmlElement('tableRow')
696
- const dataRowEls = block.dataRows.map(() => new Y.XmlElement('tableRow'))
697
- el.insert(0, [headerRowEl, ...dataRowEls])
698
-
699
- const headerCellEls = block.headerRow.map(() => new Y.XmlElement('tableHeader'))
700
- headerRowEl.insert(0, headerCellEls)
701
- block.headerRow.forEach((cellText, i) => {
702
- const paraEl = new Y.XmlElement('paragraph')
703
- headerCellEls[i]!.insert(0, [paraEl])
704
- fillTextInto(paraEl, parseInline(cellText))
705
- })
706
-
707
- block.dataRows.forEach((row, ri) => {
708
- const cellEls = row.map(() => new Y.XmlElement('tableCell'))
709
- dataRowEls[ri]!.insert(0, cellEls)
710
- row.forEach((cellText, ci) => {
711
- const paraEl = new Y.XmlElement('paragraph')
712
- cellEls[ci]!.insert(0, [paraEl])
713
- fillTextInto(paraEl, parseInline(cellText))
714
- })
715
- })
716
- break
717
- }
718
- case 'hr': break
719
- case 'callout': {
720
- el.setAttribute('type', block.calloutType)
721
- if (!block.innerBlocks.length) {
722
- const paraEl = new Y.XmlElement('paragraph')
723
- el.insert(0, [paraEl])
724
- break
725
- }
726
- const innerEls = block.innerBlocks.map(b => new Y.XmlElement(blockElName(b)))
727
- el.insert(0, innerEls)
728
- block.innerBlocks.forEach((b, i) => fillBlock(innerEls[i]!, b))
729
- break
730
- }
731
- case 'collapsible': {
732
- el.setAttribute('label', block.label)
733
- el.setAttribute('open', block.open as any)
734
- const inner = block.innerBlocks.length ? block.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
735
- const innerEls = inner.map(b => new Y.XmlElement(blockElName(b)))
736
- el.insert(0, innerEls)
737
- inner.forEach((b, i) => fillBlock(innerEls[i]!, b))
738
- break
739
- }
740
- case 'steps': {
741
- const inner = block.innerBlocks.length ? block.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
742
- const innerEls = inner.map(b => new Y.XmlElement(blockElName(b)))
743
- el.insert(0, innerEls)
744
- inner.forEach((b, i) => fillBlock(innerEls[i]!, b))
745
- break
746
- }
747
- case 'card': {
748
- if (block.title) el.setAttribute('title', block.title)
749
- if (block.icon) el.setAttribute('icon', block.icon)
750
- if (block.to) el.setAttribute('to', block.to)
751
- const inner = block.innerBlocks.length ? block.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
752
- const innerEls = inner.map(b => new Y.XmlElement(blockElName(b)))
753
- el.insert(0, innerEls)
754
- inner.forEach((b, i) => fillBlock(innerEls[i]!, b))
755
- break
756
- }
757
- case 'cardGroup': {
758
- const cardEls = block.cards.map(b => new Y.XmlElement(blockElName(b)))
759
- el.insert(0, cardEls)
760
- block.cards.forEach((b, i) => fillBlock(cardEls[i]!, b))
761
- break
762
- }
763
- case 'codeCollapse': {
764
- const codes = block.codeBlocks.length ? block.codeBlocks : [{ type: 'codeBlock' as const, lang: '', code: '' }]
765
- const codeEl = new Y.XmlElement('codeBlock')
766
- el.insert(0, [codeEl])
767
- fillBlock(codeEl, codes[0]!)
768
- break
769
- }
770
- case 'codeGroup': {
771
- const codes = block.codeBlocks.length ? block.codeBlocks : [{ type: 'codeBlock' as const, lang: '', code: '' }]
772
- const codeEls = codes.map(() => new Y.XmlElement('codeBlock'))
773
- el.insert(0, codeEls)
774
- codes.forEach((b, i) => fillBlock(codeEls[i]!, b))
775
- break
776
- }
777
- case 'codePreview': {
778
- const all = [...block.innerBlocks, ...block.codeBlocks]
779
- const inner = all.length ? all : [{ type: 'paragraph' as const, text: '' }]
780
- const innerEls = inner.map(b => new Y.XmlElement(blockElName(b)))
781
- el.insert(0, innerEls)
782
- inner.forEach((b, i) => fillBlock(innerEls[i]!, b))
783
- break
784
- }
785
- case 'codeTree': {
786
- el.setAttribute('files', block.files)
787
- break
788
- }
789
- case 'accordion': {
790
- const itemEls = block.items.map(() => new Y.XmlElement('accordionItem'))
791
- el.insert(0, itemEls)
792
- block.items.forEach((item, i) => {
793
- itemEls[i]!.setAttribute('label', item.label)
794
- if (item.icon) itemEls[i]!.setAttribute('icon', item.icon)
795
- const inner = item.innerBlocks.length ? item.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
796
- const childEls = inner.map(b => new Y.XmlElement(blockElName(b)))
797
- itemEls[i]!.insert(0, childEls)
798
- inner.forEach((b, ci) => fillBlock(childEls[ci]!, b))
799
- })
800
- break
801
- }
802
- case 'tabs': {
803
- const itemEls = block.items.map(() => new Y.XmlElement('tabsItem'))
804
- el.insert(0, itemEls)
805
- block.items.forEach((item, i) => {
806
- itemEls[i]!.setAttribute('label', item.label)
807
- if (item.icon) itemEls[i]!.setAttribute('icon', item.icon)
808
- const inner = item.innerBlocks.length ? item.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
809
- const childEls = inner.map(b => new Y.XmlElement(blockElName(b)))
810
- itemEls[i]!.insert(0, childEls)
811
- inner.forEach((b, ci) => fillBlock(childEls[ci]!, b))
812
- })
813
- break
814
- }
815
- case 'field': {
816
- if (block.name) el.setAttribute('name', block.name)
817
- el.setAttribute('type', block.fieldType)
818
- el.setAttribute('required', block.required as any)
819
- const inner = block.innerBlocks.length ? block.innerBlocks : [{ type: 'paragraph' as const, text: '' }]
820
- const innerEls = inner.map(b => new Y.XmlElement(blockElName(b)))
821
- el.insert(0, innerEls)
822
- inner.forEach((b, i) => fillBlock(innerEls[i]!, b))
823
- break
824
- }
825
- case 'fieldGroup': {
826
- const fieldEls = block.fields.map(b => new Y.XmlElement(blockElName(b)))
827
- el.insert(0, fieldEls)
828
- block.fields.forEach((b, i) => fillBlock(fieldEls[i]!, b))
829
- break
830
- }
831
- case 'image': {
832
- el.setAttribute('src', block.src)
833
- if (block.alt) el.setAttribute('alt', block.alt)
834
- if (block.width) el.setAttribute('width', block.width)
835
- if (block.height) el.setAttribute('height', block.height)
836
- break
837
- }
838
- case 'docEmbed': {
839
- el.setAttribute('docId', block.docId)
840
- break
841
- }
842
- case 'svgEmbed': {
843
- el.setAttribute('svg', block.svg)
844
- if (block.title) el.setAttribute('title', block.title)
845
- break
846
- }
847
- }
848
- }
849
-
850
- // ── Public API ───────────────────────────────────────────────────────────────
851
-
852
- export function populateYDocFromMarkdown(
853
- fragment: Y.XmlFragment,
854
- markdown: string,
855
- fallbackTitle = 'Untitled'
856
- ): void {
857
- const ydoc = fragment.doc
858
- if (!ydoc) {
859
- console.warn('[markdownToYjs] fragment has no doc — skipping population')
860
- return
861
- }
862
-
863
- const blocks = parseBlocks(markdown)
864
-
865
- let title = fallbackTitle
866
- let contentBlocks = blocks
867
- const h1 = blocks.findIndex(b => b.type === 'heading' && b.level === 1)
868
- if (h1 !== -1) {
869
- title = (blocks[h1] as { type: 'heading'; level: number; text: string }).text
870
- contentBlocks = blocks.filter((_, i) => i !== h1)
871
- }
872
- if (!contentBlocks.length) contentBlocks = [{ type: 'paragraph', text: '' }]
873
-
874
- ydoc.transact(() => {
875
- const headerEl = new Y.XmlElement('documentHeader')
876
- const metaEl = new Y.XmlElement('documentMeta')
877
- const bodyEls: Y.XmlElement[] = contentBlocks.map((b) => {
878
- switch (b.type) {
879
- case 'heading': return new Y.XmlElement('heading')
880
- case 'paragraph': return new Y.XmlElement('paragraph')
881
- case 'bulletList': return new Y.XmlElement('bulletList')
882
- case 'orderedList': return new Y.XmlElement('orderedList')
883
- case 'taskList': return new Y.XmlElement('taskList')
884
- case 'codeBlock': return new Y.XmlElement('codeBlock')
885
- case 'blockquote': return new Y.XmlElement('blockquote')
886
- case 'table': return new Y.XmlElement('table')
887
- case 'hr': return new Y.XmlElement('horizontalRule')
888
- case 'callout': return new Y.XmlElement('callout')
889
- case 'collapsible': return new Y.XmlElement('collapsible')
890
- case 'steps': return new Y.XmlElement('steps')
891
- case 'card': return new Y.XmlElement('card')
892
- case 'cardGroup': return new Y.XmlElement('cardGroup')
893
- case 'codeCollapse': return new Y.XmlElement('codeCollapse')
894
- case 'codeGroup': return new Y.XmlElement('codeGroup')
895
- case 'codePreview': return new Y.XmlElement('codePreview')
896
- case 'codeTree': return new Y.XmlElement('codeTree')
897
- case 'accordion': return new Y.XmlElement('accordion')
898
- case 'tabs': return new Y.XmlElement('tabs')
899
- case 'field': return new Y.XmlElement('field')
900
- case 'fieldGroup': return new Y.XmlElement('fieldGroup')
901
- case 'image': return new Y.XmlElement('image')
902
- case 'docEmbed': return new Y.XmlElement('docEmbed')
903
- case 'svgEmbed': return new Y.XmlElement('svgEmbed')
904
- }
905
- })
906
-
907
- fragment.insert(0, [headerEl, metaEl, ...bodyEls])
908
-
909
- const headerXt = new Y.XmlText()
910
- headerEl.insert(0, [headerXt])
911
- headerXt.insert(0, title)
912
-
913
- contentBlocks.forEach((block, i) => fillBlock(bodyEls[i]!, block))
914
- })
915
- }
1
+ // Thin re-export shim — markdown → Yjs conversion now lives in
2
+ // @abraca/convert (extracted from cou-sh + abracadabra-nuxt + this
3
+ // package). Existing imports inside the MCP server keep working
4
+ // through this re-export.
5
+ //
6
+ // New code should import from @abraca/convert directly.
7
+
8
+ export {
9
+ filenameToLabel,
10
+ parseFrontmatter,
11
+ populateYDocFromMarkdown,
12
+ type FrontmatterResult,
13
+ } from '@abraca/convert'