@portabletext/markdown 1.0.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/README.md +429 -0
- package/dist/index.d.ts +599 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1098 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/default-schema.ts +166 -0
- package/src/example-document.md +237 -0
- package/src/example-document.out.md +235 -0
- package/src/example-document.terse-pt.json +124 -0
- package/src/example-document.test.ts +87 -0
- package/src/from-portable-text/build-list-index-map.ts +133 -0
- package/src/from-portable-text/portable-text-to-markdown.ts +135 -0
- package/src/from-portable-text/render-node.ts +176 -0
- package/src/from-portable-text/renderers/block-spacing.ts +39 -0
- package/src/from-portable-text/renderers/hard-break.ts +4 -0
- package/src/from-portable-text/renderers/list-item.ts +32 -0
- package/src/from-portable-text/renderers/marks.ts +113 -0
- package/src/from-portable-text/renderers/style.ts +79 -0
- package/src/from-portable-text/renderers/type.ts +126 -0
- package/src/from-portable-text/types.ts +240 -0
- package/src/index.ts +51 -0
- package/src/key-generator.ts +32 -0
- package/src/markdown-to-portable-text.test.ts +3273 -0
- package/src/portable-text-to-markdown.test.ts +803 -0
- package/src/to-portable-text/markdown-to-portable-text.ts +1204 -0
- package/src/to-portable-text/matchers.ts +192 -0
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSpan,
|
|
3
|
+
type PortableTextBlock,
|
|
4
|
+
type PortableTextObject,
|
|
5
|
+
type PortableTextTextBlock,
|
|
6
|
+
type Schema,
|
|
7
|
+
} from '@portabletext/schema'
|
|
8
|
+
import markdownit from 'markdown-it'
|
|
9
|
+
import {
|
|
10
|
+
blockquoteStyleDefinition,
|
|
11
|
+
defaultCodeDecoratorDefinition,
|
|
12
|
+
defaultCodeObjectDefinition,
|
|
13
|
+
defaultEmDecoratorDefinition,
|
|
14
|
+
defaultHorizontalRuleObjectDefinition,
|
|
15
|
+
defaultHtmlObjectDefinition,
|
|
16
|
+
defaultImageObjectDefinition,
|
|
17
|
+
defaultLinkObjectDefinition,
|
|
18
|
+
defaultOrderedListItemDefinition,
|
|
19
|
+
defaultSchema,
|
|
20
|
+
defaultStrikeThroughDecoratorDefinition,
|
|
21
|
+
defaultStrongDecoratorDefinition,
|
|
22
|
+
defaultUnorderedListItemDefinition,
|
|
23
|
+
h1StyleDefinition,
|
|
24
|
+
h2StyleDefinition,
|
|
25
|
+
h3StyleDefinition,
|
|
26
|
+
h4StyleDefinition,
|
|
27
|
+
h5StyleDefinition,
|
|
28
|
+
h6StyleDefinition,
|
|
29
|
+
normalStyleDefinition,
|
|
30
|
+
} from '../default-schema'
|
|
31
|
+
import {defaultKeyGenerator} from '../key-generator'
|
|
32
|
+
import {
|
|
33
|
+
buildAnnotationMatcher,
|
|
34
|
+
buildDecoratorMatcher,
|
|
35
|
+
buildListItemMatcher,
|
|
36
|
+
buildObjectMatcher,
|
|
37
|
+
buildStyleMatcher,
|
|
38
|
+
type AnnotationMatcher,
|
|
39
|
+
type DecoratorMatcher,
|
|
40
|
+
type ExtractValue,
|
|
41
|
+
type ListItemMatcher,
|
|
42
|
+
type ObjectMatcher,
|
|
43
|
+
type StyleMatcher,
|
|
44
|
+
} from './matchers'
|
|
45
|
+
|
|
46
|
+
type Options = {
|
|
47
|
+
schema?: Schema
|
|
48
|
+
keyGenerator?: () => string
|
|
49
|
+
marks?: {
|
|
50
|
+
strong?: DecoratorMatcher
|
|
51
|
+
em?: DecoratorMatcher
|
|
52
|
+
code?: DecoratorMatcher
|
|
53
|
+
strikeThrough?: DecoratorMatcher
|
|
54
|
+
link?: AnnotationMatcher<{href: string; title: string | undefined}>
|
|
55
|
+
}
|
|
56
|
+
block?: {
|
|
57
|
+
normal?: StyleMatcher
|
|
58
|
+
blockquote?: StyleMatcher
|
|
59
|
+
h1?: StyleMatcher
|
|
60
|
+
h2?: StyleMatcher
|
|
61
|
+
h3?: StyleMatcher
|
|
62
|
+
h4?: StyleMatcher
|
|
63
|
+
h5?: StyleMatcher
|
|
64
|
+
h6?: StyleMatcher
|
|
65
|
+
}
|
|
66
|
+
listItem?: {
|
|
67
|
+
number?: ListItemMatcher
|
|
68
|
+
bullet?: ListItemMatcher
|
|
69
|
+
}
|
|
70
|
+
types?: {
|
|
71
|
+
code?: ObjectMatcher<{language: string | undefined; code: string}>
|
|
72
|
+
horizontalRule?: ObjectMatcher
|
|
73
|
+
html?: ObjectMatcher<{html: string}>
|
|
74
|
+
table?: ObjectMatcher<{
|
|
75
|
+
headerRows: number | undefined
|
|
76
|
+
rows: Array<{
|
|
77
|
+
_key: string
|
|
78
|
+
_type: 'row'
|
|
79
|
+
cells: Array<{
|
|
80
|
+
_type: 'cell'
|
|
81
|
+
_key: string
|
|
82
|
+
value: Array<PortableTextBlock>
|
|
83
|
+
}>
|
|
84
|
+
}>
|
|
85
|
+
}>
|
|
86
|
+
image?: ObjectMatcher<{src: string; alt: string; title: string | undefined}>
|
|
87
|
+
}
|
|
88
|
+
html?: {
|
|
89
|
+
/**
|
|
90
|
+
* How to handle inline HTML.
|
|
91
|
+
* - 'skip': Ignore inline HTML (default)
|
|
92
|
+
* - 'text': Convert inline HTML to plain text
|
|
93
|
+
*
|
|
94
|
+
* @defaultValue 'skip'
|
|
95
|
+
*/
|
|
96
|
+
inline?: 'skip' | 'text'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const codeBlockMatcher: ObjectMatcher<
|
|
101
|
+
ExtractValue<typeof defaultCodeObjectDefinition>
|
|
102
|
+
> = ({context, value, isInline}) => {
|
|
103
|
+
const defaultMatcher = buildObjectMatcher(defaultCodeObjectDefinition)
|
|
104
|
+
const codeObject = defaultMatcher({context, value, isInline})
|
|
105
|
+
|
|
106
|
+
if (!codeObject) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!('code' in codeObject)) {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return codeObject
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const imageBlockMatcher: ObjectMatcher<
|
|
118
|
+
ExtractValue<typeof defaultImageObjectDefinition>
|
|
119
|
+
> = ({context, value, isInline}) => {
|
|
120
|
+
const defaultMatcher = buildObjectMatcher(defaultImageObjectDefinition)
|
|
121
|
+
const imageObject = defaultMatcher({context, value, isInline})
|
|
122
|
+
|
|
123
|
+
if (!imageObject) {
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!('src' in imageObject)) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return imageObject
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const defaultOptions = {
|
|
135
|
+
schema: defaultSchema,
|
|
136
|
+
keyGenerator: defaultKeyGenerator,
|
|
137
|
+
html: {
|
|
138
|
+
inline: 'skip',
|
|
139
|
+
},
|
|
140
|
+
block: {
|
|
141
|
+
normal: buildStyleMatcher(normalStyleDefinition),
|
|
142
|
+
blockquote: buildStyleMatcher(blockquoteStyleDefinition),
|
|
143
|
+
h1: buildStyleMatcher(h1StyleDefinition),
|
|
144
|
+
h2: buildStyleMatcher(h2StyleDefinition),
|
|
145
|
+
h3: buildStyleMatcher(h3StyleDefinition),
|
|
146
|
+
h4: buildStyleMatcher(h4StyleDefinition),
|
|
147
|
+
h5: buildStyleMatcher(h5StyleDefinition),
|
|
148
|
+
h6: buildStyleMatcher(h6StyleDefinition),
|
|
149
|
+
},
|
|
150
|
+
listItem: {
|
|
151
|
+
number: buildListItemMatcher(defaultOrderedListItemDefinition),
|
|
152
|
+
bullet: buildListItemMatcher(defaultUnorderedListItemDefinition),
|
|
153
|
+
},
|
|
154
|
+
marks: {
|
|
155
|
+
strong: buildDecoratorMatcher(defaultStrongDecoratorDefinition),
|
|
156
|
+
em: buildDecoratorMatcher(defaultEmDecoratorDefinition),
|
|
157
|
+
code: buildDecoratorMatcher(defaultCodeDecoratorDefinition),
|
|
158
|
+
strikeThrough: buildDecoratorMatcher(
|
|
159
|
+
defaultStrikeThroughDecoratorDefinition,
|
|
160
|
+
),
|
|
161
|
+
link: buildAnnotationMatcher(defaultLinkObjectDefinition),
|
|
162
|
+
},
|
|
163
|
+
types: {
|
|
164
|
+
code: codeBlockMatcher,
|
|
165
|
+
horizontalRule: buildObjectMatcher(defaultHorizontalRuleObjectDefinition),
|
|
166
|
+
html: buildObjectMatcher(defaultHtmlObjectDefinition),
|
|
167
|
+
image: imageBlockMatcher,
|
|
168
|
+
},
|
|
169
|
+
} as const satisfies Options
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Flattens a table structure by lifting all blocks from all cells.
|
|
173
|
+
*/
|
|
174
|
+
function flattenTable(
|
|
175
|
+
table: {
|
|
176
|
+
rows: Array<{
|
|
177
|
+
_key: string
|
|
178
|
+
_type: 'row'
|
|
179
|
+
cells: Array<{
|
|
180
|
+
_type: 'cell'
|
|
181
|
+
_key: string
|
|
182
|
+
value: Array<PortableTextBlock>
|
|
183
|
+
}>
|
|
184
|
+
}>
|
|
185
|
+
headerRows: number
|
|
186
|
+
},
|
|
187
|
+
portableText: Array<PortableTextBlock>,
|
|
188
|
+
): void {
|
|
189
|
+
// Flatten the table by lifting all blocks from all cells
|
|
190
|
+
for (const row of table.rows) {
|
|
191
|
+
for (const cell of row.cells) {
|
|
192
|
+
for (const block of cell.value) {
|
|
193
|
+
portableText.push(block)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Converts a markdown string to an array of Portable Text blocks.
|
|
201
|
+
*
|
|
202
|
+
* @public
|
|
203
|
+
*/
|
|
204
|
+
export function markdownToPortableText(
|
|
205
|
+
markdown: string,
|
|
206
|
+
options?: Options,
|
|
207
|
+
): Array<PortableTextBlock> {
|
|
208
|
+
const consolidatedOptions = {
|
|
209
|
+
schema: options?.schema ?? defaultSchema,
|
|
210
|
+
keyGenerator: options?.keyGenerator ?? defaultKeyGenerator,
|
|
211
|
+
html: {
|
|
212
|
+
inline: options?.html?.inline ?? 'skip',
|
|
213
|
+
},
|
|
214
|
+
marks: {
|
|
215
|
+
...defaultOptions.marks,
|
|
216
|
+
...options?.marks,
|
|
217
|
+
},
|
|
218
|
+
block: {
|
|
219
|
+
...defaultOptions.block,
|
|
220
|
+
...options?.block,
|
|
221
|
+
},
|
|
222
|
+
listItem: {
|
|
223
|
+
...defaultOptions.listItem,
|
|
224
|
+
...options?.listItem,
|
|
225
|
+
},
|
|
226
|
+
types: {
|
|
227
|
+
...defaultOptions.types,
|
|
228
|
+
...options?.types,
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const md = markdownit({
|
|
233
|
+
html: true,
|
|
234
|
+
linkify: true,
|
|
235
|
+
typographer: false,
|
|
236
|
+
}).enable(['strikethrough', 'table'])
|
|
237
|
+
|
|
238
|
+
const tokens = md.parse(markdown, {})
|
|
239
|
+
|
|
240
|
+
const portableText: Array<PortableTextBlock> = []
|
|
241
|
+
|
|
242
|
+
// State
|
|
243
|
+
let currentBlock: PortableTextTextBlock | null = null
|
|
244
|
+
const currentListStack: Array<string | null> = []
|
|
245
|
+
const markDefRefs: Array<string> = [] // mark keys: 'strong', 'em', 'code', or link keys
|
|
246
|
+
let currentMarkDefs: Array<PortableTextObject> = []
|
|
247
|
+
let currentBlockquoteStyle: string | null = null // Track blockquote style when inside blockquote
|
|
248
|
+
let inListItem = false // Track if we're inside a list item
|
|
249
|
+
|
|
250
|
+
// Table state
|
|
251
|
+
let currentTable: {
|
|
252
|
+
rows: Array<{
|
|
253
|
+
_key: string
|
|
254
|
+
_type: 'row'
|
|
255
|
+
cells: Array<{
|
|
256
|
+
_type: 'cell'
|
|
257
|
+
_key: string
|
|
258
|
+
value: Array<PortableTextBlock>
|
|
259
|
+
}>
|
|
260
|
+
}>
|
|
261
|
+
headerRows: number
|
|
262
|
+
} | null = null
|
|
263
|
+
let currentTableRow: Array<{
|
|
264
|
+
_type: 'cell'
|
|
265
|
+
_key: string
|
|
266
|
+
value: Array<PortableTextBlock>
|
|
267
|
+
}> | null = null
|
|
268
|
+
let inTableHead = false
|
|
269
|
+
|
|
270
|
+
const startBlock = (style: string) => {
|
|
271
|
+
flushBlock()
|
|
272
|
+
currentBlock = {
|
|
273
|
+
_type: 'block' as const,
|
|
274
|
+
style,
|
|
275
|
+
children: [],
|
|
276
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
277
|
+
markDefs: [],
|
|
278
|
+
}
|
|
279
|
+
currentMarkDefs = []
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const flushBlock = () => {
|
|
283
|
+
if (!currentBlock) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Text blocks must have at least one child span
|
|
288
|
+
if (currentBlock.children.length === 0) {
|
|
289
|
+
currentBlock.children.push({
|
|
290
|
+
_type: consolidatedOptions.schema.span.name,
|
|
291
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
292
|
+
text: '',
|
|
293
|
+
marks: [],
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Assign accumulated markDefs to the block
|
|
298
|
+
currentBlock.markDefs = currentMarkDefs
|
|
299
|
+
|
|
300
|
+
portableText.push(currentBlock)
|
|
301
|
+
|
|
302
|
+
currentBlock = null
|
|
303
|
+
currentMarkDefs = []
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const addSpan = (text: string) => {
|
|
307
|
+
if (text.length === 0) {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!currentBlock) {
|
|
312
|
+
throw new Error('Expected current block')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const lastChild = currentBlock.children.at(-1)
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
isSpan({schema: consolidatedOptions.schema}, lastChild) &&
|
|
319
|
+
lastChild.marks?.every((mark) => markDefRefs.includes(mark)) &&
|
|
320
|
+
markDefRefs.every((mark) => lastChild.marks?.includes(mark))
|
|
321
|
+
) {
|
|
322
|
+
// Merge with previous span if marks match
|
|
323
|
+
lastChild.text += text
|
|
324
|
+
} else {
|
|
325
|
+
currentBlock.children.push({
|
|
326
|
+
_type: consolidatedOptions.schema.span.name,
|
|
327
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
328
|
+
text: text,
|
|
329
|
+
marks: [...markDefRefs],
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Helpers for lists
|
|
335
|
+
const listLevel = () => currentListStack.length
|
|
336
|
+
const ensureListBlock = (listItem: string) => {
|
|
337
|
+
if (!currentBlock) {
|
|
338
|
+
// Use blockquote style if inside a blockquote, otherwise use normal style
|
|
339
|
+
const style =
|
|
340
|
+
currentBlockquoteStyle ??
|
|
341
|
+
consolidatedOptions.block.normal({
|
|
342
|
+
context: {schema: consolidatedOptions.schema},
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
if (!style) {
|
|
346
|
+
console.warn('No default style found, using "normal"')
|
|
347
|
+
startBlock('normal')
|
|
348
|
+
} else {
|
|
349
|
+
startBlock(style)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!currentBlock) {
|
|
354
|
+
throw new Error('Expected current block')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
currentBlock.listItem !== listItem ||
|
|
359
|
+
currentBlock.level !== listLevel()
|
|
360
|
+
) {
|
|
361
|
+
currentBlock.listItem = listItem
|
|
362
|
+
currentBlock.level = listLevel()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Walk tokens
|
|
367
|
+
for (const token of tokens) {
|
|
368
|
+
switch (token.type) {
|
|
369
|
+
// Paragraphs
|
|
370
|
+
case 'paragraph_open': {
|
|
371
|
+
// Skip creating a new block if we're inside a list item (the list item is the block)
|
|
372
|
+
if (inListItem) {
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Use blockquote style if inside a blockquote, otherwise use normal style
|
|
377
|
+
const style =
|
|
378
|
+
currentBlockquoteStyle ??
|
|
379
|
+
consolidatedOptions.block.normal({
|
|
380
|
+
context: {schema: consolidatedOptions.schema},
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
if (!style) {
|
|
384
|
+
console.warn('No default style found, using "normal"')
|
|
385
|
+
startBlock('normal')
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
startBlock(style)
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
case 'paragraph_close':
|
|
393
|
+
// Skip flushing if we're inside a list item (list_item_close will flush)
|
|
394
|
+
if (inListItem) {
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
flushBlock()
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
// Headings
|
|
401
|
+
case 'heading_open': {
|
|
402
|
+
const level = Number(token?.tag?.slice(1))
|
|
403
|
+
|
|
404
|
+
// Map level to the appropriate heading matcher
|
|
405
|
+
const headingMatchers = {
|
|
406
|
+
1: consolidatedOptions.block.h1,
|
|
407
|
+
2: consolidatedOptions.block.h2,
|
|
408
|
+
3: consolidatedOptions.block.h3,
|
|
409
|
+
4: consolidatedOptions.block.h4,
|
|
410
|
+
5: consolidatedOptions.block.h5,
|
|
411
|
+
6: consolidatedOptions.block.h6,
|
|
412
|
+
} as const
|
|
413
|
+
|
|
414
|
+
const headingMatcher =
|
|
415
|
+
headingMatchers[level as keyof typeof headingMatchers]
|
|
416
|
+
|
|
417
|
+
const style =
|
|
418
|
+
headingMatcher?.({
|
|
419
|
+
context: {schema: consolidatedOptions.schema},
|
|
420
|
+
}) ??
|
|
421
|
+
consolidatedOptions.block.normal({
|
|
422
|
+
context: {schema: consolidatedOptions.schema},
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
if (!style) {
|
|
426
|
+
console.warn('No heading style found, using "normal"')
|
|
427
|
+
startBlock('normal')
|
|
428
|
+
break
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
startBlock(style)
|
|
432
|
+
break
|
|
433
|
+
}
|
|
434
|
+
case 'heading_close':
|
|
435
|
+
flushBlock()
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
// Blockquote
|
|
439
|
+
case 'blockquote_open': {
|
|
440
|
+
// Set the blockquote style for paragraphs inside the blockquote
|
|
441
|
+
const style =
|
|
442
|
+
consolidatedOptions.block.blockquote({
|
|
443
|
+
context: {schema: consolidatedOptions.schema},
|
|
444
|
+
}) ??
|
|
445
|
+
consolidatedOptions.block.normal({
|
|
446
|
+
context: {schema: consolidatedOptions.schema},
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
currentBlockquoteStyle = style ?? 'normal'
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
case 'blockquote_close': {
|
|
453
|
+
currentBlockquoteStyle = null
|
|
454
|
+
break
|
|
455
|
+
}
|
|
456
|
+
// Lists
|
|
457
|
+
case 'bullet_list_open': {
|
|
458
|
+
const listItem = consolidatedOptions.listItem.bullet({
|
|
459
|
+
context: {schema: consolidatedOptions.schema},
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
if (!listItem) {
|
|
463
|
+
// No list definition in schema, push null to indicate we should skip list properties
|
|
464
|
+
currentListStack.push(null)
|
|
465
|
+
break
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
currentListStack.push(listItem)
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
case 'ordered_list_open': {
|
|
472
|
+
const listItem = consolidatedOptions.listItem.number({
|
|
473
|
+
context: {schema: consolidatedOptions.schema},
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
if (!listItem) {
|
|
477
|
+
// No list definition in schema, push null to indicate we should skip list properties
|
|
478
|
+
currentListStack.push(null)
|
|
479
|
+
break
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
currentListStack.push(listItem)
|
|
483
|
+
break
|
|
484
|
+
}
|
|
485
|
+
case 'bullet_list_close':
|
|
486
|
+
case 'ordered_list_close':
|
|
487
|
+
currentListStack.pop()
|
|
488
|
+
break
|
|
489
|
+
case 'list_item_open': {
|
|
490
|
+
const listType = currentListStack.at(-1)
|
|
491
|
+
|
|
492
|
+
if (listType === undefined) {
|
|
493
|
+
throw new Error('Expected an open list')
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Flush any previous list item block before starting a new one
|
|
497
|
+
// This is needed for proper separation of list items
|
|
498
|
+
if (currentBlock) {
|
|
499
|
+
flushBlock()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// If listType is null, it means there's no list definition in the schema
|
|
503
|
+
// Just create a normal block without list properties
|
|
504
|
+
if (listType === null) {
|
|
505
|
+
// Use blockquote style if inside a blockquote, otherwise use normal style
|
|
506
|
+
const style =
|
|
507
|
+
currentBlockquoteStyle ??
|
|
508
|
+
consolidatedOptions.block.normal({
|
|
509
|
+
context: {schema: consolidatedOptions.schema},
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
if (!style) {
|
|
513
|
+
console.warn('No default style found, using "normal"')
|
|
514
|
+
startBlock('normal')
|
|
515
|
+
} else {
|
|
516
|
+
startBlock(style)
|
|
517
|
+
}
|
|
518
|
+
inListItem = true
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
ensureListBlock(listType)
|
|
523
|
+
inListItem = true
|
|
524
|
+
break
|
|
525
|
+
}
|
|
526
|
+
case 'list_item_close':
|
|
527
|
+
inListItem = false
|
|
528
|
+
flushBlock()
|
|
529
|
+
break
|
|
530
|
+
|
|
531
|
+
// Code fences / blocks
|
|
532
|
+
case 'fence': {
|
|
533
|
+
flushBlock()
|
|
534
|
+
|
|
535
|
+
const language = token.info.trim() || undefined
|
|
536
|
+
// Remove trailing newline from code content
|
|
537
|
+
const code = token.content.replace(/\n$/, '')
|
|
538
|
+
|
|
539
|
+
const codeObject = consolidatedOptions.types.code({
|
|
540
|
+
context: {
|
|
541
|
+
schema: consolidatedOptions.schema,
|
|
542
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
543
|
+
},
|
|
544
|
+
value: {language, code},
|
|
545
|
+
isInline: false,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
if (!codeObject) {
|
|
549
|
+
// For fallback to text block, check if it's multi-line
|
|
550
|
+
const hasMultipleLines = code.includes('\n')
|
|
551
|
+
|
|
552
|
+
if (hasMultipleLines) {
|
|
553
|
+
// Multi-line code without definition should still be a code object
|
|
554
|
+
portableText.push({
|
|
555
|
+
_type: 'code',
|
|
556
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
557
|
+
code,
|
|
558
|
+
})
|
|
559
|
+
} else {
|
|
560
|
+
// Single-line code becomes a text block
|
|
561
|
+
const style = consolidatedOptions.block.normal({
|
|
562
|
+
context: {schema: consolidatedOptions.schema},
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
if (!style) {
|
|
566
|
+
console.warn('No default style found, using "normal"')
|
|
567
|
+
startBlock('normal')
|
|
568
|
+
} else {
|
|
569
|
+
startBlock(style)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
addSpan(code)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
break
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
portableText.push(codeObject)
|
|
579
|
+
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Horizontal rule
|
|
584
|
+
case 'hr': {
|
|
585
|
+
flushBlock()
|
|
586
|
+
|
|
587
|
+
const hrObject = consolidatedOptions.types.horizontalRule({
|
|
588
|
+
context: {
|
|
589
|
+
schema: consolidatedOptions.schema,
|
|
590
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
591
|
+
},
|
|
592
|
+
value: {},
|
|
593
|
+
isInline: false,
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
if (!hrObject) {
|
|
597
|
+
// If there's no break definition in the schema, parse as text
|
|
598
|
+
const style = consolidatedOptions.block.normal({
|
|
599
|
+
context: {schema: consolidatedOptions.schema},
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
if (!style) {
|
|
603
|
+
console.warn('No default style found, using "normal"')
|
|
604
|
+
startBlock('normal')
|
|
605
|
+
} else {
|
|
606
|
+
startBlock(style)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
addSpan('---')
|
|
610
|
+
flushBlock()
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
portableText.push(hrObject)
|
|
615
|
+
|
|
616
|
+
break
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// HTML block
|
|
620
|
+
case 'html_block': {
|
|
621
|
+
flushBlock()
|
|
622
|
+
|
|
623
|
+
const htmlContent = token.content.trim()
|
|
624
|
+
|
|
625
|
+
if (!htmlContent) {
|
|
626
|
+
break
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const htmlObject = consolidatedOptions.types.html({
|
|
630
|
+
context: {
|
|
631
|
+
schema: consolidatedOptions.schema,
|
|
632
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
633
|
+
},
|
|
634
|
+
value: {html: htmlContent},
|
|
635
|
+
isInline: false,
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
if (!htmlObject) {
|
|
639
|
+
// If there's no HTML block definition in the schema, parse as text
|
|
640
|
+
const style = consolidatedOptions.block.normal({
|
|
641
|
+
context: {schema: consolidatedOptions.schema},
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
if (!style) {
|
|
645
|
+
console.warn('No default style found, using "normal"')
|
|
646
|
+
startBlock('normal')
|
|
647
|
+
} else {
|
|
648
|
+
startBlock(style)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
addSpan(htmlContent)
|
|
652
|
+
flushBlock()
|
|
653
|
+
break
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
portableText.push(htmlObject)
|
|
657
|
+
|
|
658
|
+
break
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
case 'code_block': {
|
|
662
|
+
flushBlock()
|
|
663
|
+
|
|
664
|
+
// Remove trailing newline from code content
|
|
665
|
+
const code = token.content.replace(/\n$/, '')
|
|
666
|
+
|
|
667
|
+
const codeObject = consolidatedOptions.types.code({
|
|
668
|
+
context: {
|
|
669
|
+
schema: consolidatedOptions.schema,
|
|
670
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
671
|
+
},
|
|
672
|
+
value: {language: undefined, code},
|
|
673
|
+
isInline: false,
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
if (!codeObject) {
|
|
677
|
+
// For fallback to text block, check if it's multi-line
|
|
678
|
+
const hasMultipleLines = code.includes('\n')
|
|
679
|
+
|
|
680
|
+
if (hasMultipleLines) {
|
|
681
|
+
// Multi-line code without definition should still be a code object
|
|
682
|
+
portableText.push({
|
|
683
|
+
_type: 'code',
|
|
684
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
685
|
+
code,
|
|
686
|
+
})
|
|
687
|
+
} else {
|
|
688
|
+
// Single-line code becomes a text block
|
|
689
|
+
const style = consolidatedOptions.block.normal({
|
|
690
|
+
context: {schema: consolidatedOptions.schema},
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
if (!style) {
|
|
694
|
+
console.warn('No default style found, using "normal"')
|
|
695
|
+
startBlock('normal')
|
|
696
|
+
} else {
|
|
697
|
+
startBlock(style)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
addSpan(code)
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
portableText.push(codeObject)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Tables
|
|
710
|
+
case 'table_open':
|
|
711
|
+
flushBlock()
|
|
712
|
+
currentTable = {rows: [], headerRows: 0}
|
|
713
|
+
break
|
|
714
|
+
|
|
715
|
+
case 'table_close': {
|
|
716
|
+
if (!currentTable) {
|
|
717
|
+
break
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Only create table object if table type is defined
|
|
721
|
+
if (consolidatedOptions.types.table) {
|
|
722
|
+
const tableObject = consolidatedOptions.types.table({
|
|
723
|
+
context: {
|
|
724
|
+
schema: consolidatedOptions.schema,
|
|
725
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
726
|
+
},
|
|
727
|
+
value: {
|
|
728
|
+
rows: currentTable.rows,
|
|
729
|
+
headerRows:
|
|
730
|
+
currentTable.headerRows > 0
|
|
731
|
+
? currentTable.headerRows
|
|
732
|
+
: undefined,
|
|
733
|
+
},
|
|
734
|
+
isInline: false,
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
if (tableObject) {
|
|
738
|
+
portableText.push(tableObject)
|
|
739
|
+
} else {
|
|
740
|
+
// If table object couldn't be created, flatten the table
|
|
741
|
+
flattenTable(currentTable, portableText)
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
// If there's no table definition in the schema, flatten the table
|
|
745
|
+
flattenTable(currentTable, portableText)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
currentTable = null
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
case 'thead_open':
|
|
753
|
+
inTableHead = true
|
|
754
|
+
break
|
|
755
|
+
|
|
756
|
+
case 'thead_close':
|
|
757
|
+
inTableHead = false
|
|
758
|
+
break
|
|
759
|
+
|
|
760
|
+
case 'tbody_open':
|
|
761
|
+
case 'tbody_close':
|
|
762
|
+
// Just markers, no action needed
|
|
763
|
+
break
|
|
764
|
+
|
|
765
|
+
case 'tr_open':
|
|
766
|
+
currentTableRow = []
|
|
767
|
+
break
|
|
768
|
+
|
|
769
|
+
case 'tr_close':
|
|
770
|
+
if (currentTable && currentTableRow) {
|
|
771
|
+
currentTable.rows.push({
|
|
772
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
773
|
+
_type: 'row',
|
|
774
|
+
cells: currentTableRow,
|
|
775
|
+
})
|
|
776
|
+
if (inTableHead) {
|
|
777
|
+
currentTable.headerRows++
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
currentTableRow = null
|
|
781
|
+
break
|
|
782
|
+
|
|
783
|
+
case 'th_open':
|
|
784
|
+
case 'td_open': {
|
|
785
|
+
// Start a new block for the table cell
|
|
786
|
+
const style = consolidatedOptions.block.normal({
|
|
787
|
+
context: {schema: consolidatedOptions.schema},
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
if (!style) {
|
|
791
|
+
console.warn('No default style found, using "normal"')
|
|
792
|
+
startBlock('normal')
|
|
793
|
+
} else {
|
|
794
|
+
startBlock(style)
|
|
795
|
+
}
|
|
796
|
+
break
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
case 'th_close':
|
|
800
|
+
case 'td_close': {
|
|
801
|
+
// Flush the current block into the cell
|
|
802
|
+
flushBlock()
|
|
803
|
+
|
|
804
|
+
// Get all blocks that were added since this cell started
|
|
805
|
+
// We need to extract them from portableText array
|
|
806
|
+
const cellBlocks: Array<PortableTextBlock> = []
|
|
807
|
+
|
|
808
|
+
// Check if we have blocks to extract (added after table_open)
|
|
809
|
+
if (portableText.length > 0) {
|
|
810
|
+
const lastBlock = portableText.at(-1)
|
|
811
|
+
if (lastBlock && lastBlock._type === 'block') {
|
|
812
|
+
cellBlocks.push(portableText.pop()!)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// If no blocks were created (empty cell), create an empty block
|
|
817
|
+
if (cellBlocks.length === 0) {
|
|
818
|
+
cellBlocks.push({
|
|
819
|
+
_type: 'block' as const,
|
|
820
|
+
style:
|
|
821
|
+
consolidatedOptions.block.normal({
|
|
822
|
+
context: {schema: consolidatedOptions.schema},
|
|
823
|
+
}) || 'normal',
|
|
824
|
+
children: [
|
|
825
|
+
{
|
|
826
|
+
_type: consolidatedOptions.schema.span.name,
|
|
827
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
828
|
+
text: '',
|
|
829
|
+
marks: [],
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
833
|
+
markDefs: [],
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Check if the cell contains a single block with a single image child
|
|
838
|
+
// If so, extract the image as a block-level image
|
|
839
|
+
const firstBlock = cellBlocks[0]
|
|
840
|
+
if (
|
|
841
|
+
cellBlocks.length === 1 &&
|
|
842
|
+
firstBlock &&
|
|
843
|
+
firstBlock._type === 'block' &&
|
|
844
|
+
'children' in firstBlock &&
|
|
845
|
+
Array.isArray(firstBlock.children) &&
|
|
846
|
+
firstBlock.children.length === 1
|
|
847
|
+
) {
|
|
848
|
+
const onlyChild = firstBlock.children[0]
|
|
849
|
+
// Check if it's an image object (not a span)
|
|
850
|
+
if (
|
|
851
|
+
typeof onlyChild === 'object' &&
|
|
852
|
+
onlyChild !== null &&
|
|
853
|
+
'_type' in onlyChild &&
|
|
854
|
+
onlyChild._type !== consolidatedOptions.schema.span.name &&
|
|
855
|
+
onlyChild._type === 'image'
|
|
856
|
+
) {
|
|
857
|
+
// Replace the block with just the image
|
|
858
|
+
cellBlocks[0] = onlyChild as PortableTextBlock
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (currentTableRow !== null) {
|
|
863
|
+
currentTableRow.push({
|
|
864
|
+
_type: 'cell',
|
|
865
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
866
|
+
value: cellBlocks,
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
break
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Inline container
|
|
873
|
+
case 'inline': {
|
|
874
|
+
// Check if we're in a table cell
|
|
875
|
+
const inTableCell = currentTableRow !== null
|
|
876
|
+
|
|
877
|
+
// Check if this is a standalone image (paragraph with only an image)
|
|
878
|
+
if (
|
|
879
|
+
token.children?.length === 1 &&
|
|
880
|
+
token.children[0]?.type === 'image'
|
|
881
|
+
) {
|
|
882
|
+
const imageToken = token.children[0]
|
|
883
|
+
if (!imageToken) {
|
|
884
|
+
break
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const src =
|
|
888
|
+
imageToken.attrs?.find(([name]) => name === 'src')?.at(1) || ''
|
|
889
|
+
const alt = imageToken.content || ''
|
|
890
|
+
const title =
|
|
891
|
+
imageToken.attrs?.find(([name]) => name === 'title')?.at(1) ||
|
|
892
|
+
undefined
|
|
893
|
+
|
|
894
|
+
const blockImageObject = consolidatedOptions.types.image({
|
|
895
|
+
context: {
|
|
896
|
+
schema: consolidatedOptions.schema,
|
|
897
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
898
|
+
},
|
|
899
|
+
value: {src, alt, title},
|
|
900
|
+
isInline: false,
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
if (blockImageObject) {
|
|
904
|
+
if (inTableCell) {
|
|
905
|
+
// In table cells, we can't push to portableText directly
|
|
906
|
+
// The block image will be handled in th_close/td_close extraction logic
|
|
907
|
+
// For now, add it as a child of the current block
|
|
908
|
+
if (currentBlock && 'children' in currentBlock) {
|
|
909
|
+
;(currentBlock as PortableTextTextBlock).children.push(
|
|
910
|
+
blockImageObject as PortableTextObject,
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
// Discard the empty paragraph block that was created by paragraph_open
|
|
915
|
+
currentBlock = null
|
|
916
|
+
currentMarkDefs = []
|
|
917
|
+
portableText.push(blockImageObject)
|
|
918
|
+
}
|
|
919
|
+
break
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
addSpan(``)
|
|
923
|
+
break
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Walk its children for text/marks/links
|
|
927
|
+
for (const childToken of token.children ?? []) {
|
|
928
|
+
switch (childToken.type) {
|
|
929
|
+
case 'text':
|
|
930
|
+
addSpan(childToken.content)
|
|
931
|
+
break
|
|
932
|
+
case 'softbreak':
|
|
933
|
+
case 'hardbreak':
|
|
934
|
+
addSpan('\n')
|
|
935
|
+
break
|
|
936
|
+
case 'code_inline': {
|
|
937
|
+
const decorator = consolidatedOptions.marks.code({
|
|
938
|
+
context: {schema: consolidatedOptions.schema},
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
if (!decorator) {
|
|
942
|
+
// No code decorator defined, just add the content without marks
|
|
943
|
+
addSpan(childToken.content)
|
|
944
|
+
break
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
markDefRefs.push(decorator)
|
|
948
|
+
addSpan(childToken.content)
|
|
949
|
+
|
|
950
|
+
// code_inline is self-contained, so we need to pop the decorator
|
|
951
|
+
const index = markDefRefs.lastIndexOf(decorator)
|
|
952
|
+
|
|
953
|
+
if (index !== -1) {
|
|
954
|
+
markDefRefs.splice(index, 1)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
break
|
|
958
|
+
}
|
|
959
|
+
case 'strong_open': {
|
|
960
|
+
const decorator = consolidatedOptions.marks.strong({
|
|
961
|
+
context: {schema: consolidatedOptions.schema},
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
if (!decorator) {
|
|
965
|
+
break
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
markDefRefs.push(decorator)
|
|
969
|
+
break
|
|
970
|
+
}
|
|
971
|
+
case 'strong_close': {
|
|
972
|
+
const decorator = consolidatedOptions.marks.strong({
|
|
973
|
+
context: {schema: consolidatedOptions.schema},
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
if (!decorator) {
|
|
977
|
+
break
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const index = markDefRefs.lastIndexOf(decorator)
|
|
981
|
+
|
|
982
|
+
if (index !== -1) {
|
|
983
|
+
markDefRefs.splice(index, 1)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
break
|
|
987
|
+
}
|
|
988
|
+
case 'em_open': {
|
|
989
|
+
const decorator = consolidatedOptions.marks.em({
|
|
990
|
+
context: {schema: consolidatedOptions.schema},
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
if (!decorator) {
|
|
994
|
+
break
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
markDefRefs.push(decorator)
|
|
998
|
+
|
|
999
|
+
break
|
|
1000
|
+
}
|
|
1001
|
+
case 'em_close': {
|
|
1002
|
+
const decorator = consolidatedOptions.marks.em({
|
|
1003
|
+
context: {schema: consolidatedOptions.schema},
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
if (!decorator) {
|
|
1007
|
+
break
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const index = markDefRefs.lastIndexOf(decorator)
|
|
1011
|
+
|
|
1012
|
+
if (index !== -1) {
|
|
1013
|
+
markDefRefs.splice(index, 1)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
break
|
|
1017
|
+
}
|
|
1018
|
+
case 's_open': {
|
|
1019
|
+
const decorator = consolidatedOptions.marks.strikeThrough({
|
|
1020
|
+
context: {schema: consolidatedOptions.schema},
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
if (!decorator) {
|
|
1024
|
+
break
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
markDefRefs.push(decorator)
|
|
1028
|
+
|
|
1029
|
+
break
|
|
1030
|
+
}
|
|
1031
|
+
case 's_close': {
|
|
1032
|
+
const decorator = consolidatedOptions.marks.strikeThrough({
|
|
1033
|
+
context: {schema: consolidatedOptions.schema},
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
if (!decorator) {
|
|
1037
|
+
break
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const index = markDefRefs.lastIndexOf(decorator)
|
|
1041
|
+
|
|
1042
|
+
if (index !== -1) {
|
|
1043
|
+
markDefRefs.splice(index, 1)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
break
|
|
1047
|
+
}
|
|
1048
|
+
case 'link_open': {
|
|
1049
|
+
const href = childToken.attrs
|
|
1050
|
+
?.find(([name]) => name === 'href')
|
|
1051
|
+
?.at(1)
|
|
1052
|
+
|
|
1053
|
+
if (!href) {
|
|
1054
|
+
break
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const title = childToken.attrs
|
|
1058
|
+
?.find(([name]) => name === 'title')
|
|
1059
|
+
?.at(1)
|
|
1060
|
+
|
|
1061
|
+
const linkObject = consolidatedOptions.marks.link({
|
|
1062
|
+
context: {
|
|
1063
|
+
schema: consolidatedOptions.schema,
|
|
1064
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
1065
|
+
},
|
|
1066
|
+
value: {href, title},
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
if (!linkObject) {
|
|
1070
|
+
break
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
currentMarkDefs.push(linkObject)
|
|
1074
|
+
markDefRefs.push(linkObject._key)
|
|
1075
|
+
break
|
|
1076
|
+
}
|
|
1077
|
+
case 'link_close': {
|
|
1078
|
+
// remove the last link key
|
|
1079
|
+
const markDefKeys = new Set(currentMarkDefs.map((d) => d._key))
|
|
1080
|
+
let lastLinkIndex: number | undefined
|
|
1081
|
+
|
|
1082
|
+
for (const markDefRef of markDefRefs.reverse()) {
|
|
1083
|
+
if (markDefKeys.has(markDefRef)) {
|
|
1084
|
+
lastLinkIndex = markDefRefs.indexOf(markDefRef)
|
|
1085
|
+
break
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (lastLinkIndex !== undefined) {
|
|
1090
|
+
const realIndex = markDefRefs.length - 1 - lastLinkIndex
|
|
1091
|
+
markDefRefs.splice(realIndex, 1)
|
|
1092
|
+
}
|
|
1093
|
+
break
|
|
1094
|
+
}
|
|
1095
|
+
case 'image': {
|
|
1096
|
+
const src =
|
|
1097
|
+
childToken.attrs?.find(([name]) => name === 'src')?.at(1) || ''
|
|
1098
|
+
const alt = childToken.content || ''
|
|
1099
|
+
|
|
1100
|
+
// Try to create an inline image first
|
|
1101
|
+
const inlineImageObject = consolidatedOptions.types.image({
|
|
1102
|
+
context: {
|
|
1103
|
+
schema: consolidatedOptions.schema,
|
|
1104
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
1105
|
+
},
|
|
1106
|
+
value: {src, alt, title: undefined},
|
|
1107
|
+
isInline: true,
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
if (inlineImageObject) {
|
|
1111
|
+
// Inline image is supported - add it to current block
|
|
1112
|
+
if (!currentBlock) {
|
|
1113
|
+
const style = consolidatedOptions.block.normal({
|
|
1114
|
+
context: {schema: consolidatedOptions.schema},
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
if (!style) {
|
|
1118
|
+
console.warn('No default style found, using "normal"')
|
|
1119
|
+
startBlock('normal')
|
|
1120
|
+
} else {
|
|
1121
|
+
startBlock(style)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// At this point currentBlock should exist
|
|
1126
|
+
if (!currentBlock) {
|
|
1127
|
+
throw new Error('Expected current block after startBlock')
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Add the image as an inline object (TypeScript assertion needed for type narrowing)
|
|
1131
|
+
;(currentBlock as PortableTextTextBlock).children.push(
|
|
1132
|
+
inlineImageObject as PortableTextObject,
|
|
1133
|
+
)
|
|
1134
|
+
break
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Inline image not supported - try block image as fallback
|
|
1138
|
+
const blockImageObject = consolidatedOptions.types.image({
|
|
1139
|
+
context: {
|
|
1140
|
+
schema: consolidatedOptions.schema,
|
|
1141
|
+
keyGenerator: consolidatedOptions.keyGenerator,
|
|
1142
|
+
},
|
|
1143
|
+
value: {src, alt, title: undefined},
|
|
1144
|
+
isInline: false,
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
if (!blockImageObject) {
|
|
1148
|
+
// Neither inline nor block image supported
|
|
1149
|
+
addSpan(``)
|
|
1150
|
+
break
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Block image supported - flush current block and add as block-level
|
|
1154
|
+
// Skip if we're in a table cell (images in cells are handled differently)
|
|
1155
|
+
if (inTableCell) {
|
|
1156
|
+
// In table cells, add the image to current block (will be extracted later)
|
|
1157
|
+
if (currentBlock && 'children' in currentBlock) {
|
|
1158
|
+
;(currentBlock as PortableTextTextBlock).children.push(
|
|
1159
|
+
blockImageObject as PortableTextObject,
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
break
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Not in table - flush current block, add image as block, start new block
|
|
1166
|
+
flushBlock()
|
|
1167
|
+
portableText.push(blockImageObject)
|
|
1168
|
+
|
|
1169
|
+
// Start a new block for any remaining content
|
|
1170
|
+
const style = consolidatedOptions.block.normal({
|
|
1171
|
+
context: {schema: consolidatedOptions.schema},
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
if (style) {
|
|
1175
|
+
startBlock(style)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
break
|
|
1179
|
+
}
|
|
1180
|
+
case 'html_inline': {
|
|
1181
|
+
// Handle inline HTML based on configuration
|
|
1182
|
+
if (consolidatedOptions.html.inline === 'text') {
|
|
1183
|
+
addSpan(childToken.content)
|
|
1184
|
+
}
|
|
1185
|
+
// 'skip' - do nothing, ignore the HTML
|
|
1186
|
+
break
|
|
1187
|
+
}
|
|
1188
|
+
default:
|
|
1189
|
+
// Ignore other inline token types by default
|
|
1190
|
+
break
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
break
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
default:
|
|
1197
|
+
break
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
flushBlock()
|
|
1202
|
+
|
|
1203
|
+
return portableText
|
|
1204
|
+
}
|