@portabletext/markdown 2.0.0 → 2.2.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 +200 -25
- package/dist/index.d.ts +460 -148
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2020 -310
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -56,6 +56,18 @@ const markdown = portableTextToMarkdown([
|
|
|
56
56
|
# Hello **world**
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
**Edit through Markdown without losing keys**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {applyMarkdownEdit, portableTextToMarkdown} from '@portabletext/markdown'
|
|
63
|
+
|
|
64
|
+
const markdown = portableTextToMarkdown(stored)
|
|
65
|
+
const editedMarkdown = markdown.replace('tomorow', 'tomorrow')
|
|
66
|
+
const edited = applyMarkdownEdit(stored, editedMarkdown)
|
|
67
|
+
// same content as parsing editedMarkdown, with the stored `_key`s
|
|
68
|
+
// kept the way the same edit in an editor would have kept them
|
|
69
|
+
```
|
|
70
|
+
|
|
59
71
|
## Supported features
|
|
60
72
|
|
|
61
73
|
| Feature | Markdown → Portable Text | Portable Text → Markdown |
|
|
@@ -81,32 +93,28 @@ const markdown = portableTextToMarkdown([
|
|
|
81
93
|
|
|
82
94
|
## Round-trip behavior
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
<https://portabletext.org> -> [https://portabletext.org](https://portabletext.org)
|
|
88
|
-
[ref link][id] -> [ref link](https://example.com "title")
|
|
89
|
-
```
|
|
96
|
+
Converting Markdown to Portable Text and back isn't a lossless mirror:
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
1. Translation preserves semantics, not source spelling: the first MD→PT→MD pass normalizes Markdown to one canonical spelling (autolinks become inline links, indented code becomes fenced code, soft-wrapped lines join into one, and so on).
|
|
99
|
+
2. The normalized Markdown is a fixpoint for plain text and the [Supported features](#supported-features) table: parsing it and serializing again reproduces it byte-for-byte.
|
|
100
|
+
3. MD→PT survival is schema-driven: a construct whose type the schema doesn't declare keeps its content and drops the structure that named it.
|
|
101
|
+
4. PT structures with no Markdown form degrade predictably on PT→MD (extra table header rows flatten into the body, deep or level-skipping lists collapse to relative nesting, unknown marks pass their text through unformatted). Unknown object types round-trip instead: block-level as a ` ```json:object ` fence, inline as a `json:object`-tagged code span, both carrying the value as JSON. A fence or span whose body isn't a JSON object with a `_type` is ordinary code.
|
|
102
|
+
5. Identity does not round-trip for text blocks: keys are regenerated on every parse, and adjacent spans with identical marks merge into one. Unknown objects keep their `_key`. [`applyMarkdownEdit`](#applymarkdownedit) restores stored keys after an edit.
|
|
103
|
+
6. A hard break and a `\n` in a span's text are exclusive counterparts in both directions: a `\n` always renders as hard-break syntax on the way out, and hard-break syntax always becomes `\n` on the way in, never the space a soft wrap joins with.
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
\*bar\* -> *bar* (serialized unescaped; a second parse reads this as emphasis)
|
|
95
|
-
```
|
|
105
|
+
The named exceptions to the fixpoint claim: an explicit-scheme URL or email keeps its text but gains a `link` mark on reparse, and a fuzzy `www.` form does too unless it carries markdown-significant punctuation; a hard break inside a heading splits into a second block on reparse, since an ATX heading is single-line; leading or trailing whitespace that CommonMark's own block parsing trims isn't part of the fixpoint; a `code` object with the reserved language `json:object` loses that language on serialization; and span text ending in `json:object` directly before a code-marked span holding a typed JSON object binds into an inline object on reparse.
|
|
96
106
|
|
|
97
|
-
|
|
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.
|
|
107
|
+
See [Markdown round-tripping](https://www.portabletext.org/conversion/markdown-round-tripping/) on the docs site for the full contract and worked examples.
|
|
102
108
|
|
|
103
109
|
## Usage
|
|
104
110
|
|
|
105
|
-
<!-- The schema table, matcher table, supported-features table
|
|
106
|
-
|
|
111
|
+
<!-- The schema table, matcher table, and supported-features table have
|
|
112
|
+
condensed twins on the docs site
|
|
107
113
|
(apps/docs/src/content/docs/conversion/markdown-to-portable-text.mdx).
|
|
108
114
|
Keep them in sync: a claim corrected in one place is stale in the
|
|
109
|
-
other.
|
|
115
|
+
other. The Round-trip behavior section above is a summary of
|
|
116
|
+
apps/docs/src/content/docs/conversion/markdown-round-tripping.mdx,
|
|
117
|
+
which is canonical; keep the two in sync the same way. -->
|
|
110
118
|
|
|
111
119
|
### `markdownToPortableText`
|
|
112
120
|
|
|
@@ -270,6 +278,8 @@ The default code block matcher requires the schema type to have a `'code'` field
|
|
|
270
278
|
|
|
271
279
|
**Links** support optional titles using `[text](url "title")` syntax. The title is captured in the `'title'` field of the `'link'` annotation.
|
|
272
280
|
|
|
281
|
+
**Line breaks**: soft-wrapped lines within a paragraph join with a single space; a hard break (two or more trailing spaces, or a backslash, before the newline) becomes a line break within the block.
|
|
282
|
+
|
|
273
283
|
**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
284
|
|
|
275
285
|
**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.
|
|
@@ -299,7 +309,7 @@ markdownToPortableText(markdown, {
|
|
|
299
309
|
})
|
|
300
310
|
```
|
|
301
311
|
|
|
302
|
-
> **Note:** Checking if the type exists in the schema isn't required, but it's good practice. Returning `undefined`
|
|
312
|
+
> **Note:** Checking if the type exists in the schema isn't required, but it's good practice. Returning `undefined` skips unsupported types.
|
|
303
313
|
|
|
304
314
|
**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:
|
|
305
315
|
|
|
@@ -417,6 +427,39 @@ markdownToPortableText(markdown, {
|
|
|
417
427
|
})
|
|
418
428
|
```
|
|
419
429
|
|
|
430
|
+
#### Reporting degradation
|
|
431
|
+
|
|
432
|
+
Every construct the schema can't represent (a decorator not in the schema, a table with no `table` block object, a task checkbox with no `task` list, and so on) degrades to a lossier shape rather than throwing. A ` ```json:object ` fence or tagged code span that fails to reconstruct (invalid JSON, or no string `_type`) reports too, as `object-carrier-invalid`, even on a schema that would otherwise accept everything: it's the payload that's unusable, not the schema. `onDegradation` observes both, one callback covering all uses. Left unset, the conversion stays silent and returns the lossiest representation it can build, since a library shouldn't log on its own initiative. Passed a function, observe the losses: it's called once, after the whole document has been walked, only when at least one construct degraded, with a report object holding every `Degradation` in encounter order and a canonical grouped `message`. Enforce against lossy output by throwing your own error from inside that callback; the throw propagates out of `markdownToPortableText`.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
markdownToPortableText(markdown, {
|
|
436
|
+
onDegradation: ({degradations, message}) => {
|
|
437
|
+
// degradations: every Degradation, in encounter order
|
|
438
|
+
// message: the same degradations grouped, snippeted, and sorted by line
|
|
439
|
+
logger.warn(message)
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Each `Degradation` carries `type`, a human-readable `message`, `line` when a source line is available, and `snippet` (the offending construct's text, truncated to 40 characters) when there's a specific piece of source text to quote. Match on `type`, not `message`: the type is the stable contract, the message can change between releases.
|
|
445
|
+
|
|
446
|
+
Enforcing means throwing your own error from inside the callback:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
markdownToPortableText('# heading\n\n**bold**', {
|
|
450
|
+
schema: compileSchema(defineSchema({})),
|
|
451
|
+
onDegradation: ({message}) => {
|
|
452
|
+
throw new Error(message)
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
// throws Error:
|
|
456
|
+
// Markdown could not be converted without loss:
|
|
457
|
+
// - line 1: `#` heading became a normal paragraph: the schema has no `h1` style ("heading")
|
|
458
|
+
// - line 3: Removed bold formatting, kept the text: the schema has no `strong` decorator ("bold")
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Repeated declines of the same kind (the same missing decorator on three spans, say) collapse into one line of `message` instead of repeating the sentence: `- Removed bold formatting, kept the text: the schema has no \`strong\` decorator (3×: "a", "b", "c")`. `degradations` stays ungrouped, one entry per occurrence.
|
|
462
|
+
|
|
420
463
|
### `portableTextToMarkdown`
|
|
421
464
|
|
|
422
465
|
```ts
|
|
@@ -506,7 +549,7 @@ The conversion is driven by **Renderers**: functions that render Portable Text e
|
|
|
506
549
|
|
|
507
550
|
Unknown types render as JSON code blocks by default; unknown styles, list items, and marks pass through their children.
|
|
508
551
|
|
|
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.
|
|
552
|
+
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 `---`. `image` also falls back when `src` is a string a Markdown parser would refuse (a `javascript:`/`vbscript:`/`file:` URI, or a `data:` URI outside `png`/`gif`/`jpeg`/`webp`), so the value survives as a `json:object` fence instead of reparsing as literal text. 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
553
|
|
|
511
554
|
> **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.
|
|
512
555
|
|
|
@@ -568,17 +611,19 @@ portableTextToMarkdown(blocks, {
|
|
|
568
611
|
|
|
569
612
|
#### What renderers receive
|
|
570
613
|
|
|
614
|
+
One question decides which text field a renderer reads: **are you wrapping markdown, or emitting something else?** `children` is markdown: nested marks are rendered and literal punctuation is backslash-escaped, so it embeds in markdown output as-is (`` `**${children}**` ``). `text` is source: the raw span text exactly as stored, for renderers that emit their own delimiters or non-markdown output (code fences, HTML tags), where escapes would appear literally. The built-in `code` decorator renderer reads `text`; every other built-in reads `children`.
|
|
615
|
+
|
|
571
616
|
**Block renderers** (`block.*`):
|
|
572
617
|
|
|
573
618
|
- `value` – the block object
|
|
574
|
-
- `children` – rendered content of the block
|
|
619
|
+
- `children` – rendered content of the block, as markdown (escaped)
|
|
575
620
|
- `index` – position in the blocks array
|
|
576
621
|
|
|
577
622
|
**Mark renderers** (`marks.*`):
|
|
578
623
|
|
|
579
624
|
- `value` – the mark definition (for annotations like links)
|
|
580
|
-
- `children` – the rendered marked content
|
|
581
|
-
- `text` – the raw text content (
|
|
625
|
+
- `children` – the rendered marked content, as markdown (escaped)
|
|
626
|
+
- `text` – the raw text content (unescaped, no nested mark rendering)
|
|
582
627
|
- `markType` – the mark type name
|
|
583
628
|
- `markKey` – the mark's key (for annotations)
|
|
584
629
|
|
|
@@ -633,9 +678,43 @@ portableTextToMarkdown(blocks, {
|
|
|
633
678
|
})
|
|
634
679
|
```
|
|
635
680
|
|
|
636
|
-
By default, unknown types render as
|
|
681
|
+
By default, unknown types render as `json:object` fences or tagged code spans that round-trip (see [Round-trip behavior](#round-trip-behavior)), and unknown marks/styles pass through their children unchanged.
|
|
682
|
+
|
|
683
|
+
#### Gating default renderers on a schema
|
|
684
|
+
|
|
685
|
+
Going to convert the markdown back with `markdownToPortableText`? Pass the same `schema` to both, and nothing the schema can't rebuild becomes markdown that gets destroyed on the way back: undeclared types travel as `json:object` fences that reparse to the same value.
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
import {compileSchema, defineSchema} from '@portabletext/schema'
|
|
689
|
+
|
|
690
|
+
const schema = compileSchema(
|
|
691
|
+
defineSchema({
|
|
692
|
+
blockObjects: [
|
|
693
|
+
{
|
|
694
|
+
name: 'code',
|
|
695
|
+
fields: [
|
|
696
|
+
{name: 'code', type: 'string'},
|
|
697
|
+
{name: 'language', type: 'string'},
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
}),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
portableTextToMarkdown(blocks, {schema})
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
A default renderer (`callout`, `code`, `horizontal-rule`, `html`, `image`, `table`) runs only when the schema declares that type at the position the node appears in: `blockObjects` for a block, `inlineObjects` for an inline object. An `image` declared in only one of the two still falls back to `unknownType` at the other position.
|
|
708
|
+
|
|
709
|
+
The gate reads type names, never field values: declaring a type doesn't validate anything, and a value's fields play no part in which renderer runs. Fields matter on the parse side instead: `markdownToPortableText` filters a construct down to its declared fields, so declare each type with the fields its values carry, or the markdown forms this gate lets through come back rebuilt without them.
|
|
710
|
+
|
|
711
|
+
An undeclared type falls back to `unknownType`, whose default output is the same `json:object` fence or tagged code span described above, so it round-trips at block and inline positions. Inside a table cell the `json:object` form is the inline code span (a GFM cell is one line, and a fence would be squashed), so an undeclared object in a cell survives too; declared types whose markdown form spans multiple lines (a code block in a cell) still flatten on reparse. Renderers you register in `types` bypass the gate entirely, whether or not the schema declares them.
|
|
712
|
+
|
|
713
|
+
Without a `schema`, every default renderer stays active.
|
|
637
714
|
|
|
638
|
-
|
|
715
|
+
#### Hard breaks
|
|
716
|
+
|
|
717
|
+
Customize how a hard break (a `\n` inside a span's text) renders:
|
|
639
718
|
|
|
640
719
|
```ts
|
|
641
720
|
portableTextToMarkdown(blocks, {
|
|
@@ -664,6 +743,102 @@ portableTextToMarkdown(blocks, {
|
|
|
664
743
|
})
|
|
665
744
|
```
|
|
666
745
|
|
|
746
|
+
### `applyMarkdownEdit`
|
|
747
|
+
|
|
748
|
+
Parsing markdown mints fresh `_key`s for text blocks (see [Round-trip behavior](#round-trip-behavior)), so converting a document to markdown, editing one word, and converting back returns what looks like a full rewrite: comment anchors detach, history churns, and granular patching is impossible. `applyMarkdownEdit` converts edited markdown back to Portable Text and restores stored `_key`s the way the same edit in an editor would have kept them:
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
import {applyMarkdownEdit, portableTextToMarkdown} from '@portabletext/markdown'
|
|
752
|
+
|
|
753
|
+
const stored = [
|
|
754
|
+
{
|
|
755
|
+
_type: 'block',
|
|
756
|
+
_key: 'b1',
|
|
757
|
+
style: 'normal',
|
|
758
|
+
children: [{_type: 'span', _key: 's1', text: 'Ships tomorow.', marks: []}],
|
|
759
|
+
markDefs: [],
|
|
760
|
+
},
|
|
761
|
+
]
|
|
762
|
+
|
|
763
|
+
const markdown = portableTextToMarkdown(stored)
|
|
764
|
+
// markdown === 'Ships tomorow.'; an agent (or anything else) fixes the typo
|
|
765
|
+
const edited = applyMarkdownEdit(stored, 'Ships tomorrow.')
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`edited`:
|
|
769
|
+
|
|
770
|
+
```json
|
|
771
|
+
[
|
|
772
|
+
{
|
|
773
|
+
"_type": "block",
|
|
774
|
+
"_key": "b1",
|
|
775
|
+
"style": "normal",
|
|
776
|
+
"children": [
|
|
777
|
+
{"_type": "span", "_key": "s1", "text": "Ships tomorrow.", "marks": []}
|
|
778
|
+
],
|
|
779
|
+
"markDefs": []
|
|
780
|
+
}
|
|
781
|
+
]
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Keys follow the edit the way they would in an editor, and when the evidence is unclear, a block gets a fresh key rather than a wrong one. The result is a value, not patches; output keys are always unique among siblings; the inputs are never mutated.
|
|
785
|
+
|
|
786
|
+
#### What keeps its key
|
|
787
|
+
|
|
788
|
+
- Unchanged and moved blocks. Repeated content pairs in order.
|
|
789
|
+
- A block rewritten in place, like typing over it. Style changes count as rewrites, and an edited table cell keeps the whole table's keys.
|
|
790
|
+
- A split keeps the key on the first non-empty fragment, like pressing enter; a merge keeps the first block's key, like pressing backspace. A soft-wrap join is a merge.
|
|
791
|
+
- A typo fix lands as a text change on the same span, and editing a link's URL keeps its annotation key. Two identical annotations in one block (the same link twice, say) pair in order, like any repeated content.
|
|
792
|
+
- A `json:object` payload keeps the `_key` it carries, unless the payload matches stored content, which keeps the stored key: editing markdown cannot re-key existing content.
|
|
793
|
+
|
|
794
|
+
#### What gets restored
|
|
795
|
+
|
|
796
|
+
Markdown cannot carry everything a block stores, so an adopted block gets back what the edit could not have touched:
|
|
797
|
+
|
|
798
|
+
- A field the dialect cannot express, like a text block's `alignment`, including a whole custom object-array field the dialect drops. A field markdown does express, like `language` on a code block, follows the edit.
|
|
799
|
+
- A custom style, list kind, or decorator markdown has no syntax for.
|
|
800
|
+
- An empty or whitespace-only paragraph, which has no markdown form at all (blank lines are the block separator): it is restored next to its surviving neighbor, and deleted along with that neighbor if the neighbor goes. This covers top-level blocks; an empty paragraph nested inside a table cell or callout content is not restored. An empty heading or list item has a visible markdown form (`## `, `- `) and round-trips like any other block.
|
|
801
|
+
- A block the edit did not touch comes back exactly as stored, span structure and unmappable marks included: an adjacent pair of spans that only differ by a decorator markdown has no syntax for keeps its split rather than merging into the one span a plain parse would produce.
|
|
802
|
+
|
|
803
|
+
#### When keys reset
|
|
804
|
+
|
|
805
|
+
- Ambiguity: when an insertion or deletion makes a match unclear, a block keeps its key only on clear evidence; everything else gets a new key, and short blocks near the change are the usual casualties.
|
|
806
|
+
- Indistinguishable edits: replacing a block with unrelated content in the same position keeps its key (the end state is identical to a rewrite), and a whole-document rewrite that keeps the block count pairs blocks in order. That last one is deliberate: a translation keeps every anchor by position, which is the behavior translate flows need. The cost is that a reorder-plus-edit with balanced counts mispairs the same way, block-level and sibling-level alike.
|
|
807
|
+
- Caps: on very large ambiguous edits, evidence gathering is size- and time-capped and degrades to fresh keys rather than waiting, so near the caps, which keys survive can vary with machine speed.
|
|
808
|
+
- Refusal: a stored value that cannot survive its own serialize→parse round trip resets the whole document to the plain conversion, every key fresh except the ones `json:object` payloads carry.
|
|
809
|
+
|
|
810
|
+
#### Options
|
|
811
|
+
|
|
812
|
+
The options bag mirrors the two converters, plus a top-level `schema`: `deserialize` takes the rest of `markdownToPortableText`'s options and `serialize` takes the rest of `portableTextToMarkdown`'s. `schema` is taken once and governs both directions, because restoring keys depends on the two serializations agreeing. Pass the same `serialize` options that produced the markdown that was edited. The `deserialize` options apply to the stored value as well as the edited markdown, with two exceptions: new keys come from `deserialize.keyGenerator` (or the built-in generator), and `onDegradation` reports only on the edited markdown.
|
|
813
|
+
|
|
814
|
+
#### Observing reconciliation
|
|
815
|
+
|
|
816
|
+
Pass `onReconciliation` to see what happened to every key. The callback fires exactly once per call, synchronously, right before the function returns:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
applyMarkdownEdit(stored, editedMarkdown, {
|
|
820
|
+
onReconciliation: (report) => {
|
|
821
|
+
if (report.keyMatching === 'skipped') {
|
|
822
|
+
// report.reason is 'round-trip-mismatch' (the stored value cannot
|
|
823
|
+
// survive its own serialize→parse round trip) or
|
|
824
|
+
// 'document-too-large'. The returned value is the plain
|
|
825
|
+
// conversion, every key fresh except the ones `json:object`
|
|
826
|
+
// payloads carry
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
report.preservedKeys // stored keys that survived, with a basis and a path
|
|
830
|
+
report.keyFallbacks // regions that got fresh keys instead of a guess
|
|
831
|
+
report.renamedKeys // keys rewritten to keep siblings unique
|
|
832
|
+
},
|
|
833
|
+
})
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Every `key` and `path` in the report matches the returned value exactly, and a path segment is a string field name, a number array index, or `{_key}` for a keyed element, the same convention as editor paths. Which keys survived, the paths, `renamedKeys`, and `keyMatching` are facts of that invocation, safe to branch on. A preserved key's `basis` names the matching method (`'content-unchanged'`, `'content-moved'`, `'content-split'`, `'content-merged'`, `'same-position'`, `'similar-content'`) and is advisory: near the evidence caps it can vary with machine speed, so never branch on it. A node absent from `preservedKeys` was not restored from the stored value, whether its key is fresh or carried by a `json:object` payload. The exported `ReconciliationReport` and `ReconciliationKeyPath` types are `@beta`.
|
|
837
|
+
|
|
838
|
+
#### Concurrent edits
|
|
839
|
+
|
|
840
|
+
Reconciling an unchanged serialization returns the stored value byte for byte for the content the edit did not touch, keys, span structure, and unmappable marks included; content the edit did touch still comes back canonicalized, with its keys restored where reconciliation can trace them. `applyMarkdownEdit` does not merge concurrent edits: reconcile against the exact value that produced the markdown, and before writing the result back, check that the stored field still equals that value. If it changed while the markdown was being edited, the edit describes a document that no longer exists, and writing it would silently overwrite the newer changes: serialize the current value and redo the edit instead.
|
|
841
|
+
|
|
667
842
|
## License
|
|
668
843
|
|
|
669
844
|
MIT © [Sanity.io](https://www.sanity.io/)
|