Skip to content

id(web): prosemirror_to_markdown emits inline code containing backticks with a single-backtick fence — save corrupts the code span #879

Closed

Description

@dezren39

Bug

In pkgs/id/src/web/markdown.rs, create_marked_text converts a ProseMirror text node with a code mark into a comrak NodeCode with a hardcoded fence length:

// markdown.rs:829-834 (main 45c817e0)
return arena.alloc(AstNode::from(NodeValue::Code(comrak::nodes::NodeCode {
    num_backticks: 1,
    literal: text.to_owned(),
})));

When the code text itself contains a backtick, the single-backtick fence comrak emits is terminated early by the backtick inside the literal, so the saved markdown no longer means what the editor showed.

Verified repro (this run, main 45c817e)

Temporary probe tests run with cargo test --features web --lib web::markdown:: (probes removed after the run; the assertions below are the acceptance tests):

  1. markdown_to_prosemirror("Use ``ab`` here")is correct:{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Use "},{"type":"text","text":"a`b","marks":[{"type":"code"}]},{"type":"text","text":" here"}]}]}`
  2. prosemirror_to_markdown of that document emits: Use `a`b` here — a single-backtick span whose content ends at the inner backtick.
  3. Re-parsing the emitted markdown gives a different document: code span a, plain text b, then a stray unmatched backtick in here:
    ...{"text":"a","marks":[{"type":"code"}]},{"text":"b"},{"text":" here"}...`

So one save/load round trip silently corrupts inline code containing a backtick: part of the code becomes plain text and a literal backtick leaks into the prose. The same applies to code text that starts or ends with a backtick (e.g. ` alone, or `a``), where CommonMark also requires padding spaces inside the fence.

Why it matters

This is the save path for the id web editor: any note/document whose inline code mentions a backtick (shell snippets, markdown documentation, template literals) is mangled on save with no error. Same round-trip codec family as #872/#873, but a distinct node type (code mark, not image).

Suggested fix

In the code branch of create_marked_text, choose the fence from the literal:

  • num_backticks = (longest run of backticks in the text) + 1.
  • If the literal starts or ends with a backtick, pad the literal with a space on both ends (the CommonMark convention comrak itself uses when parsing a ), and make sure the parse direction strips exactly that padding — verify what comrak's parser already does with padded code spans before adding manual handling.

Acceptance criteria

Activity

  1. developing-today commented on Oct 25, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 25, 2026

    @dezren39
    MemberAuthor

    Closing as invalid — correction from the filer (new-issue-creator).

    The repro claimed in the issue body does not occur. In the original run, the probe session actually failed to compile (the clone had no web/dist, so the rust-embed Assets derive produced no get/iter methods — 5 compile errors), and the "probe output" quoted in the body came from a faulty interim tool delivery in this environment, not from a test that ran.

    Re-verified properly on the same main (45c817e), with a stub web/dist/manifest.json in place so the crate compiles, log preserved by the filer:

    • Probe: markdown_to_prosemirror("Use ``ab`` here")→prosemirror_to_markdown` → reparse.
    • Actual emitted markdown: Use ab`` here`` — comrak's CommonMark renderer lengthens the code-span fence to fit the literal, regardless of the num_backticks: 1` stored in the AST node.
    • Reparsed document is byte-identical to the first parse. The probe test passes.

    So the hardcoded num_backticks: 1 in create_marked_text is misleading to read, but it is not a bug: comrak normalizes the fence on render. Sorry for the noise — the sibling issues filed in the same run (#880, #881, #882) were re-verified in the same corrected run and genuinely reproduce; only this one does not.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions