diff --git a/Cargo.lock b/Cargo.lock index 0cd795aa35a0db..4d46c193135b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5328,8 +5328,8 @@ dependencies = [ [[package]] name = "dugong" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "dugong-graphlib", "rustc-hash 2.1.1", @@ -5339,8 +5339,8 @@ dependencies = [ [[package]] name = "dugong-graphlib" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "hashbrown 0.16.1", "rustc-hash 2.1.1", @@ -10849,8 +10849,8 @@ dependencies = [ [[package]] name = "manatee" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "indexmap 2.11.4", "nalgebra", @@ -11145,8 +11145,8 @@ dependencies = [ [[package]] name = "merman" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "merman-core", "merman-render", @@ -11155,8 +11155,8 @@ dependencies = [ [[package]] name = "merman-core" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "chrono", "euclid", @@ -11181,8 +11181,8 @@ dependencies = [ [[package]] name = "merman-render" -version = "0.4.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +version = "0.6.2" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "base64 0.22.1", "chrono", @@ -15684,7 +15684,7 @@ dependencies = [ [[package]] name = "roughr-merman" version = "0.12.0" -source = "git+https://github.com/zed-industries/merman?rev=1c765dcca2ef5092fcde7bebe8374819563623ef#1c765dcca2ef5092fcde7bebe8374819563623ef" +source = "git+https://github.com/zed-industries/merman?tag=v0.6.2-with-patches#06094471f97acb10d0eebf8b92bac19ba2928eea" dependencies = [ "derive_builder", "euclid", diff --git a/crates/mermaid_render/Cargo.toml b/crates/mermaid_render/Cargo.toml index 6706e028d1d15c..bf826ab6334e5e 100644 --- a/crates/mermaid_render/Cargo.toml +++ b/crates/mermaid_render/Cargo.toml @@ -18,7 +18,7 @@ test-support = [] [dependencies] anyhow.workspace = true gpui.workspace = true -merman = { git = "https://github.com/zed-industries/merman", rev = "1c765dcca2ef5092fcde7bebe8374819563623ef", features = ["render"] } +merman = { git = "https://github.com/zed-industries/merman", tag = "v0.6.2-with-patches", features = ["render"] } quick-xml.workspace = true serde_json.workspace = true diff --git a/crates/mermaid_render/src/mermaid_render.rs b/crates/mermaid_render/src/mermaid_render.rs index 1e17d8d780b91f..fdd3e80faaafd5 100644 --- a/crates/mermaid_render/src/mermaid_render.rs +++ b/crates/mermaid_render/src/mermaid_render.rs @@ -16,20 +16,21 @@ //! //! This module uses the [`merman`] crate for rendering, rather than //! `mermaid-rs`, which was used in the previous implementation of mermaid -//! rendering in Zed. Merman provides significantly more accurate rendering, and -//! seems to be somewhat faster, but by default has poor CSS, making diagrams -//! look weird without significant cleanup. This is made worse by the fact that -//! `usvg`/`resvg` doesn't support some features that [`merman`] relies on. +//! rendering in Zed. //! -//! As such, this crate is quite large. But the code is very self-contained, and -//! has few dependencies. In fact, the [`gpui`] dependency is only needed for -//! the [`Hsla`] and [`Rgba`] color types. +//! Historically, this crate also carried generic `usvg`/`resvg` cleanup for SVG +//! constructs that merman's parity output could emit, such as HTML labels in +//! `` and CSS/attribute forms that rasterizers do not handle. +//! Since merman 0.6, that generic cleanup is exposed as merman's raster-safe SVG +//! pipeline. Zed opts into that pipeline during rendering, then keeps +//! editor-specific theme and accent color rules in this crate. The [`gpui`] +//! dependency is only needed for the [`Hsla`] and [`Rgba`] color types. //! //! The [`render_to_svg`] function operates in two stages: -//! - [`render`] the mermaid text to SVG using [`merman`]. -//! - [`postprocess`] the SVG to clean incorrect output and add styling. +//! - [`render`] the mermaid text to raster-safe SVG using [`merman`]. +//! - [`postprocess`] the SVG to add Zed theme and accent styling. //! -//! The postprocessing is also split up into stages. We parse the generated SVG +//! Zed's postprocessing is split up into stages. We parse the generated SVG //! using [`quick_xml`], which produces an iterator of //! [`Event<'_>`](quick_xml::events::Event)s. This iterator is then repeatedly //! transformed, and finally collected back into an SVG string. @@ -179,3 +180,24 @@ pub fn render_to_svg(source: &str, theme: &MermaidTheme) -> Result { let svg = postprocess::postprocess(&svg, theme)?; Ok(svg) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A flowchart with mutually nested subgraphs (`A` contains `B` and `B` + /// contains `A`) is an invalid containment cycle. Rendering it must return + /// gracefully rather than overflowing the stack and aborting the process. + #[test] + fn cyclic_subgraphs_do_not_crash() { + let source = "flowchart TD\n subgraph A\n B\n end\n subgraph B\n A\n end"; + let result = render_to_svg(source, &MermaidTheme::default()); + if let Err(err) = result { + let message = format!("{err:#}"); + assert!( + message.contains("cycle"), + "expected a cycle-related error, got: {message}" + ); + } + } +} diff --git a/crates/mermaid_render/src/postprocess.rs b/crates/mermaid_render/src/postprocess.rs index af1e61f3367c34..1f3b818c47f6fb 100644 --- a/crates/mermaid_render/src/postprocess.rs +++ b/crates/mermaid_render/src/postprocess.rs @@ -1,4 +1,4 @@ -//! Post-processing of [`merman`]-produced SVGs for rasterization with `usvg`/`resvg`. +//! Zed-specific post-processing of [`merman`]-produced SVGs. //! //! Each submodule is a specific pass that tweaks the SVG event iterator in a particular way. //! @@ -13,11 +13,8 @@ mod accent_colors; mod element_fixup; -mod fallback_fixup; -mod foreignobject_wrap; mod inject_css; mod strip_foreignobject; -mod strip_invalid_css; pub(crate) mod util; use anyhow::{Context as _, Result}; @@ -27,27 +24,21 @@ use quick_xml::events::Event; use crate::MermaidTheme; pub(super) fn postprocess(svg: &str, theme: &MermaidTheme) -> Result { - // Pass 1: foreignObject preparation (\n fix + word wrapping) - let svg = foreignobject_wrap::process(svg)?; + // merman 0.6 already applies the generic resvg-safe cleanup before this point. + // The remaining passes are Zed-specific theme and accent adjustments. + let svg_id = extract_svg_id(svg); - // Add fallbacks alongside elements - let svg = merman::render::foreign_object_label_fallback_svg_text(&svg); - - // Extract SVG id for CSS scoping (quick scan of the first element) - let svg_id = extract_svg_id(&svg); - - // Pass 2: themed post-processing pipeline. - // Each adapter takes an iterator of events and returns an iterator of events. - // Events borrow from the `svg` string — no .into_owned() per event. - let mut reader = Reader::from_str(&svg); + let mut reader = Reader::from_str(svg); reader.config_mut().check_end_names = false; let events = ReaderIter::new(reader); - let events = strip_foreignobject::process(events); - let events = fallback_fixup::process(events, theme); + // merman's resvg-safe pipeline already removes foreignObject elements and + // replaces their labels with native fallback groups. This pass keeps + // those fallback labels, but drops any that merely duplicate a native + // (e.g. user journey renders some labels both ways). + let events = strip_foreignobject::process(events, svg); let events = element_fixup::process(events, theme); let events = accent_colors::process(events, theme); - let events = strip_invalid_css::process(events); let events = inject_css::process(events, theme, &svg_id); let mut writer = quick_xml::Writer::new(Vec::with_capacity(svg.len())); @@ -111,26 +102,3 @@ impl<'a> Iterator for ReaderIter<'a> { } } } - -#[cfg(test)] -mod tests { - use super::*; - - fn default_theme() -> MermaidTheme { - MermaidTheme::default() - } - - #[test] - fn strip_css_handles_style_element_with_attributes() { - let svg = r#""#; - let result = postprocess(svg, &default_theme()).unwrap(); - assert!( - !result.contains("@keyframes"), - "Unsupported @keyframes should be stripped from