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:
1 parent
ddd9b3a3f0
commit
b8649f3464
2 files changed
+86
-20
No files matched your search
@@ -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!(
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user