yggdrasil/api/markdown/mermaid/
svg.rs1const CLUSTERS: &str = r#"<g class="clusters">"#;
4
5fn balanced_g_end(svg: &str, start: usize) -> Option<usize> {
7 let mut depth = 0usize;
8 let mut i = start;
9 while i < svg.len() {
10 i += svg[i..].find('<')?;
11 let tag = &svg[i..];
12 if tag.starts_with("</g>") {
13 depth = depth.checked_sub(1)?;
14 i += 4;
15 if depth == 0 {
16 return Some(i);
17 }
18 } else if tag.starts_with("<g ") || tag.starts_with("<g>") {
19 let close = tag.find('>')?;
20 if !tag[..close].ends_with('/') {
22 depth += 1;
23 }
24 i += close + 1;
25 } else {
26 i += 1;
27 }
28 }
29 None
30}
31
32fn cluster_rects(svg: &str) -> Vec<Vec<[f64; 4]>> {
34 fn attr(tag: &str, name: &str) -> Option<f64> {
35 let key = format!(r#" {name}=""#);
36 let at = tag.find(&key)? + key.len();
37 tag[at..].split('"').next()?.parse().ok()
38 }
39 svg.split(CLUSTERS)
40 .skip(1)
41 .map(|group| {
42 group
43 .split(r#"<g class="cluster" id=""#)
44 .skip(1)
45 .filter_map(|c| {
46 let rect = &c[c.find("<rect")?..];
47 let tag = &rect[..rect.find('>')?];
48 Some([
49 attr(tag, "x")?,
50 attr(tag, "y")?,
51 attr(tag, "width")?,
52 attr(tag, "height")?,
53 ])
54 })
55 .collect()
56 })
57 .collect()
58}
59
60pub(super) fn has_cluster_overlap(svg: &str) -> bool {
65 let contains = |p: &[f64; 4], q: &[f64; 4]| {
66 p[0] <= q[0] && p[1] <= q[1] && p[0] + p[2] >= q[0] + q[2] && p[1] + p[3] >= q[1] + q[3]
67 };
68 cluster_rects(svg).iter().any(|rects| {
69 rects.iter().enumerate().any(|(i, a)| {
70 rects[i + 1..].iter().any(|b| {
71 let ix = (a[0] + a[2]).min(b[0] + b[2]) - a[0].max(b[0]);
72 let iy = (a[1] + a[3]).min(b[1] + b[3]) - a[1].max(b[1]);
73 ix > 0.5 && iy > 0.5 && !contains(a, b) && !contains(b, a)
74 })
75 })
76 })
77}
78
79pub(super) fn lift_cluster_labels(svg: &str) -> String {
85 let mut out = String::with_capacity(svg.len() + 128);
86 let mut rest = svg;
87 while let Some(start) = rest.find(CLUSTERS) {
88 let Some(end) = balanced_g_end(rest, start) else {
89 break;
90 };
91 out.push_str(&rest[..start]);
92 lift_labels_in_group(&rest[start..end], &mut out);
93 rest = &rest[end..];
94 }
95 out.push_str(rest);
96 out
97}
98
99fn lift_labels_in_group(group: &str, out: &mut String) {
101 const LABEL: &str = r#"<g class="cluster-label""#;
102 let inner_end = group.len() - "</g>".len();
103 let mut labels = String::new();
104 let start_len = out.len();
105 out.push_str(CLUSTERS);
106 let mut i = CLUSTERS.len();
107 while let Some(rel) = group[i..inner_end].find(LABEL) {
108 let at = i + rel;
109 let Some(label_end) = balanced_g_end(group, at) else {
110 out.truncate(start_len);
111 out.push_str(group);
112 return;
113 };
114 out.push_str(&group[i..at]);
115 labels.push_str(r#"<g class="cluster" data-look="classic">"#);
116 labels.push_str(&group[at..label_end]);
117 labels.push_str("</g>");
118 i = label_end;
119 }
120 out.push_str(&group[i..inner_end]);
121 out.push_str(&labels);
122 out.push_str("</g>");
123}
124
125const SELF_LABEL_LIFT: f64 = 8.0;
127
128fn lift_self_message_labels(svg: &str) -> String {
134 const PATH: &str = "<path ";
135 const TEXT: &str = "<text ";
136 const TEXT_END: &str = "</text>";
137 let mut edits: Vec<(usize, usize)> = Vec::new();
139 let mut from = 0;
140 while let Some(rel) = svg[from..].find(PATH) {
141 let at = from + rel;
142 from = at + PATH.len();
143 let Some(tag_len) = svg[at..].find('>') else {
144 break;
145 };
146 let tag = &svg[at..at + tag_len];
147 let source = attr_value(tag, "data-from");
148 if source.is_none() || source != attr_value(tag, "data-to") {
149 continue;
150 }
151 let mut cursor = at;
152 let mut found = Vec::new();
153 while svg[..cursor].ends_with(TEXT_END) {
154 let Some(start) = svg[..cursor].rfind(TEXT) else {
155 break;
156 };
157 let Some(open_len) = svg[start..cursor].find('>') else {
158 break;
159 };
160 let open = &svg[start..start + open_len];
161 if !open.contains(r#"class="messageText""#) {
162 break;
163 }
164 let Some(value_at) = open.find(r#" y=""#).map(|i| start + i + r#" y=""#.len()) else {
165 break;
166 };
167 let Some(value_len) = svg[value_at..].find('"') else {
168 break;
169 };
170 found.push((value_at, value_at + value_len));
171 cursor = start;
172 }
173 found.reverse();
174 edits.extend(found);
175 }
176 if edits.is_empty() {
177 return svg.to_string();
178 }
179 edits.sort_unstable();
180 let mut out = String::with_capacity(svg.len());
181 let mut last = 0;
182 for (start, end) in edits {
183 let Ok(y) = svg[start..end].parse::<f64>() else {
184 continue;
185 };
186 out.push_str(&svg[last..start]);
187 out.push_str(&(y - SELF_LABEL_LIFT).to_string());
188 last = end;
189 }
190 out.push_str(&svg[last..]);
191 out
192}
193
194fn attr_value<'a>(tag: &'a str, name: &str) -> Option<&'a str> {
196 let key = format!(" {name}=\"");
197 let start = tag.find(&key)? + key.len();
198 let len = tag[start..].find('"')?;
199 Some(&tag[start..start + len])
200}
201
202pub(super) fn finish(svg: &str, extra_css: &str) -> String {
207 lift_self_message_labels(&lift_cluster_labels(svg.trim_end()))
208 .replacen("</style>", &format!("{extra_css}</style>"), 1)
209 .replace("<foreignObject ", r#"<foreignObject overflow="visible" "#)
210}