@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
package/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# `@portabletext/markdown`
|
|
2
|
+
|
|
3
|
+
> Convert Portable Text to Markdown and back again
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @portabletext/markdown
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Supported features
|
|
12
|
+
|
|
13
|
+
| Feature | Markdown → Portable Text | Portable Text → Markdown |
|
|
14
|
+
| ---------------- | ------------------------ | ------------------------ |
|
|
15
|
+
| Headings (h1–h6) | ✅ | ✅ |
|
|
16
|
+
| Paragraphs | ✅ | ✅ |
|
|
17
|
+
| Bold | ✅ | ✅ |
|
|
18
|
+
| Italic | ✅ | ✅ |
|
|
19
|
+
| Inline code | ✅ | ✅ |
|
|
20
|
+
| Strikethrough | ✅ | ✅ |
|
|
21
|
+
| Links | ✅ | ✅ |
|
|
22
|
+
| Blockquotes | ✅ | ✅ |
|
|
23
|
+
| Ordered lists | ✅ | ✅ |
|
|
24
|
+
| Unordered lists | ✅ | ✅ |
|
|
25
|
+
| Nested lists | ✅ | ✅ |
|
|
26
|
+
| Code blocks | ✅ | ✅\* |
|
|
27
|
+
| Horizontal rules | ✅ | ✅\* |
|
|
28
|
+
| Images | ✅ | ✅\* |
|
|
29
|
+
| Tables | ✅\* | ✅\* |
|
|
30
|
+
| HTML blocks | ✅ | ✅\* |
|
|
31
|
+
|
|
32
|
+
\* Requires custom configuration (see usage below)
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### `markdownToPortableText`
|
|
37
|
+
|
|
38
|
+
Converts a Markdown string to an array of Portable Text blocks.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import {markdownToPortableText} from '@portabletext/markdown'
|
|
42
|
+
|
|
43
|
+
const markdown = `
|
|
44
|
+
# Hello World
|
|
45
|
+
|
|
46
|
+
This is a **bold** and *italic* text with a [link](https://example.com).
|
|
47
|
+
|
|
48
|
+
- First item
|
|
49
|
+
- Second item
|
|
50
|
+
|
|
51
|
+
> A blockquote
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const blocks = markdownToPortableText(markdown)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The conversion is driven by two concepts:
|
|
58
|
+
|
|
59
|
+
- **Schema**: Defines what Portable Text types are available (styles, lists, decorators, annotations, block objects). The library only outputs types that exist in the schema.
|
|
60
|
+
- **Matchers**: Control how Markdown elements map to schema types. For example, the `h1` matcher maps `# Heading` to the `'h1'` style.
|
|
61
|
+
|
|
62
|
+
Out of the box, the library includes sensible defaults for both. Customize them to match your content model.
|
|
63
|
+
|
|
64
|
+
### Schema configuration
|
|
65
|
+
|
|
66
|
+
The default schema includes the following definitions:
|
|
67
|
+
|
|
68
|
+
| Type | Values |
|
|
69
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
70
|
+
| `styles` | `'normal'`, `'h1'`, `'h2'`, `'h3'`, `'h4'`, `'h5'`, `'h6'`, `'blockquote'` |
|
|
71
|
+
| `lists` | `'number'`, `'bullet'` |
|
|
72
|
+
| `decorators` | `'strong'`, `'em'`, `'code'`, `'strike-through'` |
|
|
73
|
+
| `annotations` | `'link'` (fields: `'href'`, `'title'`) |
|
|
74
|
+
| `blockObjects` | `'code'` (fields: `'language'`, `'code'`), `'image'` (fields: `'src'`, `'alt'`, `'title'`), `'horizontal-rule'`, `'html'` (fields: `'html'`), `'table'` (fields: `'headerRows'`, `'rows'`) |
|
|
75
|
+
| `inlineObjects` | `'image'` (fields: `'src'`, `'alt'`, `'title'`) |
|
|
76
|
+
|
|
77
|
+
To use a custom Schema, import `compileSchema` and `defineSchema` from `@portabletext/schema`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import {compileSchema, defineSchema} from '@portabletext/schema'
|
|
81
|
+
|
|
82
|
+
markdownToPortableText(markdown, {
|
|
83
|
+
schema: compileSchema(
|
|
84
|
+
defineSchema({
|
|
85
|
+
styles: [{name: 'normal'}, {name: 'heading 1'}],
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To use a Sanity schema, use `@portabletext/sanity-bridge` to convert it to a Portable Text Schema first:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import {sanitySchemaToPortableTextSchema} from '@portabletext/sanity-bridge'
|
|
95
|
+
|
|
96
|
+
// Convert a Sanity block array schema to a Portable Text schema
|
|
97
|
+
const schema = sanitySchemaToPortableTextSchema(sanityBlockArraySchema)
|
|
98
|
+
|
|
99
|
+
markdownToPortableText(markdown, {schema})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Matchers
|
|
103
|
+
|
|
104
|
+
Matchers map Markdown concepts to Portable Text types defined in the Schema. Each default matcher checks if a type exists in the schema and returns the appropriate value.
|
|
105
|
+
|
|
106
|
+
| Group | Matcher | Markdown | Maps to schema type |
|
|
107
|
+
| ---------- | ---------------- | ----------------------- | ------------------- |
|
|
108
|
+
| `block` | `normal` | Paragraphs | `'normal'` |
|
|
109
|
+
| | `h1`–`h6` | `#` – `######` headings | `'h1'`–`'h6'` |
|
|
110
|
+
| | `blockquote` | `>` blockquotes | `'blockquote'` |
|
|
111
|
+
| `listItem` | `bullet` | `- ` or `* ` lists | `'bullet'` |
|
|
112
|
+
| | `number` | `1. ` ordered lists | `'number'` |
|
|
113
|
+
| `marks` | `strong` | `**bold**` | `'strong'` |
|
|
114
|
+
| | `em` | `*italic*` | `'em'` |
|
|
115
|
+
| | `code` | `` `inline code` `` | `'code'` |
|
|
116
|
+
| | `strikeThrough` | `~~strikethrough~~` | `'strike-through'` |
|
|
117
|
+
| | `link` | `[text](url "title")` | `'link'` |
|
|
118
|
+
| `types` | `code` | Fenced code blocks | `'code'` |
|
|
119
|
+
| | `horizontalRule` | `---` | `'horizontal-rule'` |
|
|
120
|
+
| | `image` | `` | `'image'` |
|
|
121
|
+
| | `html` | HTML blocks | `'html'` |
|
|
122
|
+
|
|
123
|
+
#### Configuring matchers
|
|
124
|
+
|
|
125
|
+
You can provide custom matchers to change how Markdown maps to your schema.
|
|
126
|
+
|
|
127
|
+
**Custom heading style:** If your schema uses `'heading 1'` instead of `'h1'`:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
markdownToPortableText(markdown, {
|
|
131
|
+
schema: compileSchema(
|
|
132
|
+
defineSchema({
|
|
133
|
+
// Your schema including a 'heading 1' style
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
block: {
|
|
137
|
+
h1: ({context}) => {
|
|
138
|
+
// Check if 'heading 1' exists in the schema
|
|
139
|
+
const style = context.schema.styles.find((s) => s.name === 'heading 1')
|
|
140
|
+
return style?.name
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> **Note:** Checking if the type exists in the schema isn't required, but it's good practice. Returning `undefined` gracefully skips unsupported types.
|
|
147
|
+
|
|
148
|
+
**Table matcher:** Markdown tables are parsed but there's no default matcher. Provide one if your schema includes tables:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
markdownToPortableText(markdown, {
|
|
152
|
+
types: {
|
|
153
|
+
table: ({context, value}) => {
|
|
154
|
+
const tableType = context.schema.blockObjects.find(
|
|
155
|
+
(obj) => obj.name === 'table',
|
|
156
|
+
)
|
|
157
|
+
if (!tableType) return undefined
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
_type: 'table',
|
|
161
|
+
_key: context.keyGenerator(),
|
|
162
|
+
rows: value.rows,
|
|
163
|
+
headerRows: value.headerRows,
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Matchers receive:
|
|
171
|
+
|
|
172
|
+
- `context.schema` – the compiled schema to validate against
|
|
173
|
+
- `context.keyGenerator` – function to generate unique keys
|
|
174
|
+
- `value` – the parsed Markdown data (structure depends on the matcher type)
|
|
175
|
+
- `isInline` – whether the element appears inline (for `ObjectMatcher` only)
|
|
176
|
+
|
|
177
|
+
Return `undefined` to skip the element (e.g., if the type isn't in the schema).
|
|
178
|
+
|
|
179
|
+
#### Default behavior for images and code
|
|
180
|
+
|
|
181
|
+
**Images** are handled based on context:
|
|
182
|
+
|
|
183
|
+
- Standalone images (a paragraph containing only an image) become block-level `'image'` objects
|
|
184
|
+
- Images mixed with text become inline `'image'` objects (if the schema includes `'image'` in `inlineObjects`)
|
|
185
|
+
- If neither is supported, falls back to plain text: ``
|
|
186
|
+
|
|
187
|
+
The default image matcher requires the schema type to have a `'src'` field. If your `'image'` type doesn't include this field, the matcher returns `undefined`.
|
|
188
|
+
|
|
189
|
+
**Code** is handled based on the Markdown syntax:
|
|
190
|
+
|
|
191
|
+
- Fenced code blocks (` ``` `) become `'code'` block objects with `language` and `code` fields
|
|
192
|
+
- Inline code (`` ` ``) applies the `'code'` decorator to a span
|
|
193
|
+
|
|
194
|
+
The default code block matcher requires the schema type to have a `'code'` field. If your `'code'` type doesn't include this field, the matcher returns `undefined`.
|
|
195
|
+
|
|
196
|
+
**Links** support optional titles using `[text](url "title")` syntax. The title is captured in the `'title'` field of the `'link'` annotation.
|
|
197
|
+
|
|
198
|
+
**Nested lists** are handled automatically. Each list item block includes a `level` property indicating its nesting depth (1 for top-level, 2 for nested, etc.).
|
|
199
|
+
|
|
200
|
+
**HTML blocks** (like `<div>...</div>`) become `'html'` block objects with the raw HTML in the `'html'` field. Inline HTML is controlled by the `html.inline` option.
|
|
201
|
+
|
|
202
|
+
#### Other options
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
markdownToPortableText(markdown, {
|
|
206
|
+
// Custom key generator for blocks and spans
|
|
207
|
+
keyGenerator: () => nanoid(),
|
|
208
|
+
|
|
209
|
+
// Configure how inline HTML is handled (default: 'skip')
|
|
210
|
+
html: {
|
|
211
|
+
inline: 'skip' | 'text', // 'skip' ignores inline HTML, 'text' converts it to plain text
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `portableTextToMarkdown`
|
|
217
|
+
|
|
218
|
+
Converts an array of Portable Text blocks to a Markdown string.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import {portableTextToMarkdown} from '@portabletext/markdown'
|
|
222
|
+
|
|
223
|
+
const blocks = [
|
|
224
|
+
{
|
|
225
|
+
_type: 'block',
|
|
226
|
+
_key: 'abc123',
|
|
227
|
+
style: 'h1',
|
|
228
|
+
children: [{_type: 'span', _key: 'def456', text: 'Hello World', marks: []}],
|
|
229
|
+
markDefs: [],
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
const markdown = portableTextToMarkdown(blocks)
|
|
234
|
+
// # Hello World
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The conversion is driven by **Renderers**: functions that render Portable Text elements to Markdown strings. The library includes default renderers for common types; provide your own for custom block types.
|
|
238
|
+
|
|
239
|
+
#### Default renderers
|
|
240
|
+
|
|
241
|
+
| Group | Renderer | Renders | Output |
|
|
242
|
+
| ------------------- | ---------------- | ---------------------------- | --------------------- |
|
|
243
|
+
| `block` | `normal` | Paragraphs | `{children}` |
|
|
244
|
+
| | `h1`–`h6` | Headings | `# `–`###### ` |
|
|
245
|
+
| | `blockquote` | Blockquotes | `> {children}` |
|
|
246
|
+
| `marks` | `strong` | Bold text | `**{children}**` |
|
|
247
|
+
| | `em` | Italic text | `_{children}_` |
|
|
248
|
+
| | `code` | Inline code | `` `{children}` `` |
|
|
249
|
+
| | `underline` | Underlined text | `<u>{children}</u>` |
|
|
250
|
+
| | `strike-through` | Strikethrough | `~~{children}~~` |
|
|
251
|
+
| | `link` | Links | `[{children}](url)` |
|
|
252
|
+
| `listItem` | | List items (bullet & number) | `- ` or `1. ` |
|
|
253
|
+
| `hardBreak` | | Line breaks within blocks | ` \n` (two spaces) |
|
|
254
|
+
| `blockSpacing` | | Spacing between blocks | `\n\n`, `\n`, `\n>\n` |
|
|
255
|
+
| `unknownType` | | Unknown block types | JSON code block |
|
|
256
|
+
| `unknownBlockStyle` | | Unknown block styles | `{children}` |
|
|
257
|
+
| `unknownListItem` | | Unknown list item types | `- {children}` |
|
|
258
|
+
| `unknownMark` | | Unknown marks | `{children}` |
|
|
259
|
+
|
|
260
|
+
Unknown types render as JSON code blocks by default; unknown styles, list items, and marks pass through their children.
|
|
261
|
+
|
|
262
|
+
> **Note:** The `underline` renderer is included for Portable Text that uses it, but there's no standard Markdown syntax for underline, so it renders as HTML.
|
|
263
|
+
|
|
264
|
+
#### Configuring renderers
|
|
265
|
+
|
|
266
|
+
Provide custom renderers to control how Portable Text renders to Markdown.
|
|
267
|
+
|
|
268
|
+
**Custom type renderers:** Render custom block types (objects in the blocks array):
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
portableTextToMarkdown(blocks, {
|
|
272
|
+
types: {
|
|
273
|
+
callout: ({value}) => `> **${value.title}**\n> ${value.text}`,
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Custom block styles:** Override how block styles render:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
portableTextToMarkdown(blocks, {
|
|
282
|
+
block: {
|
|
283
|
+
// Use ATX-style heading with closing hashes
|
|
284
|
+
h1: ({children}) => `# ${children} #`,
|
|
285
|
+
// Use HTML for blockquotes
|
|
286
|
+
blockquote: ({children}) => `<blockquote>${children}</blockquote>`,
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Built-in type renderers:** The library exports default renderers for common block types:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import {
|
|
295
|
+
DefaultCodeBlockRenderer,
|
|
296
|
+
DefaultHorizontalRuleRenderer,
|
|
297
|
+
DefaultHtmlRenderer,
|
|
298
|
+
DefaultImageRenderer,
|
|
299
|
+
DefaultTableRenderer,
|
|
300
|
+
portableTextToMarkdown,
|
|
301
|
+
} from '@portabletext/markdown'
|
|
302
|
+
|
|
303
|
+
portableTextToMarkdown(blocks, {
|
|
304
|
+
types: {
|
|
305
|
+
'code': DefaultCodeBlockRenderer,
|
|
306
|
+
'horizontal-rule': DefaultHorizontalRuleRenderer,
|
|
307
|
+
'html': DefaultHtmlRenderer,
|
|
308
|
+
'image': DefaultImageRenderer,
|
|
309
|
+
'table': DefaultTableRenderer,
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
| Renderer | Expected value | Output |
|
|
315
|
+
| ------------------------------- | --------------------------------------------- | ---------------------- |
|
|
316
|
+
| `DefaultCodeBlockRenderer` | `{code: string, language?: string}` | ` ```lang\ncode\n``` ` |
|
|
317
|
+
| `DefaultHorizontalRuleRenderer` | (no fields required) | `---` |
|
|
318
|
+
| `DefaultHtmlRenderer` | `{html: string}` | Raw HTML |
|
|
319
|
+
| `DefaultImageRenderer` | `{src: string, alt?: string, title?: string}` | `` |
|
|
320
|
+
| `DefaultTableRenderer` | `{rows: [...], headerRows?: number}` | Markdown table |
|
|
321
|
+
|
|
322
|
+
**Other exports:** The library also exports mark renderers, block style renderers, and TypeScript types:
|
|
323
|
+
|
|
324
|
+
- `DefaultStrongRenderer`, `DefaultEmRenderer`, `DefaultCodeRenderer`, `DefaultUnderlineRenderer`, `DefaultStrikeThroughRenderer`, `DefaultLinkRenderer`
|
|
325
|
+
- `DefaultNormalRenderer`, `DefaultBlockquoteRenderer`, `DefaultH1Renderer`–`DefaultH6Renderer`
|
|
326
|
+
- `DefaultListItemRenderer`, `DefaultHardBreakRenderer`, `DefaultBlockSpacingRenderer`
|
|
327
|
+
- `BlockSpacingRenderer`, `PortableTextRenderers`, `PortableTextMarkRenderer`, etc.
|
|
328
|
+
|
|
329
|
+
#### What renderers receive
|
|
330
|
+
|
|
331
|
+
**Block renderers** (`block.*`):
|
|
332
|
+
|
|
333
|
+
- `value` – the block object
|
|
334
|
+
- `children` – rendered content of the block
|
|
335
|
+
- `index` – position in the blocks array
|
|
336
|
+
|
|
337
|
+
**Mark renderers** (`marks.*`):
|
|
338
|
+
|
|
339
|
+
- `value` – the mark definition (for annotations like links)
|
|
340
|
+
- `children` – the rendered marked content
|
|
341
|
+
- `text` – the raw text content (without nested mark rendering)
|
|
342
|
+
- `markType` – the mark type name
|
|
343
|
+
- `markKey` – the mark's key (for annotations)
|
|
344
|
+
|
|
345
|
+
**Type renderers** (`types.*`):
|
|
346
|
+
|
|
347
|
+
- `value` – the typed object
|
|
348
|
+
- `index` – position in the blocks array
|
|
349
|
+
- `isInline` – whether it appears inline or as a block
|
|
350
|
+
|
|
351
|
+
Use `isInline` to handle block vs inline objects differently:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
portableTextToMarkdown(blocks, {
|
|
355
|
+
types: {
|
|
356
|
+
image: ({value, isInline}) => {
|
|
357
|
+
if (isInline) {
|
|
358
|
+
// Skip inline images entirely by returning empty string
|
|
359
|
+
return ''
|
|
360
|
+
}
|
|
361
|
+
// Render block images as full Markdown
|
|
362
|
+
return ``
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Return an empty string to skip rendering an element entirely.
|
|
369
|
+
|
|
370
|
+
**List item renderer** (`listItem`):
|
|
371
|
+
|
|
372
|
+
- `value` – the list item block
|
|
373
|
+
- `children` – rendered content
|
|
374
|
+
- `listIndex` – position in the list (for numbered lists)
|
|
375
|
+
|
|
376
|
+
#### Handling unknown types
|
|
377
|
+
|
|
378
|
+
The library provides fallback renderers for unknown content:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
portableTextToMarkdown(blocks, {
|
|
382
|
+
// Called for block types not in `types`
|
|
383
|
+
unknownType: ({value}) => `<!-- Unknown type: ${value._type} -->`,
|
|
384
|
+
|
|
385
|
+
// Called for block styles not in `block`
|
|
386
|
+
unknownBlockStyle: ({value, children}) => children ?? '',
|
|
387
|
+
|
|
388
|
+
// Called for list item types not in `listItem`
|
|
389
|
+
unknownListItem: ({children}) => `- ${children}`,
|
|
390
|
+
|
|
391
|
+
// Called for marks not in `marks`
|
|
392
|
+
unknownMark: ({children}) => children,
|
|
393
|
+
})
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
By default, unknown types render as JSON code blocks, and unknown marks/styles pass through their children unchanged.
|
|
397
|
+
|
|
398
|
+
You can also customize hard break rendering:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
portableTextToMarkdown(blocks, {
|
|
402
|
+
// Render as HTML break instead of Markdown hard break
|
|
403
|
+
hardBreak: () => '<br />\n',
|
|
404
|
+
|
|
405
|
+
// Or render as plain newline (no trailing spaces)
|
|
406
|
+
hardBreak: () => '\n',
|
|
407
|
+
})
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Block spacing
|
|
411
|
+
|
|
412
|
+
By default, blocks are separated by double newlines (`\n\n`), with special handling for list items (single newline) and consecutive blockquotes. Customize with `blockSpacing`:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
portableTextToMarkdown(blocks, {
|
|
416
|
+
blockSpacing: ({current, next}) => {
|
|
417
|
+
// Double newline between list items instead of single
|
|
418
|
+
if (current.listItem && next.listItem) {
|
|
419
|
+
return '\n\n'
|
|
420
|
+
}
|
|
421
|
+
// Return undefined to use default spacing
|
|
422
|
+
return undefined
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## License
|
|
428
|
+
|
|
429
|
+
MIT © [Sanity.io](https://www.sanity.io/)
|