diff --git a/src/render/markdown/escape.rs b/src/render/markdown/escape.rs index f2654376..28ddb277 100644 --- a/src/render/markdown/escape.rs +++ b/src/render/markdown/escape.rs @@ -178,6 +178,33 @@ pub(crate) fn escape_url_as_text(url: &str, ctx: InlineContext) -> String { ) } +/// Prepare a code span's text for a table cell, where a pipe is the only +/// character between the fences that is still syntax. +/// +/// A backslash run already sitting in front of a pipe would pair off with the +/// escape and leave the pipe bare, so it is doubled to keep the escape intact. +/// That doubling survives into the rendered code span: GFM has no encoding for +/// a code span that contains a backslash immediately before a pipe, and an +/// intact row is worth more than the exact backslash count. +pub(crate) fn escape_cell_code_span(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + let mut backslashes = 0; + for c in text.chars() { + match c { + '|' => { + for _ in 0..=backslashes { + out.push('\\'); + } + backslashes = 0; + } + '\\' => backslashes += 1, + _ => backslashes = 0, + } + out.push(c); + } + out +} + /// Shortest backtick fence longer than any backtick run in `text`. pub(crate) fn backtick_fence(text: &str, min: usize) -> String { let longest_run = text.split(|c| c != '`').map(str::len).max().unwrap_or(0); diff --git a/src/render/markdown/inline.rs b/src/render/markdown/inline.rs index 1461e338..83a25699 100644 --- a/src/render/markdown/inline.rs +++ b/src/render/markdown/inline.rs @@ -3,7 +3,8 @@ use crate::model::{ImageSource, Inline, LinkTarget, Style, inlines_are_empty}; use crate::render::markdown::Ctx; use crate::render::markdown::escape::{ - EscapeOpts, InlineContext, backtick_fence, escape_text, escape_url_as_text, format_url, + EscapeOpts, InlineContext, backtick_fence, escape_cell_code_span, escape_text, + escape_url_as_text, format_url, }; use std::borrow::Cow; use std::fmt::Write as _; @@ -205,7 +206,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,9 +233,15 @@ 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', " "); let fence = backtick_fence(&text, 1); let pad = if text.starts_with('`') || text.ends_with('`') { " " } else { "" }; + // A row is split into cells before any code span is parsed, so a pipe is + // syntax here even though everything else between the fences is literal. + let text = match ctx { + InlineContext::TableCell => escape_cell_code_span(&text), + _ => text, + }; let _ = write!(out, "{fence}{pad}{text}{pad}{fence}"); } diff --git a/src/render/markdown/table.rs b/src/render/markdown/table.rs index 0c416c67..88f2a3de 100644 --- a/src/render/markdown/table.rs +++ b/src/render/markdown/table.rs @@ -5,7 +5,7 @@ use crate::model::{Block, Cell, CellSlot, Table}; use crate::render::markdown::Ctx; use crate::render::markdown::escape::InlineContext; -use crate::render::markdown::inline::render_inlines; +use crate::render::markdown::inline::{push_code_span, render_inlines}; struct RenderedCell { text: String, @@ -166,7 +166,7 @@ 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); + 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 a4aa8067..b6ea835e 100644 --- a/src/render/markdown/tests.rs +++ b/src/render/markdown/tests.rs @@ -295,6 +295,18 @@ 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 code = |t: &str| { + Cell::from_inlines(vec![Inline::Text { + text: t.into(), + style: Style { code: true, ..Style::PLAIN }, + }]) + }; + let md = doc(vec![table_from(vec![vec![code("a | b"), code(r"a \| b")]], 0)]); + assert_eq!(md, concat!("| | |\n", "| --- | --- |\n", r"| `a \| b` | `a \\\| b` |", "\n")); +} + #[test] fn url_angle_brackets_are_encoded_without_bracketing() { let md = doc(vec![Block::Paragraph(vec![Inline::Link {