@plutocms/docs 0.0.0-stage → 0.15.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/.gitattributes +1 -0
- package/.prettierignore +5 -0
- package/CHANGELOG.md +178 -0
- package/FEATURES.md +30 -0
- package/README.md +39 -2
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +85 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
- package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
- package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
- package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
- package/app/components/PlutoContentPreview.vue +35 -0
- package/app/composables/pluto-block-editor.ts +443 -0
- package/app/composables/pluto-content-edit-link.ts +19 -0
- package/app/composables/pluto-content-preview.ts +29 -0
- package/app/composables/pluto-content-runtime.ts +237 -0
- package/app/pages/_pluto/preview.vue +555 -0
- package/app/plugins/pluto-docs.ts +50 -0
- package/i18n/locales/en.json +253 -0
- package/i18n/locales/pt-BR.json +289 -0
- package/modules/pluto-docs/index.ts +123 -0
- package/modules/pluto-docs/manifest.ts +159 -0
- package/modules/pluto-docs/options.ts +224 -0
- package/nuxt.config.ts +36 -0
- package/package.json +55 -4
- package/server/handlers/navigation.get.ts +7 -0
- package/server/handlers/page.get.ts +7 -0
- package/server/handlers/surround.get.ts +7 -0
- package/server/plugins/pluto-docs.ts +25 -0
- package/server/utils/pluto-docs-runtime.ts +504 -0
- package/shared/types/docs.d.ts +40 -0
- package/shared/types/pluto-manifest.d.ts +6 -0
- package/shared/utils/blocks/editor.ts +357 -0
- package/shared/utils/blocks/history.ts +94 -0
- package/shared/utils/blocks/mdc-props.ts +321 -0
- package/shared/utils/blocks/operations.ts +692 -0
- package/shared/utils/blocks/parse.ts +444 -0
- package/shared/utils/blocks/presets.ts +383 -0
- package/shared/utils/blocks/preview-marks.ts +98 -0
- package/shared/utils/blocks/preview-protocol.ts +195 -0
- package/shared/utils/blocks/registry.ts +163 -0
- package/shared/utils/blocks/serialize.ts +107 -0
- package/shared/utils/blocks/tree.ts +85 -0
- package/shared/utils/blocks/types.ts +165 -0
- package/shared/utils/content-route.ts +40 -0
- package/shared/utils/docs-type.ts +290 -0
- package/shared/utils/mdc-snippets.ts +114 -0
- package/shared/utils/navigation-config.ts +19 -0
- package/shared/utils/runtime-content.ts +403 -0
- package/tailwind.lint.css +10 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// Cut a Markdown (MDC) text into blocks. Each block keeps the exact text it
|
|
2
|
+
// came from, and the doc keeps the text between the blocks, so a save with no
|
|
3
|
+
// edit writes the same bytes. The parser is `remark-mdc` on `remark-parse`:
|
|
4
|
+
// pin the versions to the ones of `@nuxtjs/mdc`.
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
PlutoBlock,
|
|
8
|
+
PlutoBlockDoc,
|
|
9
|
+
PlutoCodeBlock,
|
|
10
|
+
PlutoComponentBlock,
|
|
11
|
+
PlutoRawBlock,
|
|
12
|
+
PlutoRawReason,
|
|
13
|
+
PlutoSlice,
|
|
14
|
+
PlutoTextBlock,
|
|
15
|
+
} from './types'
|
|
16
|
+
import remarkGfm from 'remark-gfm'
|
|
17
|
+
import remarkMdc from 'remark-mdc'
|
|
18
|
+
import remarkParse from 'remark-parse'
|
|
19
|
+
import { unified } from 'unified'
|
|
20
|
+
import { parseInlineProps, parseOpener, parseYamlProps } from './mdc-props'
|
|
21
|
+
import { flatChildren } from './tree'
|
|
22
|
+
|
|
23
|
+
interface MdNode {
|
|
24
|
+
type: string
|
|
25
|
+
name?: string
|
|
26
|
+
depth?: number
|
|
27
|
+
lang?: string | null
|
|
28
|
+
meta?: string | null
|
|
29
|
+
checked?: boolean | null
|
|
30
|
+
children?: MdNode[]
|
|
31
|
+
position?: { start: { offset?: number }; end: { offset?: number } }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Context {
|
|
35
|
+
source: string
|
|
36
|
+
nextId: () => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A block with the range of the source that it covers. */
|
|
40
|
+
interface Entry {
|
|
41
|
+
block: PlutoBlock
|
|
42
|
+
start: number
|
|
43
|
+
end: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SAFE_TYPES = new Set([
|
|
47
|
+
'paragraph',
|
|
48
|
+
'heading',
|
|
49
|
+
'list',
|
|
50
|
+
'listItem',
|
|
51
|
+
'blockquote',
|
|
52
|
+
'thematicBreak',
|
|
53
|
+
'definition',
|
|
54
|
+
'text',
|
|
55
|
+
'emphasis',
|
|
56
|
+
'strong',
|
|
57
|
+
'delete',
|
|
58
|
+
'inlineCode',
|
|
59
|
+
'link',
|
|
60
|
+
'linkReference',
|
|
61
|
+
'image',
|
|
62
|
+
'imageReference',
|
|
63
|
+
'break',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
const PROSE_TYPES = new Set([
|
|
67
|
+
'paragraph',
|
|
68
|
+
'heading',
|
|
69
|
+
'list',
|
|
70
|
+
'blockquote',
|
|
71
|
+
'thematicBreak',
|
|
72
|
+
'definition',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
/** The reason why the visual editor cannot keep a piece of prose, or `undefined` when it can. */
|
|
76
|
+
function unsafeReason(
|
|
77
|
+
node: MdNode,
|
|
78
|
+
source: string
|
|
79
|
+
): PlutoRawReason | undefined {
|
|
80
|
+
switch (node.type) {
|
|
81
|
+
case 'html':
|
|
82
|
+
return 'html'
|
|
83
|
+
case 'table':
|
|
84
|
+
return 'table'
|
|
85
|
+
case 'footnoteReference':
|
|
86
|
+
case 'footnoteDefinition':
|
|
87
|
+
return 'footnote'
|
|
88
|
+
case 'textComponent':
|
|
89
|
+
// The parser reads a footnote reference as a text span.
|
|
90
|
+
return source.startsWith('[^', node.position!.start.offset!)
|
|
91
|
+
? 'footnote'
|
|
92
|
+
: 'inline-mdc'
|
|
93
|
+
case 'containerComponent':
|
|
94
|
+
case 'leafComponent':
|
|
95
|
+
return 'inline-mdc'
|
|
96
|
+
case 'code':
|
|
97
|
+
// A fenced code inside a list or a quote: only a plain language is safe.
|
|
98
|
+
return node.meta ? 'unknown' : undefined
|
|
99
|
+
}
|
|
100
|
+
if (!SAFE_TYPES.has(node.type)) {
|
|
101
|
+
return 'unknown'
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
node.type === 'listItem' &&
|
|
105
|
+
node.checked !== null &&
|
|
106
|
+
node.checked !== undefined
|
|
107
|
+
) {
|
|
108
|
+
return 'task-list'
|
|
109
|
+
}
|
|
110
|
+
for (const child of node.children ?? []) {
|
|
111
|
+
const reason = unsafeReason(child, source)
|
|
112
|
+
if (reason) {
|
|
113
|
+
return reason
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function range(node: MdNode): [number, number] {
|
|
120
|
+
return [node.position!.start.offset!, node.position!.end.offset!]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function lineStartOf(source: string, offset: number): number {
|
|
124
|
+
return source.lastIndexOf('\n', offset - 1) + 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The white space before a node, on its first line. */
|
|
128
|
+
function indentOf(source: string, start: number): string {
|
|
129
|
+
const before = source.slice(lineStartOf(source, start), start)
|
|
130
|
+
return /^[ \t]*$/.test(before) ? before : ''
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Removes the indent from each line but the first. */
|
|
134
|
+
export function dedent(text: string, indent: string): string {
|
|
135
|
+
if (!indent) {
|
|
136
|
+
return text
|
|
137
|
+
}
|
|
138
|
+
return text
|
|
139
|
+
.split('\n')
|
|
140
|
+
.map((line, index) =>
|
|
141
|
+
index > 0 && line.startsWith(indent) ? line.slice(indent.length) : line
|
|
142
|
+
)
|
|
143
|
+
.join('\n')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Removes the indent from every line. */
|
|
147
|
+
function dedentAll(text: string, indent: string): string {
|
|
148
|
+
return text
|
|
149
|
+
.split('\n')
|
|
150
|
+
.map((line) => (line.startsWith(indent) ? line.slice(indent.length) : line))
|
|
151
|
+
.join('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sliceOf(source: string, start: number, end: number): PlutoSlice {
|
|
155
|
+
return { start, end, text: source.slice(start, end) }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseCode(ctx: Context, node: MdNode): Entry {
|
|
159
|
+
const [start, end] = range(node)
|
|
160
|
+
const indent = indentOf(ctx.source, start)
|
|
161
|
+
const text = ctx.source.slice(start, end)
|
|
162
|
+
const lines = dedent(text, indent).split('\n')
|
|
163
|
+
const first = lines[0]!.replace(/\r$/, '')
|
|
164
|
+
let fenceLength = 0
|
|
165
|
+
while (first[fenceLength] === first[0]) {
|
|
166
|
+
fenceLength++
|
|
167
|
+
}
|
|
168
|
+
const fence = fenceLength >= 3 ? first.slice(0, fenceLength) : '```'
|
|
169
|
+
const info = first.slice(fenceLength).trim()
|
|
170
|
+
const closing =
|
|
171
|
+
lines.length > 1 ? /^[ \t]*([`~]{3,})[ \t]*\r?$/.exec(lines.at(-1)!) : null
|
|
172
|
+
const closed =
|
|
173
|
+
closing !== null &&
|
|
174
|
+
closing[1]![0] === fence[0] &&
|
|
175
|
+
closing[1]!.length >= fence.length
|
|
176
|
+
const code = lines.slice(1, closed ? -1 : undefined).join('\n')
|
|
177
|
+
|
|
178
|
+
// The language is the first word. A file name or a flag can follow.
|
|
179
|
+
const langEnd = info.search(/[\s[{]/)
|
|
180
|
+
const lang = langEnd === -1 ? info : info.slice(0, langEnd)
|
|
181
|
+
let rest = info.slice(lang.length).trim()
|
|
182
|
+
let filename: string | undefined
|
|
183
|
+
const nameMatch = /\[([^\]]*)\]/.exec(rest)
|
|
184
|
+
if (nameMatch) {
|
|
185
|
+
filename = nameMatch[1]
|
|
186
|
+
rest =
|
|
187
|
+
`${rest.slice(0, nameMatch.index)}${rest.slice(nameMatch.index + nameMatch[0].length)}`
|
|
188
|
+
.replace(/\s+/g, ' ')
|
|
189
|
+
.trim()
|
|
190
|
+
}
|
|
191
|
+
const block: PlutoCodeBlock = {
|
|
192
|
+
id: ctx.nextId(),
|
|
193
|
+
kind: 'code',
|
|
194
|
+
lang,
|
|
195
|
+
fence,
|
|
196
|
+
code,
|
|
197
|
+
original: sliceOf(ctx.source, start, end),
|
|
198
|
+
...(indent ? { indent } : {}),
|
|
199
|
+
...(filename === undefined ? {} : { filename }),
|
|
200
|
+
...(rest ? { meta: rest } : {}),
|
|
201
|
+
}
|
|
202
|
+
return { block, start, end }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseRaw(
|
|
206
|
+
ctx: Context,
|
|
207
|
+
start: number,
|
|
208
|
+
end: number,
|
|
209
|
+
reason: PlutoRawReason
|
|
210
|
+
): Entry {
|
|
211
|
+
const indent = indentOf(ctx.source, start)
|
|
212
|
+
const original = sliceOf(ctx.source, start, end)
|
|
213
|
+
const block: PlutoRawBlock = {
|
|
214
|
+
id: ctx.nextId(),
|
|
215
|
+
kind: 'raw',
|
|
216
|
+
reason,
|
|
217
|
+
markdown: dedent(original.text, indent),
|
|
218
|
+
original,
|
|
219
|
+
...(indent ? { indent } : {}),
|
|
220
|
+
}
|
|
221
|
+
return { block, start, end }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseText(ctx: Context, start: number, end: number): Entry {
|
|
225
|
+
const indent = indentOf(ctx.source, start)
|
|
226
|
+
const original = sliceOf(ctx.source, start, end)
|
|
227
|
+
const block: PlutoTextBlock = {
|
|
228
|
+
id: ctx.nextId(),
|
|
229
|
+
kind: 'text',
|
|
230
|
+
markdown: dedent(original.text, indent),
|
|
231
|
+
original,
|
|
232
|
+
...(indent ? { indent } : {}),
|
|
233
|
+
}
|
|
234
|
+
return { block, start, end }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const YAML_BLOCK =
|
|
238
|
+
/^(\r?\n[ \t]*)---[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*---[ \t]*(?=\r?\n|$)/
|
|
239
|
+
|
|
240
|
+
/** The common white space at the start of the non-blank lines. */
|
|
241
|
+
function commonIndent(text: string): string {
|
|
242
|
+
let common: string | undefined
|
|
243
|
+
for (const line of text.split('\n')) {
|
|
244
|
+
if (line.trim() === '') {
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
const lead = /^[ \t]*/.exec(line)![0]
|
|
248
|
+
if (common === undefined) {
|
|
249
|
+
common = lead
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
let index = 0
|
|
253
|
+
while (index < common.length && common[index] === lead[index]) {
|
|
254
|
+
index++
|
|
255
|
+
}
|
|
256
|
+
common = common.slice(0, index)
|
|
257
|
+
}
|
|
258
|
+
return common ?? ''
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseComponent(ctx: Context, node: MdNode): Entry {
|
|
262
|
+
const { source } = ctx
|
|
263
|
+
const [start, end] = range(node)
|
|
264
|
+
const rawFallback = () => parseRaw(ctx, start, end, 'unknown')
|
|
265
|
+
|
|
266
|
+
const lineEnd = source.indexOf('\n', start)
|
|
267
|
+
const line = source
|
|
268
|
+
.slice(start, lineEnd === -1 ? source.length : lineEnd)
|
|
269
|
+
.replace(/\r$/, '')
|
|
270
|
+
const opener = parseOpener(line)
|
|
271
|
+
if (!opener) {
|
|
272
|
+
return rawFallback()
|
|
273
|
+
}
|
|
274
|
+
const closer = ':'.repeat(opener.colons)
|
|
275
|
+
const openerEnd = start + line.length
|
|
276
|
+
const closerStart = end - opener.colons
|
|
277
|
+
if (source.slice(closerStart, end) !== closer || closerStart < openerEnd) {
|
|
278
|
+
return rawFallback()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// The children by slot. `default` is the content before the first `#name`.
|
|
282
|
+
const slots: Record<string, PlutoBlock[]> = {}
|
|
283
|
+
const entries: Entry[] = []
|
|
284
|
+
const take = (name: string, nodes: MdNode[]) => {
|
|
285
|
+
const parsed = parseNodes(ctx, nodes, false)
|
|
286
|
+
entries.push(...parsed)
|
|
287
|
+
slots[name] = [
|
|
288
|
+
...(slots[name] ?? []),
|
|
289
|
+
...parsed.map((entry) => entry.block),
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
// Content before the first `#name` is `default`. After a section, loose content
|
|
293
|
+
// is a `#default` section: the parser keeps no node for that marker.
|
|
294
|
+
let loose: MdNode[] = []
|
|
295
|
+
const flushLoose = () => {
|
|
296
|
+
if (loose.length > 0) {
|
|
297
|
+
take('default', loose)
|
|
298
|
+
loose = []
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const child of node.children ?? []) {
|
|
302
|
+
if (child.type === 'componentContainerSection') {
|
|
303
|
+
flushLoose()
|
|
304
|
+
take(child.name ?? 'default', child.children ?? [])
|
|
305
|
+
slots[child.name ?? 'default'] ??= []
|
|
306
|
+
} else {
|
|
307
|
+
loose.push(child)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
flushLoose()
|
|
311
|
+
|
|
312
|
+
// The children must be in source order, inside the component.
|
|
313
|
+
const ordered = flatChildren({ slots } as PlutoComponentBlock)
|
|
314
|
+
const sorted = ordered.map((block) =>
|
|
315
|
+
entries.find((entry) => entry.block === block)!
|
|
316
|
+
)
|
|
317
|
+
let cursor = openerEnd
|
|
318
|
+
for (const entry of sorted) {
|
|
319
|
+
if (entry.start < cursor || entry.end > closerStart) {
|
|
320
|
+
return rawFallback()
|
|
321
|
+
}
|
|
322
|
+
cursor = entry.end
|
|
323
|
+
}
|
|
324
|
+
const gaps: string[] = []
|
|
325
|
+
cursor = openerEnd
|
|
326
|
+
for (const entry of sorted) {
|
|
327
|
+
gaps.push(source.slice(cursor, entry.start))
|
|
328
|
+
cursor = entry.end
|
|
329
|
+
}
|
|
330
|
+
gaps.push(source.slice(cursor, closerStart))
|
|
331
|
+
|
|
332
|
+
let props: ReturnType<typeof parseInlineProps> = {
|
|
333
|
+
props: {},
|
|
334
|
+
order: [],
|
|
335
|
+
source: {},
|
|
336
|
+
}
|
|
337
|
+
let propsStyle: PlutoComponentBlock['propsStyle'] = 'inline'
|
|
338
|
+
let openerText = source.slice(start, openerEnd)
|
|
339
|
+
let yamlHead: string | undefined
|
|
340
|
+
let yamlIndent: string | undefined
|
|
341
|
+
if (opener.braces === undefined) {
|
|
342
|
+
const match = YAML_BLOCK.exec(gaps[0]!)
|
|
343
|
+
if (match) {
|
|
344
|
+
const inner = match[2]!
|
|
345
|
+
const lead = commonIndent(inner)
|
|
346
|
+
const parsed = parseYamlProps(dedentAll(inner, lead))
|
|
347
|
+
if (parsed) {
|
|
348
|
+
props = parsed
|
|
349
|
+
propsStyle = 'yaml'
|
|
350
|
+
yamlHead = match[1]!
|
|
351
|
+
yamlIndent = lead
|
|
352
|
+
openerText += match[0]
|
|
353
|
+
gaps[0] = gaps[0]!.slice(match[0].length)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
props = parseInlineProps(opener.braces)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const indent = indentOf(source, start)
|
|
361
|
+
const block: PlutoComponentBlock = {
|
|
362
|
+
id: ctx.nextId(),
|
|
363
|
+
kind: 'component',
|
|
364
|
+
name: opener.name,
|
|
365
|
+
props: props.props,
|
|
366
|
+
propsStyle,
|
|
367
|
+
propOrder: props.order,
|
|
368
|
+
propSource: props.source,
|
|
369
|
+
colons: opener.colons,
|
|
370
|
+
slots,
|
|
371
|
+
gaps,
|
|
372
|
+
openerText,
|
|
373
|
+
original: sliceOf(source, start, end),
|
|
374
|
+
...(indent ? { indent } : {}),
|
|
375
|
+
...(yamlHead === undefined ? {} : { yamlHead, yamlIndent }),
|
|
376
|
+
}
|
|
377
|
+
return { block, start, end }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Turns sibling nodes into blocks. `top` cuts a Text block at each H1 and H2. */
|
|
381
|
+
function parseNodes(ctx: Context, nodes: MdNode[], top: boolean): Entry[] {
|
|
382
|
+
const entries: Entry[] = []
|
|
383
|
+
let group: { start: number; end: number } | undefined
|
|
384
|
+
|
|
385
|
+
const flush = () => {
|
|
386
|
+
if (group) {
|
|
387
|
+
entries.push(parseText(ctx, group.start, group.end))
|
|
388
|
+
group = undefined
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const node of nodes) {
|
|
393
|
+
if (!node.position) {
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
const [start, end] = range(node)
|
|
397
|
+
if (PROSE_TYPES.has(node.type)) {
|
|
398
|
+
const reason = unsafeReason(node, ctx.source)
|
|
399
|
+
if (reason) {
|
|
400
|
+
flush()
|
|
401
|
+
entries.push(parseRaw(ctx, start, end, reason))
|
|
402
|
+
continue
|
|
403
|
+
}
|
|
404
|
+
if (top && node.type === 'heading' && (node.depth ?? 6) <= 2) {
|
|
405
|
+
flush()
|
|
406
|
+
}
|
|
407
|
+
group = group ? { start: group.start, end } : { start, end }
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
flush()
|
|
411
|
+
if (node.type === 'code') {
|
|
412
|
+
entries.push(parseCode(ctx, node))
|
|
413
|
+
} else if (node.type === 'containerComponent') {
|
|
414
|
+
entries.push(parseComponent(ctx, node))
|
|
415
|
+
} else {
|
|
416
|
+
entries.push(
|
|
417
|
+
parseRaw(ctx, start, end, unsafeReason(node, ctx.source) ?? 'unknown')
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
flush()
|
|
422
|
+
return entries
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Cuts a Markdown (MDC) text into a block doc. `serializeBlocks` of the result gives the same text. */
|
|
426
|
+
export function parseBlocks(source: string): PlutoBlockDoc {
|
|
427
|
+
const tree = unified()
|
|
428
|
+
.use(remarkParse)
|
|
429
|
+
.use(remarkGfm)
|
|
430
|
+
.use(remarkMdc)
|
|
431
|
+
.parse(source) as unknown as MdNode
|
|
432
|
+
let counter = 0
|
|
433
|
+
const ctx: Context = { source, nextId: () => `b${counter++}` }
|
|
434
|
+
const entries = parseNodes(ctx, tree.children ?? [], true)
|
|
435
|
+
|
|
436
|
+
const gaps: string[] = []
|
|
437
|
+
let cursor = 0
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
gaps.push(source.slice(cursor, entry.start))
|
|
440
|
+
cursor = entry.end
|
|
441
|
+
}
|
|
442
|
+
gaps.push(source.slice(cursor))
|
|
443
|
+
return { blocks: entries.map((entry) => entry.block), gaps }
|
|
444
|
+
}
|