Skip to repository content238 lines · 9.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:42:07.099Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
mermaid_render.rs
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