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 <heysmitbarmase@gmail.com>
This commit is contained in:
Kim SeogyuandSmit Barmase authored and GitHub committed 2026-08-10 13:04:37 +00:00
1 parent ddd9b3a3f0
commit b8649f3464
2 files changed
+86 -20

No files matched your search

+17
View File
@@ -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!(
+69 -20
View File
@@ -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<usize> {
pub(crate) fn extract_code_block_content_range(text: &str) -> Range<usize> {
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]