Skip fenced code blocks when isolating quick actions in the rich text editor

What does this MR do and why?

Every switch from plain text to rich text adds two blank lines after each line inside a fenced code block that starts with /, and they compound: // comment, /usr/bin/env and /api/v4/projects all grow two more blank lines on each round trip. A Premium customer abandoned the rich text editor over it. Pasting such code into the editor does the same, because the paste path uses the same deserializer.

Root cause: before rendering, transformQuickActions appends two blank lines to every line starting with /, so a quick action renders as its own paragraph. It is a regex over the whole document with no notion of code blocks.

The transform is now fence-aware. It scans line by line, tracks opening and closing fences of backticks or tildes per CommonMark (up to three spaces of indentation, any info string, a closing fence of the same character at least as long with nothing after it), and leaves everything inside a fence alone. An unterminated fence runs to the end of the document. Lines outside fences are transformed exactly as before, including a quick action on the line straight after a closing fence.

Scope:

  • Indented code blocks needed no handling: their lines start with four spaces or a tab, so a slash is never at column 0.
  • Fences inside list items and blockquotes were already safe for the same reason, so the tracker follows only top-level fences. The unit spec pins both.
  • HTML blocks and the quick action grammar are untouched. The complete answer is the AST-level rework kivikakk sketched in the issue: recognise quick actions on the rendered document and drop the regex entirely. This is the smallest change that stops the damage now, and it disappears with the transform when that rework lands.

Unchanged: the reference-definition case the transform exists for, all output outside fences, four-space indentation, and a backtick in an info string not opening a fence.

Resolves #578108 (closed)

Related to #577963 (closed)

Evidence

  • Reproduced end to end against the real render API: the issue's own code block, switched and edited twice, comes back with four blank lines after // testing comment at the merge base and byte-identical here.
  • Unit spec: 11 of 24 examples fail at the merge base and pass here (backtick and tilde fences, info strings, indentation, longer closing fences, a nested shorter fence, an unterminated fence, quick actions around and after fences). The other 13 pin what must not change. content_editor tree 1219/1219 under Vue 2 and Vue 3.
  • New end-to-end example in the code block shared examples. Fails at the merge base with two blank lines after each of the two slash lines. Predictive RSpec does not map it, so it first runs at tier-2 or with pipeline:run-all-rspec.
  • Root cause independently confirmed in the issue thread by armin.pasalic, with himkp on why quick actions are wrapped and kivikakk on the AST-level fix.
  • Nine matrix rows and a GIF pair below. Two planned rows are still missing and follow in a re-capture; see the caption.
Origin of the transform, and duplicates

Fix reference style links after quick actions in RTE (!187001) added it so a reference definition after a quick action parses; Explain quick actions inline in RTE (!158367) is why a quick action needs its own paragraph.

Issue 578107 is the same report filed a minute earlier, closed as a duplicate. Issue 577963 is the same defect reported a day earlier and stays open until this merges.

Screenshots or screen recordings

Scenario Before After
Fix: the issue's code block with a // comment line, plain text after two round trips 01_fix_comment_lines 01_fix_comment_lines
Fix: a shebang line and two lines that start with a path (/usr/local/bin, /opt) 02_fix_shebang_and_paths 02_fix_shebang_and_paths
Fix: a tilde fence (the editor writes backtick fences on both sides) 03_fix_tilde_fence 03_fix_tilde_fence
Nested (unchanged on both sides): a code block inside a list item 04_nested_code_block_in_list_item 04_nested_code_block_in_list_item
Nested (unchanged on both sides): a code block inside a blockquote 05_nested_code_block_in_blockquote 05_nested_code_block_in_blockquote
Edge: /label bug on the line right after the closing fence, in rich text 06_edge_quick_action_after_closing_fence 06_edge_quick_action_after_closing_fence
Unchanged: /label bug on its own line in prose, in rich text 08_same_quick_action_in_prose 08_same_quick_action_in_prose
Unchanged: /label bug applied on save after a round trip (the created issue) 09_same_quick_action_applied_on_save 09_same_quick_action_applied_on_save
Unchanged: /assign on the line after a paragraph line 10_same_quick_action_after_paragraph_line 10_same_quick_action_after_paragraph_line

Every row is staged the same way in real headless Chrome, at 9db3b520 against the merge base 26225e7e: the markdown is set in the plain text editor, switched to rich text, a word is typed into the last paragraph so the editor re-serializes, switched back, and the cycle repeated once. The five Unchanged rows are byte-identical between before and after. Two planned rows are not attached because their staging did not complete on both refs (an unterminated fence at the end of the document, and a slash inside prose); they follow in a re-capture.

Recordings

Story Before After
The issue's code block through three switches to rich text, with a word typed under the block between them comment_lines_before comment_lines_after

Three beats per side, all in rich text: the block after the first, second and third switch. Before, the block gains two blank lines after the comment line on every switch (2, 4, 6). After, the three frames differ only by the typed words.

How to set up and validate locally

  1. On the new issue page, keep the description in plain text editing and paste:

    Notes
    
    ```
    var test1 10
    // testing comment
    /usr/local/bin/deploy --dry-run
    test 1
    ```
  2. Select Switch to rich text editing. Before: two blank lines after // testing comment and two after /usr/local/bin/deploy --dry-run. After: the block renders as typed.

  3. Type a word into Notes and switch back to plain text. Before: the blank lines are now in the markdown, and every further round trip adds two more. After: unchanged apart from the word.

  4. Regressions: /label ~bug on its own line under the block still shows its inline explanation and applies on save; a slash inside prose (see /docs/path) and a fence inside a list item or blockquote are unchanged on both sides; pasting the same block from the clipboard no longer adds blank lines.

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