Skip to repository content364 lines · 12.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:22:37.773Z 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
check_invalid_attrs.rs
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("&lt;"),
262 "Double-escaped &lt; found in SVG"
263 );
264 assert!(
265 !svg.contains("&gt;"),
266 "Double-escaped &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