@rimelight/cms 0.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 +19 -0
- package/package.json +69 -0
- package/src/astro/BlockRenderer.astro +111 -0
- package/src/astro/PageRenderer.astro +23 -0
- package/src/astro/blocks/CalloutBlock.astro +78 -0
- package/src/astro/blocks/CardBlock.astro +43 -0
- package/src/astro/blocks/CardsBlock.astro +24 -0
- package/src/astro/blocks/CodeBlock.astro +16 -0
- package/src/astro/blocks/DialogueBlock.astro +23 -0
- package/src/astro/blocks/FileTreeBlock.astro +45 -0
- package/src/astro/blocks/ImageBlock.astro +17 -0
- package/src/astro/blocks/OrderedListBlock.astro +55 -0
- package/src/astro/blocks/ParagraphBlock.astro +65 -0
- package/src/astro/blocks/SceneBlock.astro +65 -0
- package/src/astro/blocks/ScriptBlock.astro +59 -0
- package/src/astro/blocks/SectionBlock.astro +46 -0
- package/src/astro/blocks/StepItemBlock.astro +25 -0
- package/src/astro/blocks/StepsBlock.astro +14 -0
- package/src/astro/blocks/TabItemBlock.astro +19 -0
- package/src/astro/blocks/TableBlock.astro +49 -0
- package/src/astro/blocks/TabsBlock.astro +67 -0
- package/src/astro/blocks/UnorderedListBlock.astro +55 -0
- package/src/auth/editor-guards.ts +148 -0
- package/src/auth/filter-blocks.ts +44 -0
- package/src/auth/filter-templates.ts +81 -0
- package/src/auth/permissions.ts +40 -0
- package/src/cache/purge.ts +37 -0
- package/src/client/components/RimelightBlock.tsx +487 -0
- package/src/client/components/RimelightBlockPicker.tsx +305 -0
- package/src/client/components/RimelightEditor.tsx +131 -0
- package/src/client/components/RimelightEditorLayout.tsx +143 -0
- package/src/client/components/RimelightPreview.tsx +354 -0
- package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
- package/src/client/components/RimelightTemplateEditor.tsx +245 -0
- package/src/client/components/editors/CalloutEditor.tsx +185 -0
- package/src/client/components/editors/CardEditor.tsx +60 -0
- package/src/client/components/editors/CardsEditor.tsx +132 -0
- package/src/client/components/editors/CodeEditor.tsx +101 -0
- package/src/client/components/editors/DialogueEditor.tsx +52 -0
- package/src/client/components/editors/FileTreeEditor.tsx +65 -0
- package/src/client/components/editors/ImageEditor.tsx +70 -0
- package/src/client/components/editors/ParagraphEditor.tsx +314 -0
- package/src/client/components/editors/SceneEditor.tsx +245 -0
- package/src/client/components/editors/ScriptEditor.tsx +245 -0
- package/src/client/components/editors/SectionEditor.tsx +139 -0
- package/src/client/components/editors/StepItemEditor.tsx +117 -0
- package/src/client/components/editors/StepsEditor.tsx +105 -0
- package/src/client/components/editors/TabItemEditor.tsx +113 -0
- package/src/client/components/editors/TableEditor.tsx +181 -0
- package/src/client/components/editors/TabsEditor.tsx +105 -0
- package/src/client/components/index.ts +7 -0
- package/src/client/loader.ts +19 -0
- package/src/client/state/autosave.ts +122 -0
- package/src/client/state/editor-store.ts +411 -0
- package/src/client/state/history.ts +251 -0
- package/src/client/state/lock-manager.ts +89 -0
- package/src/client/styles/cms-editor.css +653 -0
- package/src/client/toast.ts +12 -0
- package/src/client/utils/sortable.ts +184 -0
- package/src/client/utils/splitpane.ts +87 -0
- package/src/client/utils/tree-sanitizer.ts +33 -0
- package/src/core/page-definitions.ts +502 -0
- package/src/core/registry.ts +99 -0
- package/src/core/template-sync.ts +75 -0
- package/src/core/types/blocks.ts +367 -0
- package/src/core/types/inline.ts +23 -0
- package/src/core/types/pages.ts +36 -0
- package/src/core/types/templates.ts +32 -0
- package/src/core/types/versioning.ts +44 -0
- package/src/core/validator.ts +28 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +47 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/parser.ts +347 -0
- package/src/markdown/serializer.ts +219 -0
- package/src/migration.ts +155 -0
- package/src/schema/index.ts +8 -0
- package/src/schema/page_draft_locks.ts +13 -0
- package/src/schema/page_drafts.ts +18 -0
- package/src/schema/page_templates.ts +27 -0
- package/src/schema/page_version_approvals.ts +12 -0
- package/src/schema/page_version_comments.ts +14 -0
- package/src/schema/page_versions.ts +34 -0
- package/src/schema/pages.ts +42 -0
- package/src/schema/search.ts +23 -0
- package/src/search/query.ts +10 -0
- package/src/search/sync.ts +32 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { BaseBlock, HeadingLevel } from "../core/types/blocks"
|
|
2
|
+
import type { InlineNode } from "../core/types/inline"
|
|
3
|
+
|
|
4
|
+
function genId(): string {
|
|
5
|
+
return typeof crypto !== "undefined" && crypto.randomUUID
|
|
6
|
+
? crypto.randomUUID()
|
|
7
|
+
: Math.random().toString(36).slice(2, 11)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseInlines(text: string): InlineNode[] {
|
|
11
|
+
if (!text) return []
|
|
12
|
+
|
|
13
|
+
const inlines: InlineNode[] = []
|
|
14
|
+
// Regex to match markdown links: [text](url)
|
|
15
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
|
|
16
|
+
let lastIndex = 0
|
|
17
|
+
let match: RegExpExecArray | null
|
|
18
|
+
|
|
19
|
+
while ((match = linkRegex.exec(text)) !== null) {
|
|
20
|
+
const beforeText = text.substring(lastIndex, match.index)
|
|
21
|
+
if (beforeText) {
|
|
22
|
+
inlines.push(...parseFormattedText(beforeText))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const linkText = match[1] || ""
|
|
26
|
+
const linkUrl = match[2] || ""
|
|
27
|
+
|
|
28
|
+
inlines.push({
|
|
29
|
+
type: "link",
|
|
30
|
+
url: linkUrl,
|
|
31
|
+
children: [{ type: "text", text: linkText }]
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
lastIndex = linkRegex.lastIndex
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const remainingText = text.substring(lastIndex)
|
|
38
|
+
if (remainingText) {
|
|
39
|
+
inlines.push(...parseFormattedText(remainingText))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return inlines.length > 0 ? inlines : [{ type: "text", text }]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseFormattedText(str: string): InlineNode[] {
|
|
46
|
+
if (!str) return []
|
|
47
|
+
|
|
48
|
+
// Tokenize bold, italic, code marks
|
|
49
|
+
// Format: `code`, **bold**, *italic*, ~~strikethrough~~
|
|
50
|
+
const result: InlineNode[] = []
|
|
51
|
+
const tokenRegex = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|~~[^~]+~~)/g
|
|
52
|
+
let lastIdx = 0
|
|
53
|
+
let match: RegExpExecArray | null
|
|
54
|
+
|
|
55
|
+
while ((match = tokenRegex.exec(str)) !== null) {
|
|
56
|
+
const rawBefore = str.substring(lastIdx, match.index)
|
|
57
|
+
if (rawBefore) {
|
|
58
|
+
result.push({ type: "text", text: rawBefore })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const token = match[1] || ""
|
|
62
|
+
if (token.startsWith("`") && token.endsWith("`")) {
|
|
63
|
+
result.push({
|
|
64
|
+
type: "text",
|
|
65
|
+
text: token.slice(1, -1),
|
|
66
|
+
marks: ["code"]
|
|
67
|
+
})
|
|
68
|
+
} else if (token.startsWith("**") && token.endsWith("**")) {
|
|
69
|
+
result.push({
|
|
70
|
+
type: "text",
|
|
71
|
+
text: token.slice(2, -2),
|
|
72
|
+
marks: ["bold"]
|
|
73
|
+
})
|
|
74
|
+
} else if (token.startsWith("*") && token.endsWith("*")) {
|
|
75
|
+
result.push({
|
|
76
|
+
type: "text",
|
|
77
|
+
text: token.slice(1, -1),
|
|
78
|
+
marks: ["italic"]
|
|
79
|
+
})
|
|
80
|
+
} else if (token.startsWith("~~") && token.endsWith("~~")) {
|
|
81
|
+
result.push({
|
|
82
|
+
type: "text",
|
|
83
|
+
text: token.slice(2, -2),
|
|
84
|
+
marks: ["strikethrough"]
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
lastIdx = tokenRegex.lastIndex
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const remaining = str.substring(lastIdx)
|
|
92
|
+
if (remaining) {
|
|
93
|
+
result.push({ type: "text", text: remaining })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseMarkdownTable(tableText: string): BaseBlock | null {
|
|
100
|
+
const lines = tableText
|
|
101
|
+
.trim()
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((l) => l.trim())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
if (lines.length < 2) return null
|
|
106
|
+
|
|
107
|
+
const parseRow = (line: string) =>
|
|
108
|
+
line
|
|
109
|
+
.replace(/^\|/, "")
|
|
110
|
+
.replace(/\|$/, "")
|
|
111
|
+
.split("|")
|
|
112
|
+
.map((c) => c.trim())
|
|
113
|
+
|
|
114
|
+
const headerCells = parseRow(lines[0] || "")
|
|
115
|
+
const alignCells = parseRow(lines[1] || "")
|
|
116
|
+
|
|
117
|
+
// Verify second line is alignment separator (e.g. ---, :---:, ---:)
|
|
118
|
+
if (!alignCells.every((c) => /^[:-]+$/.test(c))) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const columns = headerCells.map((header, idx) => {
|
|
123
|
+
const alignStr = alignCells[idx] || ""
|
|
124
|
+
let align: "left" | "center" | "right" = "left"
|
|
125
|
+
if (alignStr.startsWith(":") && alignStr.endsWith(":")) {
|
|
126
|
+
align = "center"
|
|
127
|
+
} else if (alignStr.endsWith(":")) {
|
|
128
|
+
align = "right"
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
key: `col_${idx}`,
|
|
132
|
+
header,
|
|
133
|
+
align
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const rows: Record<string, string>[] = []
|
|
138
|
+
for (let i = 2; i < lines.length; i++) {
|
|
139
|
+
const cells = parseRow(lines[i] || "")
|
|
140
|
+
const rowObj: Record<string, string> = {}
|
|
141
|
+
columns.forEach((col, idx) => {
|
|
142
|
+
rowObj[col.key] = cells[idx] || ""
|
|
143
|
+
})
|
|
144
|
+
rows.push(rowObj)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: genId(),
|
|
149
|
+
type: "TableBlock",
|
|
150
|
+
props: {
|
|
151
|
+
columns,
|
|
152
|
+
rows,
|
|
153
|
+
caption: ""
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function markdownToBlocks(markdown: string): BaseBlock[] {
|
|
159
|
+
if (!markdown) return []
|
|
160
|
+
|
|
161
|
+
// Strip frontmatter if present
|
|
162
|
+
let cleanMd = markdown.trim()
|
|
163
|
+
if (cleanMd.startsWith("---")) {
|
|
164
|
+
const endFm = cleanMd.indexOf("---", 3)
|
|
165
|
+
if (endFm !== -1) {
|
|
166
|
+
cleanMd = cleanMd.substring(endFm + 3).trim()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rootBlocks: BaseBlock[] = []
|
|
171
|
+
const sectionStack: { level: number; block: BaseBlock }[] = []
|
|
172
|
+
|
|
173
|
+
const appendToCurrentScope = (block: BaseBlock) => {
|
|
174
|
+
if (sectionStack.length > 0) {
|
|
175
|
+
const topSection = sectionStack[sectionStack.length - 1]!.block
|
|
176
|
+
if (!topSection.children) topSection.children = []
|
|
177
|
+
if (!topSection.props.children) topSection.props.children = []
|
|
178
|
+
topSection.children.push(block)
|
|
179
|
+
topSection.props.children.push(block)
|
|
180
|
+
} else {
|
|
181
|
+
rootBlocks.push(block)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines = cleanMd.split("\n")
|
|
186
|
+
let i = 0
|
|
187
|
+
|
|
188
|
+
while (i < lines.length) {
|
|
189
|
+
const line = lines[i] || ""
|
|
190
|
+
const trimmed = line.trim()
|
|
191
|
+
|
|
192
|
+
if (!trimmed) {
|
|
193
|
+
i++
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 1. Code Block Fence (```)
|
|
198
|
+
if (trimmed.startsWith("```")) {
|
|
199
|
+
const lang = trimmed.slice(3).trim()
|
|
200
|
+
const codeLines: string[] = []
|
|
201
|
+
i++
|
|
202
|
+
while (i < lines.length && !(lines[i] || "").trim().startsWith("```")) {
|
|
203
|
+
codeLines.push(lines[i] || "")
|
|
204
|
+
i++
|
|
205
|
+
}
|
|
206
|
+
i++ // Skip closing ```
|
|
207
|
+
appendToCurrentScope({
|
|
208
|
+
id: genId(),
|
|
209
|
+
type: "CodeBlock",
|
|
210
|
+
props: {
|
|
211
|
+
language: lang || "text",
|
|
212
|
+
code: codeLines.join("\n"),
|
|
213
|
+
caption: ""
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Headings (# to ######)
|
|
220
|
+
const headingMatch = line.match(/^(#{1,6})\s(.*)$/)
|
|
221
|
+
if (headingMatch) {
|
|
222
|
+
const hashes = headingMatch[1] || ""
|
|
223
|
+
const title = (headingMatch[2] || "").trim()
|
|
224
|
+
const level = Math.min(6, Math.max(2, hashes.length)) as HeadingLevel
|
|
225
|
+
|
|
226
|
+
const newSection: BaseBlock = {
|
|
227
|
+
id: genId(),
|
|
228
|
+
type: "SectionBlock",
|
|
229
|
+
children: [],
|
|
230
|
+
props: {
|
|
231
|
+
title,
|
|
232
|
+
level,
|
|
233
|
+
description: "",
|
|
234
|
+
children: []
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pop sections with higher or equal level
|
|
239
|
+
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1]!.level >= level) {
|
|
240
|
+
sectionStack.pop()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
appendToCurrentScope(newSection)
|
|
244
|
+
sectionStack.push({ level, block: newSection })
|
|
245
|
+
i++
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 3. Callouts / Blockquotes (> [!NOTE], > [!WARNING], > etc.)
|
|
250
|
+
if (trimmed.startsWith(">")) {
|
|
251
|
+
const calloutLines: string[] = []
|
|
252
|
+
while (i < lines.length && (lines[i] || "").trim().startsWith(">")) {
|
|
253
|
+
calloutLines.push((lines[i] || "").trim().replace(/^>\s?/, ""))
|
|
254
|
+
i++
|
|
255
|
+
}
|
|
256
|
+
const firstLine = calloutLines[0] || ""
|
|
257
|
+
const variantMatch = firstLine.match(/^\[!([A-Z]+)\]/i)
|
|
258
|
+
let variant = "info"
|
|
259
|
+
let contentLines = calloutLines
|
|
260
|
+
|
|
261
|
+
if (variantMatch) {
|
|
262
|
+
variant = (variantMatch[1] || "info").toLowerCase()
|
|
263
|
+
contentLines = calloutLines.slice(1)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const childMd = contentLines.join("\n").trim()
|
|
267
|
+
const calloutChildren = childMd ? markdownToBlocks(childMd) : []
|
|
268
|
+
|
|
269
|
+
appendToCurrentScope({
|
|
270
|
+
id: genId(),
|
|
271
|
+
type: "CalloutBlock",
|
|
272
|
+
children: calloutChildren,
|
|
273
|
+
props: {
|
|
274
|
+
variant,
|
|
275
|
+
children: calloutChildren
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 4. Markdown Table (| Header | Header |)
|
|
282
|
+
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
283
|
+
const tableLines: string[] = []
|
|
284
|
+
while (
|
|
285
|
+
i < lines.length &&
|
|
286
|
+
(lines[i] || "").trim().startsWith("|") &&
|
|
287
|
+
(lines[i] || "").trim().endsWith("|")
|
|
288
|
+
) {
|
|
289
|
+
tableLines.push(lines[i] || "")
|
|
290
|
+
i++
|
|
291
|
+
}
|
|
292
|
+
const tableBlock = parseMarkdownTable(tableLines.join("\n"))
|
|
293
|
+
if (tableBlock) {
|
|
294
|
+
appendToCurrentScope(tableBlock)
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 5. Image ()
|
|
300
|
+
const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/)
|
|
301
|
+
if (imageMatch) {
|
|
302
|
+
appendToCurrentScope({
|
|
303
|
+
id: genId(),
|
|
304
|
+
type: "ImageBlock",
|
|
305
|
+
props: {
|
|
306
|
+
alt: imageMatch[1] || "",
|
|
307
|
+
src: imageMatch[2] || "",
|
|
308
|
+
caption: ""
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
i++
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 6. Regular Paragraph (gather lines until blank line or block start)
|
|
316
|
+
const paragraphLines: string[] = []
|
|
317
|
+
while (
|
|
318
|
+
i < lines.length &&
|
|
319
|
+
(lines[i] || "").trim() &&
|
|
320
|
+
!(lines[i] || "").trim().startsWith("```") &&
|
|
321
|
+
!(lines[i] || "").trim().startsWith("#") &&
|
|
322
|
+
!(lines[i] || "").trim().startsWith(">") &&
|
|
323
|
+
!((lines[i] || "").trim().startsWith("|") && (lines[i] || "").trim().endsWith("|")) &&
|
|
324
|
+
!/^[0-9]+\.\s/.test((lines[i] || "").trim())
|
|
325
|
+
) {
|
|
326
|
+
paragraphLines.push(lines[i] || "")
|
|
327
|
+
i++
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (paragraphLines.length > 0) {
|
|
331
|
+
const paragraphText = paragraphLines.join(" ").trim()
|
|
332
|
+
const inlines = parseInlines(paragraphText)
|
|
333
|
+
appendToCurrentScope({
|
|
334
|
+
id: genId(),
|
|
335
|
+
type: "ParagraphBlock",
|
|
336
|
+
props: {
|
|
337
|
+
text: inlines
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
} else {
|
|
341
|
+
// Advance if no block consumed
|
|
342
|
+
i++
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return rootBlocks
|
|
347
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { BaseBlock, HeadingLevel } from "../core/types/blocks"
|
|
2
|
+
import type { InlineNode, TextInlineNode, LinkInlineNode } from "../core/types/inline"
|
|
3
|
+
|
|
4
|
+
export function inlinesToMarkdown(
|
|
5
|
+
inlines: InlineNode[] | { en?: string } | string | undefined
|
|
6
|
+
): string {
|
|
7
|
+
if (!inlines) return ""
|
|
8
|
+
if (typeof inlines === "string") return inlines
|
|
9
|
+
if (!Array.isArray(inlines)) {
|
|
10
|
+
return (inlines as any).en || ""
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return inlines
|
|
14
|
+
.map((node) => {
|
|
15
|
+
if (node.type === "text") {
|
|
16
|
+
let text = (node as TextInlineNode).text || ""
|
|
17
|
+
const marks = (node as TextInlineNode).marks || []
|
|
18
|
+
if (marks.includes("code")) text = `\`${text}\``
|
|
19
|
+
if (marks.includes("bold")) text = `**${text}**`
|
|
20
|
+
if (marks.includes("italic")) text = `*${text}*`
|
|
21
|
+
if (marks.includes("strikethrough")) text = `~~${text}~~`
|
|
22
|
+
return text
|
|
23
|
+
}
|
|
24
|
+
if (node.type === "link") {
|
|
25
|
+
const link = node as LinkInlineNode
|
|
26
|
+
const linkText = (link.children || []).map((c) => c.text).join("") || link.url
|
|
27
|
+
return `[${linkText}](${link.url})`
|
|
28
|
+
}
|
|
29
|
+
if (node.type === "page_mention") {
|
|
30
|
+
const title = (node as any).displayTitle || (node as any).pageSlug || "Page"
|
|
31
|
+
return `[${title}](/${(node as any).pageSlug || ""})`
|
|
32
|
+
}
|
|
33
|
+
return ""
|
|
34
|
+
})
|
|
35
|
+
.join("")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function blocksToMarkdown(
|
|
39
|
+
blocks: BaseBlock[] = [],
|
|
40
|
+
options: { sectionLevel?: number } = {}
|
|
41
|
+
): string {
|
|
42
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return ""
|
|
43
|
+
|
|
44
|
+
const baseLevel = options.sectionLevel ?? 2
|
|
45
|
+
const lines: string[] = []
|
|
46
|
+
|
|
47
|
+
for (const block of blocks) {
|
|
48
|
+
if (!block) continue
|
|
49
|
+
const children = block.children || block.props?.children || []
|
|
50
|
+
|
|
51
|
+
switch (block.type) {
|
|
52
|
+
case "SectionBlock": {
|
|
53
|
+
const level = (block.props?.level as HeadingLevel) || (baseLevel as HeadingLevel)
|
|
54
|
+
const hashes = "#".repeat(Math.min(6, Math.max(1, level)))
|
|
55
|
+
const title = block.props?.title || ""
|
|
56
|
+
if (title) {
|
|
57
|
+
lines.push(`${hashes} ${title}`)
|
|
58
|
+
}
|
|
59
|
+
if (block.props?.description) {
|
|
60
|
+
lines.push(`_${block.props.description}_`)
|
|
61
|
+
}
|
|
62
|
+
if (children.length > 0) {
|
|
63
|
+
lines.push(blocksToMarkdown(children, { sectionLevel: Math.min(6, level + 1) }))
|
|
64
|
+
}
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "ParagraphBlock": {
|
|
69
|
+
const text = inlinesToMarkdown(block.props?.text)
|
|
70
|
+
if (text) lines.push(text)
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "CalloutBlock": {
|
|
75
|
+
const variant = (block.props?.variant || "info").toUpperCase()
|
|
76
|
+
const childMd = blocksToMarkdown(children, options)
|
|
77
|
+
const calloutHeader = `> [!${variant}]`
|
|
78
|
+
if (childMd) {
|
|
79
|
+
const indented = childMd
|
|
80
|
+
.split("\n")
|
|
81
|
+
.map((l) => (l.trim() ? `> ${l}` : `>`))
|
|
82
|
+
.join("\n")
|
|
83
|
+
lines.push(`${calloutHeader}\n${indented}`)
|
|
84
|
+
} else {
|
|
85
|
+
lines.push(calloutHeader)
|
|
86
|
+
}
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "CodeBlock": {
|
|
91
|
+
const lang = block.props?.language || ""
|
|
92
|
+
const code = block.props?.code || ""
|
|
93
|
+
const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
|
|
94
|
+
lines.push(`\`\`\`${lang}\n${code}\n\`\`\`${caption}`)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "TableBlock": {
|
|
99
|
+
const columns = block.props?.columns || []
|
|
100
|
+
const rows = block.props?.rows || []
|
|
101
|
+
if (columns.length > 0) {
|
|
102
|
+
const headerRow = `| ${columns.map((c: any) => c.header || c.key).join(" | ")} |`
|
|
103
|
+
const alignRow = `| ${columns
|
|
104
|
+
.map((c: any) => {
|
|
105
|
+
if (c.align === "center") return ":---:"
|
|
106
|
+
if (c.align === "right") return "---:"
|
|
107
|
+
return "---"
|
|
108
|
+
})
|
|
109
|
+
.join(" | ")} |`
|
|
110
|
+
const bodyRows = rows.map(
|
|
111
|
+
(r: any) => `| ${columns.map((c: any) => r[c.key] || "").join(" | ")} |`
|
|
112
|
+
)
|
|
113
|
+
const tableMd = [headerRow, alignRow, ...bodyRows].join("\n")
|
|
114
|
+
const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
|
|
115
|
+
lines.push(`${tableMd}${caption}`)
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case "ImageBlock": {
|
|
121
|
+
const alt = block.props?.alt || ""
|
|
122
|
+
const src = block.props?.src || ""
|
|
123
|
+
const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
|
|
124
|
+
lines.push(`${caption}`)
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "TabsBlock": {
|
|
129
|
+
for (const tab of children) {
|
|
130
|
+
const tabLabel = tab.props?.label || "Tab"
|
|
131
|
+
const tabChildren = tab.children || tab.props?.children || []
|
|
132
|
+
lines.push(`#### ${tabLabel}\n\n${blocksToMarkdown(tabChildren, options)}`)
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "TabItemBlock": {
|
|
138
|
+
const label = block.props?.label || "Tab"
|
|
139
|
+
lines.push(`#### ${label}\n\n${blocksToMarkdown(children, options)}`)
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "StepsBlock": {
|
|
144
|
+
let stepIndex = 1
|
|
145
|
+
for (const step of children) {
|
|
146
|
+
const title = step.props?.title || `Step ${stepIndex}`
|
|
147
|
+
const desc = step.props?.description ? ` - ${step.props.description}` : ""
|
|
148
|
+
const stepChildren = step.children || step.props?.children || []
|
|
149
|
+
lines.push(`${stepIndex}. **${title}**${desc}`)
|
|
150
|
+
if (stepChildren.length > 0) {
|
|
151
|
+
lines.push(blocksToMarkdown(stepChildren, options))
|
|
152
|
+
}
|
|
153
|
+
stepIndex++
|
|
154
|
+
}
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case "StepItemBlock": {
|
|
159
|
+
const title = block.props?.title || "Step"
|
|
160
|
+
const desc = block.props?.description ? ` - ${block.props.description}` : ""
|
|
161
|
+
lines.push(`- **${title}**${desc}`)
|
|
162
|
+
if (children.length > 0) {
|
|
163
|
+
lines.push(blocksToMarkdown(children, options))
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "CardsBlock": {
|
|
169
|
+
for (const card of children) {
|
|
170
|
+
const title = card.props?.title || ""
|
|
171
|
+
const href = card.props?.href || "#"
|
|
172
|
+
const desc = card.props?.description ? `: ${card.props.description}` : ""
|
|
173
|
+
lines.push(`- [${title}](${href})${desc}`)
|
|
174
|
+
}
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case "CardBlock": {
|
|
179
|
+
const title = block.props?.title || ""
|
|
180
|
+
const href = block.props?.href || "#"
|
|
181
|
+
const desc = block.props?.description ? `: ${block.props.description}` : ""
|
|
182
|
+
lines.push(`- [${title}](${href})${desc}`)
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case "FileTreeBlock": {
|
|
187
|
+
const renderTree = (nodes: any[], prefix = ""): string[] => {
|
|
188
|
+
const treeLines: string[] = []
|
|
189
|
+
for (const node of nodes) {
|
|
190
|
+
const icon = node.isDir ? "📁 " : "📄 "
|
|
191
|
+
const comment = node.comment ? ` # ${node.comment}` : ""
|
|
192
|
+
treeLines.push(`${prefix}${icon}${node.name}${comment}`)
|
|
193
|
+
if (node.children && node.children.length > 0) {
|
|
194
|
+
treeLines.push(...renderTree(node.children, `${prefix} `))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return treeLines
|
|
198
|
+
}
|
|
199
|
+
const treeNodes = block.props?.tree || []
|
|
200
|
+
lines.push(`\`\`\`text\n${renderTree(treeNodes).join("\n")}\n\`\`\``)
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "DialogueBlock": {
|
|
205
|
+
lines.push(`**${block.props?.character}:** "${block.props?.line || ""}"`)
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
default: {
|
|
210
|
+
if (block.props?.text) {
|
|
211
|
+
lines.push(inlinesToMarkdown(block.props.text))
|
|
212
|
+
}
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines.filter(Boolean).join("\n\n")
|
|
219
|
+
}
|