@portabletext/markdown 1.4.7 → 2.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 CHANGED
@@ -70,19 +70,44 @@ const markdown = portableTextToMarkdown([
70
70
  | Blockquotes | ✅ | ✅ |
71
71
  | Ordered lists | ✅ | ✅ |
72
72
  | Unordered lists | ✅ | ✅ |
73
- | Task lists | ✅\* | ✅\* |
73
+ | Task lists | | |
74
74
  | Nested lists | ✅ | ✅ |
75
- | Code blocks | ✅ | ✅\* |
76
- | Horizontal rules | ✅ | ✅\* |
77
- | Images | ✅ | ✅\* |
78
- | Tables | ✅\* | ✅\* |
79
- | HTML blocks | ✅ | ✅\* |
80
- | Callouts | ✅\* | ✅\* |
75
+ | Code blocks | ✅ | |
76
+ | Horizontal rules | ✅ | |
77
+ | Images | ✅ | |
78
+ | Tables | | |
79
+ | HTML blocks | ✅ | |
80
+ | Callouts | | |
81
81
 
82
- \* Requires custom configuration (see usage below)
82
+ ## Round-trip behavior
83
+
84
+ 1. Translation preserves semantics, not source spelling. The first MD→PT→MD pass normalizes Markdown to one canonical spelling: autolinks and reference links become inline links, indented code becomes fenced code, and emphasis, headings, lists, and tables each get one canonical form.
85
+
86
+ ```
87
+ <https://portabletext.org> -> [https://portabletext.org](https://portabletext.org)
88
+ [ref link][id] -> [ref link](https://example.com "title")
89
+ ```
90
+
91
+ 2. The normalized Markdown is a fixpoint for the constructs in the [Supported features](#supported-features) table above: parsing it and serializing again reproduces it byte-for-byte, pinned by a full-document round-trip test. This doesn't yet extend to plain text that happens to contain literal Markdown punctuation: serialization doesn't escape it, so a second parse reads it back as markup instead of literal text.
92
+
93
+ ```
94
+ \*bar\* -> *bar* (serialized unescaped; a second parse reads this as emphasis)
95
+ ```
96
+
97
+ 3. MD→PT survival is schema-driven. Constructs whose type the schema doesn't declare degrade predictably: they keep their content and drop the structure that named them. Marks drop formatting but keep the text (`**bar**` with no `strong` decorator in the schema becomes a plain span reading `bar`); tables flatten their cell content into top-level blocks; images fall back to their Markdown source as plain text; task-list checkboxes strip to plain list items.
98
+
99
+ 4. PT structures with no Markdown form degrade predictably on PT→MD. GFM tables have one header row, so header rows beyond the first flatten into the body. Deep or level-skipping lists collapse to relative nesting. A list's first item renders at the top level whatever its `level`, and each deeper jump between items indents one step, however many levels it skips. Multi-block table cells join their blocks with spaces. Unknown object types render as a fenced JSON block; unknown marks pass their text through unformatted.
100
+
101
+ 5. Identity does not round-trip. Keys are regenerated on every parse, and adjacent spans with identical marks merge into one.
83
102
 
84
103
  ## Usage
85
104
 
105
+ <!-- The schema table, matcher table, supported-features table, and
106
+ round-trip section have condensed twins on the docs site
107
+ (apps/docs/src/content/docs/conversion/markdown-to-portable-text.mdx).
108
+ Keep them in sync: a claim corrected in one place is stale in the
109
+ other. -->
110
+
86
111
  ### `markdownToPortableText`
87
112
 
88
113
  ```ts
@@ -162,14 +187,14 @@ Out of the box, the library includes sensible defaults for both. Customize them
162
187
 
163
188
  The default schema includes the following definitions:
164
189
 
165
- | Type | Values |
166
- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
167
- | `styles` | `'normal'`, `'h1'`, `'h2'`, `'h3'`, `'h4'`, `'h5'`, `'h6'`, `'blockquote'` |
168
- | `lists` | `'number'`, `'bullet'` |
169
- | `decorators` | `'strong'`, `'em'`, `'code'`, `'strike-through'` |
170
- | `annotations` | `'link'` (fields: `'href'`, `'title'`) |
171
- | `blockObjects` | `'code'` (fields: `'language'`, `'code'`), `'image'` (fields: `'src'`, `'alt'`, `'title'`), `'horizontal-rule'`, `'html'` (fields: `'html'`), `'table'` (fields: `'headerRows'`, `'rows'`), `'callout'` (fields: `'tone'`, `'content'`) |
172
- | `inlineObjects` | `'image'` (fields: `'src'`, `'alt'`, `'title'`) |
190
+ | Type | Values |
191
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
192
+ | `styles` | `'normal'`, `'h1'`, `'h2'`, `'h3'`, `'h4'`, `'h5'`, `'h6'`, `'blockquote'` |
193
+ | `lists` | `'number'`, `'bullet'`, `'task'` |
194
+ | `decorators` | `'strong'`, `'em'`, `'code'`, `'strike-through'` |
195
+ | `annotations` | `'link'` (fields: `'href'`, `'title'`) |
196
+ | `blockObjects` | `'code'` (fields: `'language'`, `'code'`), `'image'` (fields: `'src'`, `'alt'`, `'title'`), `'horizontal-rule'`, `'html'` (fields: `'html'`), `'table'` (the canonical nested shape, see [Default behavior](#default-behavior)), `'callout'` (fields: `'tone'`, `'content'`) |
197
+ | `inlineObjects` | `'image'` (fields: `'src'`, `'alt'`, `'title'`) |
173
198
 
174
199
  To use a custom Schema, import `compileSchema` and `defineSchema` from `@portabletext/schema`:
175
200
 
@@ -218,8 +243,38 @@ Matchers map Markdown concepts to Portable Text types defined in the Schema. Eac
218
243
  | | `image` | `![alt](src)` | `'image'` |
219
244
  | | `html` | HTML blocks | `'html'` |
220
245
  | | `callout` | `> [!NOTE]`, etc. | `'callout'` |
221
- | | `blockquote` | `>` blockquotes | `'blockquote'` |
222
- | | `list` | `- ` or `1. ` lists | `'list'` |
246
+ | | `table` | GFM pipe tables | `'table'` |
247
+ | | `blockquote`\* | `>` blockquotes | `'blockquote'` |
248
+ | | `list`\* | `- ` or `1. ` lists | `'list'` |
249
+
250
+ \* Opt-in, not registered by default: `blockquote` and `list` map onto structural container shapes, and the parser only produces those shapes when you register the matcher (see [Configuring matchers](#configuring-matchers)). Without them, blockquotes and lists parse to flat text blocks, which is the standard Portable Text shape for both, not a degraded fallback.
251
+
252
+ #### Default behavior
253
+
254
+ **Tables** (GFM pipe tables) convert by default, in the canonical shape `@portabletext/plugin-table` expects: a `table` block object (`headerRows`, `rows`), each row a `row` object (`cells`), each cell a `cell` object (`value`, an array of Portable Text blocks; a cell holding a single image becomes a standalone block-level `image` object instead of a text block wrapping it). `alignment` is a `@portabletext/markdown` extension field; `@portabletext/plugin-table` ignores it. This needs no configuration when the schema declares a `table` block object with a `rows` field (see `blockObjects` above). A schema whose `table` doesn't declare `rows`, or that doesn't declare `table` at all, produces no table object at all: the table's cell content flattens into top-level blocks, in reading order, and the table structure is discarded.
255
+
256
+ **Images** are handled based on context:
257
+
258
+ - Standalone images (a paragraph containing only an image) become block-level `'image'` objects
259
+ - Images mixed with text become inline `'image'` objects (if the schema includes `'image'` in `inlineObjects`)
260
+ - If neither is supported, falls back to plain text: `![alt](src)`
261
+
262
+ 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`.
263
+
264
+ **Code** is handled based on the Markdown syntax:
265
+
266
+ - Fenced code blocks (` ``` `) become `'code'` block objects with `language` and `code` fields
267
+ - Inline code (`` ` ``) applies the `'code'` decorator to a span
268
+
269
+ 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`.
270
+
271
+ **Links** support optional titles using `[text](url "title")` syntax. The title is captured in the `'title'` field of the `'link'` annotation.
272
+
273
+ **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.).
274
+
275
+ **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.
276
+
277
+ **Callouts** use the `> [!TYPE]` syntax (GFM alerts) where `TYPE` is one of `NOTE`, `TIP`, `WARNING`, `CAUTION`, or `IMPORTANT`. They become `'callout'` block objects with a `'tone'` field (the lowercased type name) and a `'content'` field (an array of Portable Text blocks). When the schema doesn't include a `'callout'` block object, the content falls back to blockquote-styled blocks.
223
278
 
224
279
  #### Configuring matchers
225
280
 
@@ -246,7 +301,7 @@ markdownToPortableText(markdown, {
246
301
 
247
302
  > **Note:** Checking if the type exists in the schema isn't required, but it's good practice. Returning `undefined` gracefully skips unsupported types.
248
303
 
249
- **Table matcher:** Markdown tables are parsed but there's no default matcher. Provide one if your schema includes tables:
304
+ **Table matcher:** GFM pipe tables convert by default (see [Default behavior](#default-behavior)). Provide your own matcher to map onto a differently-shaped `table` type:
250
305
 
251
306
  ```ts
252
307
  markdownToPortableText(markdown, {
@@ -288,7 +343,7 @@ The matcher receives `value.items` already assembled. Each item's `content` arra
288
343
 
289
344
  Without `types.list`, the existing flat-block path runs unchanged.
290
345
 
291
- **GFM task lists** (`- [ ]` / `- [x]`): Task lists are recognized when the schema declares a `task` list item. Without a `task` definition, the checkbox markers are stripped from the text and the items render as their surrounding list type (bullet or number). With a `task` definition, items carrying a checkbox become text blocks with `listItem: 'task'` and a `checked: boolean` field; items without a checkbox keep their surrounding list type.
346
+ **GFM task lists** (`- [ ]` / `- [x]`): Task lists are recognized when the schema declares a `task` list item; the default schema does, so task lists parse zero-config. Without a `task` definition (a custom schema that omits it), the checkbox markers are stripped from the text and the items render as their surrounding list type (bullet or number). With a `task` definition, items carrying a checkbox become text blocks with `listItem: 'task'` and a `checked: boolean` field; items without a checkbox keep their surrounding list type.
292
347
 
293
348
  ```ts
294
349
  markdownToPortableText('- [x] done\n- [ ] todo', {
@@ -348,31 +403,6 @@ Matchers receive:
348
403
 
349
404
  Return `undefined` to skip the element (e.g., if the type isn't in the schema).
350
405
 
351
- #### Default behavior for images and code
352
-
353
- **Images** are handled based on context:
354
-
355
- - Standalone images (a paragraph containing only an image) become block-level `'image'` objects
356
- - Images mixed with text become inline `'image'` objects (if the schema includes `'image'` in `inlineObjects`)
357
- - If neither is supported, falls back to plain text: `![alt](src)`
358
-
359
- 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`.
360
-
361
- **Code** is handled based on the Markdown syntax:
362
-
363
- - Fenced code blocks (` ``` `) become `'code'` block objects with `language` and `code` fields
364
- - Inline code (`` ` ``) applies the `'code'` decorator to a span
365
-
366
- 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`.
367
-
368
- **Links** support optional titles using `[text](url "title")` syntax. The title is captured in the `'title'` field of the `'link'` annotation.
369
-
370
- **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.).
371
-
372
- **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.
373
-
374
- **Callouts** use the `> [!TYPE]` syntax (GFM alerts) where `TYPE` is one of `NOTE`, `TIP`, `WARNING`, `CAUTION`, or `IMPORTANT`. They become `'callout'` block objects with a `'tone'` field (the lowercased type name) and a `'content'` field (an array of Portable Text blocks). When the schema doesn't include a `'callout'` block object, the content falls back to blockquote-styled blocks.
375
-
376
406
  #### Other options
377
407
 
378
408
  ```ts
@@ -449,34 +479,42 @@ The conversion is driven by **Renderers**: functions that render Portable Text e
449
479
 
450
480
  #### Default renderers
451
481
 
452
- | Group | Renderer | Renders | Output |
453
- | ------------------- | ---------------- | --------------------------------- | ------------------------ |
454
- | `block` | `normal` | Paragraphs | `{children}` |
455
- | | `h1`–`h6` | Headings | `# `–`###### ` |
456
- | | `blockquote` | Blockquotes | `> {children}` |
457
- | `marks` | `strong` | Bold text | `**{children}**` |
458
- | | `em` | Italic text | `_{children}_` |
459
- | | `code` | Inline code | `` `{children}` `` |
460
- | | `underline` | Underlined text | `<u>{children}</u>` |
461
- | | `strike-through` | Strikethrough | `~~{children}~~` |
462
- | | `link` | Links | `[{children}](url)` |
463
- | `listItem` | | List items (bullet, number, task) | `- `, `1. `, or `- [x] ` |
464
- | `hardBreak` | | Line breaks within blocks | ` \n` (two spaces) |
465
- | `blockSpacing` | | Spacing between blocks | `\n\n`, `\n`, `\n>\n` |
466
- | `unknownType` | | Unknown block types | JSON code block |
467
- | `unknownBlockStyle` | | Unknown block styles | `{children}` |
468
- | `unknownListItem` | | Unknown list item types | `- {children}` |
469
- | `unknownMark` | | Unknown marks | `{children}` |
482
+ | Group | Renderer | Renders | Output |
483
+ | ------------------- | ----------------- | --------------------------------- | ------------------------ |
484
+ | `types` | `callout` | `callout` block objects | `> [!TYPE]\n> content` |
485
+ | | `code` | `code` block objects | Fenced code block |
486
+ | | `horizontal-rule` | `horizontal-rule` block objects | `---` |
487
+ | | `html` | `html` block objects | Raw HTML |
488
+ | | `image` | `image` block/inline objects | `![alt](src "title")` |
489
+ | | `table` | `table` block objects | Markdown table |
490
+ | `block` | `normal` | Paragraphs | `{children}` |
491
+ | | `h1`–`h6` | Headings | `# `–`###### ` |
492
+ | | `blockquote` | Blockquotes | `> {children}` |
493
+ | `marks` | `strong` | Bold text | `**{children}**` |
494
+ | | `em` | Italic text | `_{children}_` |
495
+ | | `code` | Inline code | `` `{children}` `` |
496
+ | | `underline` | Underlined text | `<u>{children}</u>` |
497
+ | | `strike-through` | Strikethrough | `~~{children}~~` |
498
+ | | `link` | Links | `[{children}](url)` |
499
+ | `listItem` | | List items (bullet, number, task) | `- `, `1. `, or `- [x] ` |
500
+ | `hardBreak` | | Line breaks within blocks | ` \n` (two spaces) |
501
+ | `blockSpacing` | | Spacing between blocks | `\n\n`, `\n`, `\n>\n` |
502
+ | `unknownType` | | Unknown block types | JSON code block |
503
+ | `unknownBlockStyle` | | Unknown block styles | `{children}` |
504
+ | `unknownListItem` | | Unknown list item types | `- {children}` |
505
+ | `unknownMark` | | Unknown marks | `{children}` |
470
506
 
471
507
  Unknown types render as JSON code blocks by default; unknown styles, list items, and marks pass through their children.
472
508
 
509
+ The default type renderers are collision-safe: because the serializer dispatches on the `_type` name alone, `code`, `html`, `image`, `callout`, and `table` fall back to the `unknownType` renderer (a JSON code block) when a value doesn't match the shape their renderer expects (say, your own differently-shaped `code` type); `horizontal-rule` has no shape to check and always renders `---`. Register your own `types.<name>` renderer to override how any of them serialize, or to handle a same-named type of a different shape.
510
+
473
511
  > **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.
474
512
 
475
513
  #### Configuring renderers
476
514
 
477
515
  Provide custom renderers to control how Portable Text renders to Markdown.
478
516
 
479
- **Custom type renderers:** Render custom block types (objects in the blocks array):
517
+ **Custom type renderers:** Render custom block types (objects in the blocks array). A custom renderer under a default's name (see [Default renderers](#default-renderers)) replaces that default:
480
518
 
481
519
  ```ts
482
520
  portableTextToMarkdown(blocks, {
@@ -500,31 +538,19 @@ portableTextToMarkdown(blocks, {
500
538
  })
501
539
  ```
502
540
 
503
- **Built-in type renderers:** The library exports default renderers for common block types:
541
+ **Built-in type renderers:** the library exports every built-in type renderer, in case you want to compose one into a different renderer or reuse it under a different type name. Two of them are exported but not registered by default: `DefaultBlockquoteObjectRenderer` and `DefaultListRenderer` render the structural container shapes (`types.blockquote`/`types.list` on the parser side) and stay opt-in, since the parser only produces those shapes when a matcher is registered for them:
504
542
 
505
543
  ```ts
506
544
  import {
507
545
  DefaultBlockquoteObjectRenderer,
508
- DefaultCalloutRenderer,
509
- DefaultCodeBlockRenderer,
510
- DefaultHorizontalRuleRenderer,
511
- DefaultHtmlRenderer,
512
- DefaultImageRenderer,
513
546
  DefaultListRenderer,
514
- DefaultTableRenderer,
515
547
  portableTextToMarkdown,
516
548
  } from '@portabletext/markdown'
517
549
 
518
550
  portableTextToMarkdown(blocks, {
519
551
  types: {
520
- 'blockquote': DefaultBlockquoteObjectRenderer,
521
- 'callout': DefaultCalloutRenderer,
522
- 'code': DefaultCodeBlockRenderer,
523
- 'horizontal-rule': DefaultHorizontalRuleRenderer,
524
- 'html': DefaultHtmlRenderer,
525
- 'image': DefaultImageRenderer,
526
- 'list': DefaultListRenderer,
527
- 'table': DefaultTableRenderer,
552
+ blockquote: DefaultBlockquoteObjectRenderer,
553
+ list: DefaultListRenderer,
528
554
  },
529
555
  })
530
556
  ```
package/dist/index.d.ts CHANGED
@@ -454,13 +454,13 @@ declare const DefaultImageRenderer: PortableTextTypeRenderer<{
454
454
  /**
455
455
  * Renders a Portable Text table block-object back to Markdown.
456
456
  *
457
- * The PT `headerRows` field decides the header. `headerRows === 0` is an
458
- * explicit headerless table: GFM has no headerless form, so an empty header
459
- * row is emitted and every row goes in the body (that empty header reads back
460
- * as `headerRows: 0` via `markdownToPortableText`). Any other value
461
- * (`undefined` or `>= 1`) promotes `rows[0]` to the header. GFM allows exactly
462
- * one header row, so header rows beyond the first flatten into the body,
463
- * lossy, but the extra rows stay on the Portable Text side.
457
+ * The PT `headerRows` field decides the header. Missing `headerRows` and
458
+ * `headerRows === 0` both render headerless: GFM has no headerless form, so
459
+ * an empty header row is emitted and every row goes in the body (that empty
460
+ * header reads back as `headerRows: 0` via `markdownToPortableText`).
461
+ * `headerRows >= 1` promotes `rows[0]` to the header. GFM allows exactly one
462
+ * header row, so header rows beyond the first flatten into the body, lossy,
463
+ * but the extra rows stay on the Portable Text side.
464
464
  *
465
465
  * Asymmetric tables (rows of varying cell counts) are widened to match
466
466
  * the row with the most cells. Narrower rows are padded with empty cells
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../node_modules/.pnpm/@portabletext+types@4.0.2/node_modules/@portabletext/types/dist/index.d.ts","../src/from-portable-text/renderers/block-spacing.ts","../src/from-portable-text/types.ts","../src/from-portable-text/portable-text-to-markdown.ts","../src/from-portable-text/renderers/hard-break.ts","../src/from-portable-text/renderers/list-item.ts","../src/from-portable-text/renderers/style.ts","../src/from-portable-text/renderers/marks.ts","../src/from-portable-text/renderers/type.ts","../src/to-portable-text/matchers.ts","../src/to-portable-text/markdown-to-portable-text.ts"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../node_modules/.pnpm/@portabletext+types@4.0.2/node_modules/@portabletext/types/dist/index.d.ts","../src/from-portable-text/renderers/block-spacing.ts","../src/from-portable-text/types.ts","../src/from-portable-text/portable-text-to-markdown.ts","../src/from-portable-text/renderers/hard-break.ts","../src/from-portable-text/renderers/list-item.ts","../src/from-portable-text/renderers/style.ts","../src/from-portable-text/renderers/marks.ts","../src/from-portable-text/renderers/type.ts","../src/to-portable-text/matchers.ts","../src/to-portable-text/markdown-to-portable-text.ts"],"x_google_ignoreList":[0],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { compileSchema, defineSchema, isSpan, isTextBlock } from "@portabletext/schema";
1
+ import { compileSchema, defineSchema, isSpan, isTextBlock, isTypedObject } from "@portabletext/schema";
2
2
  import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
3
3
  import { alert } from "@mdit/plugin-alert";
4
4
  import markdownit from "markdown-it";
@@ -255,11 +255,75 @@ function uriLooksSafe(uri) {
255
255
  /**
256
256
  * @public
257
257
  */
258
- const DefaultUnknownMarkRenderer = ({ children }) => children, DefaultNormalRenderer = ({ children }) => !children || children.trim() === "" ? "" : children, DefaultBlockquoteRenderer = ({ children }) => children ? children.split("\n").map((line) => `> ${line}`).join("\n") : ">", DefaultH1Renderer = ({ children }) => `# ${children}`, DefaultH2Renderer = ({ children }) => `## ${children}`, DefaultH3Renderer = ({ children }) => `### ${children}`, DefaultH4Renderer = ({ children }) => `#### ${children}`, DefaultH5Renderer = ({ children }) => `##### ${children}`, DefaultH6Renderer = ({ children }) => `###### ${children}`, DefaultUnknownStyleRenderer = ({ children }) => children ?? "", DefaultCodeBlockRenderer = ({ value }) => `\`\`\`${value.language ?? ""}\n${value.code}\n\`\`\``, DefaultHorizontalRuleRenderer = () => "---", DefaultHtmlRenderer = ({ value }) => value.html, DefaultImageRenderer = ({ value }) => {
259
- let alt = escapeImageAndLinkText(value.alt ?? ""), title = value.title ? ` "${escapeImageAndLinkTitle(value.title)}"` : "";
260
- return `![${alt}](${value.src}${title})`;
261
- }, DefaultTableRenderer = ({ value, renderNode }) => {
262
- let rows = value.rows, alignment = value.alignment, headerRow = rows.at(0);
258
+ const DefaultUnknownMarkRenderer = ({ children }) => children, DefaultNormalRenderer = ({ children }) => !children || children.trim() === "" ? "" : children, DefaultBlockquoteRenderer = ({ children }) => children ? children.split("\n").map((line) => `> ${line}`).join("\n") : ">", DefaultH1Renderer = ({ children }) => `# ${children}`, DefaultH2Renderer = ({ children }) => `## ${children}`, DefaultH3Renderer = ({ children }) => `### ${children}`, DefaultH4Renderer = ({ children }) => `#### ${children}`, DefaultH5Renderer = ({ children }) => `##### ${children}`, DefaultH6Renderer = ({ children }) => `###### ${children}`, DefaultUnknownStyleRenderer = ({ children }) => children ?? "", DefaultCodeBlockRenderer = (options) => isCodeShaped(options.value) ? `\`\`\`${normalizeLanguage(options.value.language)}\n${options.value.code}\n\`\`\`` : DefaultUnknownTypeRenderer(options);
259
+ function isCodeShaped(value) {
260
+ return typeof value?.code == "string";
261
+ }
262
+ /**
263
+ * A fence info string is everything after the opening fence on the same
264
+ * line, so a real `language` can never contain a newline, and the parser
265
+ * only ever produces a string. Junk in this optional field should not send
266
+ * an otherwise valid code block to the fenced-JSON path, so it is treated
267
+ * as absent instead of guarded.
268
+ */
269
+ function normalizeLanguage(language) {
270
+ return typeof language != "string" || language.includes("\n") ? "" : language;
271
+ }
272
+ /**
273
+ * @public
274
+ */
275
+ const DefaultHorizontalRuleRenderer = () => "---", DefaultHtmlRenderer = (options) => isHtmlShaped(options.value) ? options.value.html : DefaultUnknownTypeRenderer(options);
276
+ function isHtmlShaped(value) {
277
+ return typeof value?.html == "string";
278
+ }
279
+ /**
280
+ * @public
281
+ */
282
+ const DefaultImageRenderer = (options) => {
283
+ if (!isImageShaped(options.value)) return DefaultUnknownTypeRenderer(options);
284
+ let alt = escapeImageAndLinkText(options.value.alt ?? ""), title = options.value.title ? ` "${escapeImageAndLinkTitle(options.value.title)}"` : "";
285
+ return `![${alt}](${options.value.src}${title})`;
286
+ };
287
+ function isImageShaped(value) {
288
+ let image = value;
289
+ return typeof image?.src == "string" && (image.alt == null || typeof image.alt == "string") && (image.title == null || typeof image.title == "string");
290
+ }
291
+ /**
292
+ * A table is table-shaped when everything the renderer dereferences is
293
+ * there: `rows` an array of typed objects with a `cells` array, every cell
294
+ * a typed object whose `value` array holds typed objects (`renderNode`'s
295
+ * input contract). The predicate narrows to exactly what `renderTable`
296
+ * consumes, so the renderer needs no casts. A malformed `table` value
297
+ * (e.g. a consumer's differently-shaped `table` type) falls back to the
298
+ * fenced-JSON path instead of throwing.
299
+ */
300
+ function isTableShaped(value) {
301
+ let rows = value?.rows;
302
+ return Array.isArray(rows) && rows.every((row) => isTypedObject(row) && Array.isArray(row.cells) && row.cells.every((cell) => isTypedObject(cell) && Array.isArray(cell.value) && cell.value.every(isTypedObject)));
303
+ }
304
+ /**
305
+ * Renders a Portable Text table block-object back to Markdown.
306
+ *
307
+ * The PT `headerRows` field decides the header. Missing `headerRows` and
308
+ * `headerRows === 0` both render headerless: GFM has no headerless form, so
309
+ * an empty header row is emitted and every row goes in the body (that empty
310
+ * header reads back as `headerRows: 0` via `markdownToPortableText`).
311
+ * `headerRows >= 1` promotes `rows[0]` to the header. GFM allows exactly one
312
+ * header row, so header rows beyond the first flatten into the body, lossy,
313
+ * but the extra rows stay on the Portable Text side.
314
+ *
315
+ * Asymmetric tables (rows of varying cell counts) are widened to match
316
+ * the row with the most cells. Narrower rows are padded with empty cells
317
+ * so a GFM parser doesn't silently drop the extra cells in wider rows.
318
+ *
319
+ * @public
320
+ */
321
+ const DefaultTableRenderer = (options) => {
322
+ let { value, renderNode } = options;
323
+ return isTableShaped(value) ? renderTable(value, renderNode) : DefaultUnknownTypeRenderer(options);
324
+ };
325
+ function renderTable(value, renderNode) {
326
+ let rows = value.rows, alignment = Array.isArray(value.alignment) ? value.alignment : void 0, headerRow = rows.at(0);
263
327
  if (!headerRow) return "";
264
328
  let getCellText = (cellBlocks) => cellBlocks.map((block, index) => renderNode({
265
329
  node: block,
@@ -274,33 +338,55 @@ const DefaultUnknownMarkRenderer = ({ children }) => children, DefaultNormalRend
274
338
  let align = alignment?.at(index);
275
339
  return align === "left" ? " :--- " : align === "center" ? " :---: " : align === "right" ? " ---: " : " --- ";
276
340
  }).join("|")}|`;
277
- if (value.headerRows === 0) {
278
- lines.push(renderCells([])), lines.push(delimiter);
279
- for (let row of rows) lines.push(renderRow(row.cells));
280
- } else {
341
+ if ((Number(value.headerRows) || 0) >= 1) {
281
342
  lines.push(renderRow(headerRow.cells)), lines.push(delimiter);
282
343
  for (let i = 1; i < rows.length; i++) {
283
344
  let row = rows.at(i);
284
345
  row && lines.push(renderRow(row.cells));
285
346
  }
347
+ } else {
348
+ lines.push(renderCells([])), lines.push(delimiter);
349
+ for (let row of rows) lines.push(renderRow(row.cells));
286
350
  }
287
351
  return lines.join("\n");
288
- }, DefaultCalloutRenderer = ({ value, renderNode }) => {
289
- let prefixed = value.content.map((block, index) => renderNode({
290
- node: {
352
+ }
353
+ /**
354
+ * @public
355
+ */
356
+ const DefaultCalloutRenderer = (options) => {
357
+ if (!isCalloutShaped(options.value)) return DefaultUnknownTypeRenderer(options);
358
+ let { renderNode } = options, prefixed = options.value.content.map((block, index) => renderNode({
359
+ node: block._type === "block" ? {
291
360
  ...block,
292
361
  style: "normal"
293
- },
362
+ } : block,
294
363
  index,
295
364
  isInline: !1,
296
365
  renderNode
297
366
  })).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
298
- return `> [!${value.tone.toUpperCase()}]\n${prefixed}`;
299
- }, DefaultBlockquoteObjectRenderer = ({ value, renderNode }) => value.content.map((block, index) => renderNode({
300
- node: {
367
+ return `> [!${options.value.tone.toUpperCase()}]\n${prefixed}`;
368
+ };
369
+ function isCalloutShaped(value) {
370
+ let callout = value;
371
+ return typeof callout?.tone == "string" && Array.isArray(callout.content) && callout.content.every(isTypedObject);
372
+ }
373
+ /**
374
+ * Renders a structural blockquote block-object (the `types.blockquote` shape
375
+ * produced by `markdownToPortableText` when a `types.blockquote` matcher is
376
+ * provided) back to Markdown. Each content block is rendered via the
377
+ * recursive renderer pipeline, joined with blank lines, and every line is
378
+ * prefixed with `> ` to form a Markdown blockquote.
379
+ *
380
+ * Distinct from `DefaultBlockquoteRenderer`, which renders flat-path text
381
+ * blocks with `style: 'blockquote'`.
382
+ *
383
+ * @public
384
+ */
385
+ const DefaultBlockquoteObjectRenderer = ({ value, renderNode }) => value.content.map((block, index) => renderNode({
386
+ node: block._type === "block" ? {
301
387
  ...block,
302
388
  style: "normal"
303
- },
389
+ } : block,
304
390
  index,
305
391
  isInline: !1,
306
392
  renderNode
@@ -332,7 +418,14 @@ const DefaultUnknownTypeRenderer = ({ value, isInline }) => {
332
418
  let json = `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
333
419
  return isInline ? `\n${json}\n` : json;
334
420
  }, defaultRenderers = {
335
- types: {},
421
+ types: {
422
+ callout: DefaultCalloutRenderer,
423
+ code: DefaultCodeBlockRenderer,
424
+ "horizontal-rule": DefaultHorizontalRuleRenderer,
425
+ html: DefaultHtmlRenderer,
426
+ image: DefaultImageRenderer,
427
+ table: DefaultTableRenderer
428
+ },
336
429
  block: {
337
430
  normal: DefaultNormalRenderer,
338
431
  blockquote: DefaultBlockquoteRenderer,
@@ -453,7 +546,24 @@ const normalStyleDefinition = { name: "normal" }, h1StyleDefinition = { name: "h
453
546
  },
454
547
  {
455
548
  name: "rows",
456
- type: "array"
549
+ type: "array",
550
+ of: [{
551
+ type: "object",
552
+ name: "row",
553
+ fields: [{
554
+ name: "cells",
555
+ type: "array",
556
+ of: [{
557
+ type: "object",
558
+ name: "cell",
559
+ fields: [{
560
+ name: "value",
561
+ type: "array",
562
+ of: [{ type: "block" }, { type: "image" }]
563
+ }]
564
+ }]
565
+ }]
566
+ }]
457
567
  }
458
568
  ]
459
569
  }, defaultCalloutObjectDefinition = {
@@ -564,6 +674,13 @@ const codeBlockMatcher = ({ context, value, isInline }) => {
564
674
  isInline
565
675
  });
566
676
  if (imageObject && "src" in imageObject) return imageObject;
677
+ }, tableBlockMatcher = ({ context, value, isInline }) => {
678
+ let tableObject = buildObjectMatcher(defaultTableObjectDefinition)({
679
+ context,
680
+ value,
681
+ isInline
682
+ });
683
+ if (tableObject && "rows" in tableObject) return tableObject;
567
684
  }, defaultOptions = {
568
685
  schema: defaultSchema,
569
686
  keyGenerator: defaultKeyGenerator,
@@ -595,7 +712,8 @@ const codeBlockMatcher = ({ context, value, isInline }) => {
595
712
  horizontalRule: buildObjectMatcher(defaultHorizontalRuleObjectDefinition),
596
713
  html: buildObjectMatcher(defaultHtmlObjectDefinition),
597
714
  image: imageBlockMatcher,
598
- callout: buildObjectMatcher(defaultCalloutObjectDefinition)
715
+ callout: buildObjectMatcher(defaultCalloutObjectDefinition),
716
+ table: tableBlockMatcher
599
717
  }
600
718
  };
601
719
  /**