Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:34:28.436Z 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

render.rs

192 lines · 8.0 KB · rust
1use std::sync::atomic::{AtomicU64, Ordering};
2
3use anyhow::{Context as _, Result, anyhow};
4
5use crate::{MermaidTheme, css_color};
6
7pub(super) fn render_mermaid(source: &str, theme: &MermaidTheme) -> Result<String> {
8    static COUNTER: AtomicU64 = AtomicU64::new(0);
9    let id = COUNTER.fetch_add(1, Ordering::Relaxed);
10    let diagram_id = format!("merman-{id}");
11
12    let config = to_merman_config(theme);
13    let renderer = merman::render::HeadlessRenderer::new()
14        .with_site_config(config)
15        .with_vendored_text_measurer()
16        .with_diagram_id(&diagram_id);
17    // Apply merman's raster-safe pipeline before Zed-specific styling. The
18    // pipeline handles generic rasterizer compatibility cleanup: foreignObject
19    // fallback text, unsupported CSS removal, and invalid SVG attribute cleanup.
20    // Zed also strips merman's existing `!important` declarations before
21    // injecting its own theme CSS so host styling wins consistently in usvg/resvg.
22    let pipeline = merman::render::SvgPipeline::resvg_safe()
23        .with_postprocessor(merman::render::CssOverridePostprocessor::strip_existing_important());
24
25    let svg = renderer
26        .render_svg_with_pipeline_sync(source, &pipeline)
27        .context("merman render failed")?
28        .ok_or_else(|| anyhow!("merman returned no SVG for the given input"))?;
29
30    Ok(svg)
31}
32
33fn to_merman_config(theme: &MermaidTheme) -> merman::MermaidConfig {
34    let primary = css_color(theme.primary_color);
35    let primary_text = css_color(theme.primary_text_color);
36    let primary_border = css_color(theme.primary_border_color);
37    let line = css_color(theme.line_color);
38    let secondary = css_color(theme.secondary_color);
39    let tertiary = css_color(theme.tertiary_color);
40    let background = css_color(theme.background);
41    let cluster_bg = css_color(theme.cluster_background);
42    let cluster_border = css_color(theme.cluster_border);
43    let edge_label_bg = css_color(theme.edge_label_background);
44    let text = css_color(theme.text_color);
45    let note_bg = css_color(theme.note_background);
46    let note_border = css_color(theme.note_border);
47    let actor_bg = css_color(theme.actor_background);
48    let actor_border = css_color(theme.actor_border);
49    let activation_bg = css_color(theme.activation_background);
50    let activation_border = css_color(theme.activation_border);
51    let er_odd = css_color(theme.er_attr_bg_odd);
52    let er_even = css_color(theme.er_attr_bg_even);
53    let git: [String; 8] = theme.git_branch_colors.map(css_color);
54    let git_lbl: [String; 8] = theme.git_branch_label_colors.map(css_color);
55
56    let mut theme_vars = serde_json::json!({
57        "primaryColor": primary,
58        "primaryTextColor": primary_text,
59        "primaryBorderColor": primary_border,
60        "lineColor": line,
61        "secondaryColor": secondary,
62        "secondaryTextColor": text,
63        "tertiaryColor": tertiary,
64        "tertiaryTextColor": text,
65        "background": background,
66        "mainBkg": primary,
67        "nodeBorder": primary_border,
68        "nodeTextColor": primary_text,
69        "clusterBkg": cluster_bg,
70        "clusterBorder": cluster_border,
71        "titleColor": text,
72        "edgeLabelBackground": edge_label_bg,
73        "textColor": text,
74        "fontFamily": theme.font_family,
75        "noteBkgColor": note_bg,
76        "noteBorderColor": note_border,
77        "noteTextColor": text,
78        "actorBkg": actor_bg,
79        "actorBorder": actor_border,
80        "actorTextColor": primary_text,
81        "labelTextColor": text,
82        "loopTextColor": text,
83        "signalColor": text,
84        "signalTextColor": text,
85        "activationBkgColor": activation_bg,
86        "activationBorderColor": activation_border,
87        "classText": text,
88        "labelColor": primary_text,
89        "attributeBackgroundColorOdd": er_odd,
90        "attributeBackgroundColorEven": er_even,
91        "pieTitleTextColor": text,
92        "pieSectionTextColor": text,
93        "pieLegendTextColor": text,
94        "pieStrokeColor": primary_border,
95        "pieOuterStrokeColor": primary_border,
96        "quadrant1Fill": primary,
97        "quadrant2Fill": primary,
98        "quadrant3Fill": primary,
99        "quadrant4Fill": primary,
100        "quadrant1TextFill": text,
101        "quadrant2TextFill": text,
102        "quadrant3TextFill": text,
103        "quadrant4TextFill": text,
104        "quadrantPointFill": line,
105        "quadrantPointTextFill": text,
106        "quadrantTitleFill": text,
107        "quadrantXAxisTextFill": text,
108        "quadrantYAxisTextFill": text,
109        "quadrantExternalBorderStrokeFill": primary_border,
110        "quadrantInternalBorderStrokeFill": primary_border,
111    });
112
113    if let Some(map) = theme_vars.as_object_mut() {
114        for (((i, color), label), pie_number) in git.iter().enumerate().zip(&git_lbl).zip(1..) {
115            map.insert(format!("cScale{i}"), color.clone().into());
116            map.insert(format!("cScaleLabel{i}"), label.clone().into());
117            map.insert(format!("pie{pie_number}"), color.clone().into());
118        }
119    }
120
121    merman::MermaidConfig::from_value(serde_json::json!({
122        "theme": "base",
123        "darkMode": theme.dark_mode,
124        "fontFamily": theme.font_family,
125        // resvg can't rasterize HTML `<foreignObject>` labels, and the
126        // fallback that replaces them loses soft wrapping, so emit native SVG
127        // text labels. Nodes read the top-level key; edges read `flowchart`.
128        "htmlLabels": false,
129        "flowchart": {
130            "htmlLabels": false,
131            "padding": 16,
132        },
133        "themeVariables": theme_vars,
134    }))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn render_stage_applies_resvg_safe_pipeline() {
143        let html_label_source =
144            "classDiagram\n    class Shelter {\n        -List~Animal~ animals\n    }";
145        let html_label_svg =
146            render_mermaid(html_label_source, &MermaidTheme::default()).expect("render failed");
147
148        assert!(
149            !html_label_svg.contains("<foreignObject"),
150            "got: {html_label_svg}"
151        );
152        assert!(
153            !html_label_svg.contains("&amp;lt;"),
154            "got: {html_label_svg}"
155        );
156
157        let css_source = "sequenceDiagram\n    Alice->>Bob: Hello\n    Bob-->>Alice: Hi";
158        let css_svg = render_mermaid(css_source, &MermaidTheme::default()).expect("render failed");
159
160        assert!(!css_svg.contains("@keyframes"), "got: {css_svg}");
161        assert!(!css_svg.contains("@-webkit-keyframes"), "got: {css_svg}");
162        assert!(!css_svg.contains(":root"), "got: {css_svg}");
163        assert!(!css_svg.contains("animation:"), "got: {css_svg}");
164        assert!(!css_svg.contains("animation-name:"), "got: {css_svg}");
165        assert!(!css_svg.contains("!important"), "got: {css_svg}");
166    }
167
168    /// Soft-wrapped labels must render as native SVG `<tspan>` lines rather
169    /// than the resvg-safe pipeline's single-line `<foreignObject>` fallback,
170    /// which overflows the node box (see the `htmlLabels` comment in
171    /// [`to_merman_config`]). If the fallback marker reappears after a merman
172    /// upgrade, the config has stopped disabling HTML labels.
173    #[test]
174    fn long_labels_render_as_wrapped_svg_text() {
175        let source = "flowchart TD\n    \
176            A[\"Pass 2: search transcript with annotation blocks excised, \
177            map offsets back to buffer space\"] --> \
178            |ambiguous or zero| B[\"Error describing where matches were found\"]";
179        let svg = render_mermaid(source, &MermaidTheme::default()).expect("render failed");
180
181        assert!(
182            !svg.contains(r#"data-merman-foreignobject="fallback""#),
183            "labels went through the foreignObject fallback, which loses soft wrapping: {svg}"
184        );
185        let wrapped_line_count = svg.matches("text-outer-tspan").count();
186        assert!(
187            wrapped_line_count > 3,
188            "expected long labels to wrap onto multiple tspan lines, got {wrapped_line_count}: {svg}"
189        );
190    }
191}
192
Served at tenant.openagents/omega Member data and write actions are omitted.