Skip to content

id(web): emphasis/strong/strikethrough wrapping an image are silently dropped on save — mixed text+image spans split and leak   entities #1407

Closed

Description

@dezren39

Summary

In pkgs/id's web markdown codec, a mark that wraps an image (emphasis, strong, strikethrough) is silently lost. When the mark wraps only an image, it disappears entirely; when it wraps text plus an image, the saved markdown splits the mark around the image and leaks literal   HTML entities into the file.

Evidence (verified this run on main 37418e56)

Probe: verbatim copy of pkgs/id/src/web/markdown.rs in a standalone crate (comrak 0.56.0), hidden_files/probe-2026-10-27-creator-a4/out4.log:

Input Saved output
*![alt](/x.png)* ![alt](/x.png) — emphasis gone
**![alt](/x.png)** ![alt](/x.png) — strong gone
~~![alt](/x.png)~~ ![alt](/x.png) — strikethrough gone
*[![alt](/x.png)](https://e.com)* [![alt](/x.png)](https://e.com) — link survives, emphasis gone
*text ![alt](/x.png) tail* *text *![alt](/x.png)* tail* — one emphasis span becomes two, with literal   entities in the saved file, and the image is no longer inside either span's text (re-parse gives the same split PM, so it is stable corruption, not a fixpoint error)
**text ![alt](/x.png) tail** **text **![alt](/x.png)** tail** — same for strong
- *![alt](/x.png)* item - ![alt](/x.png) item — same loss inside list items

Control: *text only* round-trips exactly. The parsed ProseMirror for *![alt](/x.png)* is a bare image node with no marks at all ({"type":"image","attrs":{"alt":"alt","src":"/x.png"}}).

Root cause

  • Parse direction: the NodeValue::Image arm in convert_node (pkgs/id/src/web/markdown.rs:354-393) receives the surrounding marks in active_marks but only carries the link mark forward, copying it into the image node's linkHref/linkTitle attrs (the id(web): linked images [![alt](src)](href) lose their link in the markdown ↔ ProseMirror codec — save strips the href #880 precedent). em / strong / strikethrough marks in active_marks are ignored, and the emitted image node has no representation for them.
  • Save direction: the "image" arm of json_to_ast (pkgs/id/src/web/markdown.rs:1308-1360) and the post-processing wrap (:1503-1519) only know how to re-wrap an image in a Link node from linkHref; there is no equivalent for emphasis/strong/strikethrough, and the marked text runs on either side of the image are emitted as separate mark-wrapper nodes, whose boundary spaces comrak escapes as  .

Why it matters

Italic/bold captions and callouts that include an inline image are ordinary markdown (common in notes imported from other tools, which id syncs as plain files). Today such a document loses its formatting on the first open→save, and the mixed text+image case additionally writes   entities into the user's file — visible garbage in any other editor. The loss is silent: no error, and the result is a stable fixpoint, so nothing downstream notices.

Acceptance criteria

  • *![alt](/x.png)*, **![alt](/x.png)**, ~~![alt](/x.png)~~ and the linked-image form *[![alt](/x.png)](href)* round-trip with the mark preserved (exact markdown or an equivalent form that re-parses to the same marked structure, with no mark loss).
  • *text ![alt](/x.png) tail* (and the strong/strikethrough equivalents) save as a single emphasis span containing the image, with no   or other HTML entities introduced into the saved markdown.
  • The representation chosen is coherent with the existing id(web): linked images [![alt](src)](href) lose their link in the markdown ↔ ProseMirror codec — save strips the href #880 linkHref/linkTitle pattern for wrapping an image, and the editor either renders it or the limitation is explicitly documented in the issue's resolution.
  • Regression tests in pkgs/id/src/web/markdown.rs cover image-only and mixed text+image mark spans in both directions; existing web::markdown tests still pass.

Related: #880 (linked images carried via attrs — the pattern to extend), #1045 (unknown marks dropped on save — a different, text-node policy question), #968 (mark re-nesting for text runs).

Activity

  1. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator job (lane A, slug new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-27 06:18 CDT
    • Found via: filed this run
    • Task: Filed after verification on main 37418e5 with a verbatim-codec probe crate (comrak 0.56.0): marks wrapping an image are dropped at parse (Image arm ignores non-link active_marks), and mixed text+image spans save split with literal entities.
    • Expectation: effort/s — triage picks the representation (extend the id(web): linked images [![alt](src)](href) lose their link in the markdown ↔ ProseMirror codec — save strips the href #880 attr-carry pattern or give the image node real marks), then fix with regression tests both directions.
    • Links: probe log hidden_files/probe-2026-10-27-creator-a4/out4.log; body source hidden_files/issue-code-id-image-marks-body.md
  4. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 27, 2026
  6. 25 remaining items

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 28, 2026
  8. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Queue task 1477 parked on CI again (Lane B): PR #1630 stands at 31 SUCCESS / 0 FAILURE with only the aggregate 'nix flake check' job still QUEUED (runner stall #1599). Resume = merge PR #1630 when the aggregate flips green, then complete fixed_merged.

  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 28, 2026
  10. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:in-reviewtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions