Rich text editor writes an unrenderable closing delimiter when a formatted run ends at a link

Summary

When a bold, italic or strikethrough run in the rich text editor ends with a link and the next character is a letter or digit with no space (for example bold applied over see docs where docs is a link, then ify typed directly after the link), the markdown serializer writes the closing delimiter directly after the link: **see [docs](https://docs.gitlab.com)**ify. CommonMark does not treat a delimiter run that sits between ) and a letter as a closer, so the asterisks render literally.

Before Keep formatting marks in one span around links in the rich text editor (!254236) the serializer split the run instead (**see** [**docs**](https://docs.gitlab.com)ify), which rendered. With Fix reference spacing, intraword emphasis and quoted references on rich text editor save (!254251) the italic form behaves the same way (x*y [z](u)*w).

A second edge shares the root. A run that begins with a link and continues with a space and another marked run, **[docs](url) [more](url2)**, serializes as [**docs **](url)[**more**](url) before and after !254236: prosemirror-markdown's renderInline handles a whitespace-only text node whose marks differ from the open ones by pushing the space into the link text.

Steps to reproduce

  1. In a description in rich text mode, type see docs, turn docs into a link, select see docs and apply bold.
  2. Place the caret directly after the link and type ify with no space.
  3. Save, then view the rendered description and the plain text source.

Expected: the text renders as bold with the link inside it, and the saved markdown is renderable.

Actual: the rendered text shows literal ** after the link; the saved markdown has a closing delimiter in a position the parser cannot use.

Root cause

app/assets/javascripts/content_editor/services/serializer/link.js sets mixable: true (from !254236), so prosemirror-markdown keeps an open mark across a link, which fixes the common case of formatting wrapping a link. prosemirror-markdown has no flanking check: it does not know that a closer directly after ) and directly before a word character cannot close. The whitespace-only-node edge comes from renderInline reordering marks on a text node that contains only a space.

Proposed fix

In the markdown serializer, when a mark's closing delimiter would land directly after a link and directly before a letter, digit or underscore, emit the split form for that run (close the mark before the link and reopen it inside the link text), which the parser reads correctly. Keep a whitespace-only text node outside the link when its marks differ from the open ones. Cover bold, italic, strikethrough and bold plus italic at word boundaries and inside words in spec/frontend/content_editor/services/serializer/link_spec.js. The same change may be worth proposing upstream to prosemirror-markdown.

Impact

A survey of the 55,755 open gitlab-org/gitlab issue descriptions on 2026-09-09 (local mirror of the tracker) found 1,761 descriptions with a bold, italic or strikethrough run wrapping a link (the shape !254236 protects on the next rich text edit), 553 with a run ending exactly at a link followed by a space or punctuation (unaffected either way), and 3 with a closing delimiter directly before a letter (already unrenderable on master). The edge can only be produced by an edit in rich text; no stored markdown round-trips into it.

Related: Formatting text with links breaks in rich-text editor (RTE) (#514245).