Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/render/markdown/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ fn render_text_run(
}
if !core.is_empty() {
if style.code {
push_code_span(core, out);
push_code_span(core, ctx, out);
} else {
let mut open = String::new();
if style.strike {
Expand All @@ -232,8 +232,10 @@ fn render_text_run(
}
}

pub(crate) fn push_code_span(text: &str, out: &mut String) {
pub(crate) fn push_code_span(text: &str, ctx: InlineContext, out: &mut String) {
let text = text.replace('\n', " ");
// A raw pipe splits a GFM table cell even inside a code span.
let text = if ctx == InlineContext::TableCell { text.replace('|', "\\|") } else { text };

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When code content already has an odd number of backslashes before |, this replacement makes the run even and the generated GFM row can still split at that pipe. Add a backslash only when the existing run is even, and cover code such as a \| b with a regression test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/render/markdown/inline.rs, line 238:

<comment>When code content already has an odd number of backslashes before `|`, this replacement makes the run even and the generated GFM row can still split at that pipe. Add a backslash only when the existing run is even, and cover code such as `a \| b` with a regression test.</comment>

<file context>
@@ -232,8 +232,10 @@ fn render_text_run(
+pub(crate) fn push_code_span(text: &str, ctx: InlineContext, out: &mut String) {
     let text = text.replace('\n', " ");
+    // A raw pipe splits a GFM table cell even inside a code span.
+    let text = if ctx == InlineContext::TableCell { text.replace('|', "\\|") } else { text };
     let fence = backtick_fence(&text, 1);
     let pad = if text.starts_with('`') || text.ends_with('`') { " " } else { "" };
</file context>
Suggested change
let text = if ctx == InlineContext::TableCell { text.replace('|', "\\|") } else { text };
let text = if ctx == InlineContext::TableCell {
let mut escaped = String::with_capacity(text.len());
let mut backslashes = 0;
for c in text.chars() {
if c == '|' && backslashes % 2 == 0 {
escaped.push('\\');
}
escaped.push(c);
backslashes = if c == '\\' { backslashes + 1 } else { 0 };
}
escaped
} else {
text
};
Fix with cubic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful look — I checked this against the GFM reference implementation and against GitHub itself, and the row cannot split there: GFM's cell scanning is not parity-based. The cell scanner (ext_scanners.re L32–L35, table_cell = (escaped_char|[^|\r\n])+) matches greedily, so a pipe preceded by any backslash never ends a cell, and unescape_pipes() then strips exactly one backslash before each |. So for code text a \| b, the emitted `a \\| b` renders back as <code>a \| b</code> — an exact round-trip.

Verified via GitHub's own renderer (gh api /markdown):

| A | B |
| --- | --- |
| `one \| two` | X |
| `one \\| two` | Y |

Both rows keep two columns, and the cells come back as <code>one | two</code> and <code>one \| two</code>. pulldown-cmark matches GitHub exactly; comrak also keeps every row intact.

The parity variant would instead leave a \| b unescaped, and GFM unescapes \|| inside code spans too (spec §4.10, example 200), so the backslash would be silently dropped — on every renderer I tested (GitHub, comrak, pulldown-cmark, marked, micromark).

For completeness: marked and micromark do split on `one \\| two` — their cell splitters count backslash parity and deviate from cmark-gfm here. Under those parsers a backslash directly before a pipe inside a code span is unrepresentable either way (escaped, the row splits; unescaped, the backslash is lost), so I kept the encoding that GitHub and the reference implementation render exactly.

Added a regression test covering a \| b in d6fdf04.

let fence = backtick_fence(&text, 1);
let pad = if text.starts_with('`') || text.ends_with('`') { " " } else { "" };
let _ = write!(out, "{fence}{pad}{text}{pad}{fence}");
Expand Down
6 changes: 5 additions & 1 deletion src/render/markdown/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ fn cell_block_text(block: &Block, rc: &Ctx, parts: &mut Vec<String>) {
let t = text.trim();
if !t.is_empty() {
let mut s = String::new();
crate::render::markdown::inline::push_code_span(t, &mut s);
crate::render::markdown::inline::push_code_span(
t,
InlineContext::TableCell,
&mut s,
);
parts.push(s);
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/render/markdown/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,31 @@ fn url_pipes_cannot_split_table_cells() {
assert_eq!(md, "| |\n| --- |\n| [https://e.test/a\\|b](https://e.test/a%7Cb) |\n");
}

#[test]
fn code_span_pipes_cannot_split_table_cells() {
let md = doc(vec![table_from(
vec![
vec![
Cell::from_inlines(vec![Inline::plain("Operator")]),
Cell::from_inlines(vec![Inline::plain("Meaning")]),
],
vec![
Cell::from_inlines(vec![styled("a | b", Style { code: true, ..Style::PLAIN })]),
Cell::from_inlines(vec![Inline::plain("bitwise or")]),
],
],
1,
)]);
assert_eq!(md, "| Operator | Meaning |\n| --- | --- |\n| `a \\| b` | bitwise or |\n");
}

#[test]
fn code_block_pipes_cannot_split_table_cells() {
let cell = Cell::new(vec![Block::CodeBlock { lang: None, text: "ls | wc -l".into() }]);
let md = doc(vec![table_from(vec![vec![cell]], 0)]);
assert_eq!(md, "| |\n| --- |\n| `ls \\| wc -l` |\n");
}

#[test]
fn url_angle_brackets_are_encoded_without_bracketing() {
let md = doc(vec![Block::Paragraph(vec![Inline::Link {
Expand Down