@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.
- package/dist/abracadabra-mcp.cjs +12096 -12641
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +12078 -12625
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/package.json +12 -8
- package/src/converters/markdownToYjs.ts +13 -915
- package/src/converters/page-types.ts +10 -0
- package/src/converters/yjsToMarkdown.ts +12 -346
- package/src/crypto.ts +5 -4
- package/src/index.ts +29 -2
- package/src/resources/server-info.ts +1 -1
- package/src/schema/loader.ts +139 -0
- package/src/schema/validator.ts +31 -0
- package/src/server.ts +75 -108
- package/src/tools/channel.ts +9 -9
- package/src/tools/meta.ts +23 -2
- package/src/tools/tree.ts +73 -8
|
@@ -1,915 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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'
|