Keep formatting marks in one span around links in the rich text editor

What does this MR do and why?

A bold, italic or strikethrough span that wraps a link is split into separate marked segments around the link on the next rich text save. **see [docs](https://docs.gitlab.com) now** becomes **see** [**docs**](https://docs.gitlab.com) **now**. Any edit anywhere in the document triggers it, because the editor re-serializes the whole document on change; a save with no edit is safe.

Bold and italic still render after the split, so the damage surfaces in the plain text editor, in diffs and in description history. Strikethrough visibly loses its line through the spaces around the link, and a link followed by inline code inside a bold span saves as [**docs **](url), which renders literal asterisks inside the link text.

Root cause: prosemirror-markdown reorders a text node's marks to match the marks already open, but only moves marks whose serializer sets mixable: true, and it stops at the first mark without the flag. Every other inline mark sets it; the link serializer never did. Tiptap's Link extension has priority 1000, so the link sorts first and the reorder stops there, closing the open bold before the link and reopening it inside.

This MR sets mixable: true on the link serializer, as upstream prosemirror-markdown does for its own link mark.

Unchanged: a link with no other marks, marks inside a link ([**docs**](url)), autolinks, titled links, upload links, reference and wiki links, and inline code next to a link. A bold span that begins with a link still serializes as [**docs**](url) **now**, because marks carry no nesting order in the document and the serializer opens them in schema order. That output is valid and renders identically.

Resolves #514245

Follow-up, not changed here: #628260

Evidence

  • Round trip over 33 fixtures rendered by the production markdown API (bold, italic, both, strikethrough, two links in one span, link at span end, inline code adjacent, bare URL, titled link, upload link, and a bold link inside a nested list item, table cell, blockquote, heading and task item): byte-identical with this change, split at the merge base. Fixtures with no mark wrapping a link are byte-identical on both.
  • Jest: 12 new examples in serializer/link_spec.js fail at the merge base and pass here; 2 more pin the unchanged cases. Spec 25/25; whole content_editor tree 1212/1212 under both Vue 2 and Vue 3.
  • Corpus check on 55,755 open gitlab-org/gitlab issue descriptions (2026-09-09): 1,761 carry a mark wrapping a link, which is the shape this protects on the next rich text edit.
  • Screenshots and recordings below: 12 matrix rows and 3 GIF stories.
How the flag was lost

The editor's first link serializer carried mixable: true, like upstream's. Allow rendering block content in tables (!66187) rewrote the link block in 2021 and dropped the flag (commit 9034aab3ca53); the later rewrites in Preserve bold, italic, code, and link syntax in the Content Editor (!87157) and Split markdown serializer for marks in separate files (!159630) kept that shape. Bold Text Rendering Bug (#523066) is the same defect, closed as a duplicate.

Screenshots or screen recordings

Scenario Before After
Fix: bold around a link; a word typed on another line, saved (plain text source of the saved description) 01_fix_bold_source 01_fix_bold_source
Fix: italic around a link, edited elsewhere and saved (source) 02_fix_italic_source 02_fix_italic_source
Fix: bold and italic around a link, edited elsewhere and saved (source) 03_fix_bold_italic_source 03_fix_bold_italic_source
Fix: strikethrough around a link, edited elsewhere and saved (rendered) 04_fix_strike_rendered 04_fix_strike_rendered
Fix: the report's steps, bold applied in rich text over a line that contains a link (source) 05_fix_typed_bold_source 05_fix_typed_bold_source
Fix: inline code next to the link inside bold, edited elsewhere and saved (rendered) 06_fix_code_adjacent_rendered 06_fix_code_adjacent_rendered
Fix: a bare URL inside bold, edited elsewhere and saved (source) 07_fix_autolink_source 07_fix_autolink_source
Edge: two links in one bold span, edited elsewhere and saved (source) 08_edge_two_links_source 08_edge_two_links_source
Edge: a bold link inside a nested list item and a table cell, edited elsewhere and saved (source) 09_edge_nested_list_table_source 09_edge_nested_list_table_source
Edge, unchanged by design: a bold span that begins with a link keeps the link outermost on both sides (source) 10_edge_bold_starts_with_link_source 10_edge_bold_starts_with_link_source
Unchanged: marks inside a link, bold text and a plain link, edited elsewhere and saved (source) 11_same_marks_inside_link_source 11_same_marks_inside_link_source
Unchanged: the same description rendered 12_same_marks_inside_link_rendered 12_same_marks_inside_link_rendered

Each row seeds the description, opens it in rich text, types " (edited)" on the plain first line, saves, then shows the description rendered or its saved markdown reopened in the plain text editor. Row 5 follows the report's own steps instead (select the line with the link, apply bold). Rows 10 to 12 are byte-identical between the sides.

Recordings

Story Before After
Bold text with a link followed by inline code; a word typed on another line and saved: the rendered link text shows literal asterisks before the fix code_adjacent_before code_adjacent_after
The report's steps: a line with a link selected in rich text, bold applied, saved: the split shows in the saved markdown, the rendered bold looks the same typed_bold_before typed_bold_after
Unchanged: marks inside a link, bold text and a plain link, after the same edit-and-save preserved_before preserved_after

Four beats per side: rendered, editor with the edit typed, rendered after save, saved markdown. Beats before the save are byte-identical; the first story diverges at the rendered beat, the second only at the source beat, the third never.

How to set up and validate locally

  1. Set an issue description in the plain text editor to **see [docs](https://docs.gitlab.com) now** and save.
  2. Edit it, switch to rich text, type a word on any line, and save.
  3. Reopen the description in the plain text editor. Before: **see** [**docs**](https://docs.gitlab.com) **now**. After: the span is intact.
  4. Repeat with _read the [guide](url) first_ and ~~old [link](url) text~~ (strikethrough visibly breaks before the fix). Also try a bold span holding a link followed by an inline code span: before the fix the rendered link text shows literal asterisks.
  5. In rich text, type see docs now, link docs, select the line, press Cmd+B and save: the markdown stays **see [docs](url) now**.
  6. Regressions: [**docs**](url), a plain link, **bold** and a link in one paragraph, and a bold link in a list item or table cell all round-trip unchanged.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Paul W

Merge request reports

Loading
Loading