diff --git a/src/render/markdown/inline.rs b/src/render/markdown/inline.rs index 1461e33..35fd089 100644 --- a/src/render/markdown/inline.rs +++ b/src/render/markdown/inline.rs @@ -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 { @@ -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 }; let fence = backtick_fence(&text, 1); let pad = if text.starts_with('`') || text.ends_with('`') { " " } else { "" }; let _ = write!(out, "{fence}{pad}{text}{pad}{fence}"); diff --git a/src/render/markdown/table.rs b/src/render/markdown/table.rs index 0c416c6..6a2dd26 100644 --- a/src/render/markdown/table.rs +++ b/src/render/markdown/table.rs @@ -166,7 +166,11 @@ fn cell_block_text(block: &Block, rc: &Ctx, parts: &mut Vec) { 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); } } diff --git a/src/render/markdown/tests.rs b/src/render/markdown/tests.rs index a4aa806..0c9c75c 100644 --- a/src/render/markdown/tests.rs +++ b/src/render/markdown/tests.rs @@ -295,6 +295,53 @@ 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 code_span_backslash_before_pipe_round_trips() { + // GFM's cell scanner never splits at a pipe preceded by a backslash + // (`table_cell = (escaped_char|[^|\r\n])+` is matched greedily, no parity + // counting), and unescape_pipes() strips exactly one backslash before each + // `|`. Emitting `\\|` for code text `\|` therefore renders back as `\|`. + let md = doc(vec![table_from( + vec![ + vec![ + Cell::from_inlines(vec![Inline::plain("Pattern")]), + 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("BRE alternation")]), + ], + ], + 1, + )]); + assert_eq!(md, "| Pattern | Meaning |\n| --- | --- |\n| `a \\\\| b` | BRE alternation |\n"); +} + #[test] fn url_angle_brackets_are_encoded_without_bracketing() { let md = doc(vec![Block::Paragraph(vec![Inline::Link {