Fix copying HTML comments in the rich text editor

What does this MR do and why?

The rich text editor's copy and cut handler builds the text/html clipboard payload with ProseMirror's DOMSerializer, which renders every node in the selection through its toDOM spec. The htmlComment node (an HTML comment, shown as a grey dashed placeholder) had a Vue node view but no renderHTML, so the serializer threw this.nodes[node.type.name] is not a function for any selection containing a comment. The throw happens before the handler writes any clipboard format or prevents the default action, so the browser's native copy runs instead: the clipboard gets the node view's DOM as HTML, and pasting that either turns the comment into plain text or, when the selection also holds a label reference, throws again while parsing. Issue and merge request templates put an HTML comment in most descriptions, so selecting a description and copying it failed for anyone using the rich text editor.

Give the comment node a DOM representation: an empty comment element that carries its text in data-description. Other applications paste nothing for it, the same as for a real HTML comment, and the editor's HTML parser reads it back through the comment tag rule the markdown deserializer already feeds (the deserializer turns <!-- --> into comment elements before parsing). The text/x-gfm payload, which the editor prefers when pasting into itself and which the plain text editor reads, keeps carrying <!-- text --> unchanged, and text/plain keeps only the visible text.

Three node types exist only to host a plugin or a parse rule and never enter a document (colorChip, loading, suggestions). They get a minimal renderer so the editor factory spec can assert, without exceptions, that every node and mark type in the schema has a DOM representation. prosemirror-view renders through the same toDOM in its drag serializer and as the fallback for nodes without a node view, so the invariant covers those paths too.

Nothing changes for selections without a comment: the HTML, plain text and markdown payloads for paragraphs, lists, tables and colour chips are the same as before.

Resolves #498708 (closed)

References

  • Reproduced with the reporter's steps on the merge base: a description containing <!-- describe the bug --> and a label reference, select all, copy, delete, paste. The copy logs Uncaught TypeError: this.nodes[t.type.name] is not a function from copy_paste.js, and the paste logs Cannot read properties of null (reading 'className') from reference.js and inserts nothing. Without the label, the paste brings the comment's text back as a normal paragraph. Cut fails in two shapes: when the selection starts in a text paragraph, Chrome performs its native cut and the paste brings the comment back as italic text; when the selection starts on the comment block, nothing is cut and the clipboard keeps its previous content.
  • New jest examples: 9 fail at the merge base and pass with this change. In the copy/paste extension spec, copying and cutting a selection with a comment writes text/plain, text/html and text/x-gfm and prevents the default action, cut still removes the selection, and pasting the HTML payload back restores the comment node. A new html_comment extension spec covers the deserializer's comment element, the rendered element and its round trip. The editor factory spec asserts that every node type (except the top node) and every mark type has a DOM representation; at the merge base it lists colorChip, htmlComment, loading and suggestions. The touched specs pass 75/75 under Vue 2 and Vue 3.
  • The comment node arrived in Add support for rendering HTML comments in rich text editor (!158618) with a node view and no renderHTML; the clipboard handler has used DOMSerializer since 2023.
  • The issue's second error is the paste of the natively copied HTML: referenceLabel parses span.gl-label, and the label chip's node view DOM has no anchor, so getAnchor(element).className fails. With the copy handler working, a paste inside the editor takes the text/x-gfm path and never parses that HTML. Hardening that parse rule against external HTML is left out of this change.

Screenshots or screen recordings

Scenario Before After
Fix: the issue's case. A description with a template comment and a label reference, select all, copy, delete, paste 01_fix_issue_repro_comment_and_label 01_fix_issue_repro_comment_and_label
Fix: comment and paragraph, select all, copy, delete, paste 02_fix_copy_comment_and_paragraph 02_fix_copy_comment_and_paragraph
Fix: comment and paragraph, select all, cut, paste (the selection starts in a text paragraph) 03_fix_cut_comment_and_paragraph 03_fix_cut_comment_and_paragraph
Fix: select all, cut, paste when the selection starts on the comment block (a known paragraph was copied first) 12_fix_cut_starting_on_comment 12_fix_cut_starting_on_comment
Edge: comment inside a list item, copied with the list 04_edge_comment_inside_list_item 04_edge_comment_inside_list_item
Edge: comment inside a table cell, copied with the table 05_edge_comment_inside_table_cell 05_edge_comment_inside_table_cell
Edge: selection from mid-paragraph, across the comment block, to mid-paragraph, pasted below 06_edge_partial_selection_across_comment 06_edge_partial_selection_across_comment
Edge: copied in rich text, pasted into the plain text editor 07_edge_paste_into_plain_text_editor 07_edge_paste_into_plain_text_editor
Unchanged: paragraph with bold text copied and pasted 08_same_plain_paragraph 08_same_plain_paragraph
Unchanged: nested list copied and pasted 09_same_nested_list 09_same_nested_list
Unchanged: table copied and pasted 10_same_table 10_same_table
Unchanged: paragraph with a colour chip copied and pasted 11_same_colour_chip 11_same_colour_chip

Every row is staged in real headless Chrome with the real clipboard: the markdown is entered in the plain text editor, the editor is switched to rich text, the content is selected and copied or cut with the keyboard, then pasted. Before the fix the copy handler throws before writing to the clipboard, so the before side shows what the browser's native copy leaves behind: the comment's text as a normal paragraph (or as italic text after a native cut), an empty editor when the paste itself fails on the label chip's HTML, or nothing cut and the previous clipboard content pasted when the selection starts on the comment block. The four Unchanged rows are byte-identical between before and after.

Recording

The issue's case as a sequence: all selected, copied and deleted, pasted. The first two frames are byte-identical between the two sides; they diverge at the paste.

Before After
before_issue_repro after_issue_repro

How to set up and validate locally

  1. In a project that has a label (bug below), open the new issue page. With the description in plain text mode, enter:
    <!-- describe the bug -->
    
    ~bug is broken
  2. Switch to rich text editing: the comment renders as a grey dashed placeholder and the label as a chip.
  3. Select all and copy. Before the fix, the browser console shows Uncaught TypeError: this.nodes[t.type.name] is not a function. After the fix, no error.
  4. Delete the content and paste. Before the fix, nothing is pasted and the console shows Cannot read properties of null (reading 'className'). After the fix, the comment and the label come back.
  5. Repeat with cut instead of copy (once with the selection starting on the comment block, once starting in a paragraph above it), with the comment inside a list item and inside a table cell, and with a selection that only partly covers the paragraphs around the comment.
  6. Switch to plain text editing, select all in the text area and paste: the markdown includes <!-- describe the bug -->.
  7. Regression checks: copy and paste a plain paragraph, a nested list, a table and a paragraph with a colour chip such as `#FF0000`; they behave as before.

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