#8294·tiptap

[Bug] @tiptap/markdown: escaped pipe in a table cell is lost on serialize, then destroys cell content on the next round trip

Author: Abdo-El-MobayadCreated Sep 1, 2026Updated Sep 12, 2026
Labelsarea: markdowncomplexity: easyimpact: medium

Description

A GFM table cell containing an escaped pipe (\|) round-trips destructively through @tiptap/markdown: parsing reads the cell correctly, but serialization emits the pipe unescaped, so the next parse splits the cell at that pipe and the content after it is silently lost.

Affected versions

  • @tiptap/markdown 3.31.0
  • @tiptap/extension-table 3.31.0
  • @tiptap/starter-kit 3.31.0
  • Node 20.20.2, no DOM involved

Minimal reproduction

typescript
import { MarkdownManager } from "@tiptap/markdown";
import StarterKit from "@tiptap/starter-kit";
import { TableKit } from "@tiptap/extension-table";

const manager = new MarkdownManager({ extensions: [StarterKit, TableKit] });

const source = "| Col | Value |\n| --- | --- |\n| a | uses \| pipe |";

const pass1 = manager.serialize(manager.parse(source));
const pass2 = manager.serialize(manager.parse(pass1));

console.log(pass1);
console.log(pass2);

Actual output

Pass 1 (the escape is gone, so the cell markup is now broken):

| Col | Value       |
| --- | ----------- |
| a   | uses | pipe |

Pass 2 (the broken markup now parses as an extra column and the text after the pipe is gone):

| Col | Value |
| --- | ----- |
| a   | uses  |

Walking the parsed documents' text nodes shows the data loss happens at the pass-1 parse already: the source parses to cell text uses | pipe, but re-parsing pass 1 yields only uses.

Expected

serialize should re-escape a literal | inside table cell text as \| (the GFM spelling the parser itself understands), so serialize(parse(x)) reaches a fixpoint instead of losing content.

Notes on the likely cause

MarkdownManager.escapeMarkdownSyntax escapes [\`*_[\]~] but not |, and the table serialization path does not appear to apply escapeTableCellPipes (exported by @tiptap/extension-table) to text children, so cell text containing a literal pipe is emitted raw.

Thanks for the markdown work in v3; outside this edge it has held up very well under round-trip testing.