From b8649f34643dc250faa0918ae1f80fc58df2c25a Mon Sep 17 00:00:00 2001 From: Kim Seogyu Date: Mon, 10 Aug 2026 22:04:37 +0900 Subject: [PATCH] markdown: Support triple-tilde Mermaid fences (#62257) # Objective Zed's Markdown parser accepts tilde-fenced code blocks, but Mermaid extraction only strips backtick fences. As a result, a block like this is parsed as Mermaid while the fence itself is still passed to the renderer: ```markdown ~~~mermaid graph TD; ~~~ ``` ## Solution Teach the Markdown code-block helpers to recognize triple-tilde fences alongside triple-backtick fences. The change stays in the existing parsing path, so Mermaid rendering does not need a separate special case. A regression test covers extraction from a tilde-fenced Mermaid block. ## Testing - `cargo fmt --all -- --check` - `cargo test -p markdown` (138 tests) - `./script/clippy -p markdown` - `cargo build -p zed` - Opened a `~~~mermaid` block in the built Zed Dev app on macOS and verified that Markdown Preview renders the diagram ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed Mermaid diagrams in Markdown previews when they use triple-tilde fences. --------- Co-authored-by: Smit Barmase --- crates/markdown/src/mermaid.rs | 17 +++++++ crates/markdown/src/parser.rs | 89 ++++++++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs index 32dccf67ce..0d4bc76108 100644 --- a/crates/markdown/src/mermaid.rs +++ b/crates/markdown/src/mermaid.rs @@ -1141,6 +1141,23 @@ mod tests { assert_eq!(diagram.contents.scale, 150); } + #[test] + fn test_extract_mermaid_diagrams_with_tilde_fence() { + let markdown = "~~~mermaid\ngraph TD;\n~~~"; + let events = + crate::parser::parse_markdown_with_options(markdown, false, false, false).events; + let diagrams = extract_mermaid_diagrams(markdown, &events); + + assert_eq!(diagrams.len(), 1); + assert_eq!( + diagrams + .values() + .next() + .map(|diagram| diagram.contents.contents.as_ref()), + Some("graph TD;") + ); + } + #[test] fn test_unsupported_diagram_types_are_skipped() { let markdown = concat!( diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index f70c0c69a4..fc9256d5ab 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -331,7 +331,9 @@ pub(crate) fn parse_markdown_with_options( ref info, )) => { within_code_block = true; - let content_range = extract_code_block_content_range(&text[range.clone()]); + let code_block_source = &text[range.clone()]; + let content_range = extract_code_block_content_range(code_block_source); + let is_fenced_closed = content_range.end < code_block_source.len(); let content_range = content_range.start + range.start..content_range.end + range.start; @@ -340,17 +342,6 @@ pub(crate) fn parse_markdown_with_options( .bytes() .filter(|c| *c == b'\n') .count(); - let is_fenced_closed = { - let code_block_source = &text[range.clone()]; - code_block_source - .trim_end() - .lines() - .last() - .is_some_and(|line| { - let trimmed = line.trim_start(); - trimmed.len() >= 3 && trimmed.chars().all(|c| c == '`') - }) - }; let metadata = CodeBlockMetadata { content_range, @@ -920,17 +911,51 @@ fn extract_code_content_range(text: &str) -> Range { pub(crate) fn extract_code_block_content_range(text: &str) -> Range { let mut range = 0..text.len(); - if text.starts_with("```") { - range.start += 3; + let Some(fence_character) = text + .as_bytes() + .first() + .copied() + .filter(|character| matches!(character, b'`' | b'~')) + else { + return range; + }; + let opening_fence_len = text + .bytes() + .take_while(|character| *character == fence_character) + .count(); + if opening_fence_len < 3 { + return range; + } - if let Some(newline_ix) = text[range.clone()].find('\n') { - range.start += newline_ix + 1; + range.start += opening_fence_len; + if let Some(newline_ix) = text[range.clone()].find('\n') { + range.start += newline_ix + 1; + } + + let text_without_line_ending = + text.trim_end_matches(|character| matches!(character, '\r' | '\n')); + let closing_line_start = text_without_line_ending + .rfind('\n') + .map_or(0, |newline_ix| newline_ix + 1); + if closing_line_start >= range.start { + let closing_line = &text_without_line_ending[closing_line_start..]; + let closing_fence = closing_line.trim_start_matches(' '); + let indentation_len = closing_line.len() - closing_fence.len(); + let closing_fence_len = closing_fence + .bytes() + .take_while(|character| *character == fence_character) + .count(); + let trailing_characters = &closing_fence[closing_fence_len..]; + if indentation_len <= 3 + && closing_fence_len >= opening_fence_len + && trailing_characters + .bytes() + .all(|character| matches!(character, b' ' | b'\t')) + { + range.end = closing_line_start; } } - if !range.is_empty() && text.ends_with("```") { - range.end -= 3; - } if range.start > range.end { range.end = range.start; } @@ -1315,6 +1340,18 @@ mod tests { ..Default::default() } ); + + for markdown in ["```mermaid\ngraph TD;\n~~~", "~~~~mermaid\ngraph TD;\n~~~"] { + let parsed = parse_markdown_with_options(markdown, false, false, false); + let metadata = parsed.events.iter().find_map(|(_, event)| match event { + Start(CodeBlock { metadata, .. }) => Some(metadata), + _ => None, + }); + assert_eq!( + metadata.map(|metadata| metadata.is_fenced_closed), + Some(false) + ); + } } fn assert_code_block_does_not_emit_links(markdown: &str) { @@ -1541,9 +1578,21 @@ mod tests { let input = "```python\nprint('hello')\nprint('world')\n```"; assert_eq!(extract_code_block_content_range(input), 10..40); + let input = "~~~~mermaid\ngraph TD;\n~~~~"; + let content_range = extract_code_block_content_range(input); + assert_eq!(&input[content_range], "graph TD;\n"); + + let input = "~~~mermaid\ngraph TD;\n ~~~~"; + let content_range = extract_code_block_content_range(input); + assert_eq!(&input[content_range], "graph TD;\n ~~~~"); + + let input = "~~~mermaid\ngraph TD;\n ~~~~ \t"; + let content_range = extract_code_block_content_range(input); + assert_eq!(&input[content_range], "graph TD;\n"); + // Malformed input let input = "`````"; - assert_eq!(extract_code_block_content_range(input), 3..3); + assert_eq!(extract_code_block_content_range(input), 5..5); } #[test]