Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ term_size = "0.3.2"
resvg = { version = "0.48.1", default-features = false, features = ["text", "system-fonts", "raster-images"] }
unicode-width = "0.2.2"
htmlentity = "1.3.2"
merman = { version = "0.8.0-alpha.3", default-features = false, features = ["render"] }
merman = { version = "0.8.0-alpha.3", default-features = false, features = ["render", "ascii"] }
# Pulled in directly only to enable label/URL sanitization, which strips external
# resource references (e.g. file:// image hrefs) before the SVG reaches resvg.
merman-core = { version = "0.8.0-alpha.3", default-features = false, features = ["full-sanitization"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Pipe your llm output through it, or a markdown file from the web, and it will di

- display images referenced in markdown in your terminal that supports the kitty protocol (kitty, ghostty, alacritty, and others)
- syntax-highlight fenced code blocks for many languages using [syntect](https://github.com/trishume/syntect)
- render [mermaid diagrams](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams) into images and display them in your terminal
- render [mermaid diagrams](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams) into images and display them in your terminal, or as box-drawing text diagrams when image output isn't available
- parse a subset of HTML and display it sensibly in the terminal
- accept input from stdin, files, or URLs
- Use [OSC8 hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) to make links clickable
Expand Down
36 changes: 33 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,9 +1490,13 @@ impl StreamingParser {
}

fn format_code_block(&self, lines: &[String], info: &str) -> String {
// Attempt to render mermaid diagrams as images when image protocol is enabled
if info.eq_ignore_ascii_case("mermaid") && self.image_protocol != ImageProtocol::None {
if let Some(rendered) = self.try_render_mermaid(lines) {
if info.eq_ignore_ascii_case("mermaid") {
if self.image_protocol != ImageProtocol::None {
if let Some(rendered) = self.try_render_mermaid(lines) {
return rendered;
}
}
if let Some(rendered) = self.try_render_mermaid_ascii(lines) {
return rendered;
}
// Fall through to normal code block rendering on failure
Expand Down Expand Up @@ -1584,6 +1588,32 @@ impl StreamingParser {
}
}

/// Try to render a mermaid diagram as a box-drawing text diagram.
/// Returns `None` when the diagram family has no ASCII renderer, rendering fails,
/// or the result is too wide for the output width (where terminal wrapping would
/// garble the drawing).
fn try_render_mermaid_ascii(&self, lines: &[String]) -> Option<String> {
let source = lines.join("\n");

let diagram = merman::ascii::HeadlessAsciiRenderer::new()
.with_charset(merman::ascii::AsciiCharset::Unicode)
.render_ascii_sync(&source)
.ok()??;

let diagram = diagram.trim_end_matches('\n');
if diagram.is_empty() {
return None;
}
if diagram
.lines()
.any(|line| self.display_width(line) > self.width)
{
return None;
}

Some(format!("{}\n\n", diagram))
}

/// Parse a task list item marker at the start of content.
/// Returns Some((is_checked, remaining_content)) if a task list marker is found.
/// Task list markers are: [ ] (unchecked) or [x]/[X] (checked), followed by whitespace.
Expand Down
39 changes: 35 additions & 4 deletions tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1589,19 +1589,50 @@ mod mermaid_rendering {
}

#[test]
fn test_mermaid_renders_as_code_without_images() {
fn test_mermaid_renders_as_ascii_without_images() {
let mut p = plain_parser();
let output = feed_all(&mut p, "```mermaid\nflowchart LR\n A-->B-->C\n```\n");
// Should NOT contain kitty protocol
assert!(
!output.contains("\x1b_G"),
"should not contain kitty graphics protocol"
);
// Should contain the mermaid source as code
let stripped = super::strip_ansi(&output);
assert!(
!stripped.contains("flowchart LR"),
"should render a diagram, not the source, got: {:?}",
stripped
);
assert!(
stripped.contains('│') && stripped.contains('A'),
"should draw a box-drawing diagram, got: {:?}",
stripped
);
}

#[test]
fn test_mermaid_ascii_unsupported_falls_back_to_code() {
let mut p = plain_parser();
let output = feed_all(&mut p, "```mermaid\nnotADiagramType\n a b c\n```\n");
let stripped = super::strip_ansi(&output);
assert!(
stripped.contains("notADiagramType"),
"fallback should show mermaid source as code, got: {:?}",
stripped
);
}

#[test]
fn test_mermaid_ascii_too_wide_falls_back_to_code() {
let mut p = StreamingParser::with_width("base16-ocean.dark", ImageProtocol::None, 20);
let output = feed_all(
&mut p,
"```mermaid\nflowchart LR\n Alpha-->Beta-->Gamma-->Delta\n```\n",
);
let stripped = super::strip_ansi(&output);
assert!(
stripped.contains("flowchart LR"),
"should show mermaid source as code"
"diagram wider than the output width should fall back to code, got: {:?}",
stripped
);
}

Expand Down