Skip to content

fix: serialize images the parser would refuse as json:object fences - #3265

Merged
christianhg merged 1 commit into
mainfrom
markdown-guard-unparsable-image-src
Sep 11, 2026
Merged

fix: serialize images the parser would refuse as json:object fences#3265
christianhg merged 1 commit into
mainfrom
markdown-guard-unparsable-image-src

Conversation

@christianhg

@christianhg christianhg commented Sep 10, 2026

Copy link
Copy Markdown
Member

A stored image whose src the markdown parser refuses no longer serializes to markdown that destroys it. Affected srcs: data: URIs outside image/(gif|png|jpeg|webp), notably data:image/svg+xml, and script-ish protocols (javascript:, vbscript:, file:). Such images now serialize as a json:object fence (tagged code span inline) that reparses to the identical image, _key included; accepted srcs keep their \![alt](src) form unchanged.

The asymmetry this closes: the parse side runs markdown-it's default validateLink (an XSS guard), so \![logo](data:image/svg+xml;…) never forms an image token, it parses as literal text. A stored SVG-data-URI image therefore round-tripped into escaped text, and in the edit loop the type change refused the whole origin trace and churned every key in the document, the field report that surfaced this. The guard uses the parser's own validator (one module-level validateLink reference, and the parse side never overrides it), so there are no duplicated regexes to drift.

Pinned red-first: SVG data URI and javascript: srcs to the fence at block position and the code span inline, an accepted PNG data URI keeping its markdown form, and the fence round-tripping to the identical value. Probed and needing no fix: the link annotation's href cannot hit the same trap, DefaultLinkRenderer's protocol allowlist is a strict subset of what validateLink accepts. One corner is inherent and documented on the tracking ticket instead: an SVG data-URI image typed into markdown still becomes plain text silently, markdown-it swallows the token before degradation reporting can see an image.


Note

Low Risk
Localized serializer change for image src validation with round-trip tests; normal image URLs and accepted data URIs are unaffected.

Overview
DefaultImageRenderer now rejects image src values that markdown-it's default validateLink would block (e.g. javascript: / vbscript: / file:, and data: URIs other than png/gif/jpeg/webp). Those images serialize as json:object fences (block) or tagged code spans (inline) via the existing unknownType path instead of ![alt](src), so PT→MD→PT preserves the image object instead of turning it into literal text.

Accepted src values are unchanged. The guard reuses a module-level markdown-it validateLink reference aligned with the parser. README and a changeset document the behavior; tests cover SVG/javascript: fallbacks, PNG data URIs, inline cases, and round-trip.

Reviewed by Cursor Bugbot for commit 9555856. Bugbot is set up for automated code reviews on this repo. Configure here.

`DefaultImageRenderer` emitted `![alt](src)` for any string `src`, but
the parse side runs markdown-it's default `validateLink`, which
rejects `javascript:`/`vbscript:`/`file:` protocols and every `data:`
URI outside `image/(gif|png|jpeg|webp)`, an XSS guard that notably
excludes `data:image/svg+xml`. A stored image with such a `src`
therefore serialized to markdown its own reparse refuses: the token
never forms, the image degrades to literal text, and in the edit loop
the type change refuses the whole origin trace and regenerates every
key in the document. A self-destroying serialization, keyed on the
value rather than the schema.

The renderer's shape check now also validates `src` with the parser's
own validator, one module-level `new markdownit().validateLink`
reference (the parse side never overrides it, so this mirrors exactly
what the reparse will accept, with no duplicated regexes to drift),
and a refused `src` falls back to the `json:object` form like any
other shape failure, which reparses to the identical image, `_key`
included.

Pinned red-first: SVG data URI and `javascript:` srcs to the fence at
block position and the tagged code span inline, an accepted PNG data
URI keeping its markdown form, and the fence round-tripping to the
identical value. The link annotation's `href` needs no matching guard:
`DefaultLinkRenderer`'s own allowlist (`http`/`https`/`mailto`/`tel`)
is a strict subset of what `validateLink` accepts, so a
parser-refusable link is unemittable; probed, not just reasoned.
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portable-text-editor-documentation Ready Ready Preview Sep 10, 2026 7:52pm UTC
portable-text-example-basic Ready Ready Preview Sep 10, 2026 7:52pm UTC
portable-text-playground Ready Ready Preview Sep 10, 2026 7:52pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9555856

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@portabletext/markdown Patch

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

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Bundle Stats

✅ No significant changes.

All scenario measurements (7)

🗺️ @portabletext/editor / @portabletext/editor · @portabletext/editor / @portabletext/editor/behaviors · @portabletext/editor / @portabletext/editor/plugins · @portabletext/editor / @portabletext/editor/selectors · @portabletext/editor / @portabletext/editor/traversal · @portabletext/editor / @portabletext/editor/utils · @portabletext/markdown / @portabletext/markdown · Artifacts

Scenario Kind Bundle (raw / gzip) Gzip change Import time Import change
⚪ @portabletext/editor / @portabletext/editor export 1.09 MB / 254.3 KB None 68 ms +1 ms, +1.4%
⚪ @portabletext/editor / @portabletext/editor/behaviors export 4.0 KB / 1.4 KB None 2 ms +0 ms, +2.9%
⚪ @portabletext/editor / @portabletext/editor/plugins export 5.1 KB / 1.8 KB None 7 ms +0 ms, +0.6%
⚪ @portabletext/editor / @portabletext/editor/selectors export 94.7 KB / 21.7 KB None 8 ms -0 ms, -2.7%
⚪ @portabletext/editor / @portabletext/editor/traversal export 42.8 KB / 11.2 KB None 6 ms +0 ms, +0.2%
⚪ @portabletext/editor / @portabletext/editor/utils export 33.8 KB / 9.1 KB None 6 ms -0 ms, -1.6%
⚪ @portabletext/markdown / @portabletext/markdown export 313.5 KB / 91.1 KB +184 B, +0.2% 45 ms +2 ms, +5.7%

Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time.

@christianhg
christianhg marked this pull request as ready for review September 11, 2026 05:57
@christianhg
christianhg merged commit 3b2d278 into main Sep 11, 2026
21 checks passed
@christianhg
christianhg deleted the markdown-guard-unparsable-image-src branch September 11, 2026 07:02
@ecoscript ecoscript Bot mentioned this pull request Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant