Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:42:07.099Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

mermaid_render.rs

238 lines · 9.7 KB · rust
1// for a very big json! macro
2#![recursion_limit = "256"]
3
4//! Crate for rendering Mermaid diagram strings to SVG strings.
5//!
6//! The entrypoint to this crate is [`render_to_svg`].
7//!
8//! It takes a `&str` and a [`MermaidTheme`]. The output is an SVG with the
9//! following properties:
10//! - The style matches the provided theme
11//! - Nodes are given accent colors, even if none are provided in the mermaid
12//!   source.
13//! - The SVG has been tweaked based on the assumption that it will be rasterized
14//!   using `usvg`/`resvg`. Some bugs/quirks of `usvg`/`resvg` are accounted for
15//!   in this crate.
16//!
17//! This module uses the [`merman`] crate for rendering, rather than
18//! `mermaid-rs`, which was used in the previous implementation of mermaid
19//! rendering in Zed.
20//!
21//! Historically, this crate also carried generic `usvg`/`resvg` cleanup for SVG
22//! constructs that merman's parity output could emit, such as HTML labels in
23//! `<foreignObject>` and CSS/attribute forms that rasterizers do not handle.
24//! Since merman 0.6, that generic cleanup is exposed as merman's raster-safe SVG
25//! pipeline. Zed opts into that pipeline during rendering, then keeps
26//! editor-specific theme and accent color rules in this crate. The [`gpui`]
27//! dependency is only needed for the [`Hsla`] and [`Rgba`] color types.
28//!
29//! The [`render_to_svg`] function operates in two stages:
30//! - [`render`] the mermaid text to raster-safe SVG using [`merman`].
31//! - [`postprocess`] the SVG to add Zed theme and accent styling.
32//!
33//! Zed's postprocessing is split up into stages. We parse the generated SVG
34//! using [`quick_xml`], which produces an iterator of
35//! [`Event<'_>`](quick_xml::events::Event)s. This iterator is then repeatedly
36//! transformed, and finally collected back into an SVG string.
37//!
38//! This approach:
39//! - Avoids doing multiple expensive string insertions.
40//! - Avoids parsing the SVG multiple times (without needing to put all the
41//!   logic in one huge function).
42//! - But is quite a bit more complex.
43//!
44//! I think this complexity is justified because of the drastic performance
45//! impact, as well as the low-risk nature; this code cannot panic, and errors
46//! in the output just produce weird-looking diagrams.
47//!
48//! ## Color handling
49//!
50//! We try to match the users theme, and also apply accent colors to diagrams to
51//! make them more visually interesting. Accent colors are derived from the
52//! `player_colors` in the Zed theme.
53//!
54//! There are three parts to color handling:
55//!
56//! 1. A [`merman::MermaidConfig`] is passed when initially rendering the
57//!    diagram. This sets most "normal" colors (background, text, etc.). However,
58//!    it's not possible to color nodes individually, and not all parts of the
59//!    diagrams are correctly themed.
60//! 2. `postprocess::accent_colors` injects custom CSS classes (e.g.
61//!    `zed-accent-0`) to specific elements, based on the diagram type and
62//!    node.
63//! 3. `postprocess::inject_css` injects CSS rules for the classes applied by
64//!    `accent_colors`
65
66mod postprocess;
67mod render;
68
69use anyhow::Result;
70use gpui::{Hsla, Rgba};
71
72#[derive(Debug, Clone, Copy)]
73pub struct AccentColor {
74    pub foreground: Hsla,
75    pub background: Hsla,
76}
77
78#[derive(Debug, Clone)]
79pub struct MermaidTheme {
80    pub dark_mode: bool,
81    pub font_family: String,
82    pub background: Hsla,
83    pub primary_color: Hsla,
84    pub primary_text_color: Hsla,
85    pub primary_border_color: Hsla,
86    pub secondary_color: Hsla,
87    pub tertiary_color: Hsla,
88    pub line_color: Hsla,
89    pub text_color: Hsla,
90    pub edge_label_background: Hsla,
91    pub cluster_background: Hsla,
92    pub cluster_border: Hsla,
93    pub note_background: Hsla,
94    pub note_border: Hsla,
95    pub actor_background: Hsla,
96    pub actor_border: Hsla,
97    pub activation_background: Hsla,
98    pub activation_border: Hsla,
99    pub git_branch_colors: [Hsla; 8],
100    pub git_branch_label_colors: [Hsla; 8],
101    pub er_attr_bg_odd: Hsla,
102    pub er_attr_bg_even: Hsla,
103    pub error_color: Hsla,
104    pub warning_color: Hsla,
105    pub accent_colors: Vec<AccentColor>,
106}
107
108/// Default theme for testing.
109#[cfg(any(test, feature = "test-support"))]
110impl Default for MermaidTheme {
111    fn default() -> Self {
112        use gpui::{hsla, rgb};
113        let git_branch_colors: [Hsla; 8] = [
114            hsla(240.0 / 360.0, 1.0, 0.462_745_1, 1.0),
115            hsla(60.0 / 360.0, 1.0, 0.435_294_12, 1.0),
116            hsla(80.0 / 360.0, 1.0, 0.462_745_1, 1.0),
117            hsla(210.0 / 360.0, 1.0, 0.462_745_1, 1.0),
118            hsla(180.0 / 360.0, 1.0, 0.462_745_1, 1.0),
119            hsla(150.0 / 360.0, 1.0, 0.462_745_1, 1.0),
120            hsla(300.0 / 360.0, 1.0, 0.462_745_1, 1.0),
121            hsla(0.0, 1.0, 0.462_745_1, 1.0),
122        ];
123        let git_branch_label_colors: [Hsla; 8] =
124            git_branch_colors.map(crate::text_color_for_background);
125
126        Self {
127            dark_mode: false,
128            font_family: "Inter, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", \"DejaVu Sans\", \"Liberation Sans\", sans-serif, \"Noto Color Emoji\", \"Apple Color Emoji\", \"Segoe UI Emoji\"".to_string(),
129            background: rgb(0xFFFFFF).into(),
130            primary_color: rgb(0xF8FAFC).into(),
131            primary_text_color: rgb(0x0F172A).into(),
132            primary_border_color: rgb(0x94A3B8).into(),
133            secondary_color: rgb(0xE2E8F0).into(),
134            tertiary_color: rgb(0xFFFFFF).into(),
135            line_color: rgb(0x64748B).into(),
136            text_color: rgb(0x0F172A).into(),
137            edge_label_background: rgb(0xFFFFFF).into(),
138            cluster_background: rgb(0xF1F5F9).into(),
139            cluster_border: rgb(0xCBD5E1).into(),
140            note_background: rgb(0xFFF7ED).into(),
141            note_border: rgb(0xFDBA74).into(),
142            actor_background: rgb(0xF8FAFC).into(),
143            actor_border: rgb(0x94A3B8).into(),
144            activation_background: rgb(0xE2E8F0).into(),
145            activation_border: rgb(0x94A3B8).into(),
146            git_branch_colors,
147            git_branch_label_colors,
148            er_attr_bg_odd: rgb(0x94A3B8).into(),
149            er_attr_bg_even: rgb(0x0F172A).into(),
150            error_color: rgb(0xDC2626).into(),
151            warning_color: rgb(0xD97706).into(),
152            accent_colors: Vec::new(),
153        }
154    }
155}
156
157/// Formats a color as a CSS hex color for embedding in SVG/CSS.
158///
159/// Emits `#rrggbb` for fully opaque colors and `#rrggbbaa` when the input
160/// has any transparency, so translucent theme colors (e.g. `ghost_element_hover`
161/// from Zed's UI palette) round-trip without silently losing their alpha.
162pub(crate) fn css_color(color: Hsla) -> String {
163    let rgba = Rgba::from(color);
164    let r = (rgba.r.clamp(0.0, 1.0) * 255.0).round() as u8;
165    let g = (rgba.g.clamp(0.0, 1.0) * 255.0).round() as u8;
166    let b = (rgba.b.clamp(0.0, 1.0) * 255.0).round() as u8;
167    let a = (rgba.a.clamp(0.0, 1.0) * 255.0).round() as u8;
168    if a == 0xff {
169        format!("#{r:02x}{g:02x}{b:02x}")
170    } else {
171        format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
172    }
173}
174
175pub use postprocess::util::text_color_for_background;
176
177/// See the [module-level docs][crate] for more info.
178pub fn render_to_svg(source: &str, theme: &MermaidTheme) -> Result<String> {
179    let svg = render::render_mermaid(source, theme)?;
180    let svg = postprocess::postprocess(&svg, theme)?;
181    Ok(svg)
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn mermaid_diagram_with_mixed_weight_combining_marks_does_not_panic() {
190        const IBM_PLEX_REGULAR: &[u8] =
191            include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
192        const IBM_PLEX_SEMIBOLD: &[u8] =
193            include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf");
194
195        let zalgo = "Ne\u{0301}\u{0302}\u{0303}\u{0304}\u{0306}\u{0307}\u{0308}\u{030a}d";
196        let source = format!("flowchart TD\n  A[\"**{zalgo}** {zalgo}\"]");
197        let svg = render_to_svg(&source, &MermaidTheme::default())
198            .expect("mermaid diagram should render to SVG");
199
200        let mut db = usvg::fontdb::Database::new();
201        db.load_font_data(IBM_PLEX_REGULAR.to_vec());
202        db.load_font_data(IBM_PLEX_SEMIBOLD.to_vec());
203        db.set_sans_serif_family("IBM Plex Sans");
204        let options = usvg::Options {
205            fontdb: std::sync::Arc::new(db),
206            ..Default::default()
207        };
208
209        usvg::Tree::from_data(svg.as_bytes(), &options)
210            .expect("rasterizing mermaid text should not panic");
211    }
212
213    /// An ER diagram whose attribute-block tokens begin with a multibyte
214    /// UTF-8 character (e.g. CJK type/field names) must not panic while the
215    /// lexer probes for the two-character `PK`/`FK`/`UK` keys.
216    #[test]
217    fn er_multibyte_attribute_does_not_crash() {
218        let source = "erDiagram\n顧客 {\n  文字列 名前\n}";
219        let _ = render_to_svg(source, &MermaidTheme::default());
220    }
221
222    /// A flowchart with mutually nested subgraphs (`A` contains `B` and `B`
223    /// contains `A`) is an invalid containment cycle. Rendering it must return
224    /// gracefully rather than overflowing the stack and aborting the process.
225    #[test]
226    fn cyclic_subgraphs_do_not_crash() {
227        let source = "flowchart TD\n  subgraph A\n    B\n  end\n  subgraph B\n    A\n  end";
228        let result = render_to_svg(source, &MermaidTheme::default());
229        if let Err(err) = result {
230            let message = format!("{err:#}");
231            assert!(
232                message.contains("cycle"),
233                "expected a cycle-related error, got: {message}"
234            );
235        }
236    }
237}
238
Served at tenant.openagents/omega Member data and write actions are omitted.