diff --git a/crates/formats/src/markdown.rs b/crates/formats/src/markdown.rs index 93ef23efe..23a628e02 100644 --- a/crates/formats/src/markdown.rs +++ b/crates/formats/src/markdown.rs @@ -1335,7 +1335,9 @@ fn write_blocks(blocks: &[FBlock], out: &mut Vec) { i += 1; } let longest = lines.iter().map(|l| l.chars().filter(|c| *c == '`').count()).max().unwrap_or(0); - let fence = "`".repeat(3.max(longest + 1).min(64)); + // The fence must be longer than the longest run in the content, or a content + // line is read back as the closing fence (the `.min(64)` cap did exactly that). + let fence = "`".repeat(3.max(longest + 1)); out.push(fence.clone()); out.extend(lines); out.push(fence); diff --git a/crates/formats/src/tests.rs b/crates/formats/src/tests.rs index caad39dce..b8704e716 100644 --- a/crates/formats/src/tests.rs +++ b/crates/formats/src/tests.rs @@ -214,6 +214,20 @@ fn markdown_round_trip() { check_round_trip("md", true); } +/// A code line of backticks must not be mistaken for the closing fence when the file comes +/// back: the writer's fence has to be one backtick longer than any content line. +#[test] +fn markdown_code_fence_outgrows_its_content() { + let line = "`".repeat(100); + let doc = import("md", format!(" {line}\n").as_bytes()).unwrap().unwrap(); + assert_eq!(doc.plain_text(Default::default()).trim(), line); + let out = export("md", &doc).unwrap().unwrap(); + let text = String::from_utf8(out).unwrap(); + assert!(text.contains(&"`".repeat(101)), "the fence is longer than the content: {text}"); + let back = import("md", text.as_bytes()).unwrap().unwrap(); + assert_eq!(back.plain_text(Default::default()).trim(), line, "the code line survives a round trip"); +} + #[test] fn html_round_trip() { check_round_trip("html", true);