@portabletext/markdown 1.0.3 → 1.0.5
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/index.d.ts +112 -107
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -9
- package/src/default-schema.ts +0 -166
- package/src/example-document.md +0 -237
- package/src/example-document.out.md +0 -235
- package/src/example-document.terse-pt.json +0 -124
- package/src/example-document.test.ts +0 -87
- package/src/from-portable-text/build-list-index-map.ts +0 -133
- package/src/from-portable-text/portable-text-to-markdown.ts +0 -135
- package/src/from-portable-text/render-node.ts +0 -176
- package/src/from-portable-text/renderers/block-spacing.ts +0 -39
- package/src/from-portable-text/renderers/hard-break.ts +0 -4
- package/src/from-portable-text/renderers/list-item.ts +0 -32
- package/src/from-portable-text/renderers/marks.ts +0 -113
- package/src/from-portable-text/renderers/style.ts +0 -79
- package/src/from-portable-text/renderers/type.ts +0 -126
- package/src/from-portable-text/types.ts +0 -240
- package/src/index.ts +0 -51
- package/src/key-generator.ts +0 -32
- package/src/markdown-to-portable-text.test.ts +0 -3273
- package/src/portable-text-to-markdown.test.ts +0 -803
- package/src/to-portable-text/markdown-to-portable-text.ts +0 -1204
- package/src/to-portable-text/matchers.ts +0 -192
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
"h1:Markdown to Portable Text: A Complete Guide",
|
|
3
|
-
"Converting markdown to Portable Text is a ,powerful, way to bridge the gap between ,simple text formatting, and ,structured content,.",
|
|
4
|
-
"h2:Why Portable Text?",
|
|
5
|
-
"Portable Text is a ,specification, for ,rich text, that is:",
|
|
6
|
-
">-:Platform ,agnostic",
|
|
7
|
-
">-:Structured, and ,queryable",
|
|
8
|
-
">-:Designed for ,modern content workflows",
|
|
9
|
-
"Visit ,https://portabletext.org, for more information, or check out the ,official documentation,.",
|
|
10
|
-
"{horizontal-rule}",
|
|
11
|
-
"h2:Supported Features",
|
|
12
|
-
"h3:Text Formatting",
|
|
13
|
-
"You can use ,bold text,, ,italic text,, ,inline code,, and even ,strikethrough, text. The parser handles ,nested formatting, gracefully, including ,bold and italic, combined.",
|
|
14
|
-
"Here's some ,code with **bold inside**, and ,text with ,code inside, to test edge cases.",
|
|
15
|
-
"h3:Links and Images",
|
|
16
|
-
"Reference-style links work too! Check out this ,example link, that uses references defined elsewhere.",
|
|
17
|
-
"{image}",
|
|
18
|
-
"Here's an inline image in text: ,{image}, followed by more text.",
|
|
19
|
-
"h3:Code Blocks",
|
|
20
|
-
"Fenced code blocks with syntax highlighting:",
|
|
21
|
-
"{code}",
|
|
22
|
-
"Indented code blocks also work:",
|
|
23
|
-
"{code}",
|
|
24
|
-
"h3:Blockquotes",
|
|
25
|
-
"q:Markdown is a lightweight markup language for creating formatted text.",
|
|
26
|
-
"q:It was created by John Gruber in 2004.",
|
|
27
|
-
"Nested blockquotes are supported:",
|
|
28
|
-
"q:This is the outer quote",
|
|
29
|
-
"q:And this is nested deeper",
|
|
30
|
-
"q:With multiple paragraphs",
|
|
31
|
-
"h3:Lists",
|
|
32
|
-
"h4:Unordered Lists",
|
|
33
|
-
">-:Bold item, in a list",
|
|
34
|
-
">-:Italic item, with ,a link",
|
|
35
|
-
">-:Item with ,inline code",
|
|
36
|
-
">>-:Nested item one",
|
|
37
|
-
">>-:Nested item two",
|
|
38
|
-
">>>-:Even deeper nesting",
|
|
39
|
-
">-:Back to top level",
|
|
40
|
-
"h4:Ordered Lists",
|
|
41
|
-
">#:First step: Parse the markdown",
|
|
42
|
-
">#:Second step: Generate tokens",
|
|
43
|
-
">#:Third step: Transform to Portable Text",
|
|
44
|
-
">>#:Map block types",
|
|
45
|
-
">>#:Handle inline formatting",
|
|
46
|
-
">>#:Preserve structure",
|
|
47
|
-
"h4:Mixed Lists",
|
|
48
|
-
">#:Ordered parent",
|
|
49
|
-
">>-:Unordered child",
|
|
50
|
-
">>-:Another unordered",
|
|
51
|
-
">>>#:Back to ordered",
|
|
52
|
-
">>>#:Still ordered",
|
|
53
|
-
">#:Continue ordered parent",
|
|
54
|
-
"h3:Tables",
|
|
55
|
-
"Here's a comparison of different content formats:",
|
|
56
|
-
"{table}",
|
|
57
|
-
"Feature support matrix:",
|
|
58
|
-
"{table}",
|
|
59
|
-
"h3:Horizontal Rules",
|
|
60
|
-
"You can separate sections with horizontal rules:",
|
|
61
|
-
"{horizontal-rule}",
|
|
62
|
-
"They create visual breaks in the content.",
|
|
63
|
-
"{horizontal-rule}",
|
|
64
|
-
"h3:All Heading Levels",
|
|
65
|
-
"h1:Heading 1",
|
|
66
|
-
"h2:Heading 2",
|
|
67
|
-
"h3:Heading 3",
|
|
68
|
-
"h4:Heading 4",
|
|
69
|
-
"h5:Heading 5",
|
|
70
|
-
"h6:Heading 6",
|
|
71
|
-
"h2:Advanced Examples",
|
|
72
|
-
"h3:Overlapping Formatting",
|
|
73
|
-
"This paragraph contains ,bold with ,italic inside, and ,italic with ,bold inside, to test proper nesting.",
|
|
74
|
-
"You can also have ,bold with ,code, and ,italic with ,code, and even ,code with **bold** and _italic_, (though the formatting inside code should be preserved as-is).",
|
|
75
|
-
"h3:Links with Formatting",
|
|
76
|
-
"Here's a ,bold link, and an ,italic link, and even a ,code link,.",
|
|
77
|
-
"h3:Complex List Items",
|
|
78
|
-
">-:Item with ,bold,, ,italic,, and ,code",
|
|
79
|
-
">-:Item with a ,link to ,bold, content",
|
|
80
|
-
">-:Item with an inline image ,{image}, in the middle",
|
|
81
|
-
">>-:Nested ,bold item, with ,italic, and ,code",
|
|
82
|
-
">>-:Another nested item",
|
|
83
|
-
"h3:Line Breaks",
|
|
84
|
-
"This is a line with a hard break\nthat continues on the next line but stays in the same paragraph.",
|
|
85
|
-
"Another paragraph with a break\nand more content.",
|
|
86
|
-
"h3:Autolinks",
|
|
87
|
-
"Check out these autolinks: ,https://example.com, and ,mailto:hello@example.com, for quick linking.",
|
|
88
|
-
"h3:HTML Passthrough",
|
|
89
|
-
"{html}",
|
|
90
|
-
"Inline HTML like highlighted text can be handled too.",
|
|
91
|
-
"h3:Reference Links",
|
|
92
|
-
"This is ,reference link one, and this is ,reference link two,.",
|
|
93
|
-
"You can also use ,implicit references, by just using the text as the reference.",
|
|
94
|
-
"h2:Implementation Notes",
|
|
95
|
-
"The transformation process involves several key steps:",
|
|
96
|
-
">#:Lexical Analysis,: Parse markdown into tokens",
|
|
97
|
-
">#:Syntax Tree Building,: Construct an AST from tokens",
|
|
98
|
-
">#:Transformation,: Map markdown AST to Portable Text structure",
|
|
99
|
-
">#:Validation,: Ensure output conforms to schema",
|
|
100
|
-
"h3:Edge Cases",
|
|
101
|
-
"h4:Empty Blocks",
|
|
102
|
-
"Sometimes you have empty paragraphs:",
|
|
103
|
-
"Or consecutive horizontal rules:",
|
|
104
|
-
"{horizontal-rule}",
|
|
105
|
-
"{horizontal-rule}",
|
|
106
|
-
"h4:Special Characters",
|
|
107
|
-
"Text with special characters like ,&,, ,<,, and ,>, should be handled correctly.",
|
|
108
|
-
"h4:URLs in Text",
|
|
109
|
-
"Raw URLs like ,https://example.com, (without angle brackets) may or may not become links depending on the parser configuration.",
|
|
110
|
-
"h2:Conclusion",
|
|
111
|
-
"This document demonstrates the ,comprehensive, support for ,markdown features, in the ,markdown-to-portable-text, converter. With proper handling of:",
|
|
112
|
-
">-:All basic formatting",
|
|
113
|
-
">-:Complex nesting",
|
|
114
|
-
">-:Various link types",
|
|
115
|
-
">-:Images (block and inline)",
|
|
116
|
-
">-:Code blocks",
|
|
117
|
-
">-:Lists of all types",
|
|
118
|
-
">-:Tables",
|
|
119
|
-
">-:And much more!",
|
|
120
|
-
"The result is ,robust, reliable, content transformation that preserves both structure and semantics.",
|
|
121
|
-
"{horizontal-rule}",
|
|
122
|
-
"Happy converting!, 🎉",
|
|
123
|
-
"Last updated: 2025"
|
|
124
|
-
]
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import type {BlockObjectDefinition} from '@portabletext/schema'
|
|
4
|
-
import {createTestKeyGenerator, getTersePt} from '@portabletext/test'
|
|
5
|
-
import {describe, expect, test} from 'vitest'
|
|
6
|
-
import {defaultSchema} from './default-schema'
|
|
7
|
-
import {portableTextToMarkdown} from './from-portable-text/portable-text-to-markdown'
|
|
8
|
-
import {
|
|
9
|
-
DefaultCodeBlockRenderer,
|
|
10
|
-
DefaultTableRenderer,
|
|
11
|
-
} from './from-portable-text/renderers/type'
|
|
12
|
-
import {markdownToPortableText} from './to-portable-text/markdown-to-portable-text'
|
|
13
|
-
import {buildObjectMatcher} from './to-portable-text/matchers'
|
|
14
|
-
|
|
15
|
-
const exampleDocumentMarkdown = fs.readFileSync(
|
|
16
|
-
path.resolve(__dirname, 'example-document.md'),
|
|
17
|
-
'utf-8',
|
|
18
|
-
)
|
|
19
|
-
const exampleDocumentMarkdownOut = fs
|
|
20
|
-
.readFileSync(path.resolve(__dirname, 'example-document.out.md'), 'utf-8')
|
|
21
|
-
// Account for hard break spaces that may be stripped by editors/tools
|
|
22
|
-
.replace('hard break\nthat continues', 'hard break \nthat continues')
|
|
23
|
-
.replace('with a break\nand more', 'with a break \nand more')
|
|
24
|
-
const exampleDocumentTersePt = JSON.parse(
|
|
25
|
-
fs.readFileSync(
|
|
26
|
-
path.resolve(__dirname, 'example-document.terse-pt.json'),
|
|
27
|
-
'utf-8',
|
|
28
|
-
),
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
const tableObjectDefinition = {
|
|
32
|
-
name: 'table',
|
|
33
|
-
fields: [
|
|
34
|
-
{name: 'headerRows', type: 'number'},
|
|
35
|
-
{name: 'rows', type: 'array'},
|
|
36
|
-
],
|
|
37
|
-
} as const satisfies BlockObjectDefinition
|
|
38
|
-
|
|
39
|
-
const tableObjectMatcher = buildObjectMatcher(tableObjectDefinition)
|
|
40
|
-
|
|
41
|
-
describe('example document', () => {
|
|
42
|
-
test('markdown to portable text', () => {
|
|
43
|
-
const keyGenerator = createTestKeyGenerator()
|
|
44
|
-
const blocks = markdownToPortableText(exampleDocumentMarkdown, {
|
|
45
|
-
keyGenerator,
|
|
46
|
-
schema: {
|
|
47
|
-
...defaultSchema,
|
|
48
|
-
blockObjects: [...defaultSchema.blockObjects, tableObjectDefinition],
|
|
49
|
-
},
|
|
50
|
-
types: {
|
|
51
|
-
table: tableObjectMatcher,
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
const tersePt = getTersePt({schema: defaultSchema, value: blocks})
|
|
55
|
-
|
|
56
|
-
expect(tersePt).toEqual(exampleDocumentTersePt)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('portable text to markdown', () => {
|
|
60
|
-
const keyGenerator = createTestKeyGenerator()
|
|
61
|
-
const blocks = markdownToPortableText(exampleDocumentMarkdown, {
|
|
62
|
-
keyGenerator,
|
|
63
|
-
schema: {
|
|
64
|
-
...defaultSchema,
|
|
65
|
-
blockObjects: [...defaultSchema.blockObjects, tableObjectDefinition],
|
|
66
|
-
},
|
|
67
|
-
types: {
|
|
68
|
-
table: tableObjectMatcher,
|
|
69
|
-
},
|
|
70
|
-
html: {
|
|
71
|
-
inline: 'text',
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
const markdown = portableTextToMarkdown(blocks, {
|
|
75
|
-
types: {
|
|
76
|
-
'horizontal-rule': () => '---',
|
|
77
|
-
'table': DefaultTableRenderer,
|
|
78
|
-
'code': DefaultCodeBlockRenderer,
|
|
79
|
-
'html': ({value}) => value.html || '',
|
|
80
|
-
'image': ({value}) =>
|
|
81
|
-
``,
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
expect(`${markdown}\n`).toBe(exampleDocumentMarkdownOut)
|
|
86
|
-
})
|
|
87
|
-
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
compileSchema,
|
|
3
|
-
defineSchema,
|
|
4
|
-
isTextBlock,
|
|
5
|
-
type PortableTextBlock,
|
|
6
|
-
} from '@portabletext/schema'
|
|
7
|
-
import type {ArbitraryTypedObject, TypedObject} from '@portabletext/types'
|
|
8
|
-
import {defaultKeyGenerator} from '../key-generator'
|
|
9
|
-
|
|
10
|
-
const schema = compileSchema(defineSchema({}))
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Builds a map of list item `_key`s to their index.
|
|
14
|
-
*
|
|
15
|
-
* Mutates the blocks in place by adding a `_key` if necessary.
|
|
16
|
-
*/
|
|
17
|
-
export function buildListIndexMap<
|
|
18
|
-
Block extends TypedObject = PortableTextBlock | ArbitraryTypedObject,
|
|
19
|
-
>(blocks: Array<Block>): Map<string, number> {
|
|
20
|
-
const levelIndexMaps = new Map<string, Map<number, number>>()
|
|
21
|
-
const listIndexMap = new Map<string, number>()
|
|
22
|
-
|
|
23
|
-
let previousListItem:
|
|
24
|
-
| {
|
|
25
|
-
listItem: string
|
|
26
|
-
level: number
|
|
27
|
-
}
|
|
28
|
-
| undefined
|
|
29
|
-
|
|
30
|
-
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
|
31
|
-
const block = blocks.at(blockIndex)
|
|
32
|
-
|
|
33
|
-
if (block === undefined) {
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!block._key) {
|
|
38
|
-
block._key = defaultKeyGenerator()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Clear the state if we encounter a non-text block
|
|
42
|
-
if (!isTextBlock({schema}, block)) {
|
|
43
|
-
levelIndexMaps.clear()
|
|
44
|
-
previousListItem = undefined
|
|
45
|
-
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Clear the state if we encounter a non-list text block
|
|
50
|
-
if (block.listItem === undefined || block.level === undefined) {
|
|
51
|
-
levelIndexMaps.clear()
|
|
52
|
-
previousListItem = undefined
|
|
53
|
-
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// If we encounter a new list item, we set the initial index to 1 for the
|
|
58
|
-
// list type on that level.
|
|
59
|
-
if (!previousListItem) {
|
|
60
|
-
const listIndex = 1
|
|
61
|
-
const levelIndexMap =
|
|
62
|
-
levelIndexMaps.get(block.listItem) ?? new Map<number, number>()
|
|
63
|
-
levelIndexMap.set(block.level, listIndex)
|
|
64
|
-
levelIndexMaps.set(block.listItem, levelIndexMap)
|
|
65
|
-
|
|
66
|
-
listIndexMap.set(block._key, listIndex)
|
|
67
|
-
|
|
68
|
-
previousListItem = {
|
|
69
|
-
listItem: block.listItem,
|
|
70
|
-
level: block.level,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
continue
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// If the previous list item is of the same type but on a lower level, we
|
|
77
|
-
// need to reset the level index map for that type.
|
|
78
|
-
if (
|
|
79
|
-
previousListItem.listItem === block.listItem &&
|
|
80
|
-
previousListItem.level < block.level
|
|
81
|
-
) {
|
|
82
|
-
const listIndex = 1
|
|
83
|
-
const levelIndexMap =
|
|
84
|
-
levelIndexMaps.get(block.listItem) ?? new Map<number, number>()
|
|
85
|
-
levelIndexMap.set(block.level, listIndex)
|
|
86
|
-
levelIndexMaps.set(block.listItem, levelIndexMap)
|
|
87
|
-
|
|
88
|
-
listIndexMap.set(block._key, listIndex)
|
|
89
|
-
|
|
90
|
-
previousListItem = {
|
|
91
|
-
listItem: block.listItem,
|
|
92
|
-
level: block.level,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Reset other list types at current level and deeper
|
|
99
|
-
levelIndexMaps.forEach((levelIndexMap, listItem) => {
|
|
100
|
-
if (listItem === block.listItem) {
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Reset all levels that are >= current level
|
|
105
|
-
const levelsToDelete: number[] = []
|
|
106
|
-
|
|
107
|
-
levelIndexMap.forEach((_, level) => {
|
|
108
|
-
if (level >= block.level!) {
|
|
109
|
-
levelsToDelete.push(level)
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
levelsToDelete.forEach((level) => {
|
|
114
|
-
levelIndexMap.delete(level)
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const levelIndexMap =
|
|
119
|
-
levelIndexMaps.get(block.listItem) ?? new Map<number, number>()
|
|
120
|
-
const levelCounter = levelIndexMap.get(block.level) ?? 0
|
|
121
|
-
levelIndexMap.set(block.level, levelCounter + 1)
|
|
122
|
-
levelIndexMaps.set(block.listItem, levelIndexMap)
|
|
123
|
-
|
|
124
|
-
listIndexMap.set(block._key, levelCounter + 1)
|
|
125
|
-
|
|
126
|
-
previousListItem = {
|
|
127
|
-
listItem: block.listItem,
|
|
128
|
-
level: block.level,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return listIndexMap
|
|
133
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ArbitraryTypedObject,
|
|
3
|
-
PortableTextBlock,
|
|
4
|
-
TypedObject,
|
|
5
|
-
} from '@portabletext/types'
|
|
6
|
-
import {buildListIndexMap} from './build-list-index-map'
|
|
7
|
-
import {createRenderNode} from './render-node'
|
|
8
|
-
import {
|
|
9
|
-
DefaultBlockSpacingRenderer,
|
|
10
|
-
type BlockSpacingRenderer,
|
|
11
|
-
} from './renderers/block-spacing'
|
|
12
|
-
import {DefaultHardBreakRenderer} from './renderers/hard-break'
|
|
13
|
-
import {
|
|
14
|
-
DefaultListItemRenderer,
|
|
15
|
-
DefaultUnknownListItemRenderer,
|
|
16
|
-
} from './renderers/list-item'
|
|
17
|
-
import {
|
|
18
|
-
DefaultCodeRenderer,
|
|
19
|
-
DefaultEmRenderer,
|
|
20
|
-
DefaultLinkRenderer,
|
|
21
|
-
DefaultStrikeThroughRenderer,
|
|
22
|
-
DefaultStrongRenderer,
|
|
23
|
-
DefaultUnderlineRenderer,
|
|
24
|
-
DefaultUnknownMarkRenderer,
|
|
25
|
-
} from './renderers/marks'
|
|
26
|
-
import {
|
|
27
|
-
DefaultBlockquoteRenderer,
|
|
28
|
-
DefaultH1Renderer,
|
|
29
|
-
DefaultH2Renderer,
|
|
30
|
-
DefaultH3Renderer,
|
|
31
|
-
DefaultH4Renderer,
|
|
32
|
-
DefaultH5Renderer,
|
|
33
|
-
DefaultH6Renderer,
|
|
34
|
-
DefaultNormalRenderer,
|
|
35
|
-
DefaultUnknownStyleRenderer,
|
|
36
|
-
} from './renderers/style'
|
|
37
|
-
import {DefaultUnknownTypeRenderer} from './renderers/type'
|
|
38
|
-
import type {PortableTextRenderers} from './types'
|
|
39
|
-
|
|
40
|
-
const defaultRenderers: PortableTextRenderers = {
|
|
41
|
-
types: {},
|
|
42
|
-
|
|
43
|
-
block: {
|
|
44
|
-
normal: DefaultNormalRenderer,
|
|
45
|
-
blockquote: DefaultBlockquoteRenderer,
|
|
46
|
-
h1: DefaultH1Renderer,
|
|
47
|
-
h2: DefaultH2Renderer,
|
|
48
|
-
h3: DefaultH3Renderer,
|
|
49
|
-
h4: DefaultH4Renderer,
|
|
50
|
-
h5: DefaultH5Renderer,
|
|
51
|
-
h6: DefaultH6Renderer,
|
|
52
|
-
},
|
|
53
|
-
marks: {
|
|
54
|
-
'em': DefaultEmRenderer,
|
|
55
|
-
'strong': DefaultStrongRenderer,
|
|
56
|
-
'code': DefaultCodeRenderer,
|
|
57
|
-
'underline': DefaultUnderlineRenderer,
|
|
58
|
-
'strike-through': DefaultStrikeThroughRenderer,
|
|
59
|
-
'link': DefaultLinkRenderer,
|
|
60
|
-
},
|
|
61
|
-
listItem: DefaultListItemRenderer,
|
|
62
|
-
hardBreak: DefaultHardBreakRenderer,
|
|
63
|
-
|
|
64
|
-
unknownType: DefaultUnknownTypeRenderer,
|
|
65
|
-
unknownMark: DefaultUnknownMarkRenderer,
|
|
66
|
-
unknownListItem: DefaultUnknownListItemRenderer,
|
|
67
|
-
unknownBlockStyle: DefaultUnknownStyleRenderer,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type Options = Partial<PortableTextRenderers> & {
|
|
71
|
-
blockSpacing?: BlockSpacingRenderer
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @public
|
|
76
|
-
*/
|
|
77
|
-
export function portableTextToMarkdown<
|
|
78
|
-
Block extends TypedObject = PortableTextBlock | ArbitraryTypedObject,
|
|
79
|
-
>(blocks: Array<Block>, options: Options = {}): string {
|
|
80
|
-
const renderers = {
|
|
81
|
-
block: {
|
|
82
|
-
...defaultRenderers.block,
|
|
83
|
-
...options.block,
|
|
84
|
-
},
|
|
85
|
-
listItem: options.listItem ?? defaultRenderers.listItem,
|
|
86
|
-
marks: {
|
|
87
|
-
...defaultRenderers.marks,
|
|
88
|
-
...options.marks,
|
|
89
|
-
},
|
|
90
|
-
types: {
|
|
91
|
-
...defaultRenderers.types,
|
|
92
|
-
...options.types,
|
|
93
|
-
},
|
|
94
|
-
hardBreak: options.hardBreak ?? defaultRenderers.hardBreak,
|
|
95
|
-
unknownType: options.unknownType ?? defaultRenderers.unknownType,
|
|
96
|
-
unknownBlockStyle:
|
|
97
|
-
options.unknownBlockStyle ?? defaultRenderers.unknownBlockStyle,
|
|
98
|
-
unknownListItem:
|
|
99
|
-
options.unknownListItem ?? defaultRenderers.unknownListItem,
|
|
100
|
-
unknownMark: options.unknownMark ?? defaultRenderers.unknownMark,
|
|
101
|
-
}
|
|
102
|
-
const renderBlockSpacing = options.blockSpacing ?? DefaultBlockSpacingRenderer
|
|
103
|
-
|
|
104
|
-
const listIndexMap = buildListIndexMap(blocks)
|
|
105
|
-
const renderNode = createRenderNode(renderers, listIndexMap)
|
|
106
|
-
|
|
107
|
-
return blocks
|
|
108
|
-
.map((node, index) => {
|
|
109
|
-
const renderedNode = renderNode({
|
|
110
|
-
node,
|
|
111
|
-
index,
|
|
112
|
-
isInline: false,
|
|
113
|
-
renderNode,
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (index === blocks.length - 1) {
|
|
117
|
-
return renderedNode
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const nextNode = blocks.at(index + 1)
|
|
121
|
-
|
|
122
|
-
if (!nextNode) {
|
|
123
|
-
return renderedNode
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const blockSpacing =
|
|
127
|
-
renderBlockSpacing({
|
|
128
|
-
current: node,
|
|
129
|
-
next: nextNode,
|
|
130
|
-
}) ?? '\n\n'
|
|
131
|
-
|
|
132
|
-
return `${renderedNode}${blockSpacing}`
|
|
133
|
-
})
|
|
134
|
-
.join('')
|
|
135
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildMarksTree,
|
|
3
|
-
isPortableTextBlock,
|
|
4
|
-
isPortableTextListItemBlock,
|
|
5
|
-
isPortableTextToolkitSpan,
|
|
6
|
-
isPortableTextToolkitTextNode,
|
|
7
|
-
spanToPlainText,
|
|
8
|
-
type ToolkitNestedPortableTextSpan,
|
|
9
|
-
type ToolkitTextNode,
|
|
10
|
-
} from '@portabletext/toolkit'
|
|
11
|
-
import type {
|
|
12
|
-
PortableTextBlock,
|
|
13
|
-
PortableTextListItemBlock,
|
|
14
|
-
PortableTextMarkDefinition,
|
|
15
|
-
PortableTextSpan,
|
|
16
|
-
TypedObject,
|
|
17
|
-
} from '@portabletext/types'
|
|
18
|
-
import {defaultKeyGenerator} from '../key-generator'
|
|
19
|
-
import type {PortableTextRenderers, RenderNode, Serializable} from './types'
|
|
20
|
-
|
|
21
|
-
interface SerializedBlock {
|
|
22
|
-
_key: string
|
|
23
|
-
children: string
|
|
24
|
-
index: number
|
|
25
|
-
isInline: boolean
|
|
26
|
-
node: PortableTextBlock | PortableTextListItemBlock
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const createRenderNode = (
|
|
30
|
-
renderers: PortableTextRenderers,
|
|
31
|
-
listIndexMap: Map<string, number>,
|
|
32
|
-
): RenderNode => {
|
|
33
|
-
function renderNode<N extends TypedObject>(options: Serializable<N>): string {
|
|
34
|
-
const {node, index, isInline} = options
|
|
35
|
-
|
|
36
|
-
if (isPortableTextListItemBlock(node)) {
|
|
37
|
-
return renderListItem(node, index)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (isPortableTextToolkitSpan(node)) {
|
|
41
|
-
return renderSpan(node)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (isPortableTextBlock(node)) {
|
|
45
|
-
return renderBlock(node, index, isInline)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (isPortableTextToolkitTextNode(node)) {
|
|
49
|
-
return renderText(node)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return renderCustomBlock(node, index, isInline)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function renderListItem(
|
|
56
|
-
node: PortableTextListItemBlock<
|
|
57
|
-
PortableTextMarkDefinition,
|
|
58
|
-
PortableTextSpan
|
|
59
|
-
>,
|
|
60
|
-
index: number,
|
|
61
|
-
): string {
|
|
62
|
-
const renderer = renderers.listItem
|
|
63
|
-
const handler =
|
|
64
|
-
typeof renderer === 'function' ? renderer : renderer[node.listItem]
|
|
65
|
-
const itemHandler = handler || renderers.unknownListItem
|
|
66
|
-
|
|
67
|
-
// Build the text content from the block
|
|
68
|
-
const tree = buildMarksTree(node)
|
|
69
|
-
const textContent = tree
|
|
70
|
-
.map((child, i) => {
|
|
71
|
-
return renderNode({node: child, isInline: true, index: i, renderNode})
|
|
72
|
-
})
|
|
73
|
-
.join('')
|
|
74
|
-
|
|
75
|
-
let children = textContent
|
|
76
|
-
|
|
77
|
-
if (node.style && node.style !== 'normal') {
|
|
78
|
-
// Wrap any other style in whatever the block component says to use
|
|
79
|
-
const {listItem: _listItem, ...blockNode} = node
|
|
80
|
-
children = renderNode({
|
|
81
|
-
node: blockNode,
|
|
82
|
-
index,
|
|
83
|
-
isInline: false,
|
|
84
|
-
renderNode,
|
|
85
|
-
})
|
|
86
|
-
// Strip trailing newlines from block styles - list item component handles spacing
|
|
87
|
-
children = children.replace(/\n+$/, '')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return itemHandler({
|
|
91
|
-
value: node,
|
|
92
|
-
index,
|
|
93
|
-
listIndex: node._key ? listIndexMap.get(node._key) : undefined,
|
|
94
|
-
isInline: false,
|
|
95
|
-
renderNode,
|
|
96
|
-
children,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderSpan(node: ToolkitNestedPortableTextSpan): string {
|
|
101
|
-
const {markDef, markType, markKey} = node
|
|
102
|
-
const span = renderers.marks[markType] || renderers.unknownMark
|
|
103
|
-
const children = node.children.map((child, childIndex) =>
|
|
104
|
-
renderNode({node: child, index: childIndex, isInline: true, renderNode}),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
return span({
|
|
108
|
-
text: spanToPlainText(node),
|
|
109
|
-
value: markDef,
|
|
110
|
-
markType,
|
|
111
|
-
markKey,
|
|
112
|
-
renderNode,
|
|
113
|
-
children: children.join(''),
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function renderBlock(
|
|
118
|
-
node: PortableTextBlock,
|
|
119
|
-
index: number,
|
|
120
|
-
isInline: boolean,
|
|
121
|
-
): string {
|
|
122
|
-
const {_key, ...props} = serializeBlock({node, index, isInline, renderNode})
|
|
123
|
-
const style = props.node.style || 'normal'
|
|
124
|
-
const handler =
|
|
125
|
-
typeof renderers.block === 'function'
|
|
126
|
-
? renderers.block
|
|
127
|
-
: renderers.block[style]
|
|
128
|
-
const block = handler || renderers.unknownBlockStyle
|
|
129
|
-
|
|
130
|
-
return block({...props, value: props.node, renderNode})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function renderText(node: ToolkitTextNode): string {
|
|
134
|
-
if (node.text === '\n') {
|
|
135
|
-
return renderers.hardBreak()
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return node.text
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function renderCustomBlock(
|
|
142
|
-
value: TypedObject,
|
|
143
|
-
index: number,
|
|
144
|
-
isInline: boolean,
|
|
145
|
-
): string {
|
|
146
|
-
const component = renderers.types[value._type] ?? renderers.unknownType
|
|
147
|
-
|
|
148
|
-
return component({
|
|
149
|
-
value,
|
|
150
|
-
isInline,
|
|
151
|
-
index,
|
|
152
|
-
renderNode,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return renderNode
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function serializeBlock(
|
|
160
|
-
options: Serializable<PortableTextBlock>,
|
|
161
|
-
): SerializedBlock {
|
|
162
|
-
const {node, index, isInline, renderNode} = options
|
|
163
|
-
const tree = buildMarksTree(node)
|
|
164
|
-
|
|
165
|
-
const renderedChildren = tree.map((child, i) =>
|
|
166
|
-
renderNode({node: child, isInline: true, index: i, renderNode}),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
_key: node._key || defaultKeyGenerator(),
|
|
171
|
-
children: renderedChildren.join(''),
|
|
172
|
-
index,
|
|
173
|
-
isInline,
|
|
174
|
-
node,
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isPortableTextBlock,
|
|
3
|
-
isPortableTextListItemBlock,
|
|
4
|
-
} from '@portabletext/toolkit'
|
|
5
|
-
import type {TypedObject} from '@portabletext/types'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @public
|
|
9
|
-
*/
|
|
10
|
-
export type BlockSpacingRenderer = (options: {
|
|
11
|
-
current: TypedObject
|
|
12
|
-
next: TypedObject
|
|
13
|
-
}) => string | undefined
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @public
|
|
17
|
-
*/
|
|
18
|
-
export const DefaultBlockSpacingRenderer: BlockSpacingRenderer = ({
|
|
19
|
-
current,
|
|
20
|
-
next,
|
|
21
|
-
}) => {
|
|
22
|
-
if (
|
|
23
|
-
isPortableTextListItemBlock(current) &&
|
|
24
|
-
isPortableTextListItemBlock(next)
|
|
25
|
-
) {
|
|
26
|
-
return '\n'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
isPortableTextBlock(current) &&
|
|
31
|
-
isPortableTextBlock(next) &&
|
|
32
|
-
current.style === 'blockquote' &&
|
|
33
|
-
next.style === 'blockquote'
|
|
34
|
-
) {
|
|
35
|
-
return '\n>\n'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return '\n\n'
|
|
39
|
-
}
|