Skip to content

id(web): link/image destinations with a space are silently rewritten to %20 on save — angle-bracket form lost, relative file links break #1019

Closed

Description

@dezren39

Summary

A link or image destination that contains a space is silently rewritten to its %20-encoded form on save, and the ProseMirror document itself changes (no round-trip fixpoint at the PM level). The angle-bracket destination the author wrote — the only CommonMark form that carries a space verbatim — is never re-emitted.

Evidence (verified 2026-10-26 on main 786d2129, standalone verbatim-codec probe)

Probe: hidden_files/probe-2026-10-26-1615/out4.log (crate = current pkgs/id/src/web/markdown.rs copied verbatim + round-trip harness).

Markdown → PM → markdown:

IN : "[notes](<my notes.md>)\n"
PM : href "my notes.md"
OUT: "[notes](my%20notes.md)\n"
PM2: href "my%20notes.md"        SAMEPM: false

IN : "![a](<my pic.png>)\n"
PM : src "my pic.png"
OUT: "![a](my%20pic.png)\n"
PM2: src "my%20pic.png"          SAMEPM: false

PM → markdown shows the same: an editor-created link with href: "https://e.com/a b" saves as [t](https://e.com/a%20b) and reparses with the mutated href (probe case link-href-space, out3.log).

Why it matters

The codec passes destinations verbatim in both directions (markdown.rs:299 parse arm copies link.url into the PM href; markdown.rs:1223 save arm copies it back into NodeLink.url) — the mutation happens in comrak's CommonMark renderer, which percent-encodes a space in a bare destination instead of emitting the <…> angle form. For an absolute URL the encoded form usually still resolves, but for a relative file link — the common case in id's own documents — my%20notes.md names a file that does not exist, so one save silently breaks the link. It also means the editor's in-memory href and the file's href diverge after the first save.

Acceptance criteria

  • Round trip preserves a destination containing a space exactly: [notes](<my notes.md>) saves byte-identically (angle form retained), and PM → markdown → PM returns the identical href/src.
  • Same for image src.
  • Destinations without spaces are unchanged; existing web::markdown tests pass.

Suggested direction

The fix layer needs a triage decision: comrak's renderer offers no angle-destination option, so options are (a) a targeted post-process of the rendered output for destinations known (from the AST) to contain spaces, (b) pre-render substitution with a placeholder that is restored afterwards, or (c) documenting + normalizing on the parse side instead (accept %20 in PM as canonical — but that still rewrites the user's file on first save). Any of these beats the current silent mutation; (a)/(b) preserve the file.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Oct 26, 2026
  2. developing-today commented on Oct 26, 2026

    @developing-today
    Contributor
  3. added
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Oct 26, 2026
  4. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job
    • Session / ID: new-issue-creator cron run 2026-10-26 16:10 CDT
    • Found via: filed this run
    • Task: Verified with a standalone verbatim-codec probe on main 786d212 (log hidden_files/probe-2026-10-26-1615/out4.log): link hrefs and image srcs containing a space (angle-bracket form in the file, or set in the editor) save as %20-encoded bare destinations and reparse with the mutated value (SAMEPM: false) — codec passes destinations verbatim (markdown.rs:299, :1223), so comrak's renderer makes the substitution and never re-emits the <...> form. Enqueued as a triage packet to repo-health-code-pilot — the fix layer (renderer post-process vs placeholder vs parse-side normalization) is a design choice.
    • Expectation: triage picks a fix layer; done when a destination with a space round-trips byte-identically in both directions.
    • Links: probe evidence in goal hidden_files (see issue body)
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 26, 2026
  6. 6 remaining items

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added a commit that references this issue on Oct 26, 2026
  10. dezren39 commented on Oct 26, 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)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions