You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
id(web): prosemirror_to_markdown emits img src/alt/title unescaped — quotes truncate values and can inject attributes on reparse #873
The reverse conversion in pkgs/id/src/web/markdown.rs emits an HTML <img> tag by interpolating the image node's src/alt/title values into double-quoted attributes with no escaping at all (markdown.rs:730-754, the width.is_some() || height.is_some() branch; there is no escape helper anywhere in markdown.rs). Any value containing a " breaks the tag, and the breakage is lossy on the next load:
Node {src: "x.png", alt: "a \"quoted\" caption", width: 100} is written to the saved markdown as <img src="x.png" alt="a "quoted" caption" width="100" />. Re-parsing that tag yields alt = "a " — the rest of the caption is silently dropped.
Worse, a value can inject attributes: alt = "x\" title=\"injected" is emitted as <img src="x.png" alt="x" title="injected" width="100" />, and the re-parse produces a real title = "injected" the document never had. The same applies to src and title values containing quotes (a crafted src can manufacture alt/width attributes on reparse).
Verified this run (2026-10-25) on main 7ce0d74c with a standalone rustc harness replicating the emission format! code and re-parsing with parse_img_tag's verbatim attr_value helper: both cases above reproduce exactly as shown.
Why it matters
Image node attributes are strings the node model places no restriction on: alt text comes from markdown image syntax, from uploaded-file names (result.name in the web editor), and from collaborating clients' document JSON. Whenever such an image also carries a width or height — exactly the case the HTML-emission branch exists for — saving the document writes a malformed tag, and the next open/save cycle silently truncates the attribute or invents new ones. That is slow-motion document corruption of the same class as #872 (which covers the parse direction of this same tag codec), but a distinct defect with a distinct fix: this one is missing output escaping, not substring matching.
Suggested fix
Escape attribute values on emission: at minimum " → " (and & → & first, so existing entities are not double-decoded), for src, alt, and title. Note the interplay with #872: the parser (parse_img_tag) does not currently decode entities, so emission and parsing should agree on one convention — if emission escapes, parsing should decode "/& in values so the round trip is value-preserving. Coordinate the two fixes so the pair forms a consistent codec rather than two half-codecs.
Acceptance criteria
Round-trip pin: an image node with alt = "a \"quoted\" caption" and a width survives ProseMirror → markdown → ProseMirror with the alt value byte-identical.
An alt/title/src value containing " title=" (or similar) cannot create or alter other attributes on reparse.
Values containing & round-trip without double-escaping or entity corruption (state the chosen convention in a test).
Existing image conversion tests in markdown.rs keep passing.
Related: #872 (parse direction of the same <img> attribute codec).
Single-repo: this conversion code exists only in developing-today/code (pkgs/id).
Activity
fix(id-web): img attribute codec — sequential parse, escaped emission (…