Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:27:56.398Z 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

check_invalid_attrs.rs

364 lines · 12.4 KB · rust
1use gpui::Hsla;
2use mermaid_render::MermaidTheme;
3
4fn rgb(r: u8, g: u8, b: u8) -> Hsla {
5    gpui::Rgba {
6        r: r as f32 / 255.0,
7        g: g as f32 / 255.0,
8        b: b as f32 / 255.0,
9        a: 1.0,
10    }
11    .into()
12}
13
14const DIAGRAMS: &[(&str, &str)] = &[
15    (
16        "flowchart",
17        "flowchart TD\n    A[Hello] --> B[World]\n    B --> C{Decision}\n    C -->|Yes| D[OK]\n    C -->|No| E[Fail]",
18    ),
19    (
20        "sequence",
21        "sequenceDiagram\n    Alice->>Bob: Hello\n    Bob-->>Alice: Hi\n    Note over Alice,Bob: A note",
22    ),
23    (
24        "state",
25        "stateDiagram-v2\n    [*] --> Active\n    Active --> [*]",
26    ),
27    (
28        "er",
29        "erDiagram\n    A { int id PK }\n    B { int id PK }\n    A ||--o{ B : has",
30    ),
31    (
32        "class",
33        "classDiagram\n    class Foo {\n        +bar() void\n    }",
34    ),
35    ("pie", "pie title Test\n    \"A\" : 42\n    \"B\" : 58"),
36    (
37        "gantt",
38        "gantt\n    title Test\n    dateFormat YYYY-MM-DD\n    section S\n        Task :a1, 2025-01-01, 7d",
39    ),
40    ("mindmap", "mindmap\n  root((Root))\n    Child1\n    Child2"),
41    (
42        "journey",
43        "journey\n    title Test\n    section S\n        Task: 5: Actor",
44    ),
45    (
46        "gitgraph",
47        "gitGraph\n    commit id: \"init\"\n    branch dev\n    commit id: \"feat\"\n    checkout main\n    merge dev",
48    ),
49    (
50        "quadrant",
51        "quadrantChart\n    title Test\n    x-axis Low --> High\n    y-axis Low --> High\n    A: [0.3, 0.8]\n    B: [0.7, 0.4]",
52    ),
53    (
54        "timeline",
55        "timeline\n    title Test\n    section 2020s\n        2020 : Event A\n        2022 : Event B",
56    ),
57    (
58        "xychart",
59        "xychart-beta\n    title Test\n    x-axis [\"A\", \"B\", \"C\"]\n    y-axis \"Val\" 0 --> 10\n    bar [3, 7, 5]",
60    ),
61];
62
63fn rgb_theme() -> MermaidTheme {
64    MermaidTheme {
65        dark_mode: true,
66        font_family: "system-ui".to_string(),
67        background: rgb(40, 44, 51),
68        primary_color: rgb(47, 52, 62),
69        primary_text_color: rgb(220, 224, 229),
70        primary_border_color: rgb(70, 75, 87),
71        secondary_color: rgb(46, 52, 62),
72        tertiary_color: rgb(54, 60, 70),
73        line_color: rgb(70, 75, 87),
74        text_color: rgb(220, 224, 229),
75        edge_label_background: rgb(40, 44, 51),
76        cluster_background: rgb(47, 52, 62),
77        cluster_border: rgb(54, 60, 70),
78        note_background: rgb(47, 52, 62),
79        note_border: rgb(54, 60, 70),
80        actor_background: rgb(46, 52, 62),
81        actor_border: rgb(70, 75, 87),
82        activation_background: rgb(54, 60, 70),
83        activation_border: rgb(70, 75, 87),
84        git_branch_colors: [
85            rgb(116, 173, 232),
86            rgb(190, 80, 70),
87            rgb(191, 149, 106),
88            rgb(180, 119, 207),
89            rgb(110, 180, 191),
90            rgb(208, 114, 119),
91            rgb(222, 193, 132),
92            rgb(161, 193, 129),
93        ],
94        git_branch_label_colors: [
95            rgb(116, 173, 232),
96            rgb(190, 80, 70),
97            rgb(191, 149, 106),
98            rgb(180, 119, 207),
99            rgb(110, 180, 191),
100            rgb(208, 114, 119),
101            rgb(222, 193, 132),
102            rgb(161, 193, 129),
103        ]
104        .map(mermaid_render::text_color_for_background),
105        er_attr_bg_odd: rgb(47, 52, 62),
106        er_attr_bg_even: rgb(46, 52, 62),
107        error_color: rgb(220, 38, 38),
108        warning_color: rgb(217, 119, 6),
109        accent_colors: vec![
110            mermaid_render::AccentColor {
111                foreground: rgb(116, 173, 232),
112                background: rgb(116, 173, 232),
113            },
114            mermaid_render::AccentColor {
115                foreground: rgb(190, 80, 70),
116                background: rgb(190, 80, 70),
117            },
118            mermaid_render::AccentColor {
119                foreground: rgb(191, 149, 106),
120                background: rgb(191, 149, 106),
121            },
122            mermaid_render::AccentColor {
123                foreground: rgb(180, 119, 207),
124                background: rgb(180, 119, 207),
125            },
126            mermaid_render::AccentColor {
127                foreground: rgb(110, 180, 191),
128                background: rgb(110, 180, 191),
129            },
130            mermaid_render::AccentColor {
131                foreground: rgb(208, 114, 119),
132                background: rgb(208, 114, 119),
133            },
134            mermaid_render::AccentColor {
135                foreground: rgb(222, 193, 132),
136                background: rgb(222, 193, 132),
137            },
138            mermaid_render::AccentColor {
139                foreground: rgb(161, 193, 129),
140                background: rgb(161, 193, 129),
141            },
142        ],
143    }
144}
145
146fn check_svg_issues(name: &str, svg: &str) -> Vec<String> {
147    let bad_patterns = [
148        "fill=\"\"",
149        "stroke=\"\"",
150        "width=\"\"",
151        "height=\"\"",
152        "NaN",
153        // Also check for empty values in style attributes
154        "fill: ;",
155        "fill:;",
156        "stroke: ;",
157        "stroke:;",
158        // Check for attributes with just whitespace
159        "fill=\" \"",
160    ];
161    let mut issues = Vec::new();
162    for pattern in &bad_patterns {
163        let mut start = 0;
164        while let Some(pos) = svg[start..].find(pattern) {
165            let abs = start + pos;
166            let ctx_start = abs.saturating_sub(100);
167            let ctx_end = (abs + pattern.len() + 60).min(svg.len());
168            issues.push(format!(
169                "{name}: found `{pattern}` at byte {abs}:\n  ...{}...\n",
170                &svg[ctx_start..ctx_end]
171            ));
172            start = abs + pattern.len();
173        }
174    }
175
176    // Parse with quick-xml to find ANY empty attribute values on visual elements
177    use quick_xml::events::Event;
178    let mut reader = quick_xml::Reader::from_str(svg);
179    loop {
180        match reader.read_event() {
181            Ok(Event::Eof) => break,
182            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
183                let tag = String::from_utf8_lossy(e.name().local_name().as_ref()).to_string();
184                for attr in e.attributes().flatten() {
185                    let key = String::from_utf8_lossy(attr.key.local_name().as_ref()).to_string();
186                    let val = attr.unescape_value().unwrap_or_default();
187                    let visual_attr = matches!(
188                        key.as_str(),
189                        "fill"
190                            | "stroke"
191                            | "width"
192                            | "height"
193                            | "x"
194                            | "y"
195                            | "r"
196                            | "cx"
197                            | "cy"
198                            | "rx"
199                            | "ry"
200                            | "stroke-width"
201                    );
202                    if visual_attr && val.is_empty() {
203                        issues.push(format!("{name}: <{tag}> has empty {key}=\"\"\n"));
204                    }
205                    // Check for CSS length units that usvg can't parse
206                    if visual_attr
207                        && matches!(key.as_str(), "width" | "height")
208                        && val.ends_with("px")
209                    {
210                        issues.push(format!("{name}: <{tag}> has {key}=\"{val}\" (px suffix)\n"));
211                    }
212                }
213            }
214            Err(e) => {
215                issues.push(format!("{name}: XML parse error: {e}\n"));
216                break;
217            }
218            _ => {}
219        }
220    }
221
222    issues
223}
224
225#[test]
226fn accent_colors_auto_applied_to_nodes() {
227    let theme = rgb_theme();
228
229    // A plain state diagram with no :::accent syntax should get
230    // automatic accent colors applied to its node groups.
231    let source = "stateDiagram-v2\n    [*] --> Idle\n    Idle --> Processing\n    Processing --> Done\n    Done --> [*]";
232
233    let svg = mermaid_render::render_to_svg(source, &theme).expect("render failed");
234
235    // accent_fill_and_text darkens the background color for dark mode.
236    // The stroke colors are direct hex conversions of the accent rgb values.
237    // With 3 states (Idle, Processing, Done), we expect at least accent0 and
238    // accent1 stroke colors to appear.
239    let accent0_stroke = "#74ade8"; // rgb(116, 173, 232) -> hex
240    let accent1_stroke = "#be5046"; // rgb(190, 80, 70) -> hex
241
242    assert!(
243        svg.contains(accent0_stroke),
244        "Expected accent0 stroke color ({accent0_stroke}) in auto-colored state diagram SVG.\n\
245         This means auto-coloring did not apply accent colors to node groups.\n\
246         SVG snippet: {}...",
247        &svg[..svg.len().min(2000)]
248    );
249    assert!(
250        svg.contains(accent1_stroke),
251        "Expected accent1 stroke color ({accent1_stroke}) in auto-colored state diagram SVG."
252    );
253}
254
255#[test]
256fn generics_not_double_escaped() {
257    let theme = rgb_theme();
258    let source = "classDiagram\n    class Shelter {\n        -List~Animal~ animals\n        +adopt(Animal a) bool\n    }";
259    let svg = mermaid_render::render_to_svg(source, &theme).expect("render failed");
260    assert!(
261        !svg.contains("&amp;lt;"),
262        "Double-escaped &amp;lt; found in SVG"
263    );
264    assert!(
265        !svg.contains("&amp;gt;"),
266        "Double-escaped &amp;gt; found in SVG"
267    );
268}
269
270#[test]
271fn class_diagram_label_text_uses_accent_classes() {
272    let theme = rgb_theme();
273    let source = r#"classDiagram
274    class Animal {
275        +String name
276        +makeSound() void
277    }
278    class Dog {
279        +String breed
280        +bark() void
281    }
282    Dog --|> Animal"#;
283
284    let svg = mermaid_render::render_to_svg(source, &theme).expect("render failed");
285
286    use quick_xml::events::Event;
287    let mut reader = quick_xml::Reader::from_str(&svg);
288    let mut accent_classes: Vec<String> = Vec::new();
289    loop {
290        match reader.read_event() {
291            Ok(Event::Eof) => break,
292            Ok(Event::Start(e)) if e.name().as_ref() == b"text" => {
293                if let Ok(Some(class_attr)) = e.try_get_attribute("class") {
294                    let class = class_attr.unescape_value().unwrap_or_default().to_string();
295                    for token in class.split_whitespace() {
296                        if token.starts_with("zed-accent-") {
297                            accent_classes.push(token.to_string());
298                        }
299                    }
300                }
301            }
302            _ => {}
303        }
304    }
305
306    assert!(
307        !accent_classes.is_empty(),
308        "expected zed-accent-N classes on label text elements",
309    );
310}
311
312#[test]
313fn sequence_diagram_tspan_uses_accent_classes() {
314    let theme = rgb_theme();
315    let source = "sequenceDiagram\n    participant Database";
316    let svg = mermaid_render::render_to_svg(source, &theme).expect("render failed");
317
318    use quick_xml::events::Event;
319    let mut reader = quick_xml::Reader::from_str(&svg);
320    let mut accent_classes: Vec<String> = Vec::new();
321    loop {
322        match reader.read_event() {
323            Ok(Event::Eof) => break,
324            Ok(Event::Start(e)) if e.name().as_ref() == b"tspan" => {
325                if let Ok(Some(class_attr)) = e.try_get_attribute("class") {
326                    let class = class_attr.unescape_value().unwrap_or_default().to_string();
327                    for token in class.split_whitespace() {
328                        if token.starts_with("zed-accent-") {
329                            accent_classes.push(token.to_string());
330                        }
331                    }
332                }
333            }
334            _ => {}
335        }
336    }
337
338    assert!(
339        !accent_classes.is_empty(),
340        "expected zed-accent-N classes on tspan elements in sequence diagram",
341    );
342}
343
344#[test]
345fn no_empty_attributes_or_nan_with_rgb_theme() {
346    let theme = rgb_theme();
347    let mut all_issues = Vec::new();
348
349    for (name, source) in DIAGRAMS {
350        match mermaid_render::render_to_svg(source, &theme) {
351            Ok(svg) => all_issues.extend(check_svg_issues(name, &svg)),
352            Err(e) => eprintln!("{name}: render failed (skipped): {e}"),
353        }
354    }
355
356    if !all_issues.is_empty() {
357        panic!(
358            "Found {} issues in merman SVG output (rgb theme):\n\n{}",
359            all_issues.len(),
360            all_issues.join("\n")
361        );
362    }
363}
364
Served at tenant.openagents/omega Member data and write actions are omitted.