feat: report serialize-side degradation via onDegradation in portableTextToMarkdown - #3289
christianhg wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: bd25150 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Bundle StatsWarning 1 significant change. @portabletext/markdown🔴 All scenario measurements (7)🗺️
Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time. |
251172d to
a321802
Compare
…bleTextToMarkdown`
`markdownToPortableText` reports every lossy construct through its
`onDegradation` callback, but `portableTextToMarkdown` had no
counterpart: an annotation with no mark renderer routed through
`unknownMark`, which emits children only, so the annotation and its
data vanished from the markdown with no signal, and a round trip
through `applyMarkdownEdit` returned the block with `markDefs: []`.
Consumers discovered serialize-side loss by diffing round-trip output.
The serializer now accepts its own `onDegradation`, mirroring the
parse side's contract: one callback, at most once, after the walk,
only when at least one construct degraded, with `{degradations,
message}`. Events are emitted exactly where the walk resolves a
fallback renderer and the resolved fallback is the library default
(identity check): `annotation-dropped` and `decorator-dropped` from
`unknownMark` split on markDef presence, `style-fallback` from the
default unknown-style renderer, and `list-item-fallback` from the
default unknown-list-item renderer, which is reachable only behind a
consumer-supplied partial `listItem` map. A consumer-supplied fallback
suppresses its family's reports. Losses inside nested content anchor
to the enclosing top-level block via a current-block slot, and block
`_key`s are snapshotted before `buildListIndexMap` mutates keyless
blocks so a keyless block reports as keyless. Serialized output is
byte-identical with and without the option, pinned by a test.
The serializer owns its own `SerializeDegradation` union rather than
sharing the parse side's `Degradation` (different anchor fields,
independent evolution). Each event carries a `path` addressing the
lossy node from the top-level array down (keyed segments where nodes
have a `_key`, numeric indexes otherwise), reconstructed at report
time by searching the enclosing top-level block for the reported node:
by reference for blocks and list items, and by a constrained `_key`
match for spans, since the mark tree rebuilds every marked span into a
fresh wrapper object. The span match requires span `_type` and a
`children` container so spec-legal duplicate keys at other levels
cannot capture the path, and the search falls back to the top-level
segment alone when the node cannot be re-found (a keyless nested span,
a renderer that clones nodes). The grouping and truncation helpers are
duplicated from the parse side with keep-in-sync pointers both ways,
since the originals are unexported internals. `applyMarkdownEdit`'s
`serialize` options now `Omit` `onDegradation`, and both internal
serialization sites strip a type-bypassing callback: those runs
produce alignment input, never consumer-facing output. Losses decided
inside default renderers (unsafe links, table header demotion, empty
tables) are deliberately out of scope: reporting them needs a reporter
channel in the public renderer signature or mirrored predicates, a
separate mechanism.
a321802 to
bd25150
Compare
portableTextToMarkdownnow takes anonDegradationoption for the losses its own default renderers fall back to: an annotation or decorator with no mark renderer (annotation-dropped,decorator-dropped), a block style with no renderer (style-fallback), or a list-item kind with no renderer (list-item-fallback). One callback, called at most once, after the whole document has been walked, only when at least one construct degraded, with each loss reported as aSerializeDegradation:Providing your own renderer for the affected construct suppresses its report:
onDegradationonly fires for fallbacks the library's own default renderers produced, never for a consumer'sunknownMark,unknownBlockStyle, orunknownListItem.list-item-fallbacknever fires under the default configuration, since the defaultlistItemrenderer already covers every kind: it only fires behind a consumer-supplied partiallistItemmap.applyMarkdownEdit'sserializeoption doesn't acceptonDegradation, since that serialization only ever runs internally to alignstoredPortableText, never oneditedMarkdownitself.Before this, the deserialize direction reported every lossy construct while the serialize direction was silent: a custom link annotation serialized to plain text and a round trip returned the block with
markDefs: [], discoverable only by diffing output. The reporting boundary is deliberate: events fire exactly where the render walk resolves a library-default fallback renderer, pinned per family with suppression tests, while losses decided inside default renderers (unsafe links, table header demotion, empty tables) are a separate mechanism and out of scope here. Serialized output is byte-identical with and without the option, pinned by a test, and the payload mirrors the parse side's contract with apathanchor instead of source lines: keyed segments where nodes carry a_key, numeric indexes otherwise, down to the lossy node itself (a table cell's degraded span reports its full path throughrowsandcells). The path is best-effort, reconstructed by re-finding the reported node inside its top-level block, and falls back to the top-level segment alone when the node cannot be re-found, which the docs state.Note
Low Risk
Additive minor API and observability only; markdown output is unchanged when the callback is omitted, with tests pinning byte-identical serialization.
Overview
Adds serialize-side degradation reporting to mirror the existing parse-side
onDegradationflow.portableTextToMarkdownaccepts an optionalonDegradationcallback (at most once per run, only when something lossy happened) with structuredSerializeDegradationentries plus a groupedmessage.Losses are recorded when library default fallbacks run: missing mark renderers (
annotation-dropped/decorator-dropped), unknown block styles (style-fallback), and uncovered list-item kinds behind a partiallistItemmap (list-item-fallback). CustomunknownMark/unknownBlockStyle/unknownListItemhandlers suppress those reports. Each event includes a best-effortpathinto the document (including nested table cells), optional truncatedsnippet, and stabletypefor consumers to match on.applyMarkdownEditexplicitly omitsonDegradationfromserializeand strips it from internal canonicalization so alignment serialization stays silent.SerializeDegradationis exported from the package entry; README documents the PT→MD reporting contract.Reviewed by Cursor Bugbot for commit bd25150. Bugbot is set up for automated code reviews on this repo. Configure here.