Skip to main content

yggdrasil/api/markdown/mermaid/
svg.rs

1//! merman 输出 SVG 的后处理:子图重叠检测与子图标题置顶。
2
3const CLUSTERS: &str = r#"<g class="clusters">"#;
4
5/// 返回从 `start`(指向某个 `<g` 开标签)起、与之配对的 `</g>` 结束位置(不含)。
6fn 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            // 自闭合 `<g .../>` 不改变深度。
21            if !tag[..close].ends_with('/') {
22                depth += 1;
23            }
24            i += close + 1;
25        } else {
26            i += 1;
27        }
28    }
29    None
30}
31
32/// 取 `<g class="cluster" ...><rect ... x= y= width= height=>` 的矩形,按 clusters 组分组。
33fn 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
60/// 同一 clusters 组内两个子图框相交超过 0.5px 且互不包含(不是嵌套关系)即视为重叠。
61///
62/// Dagre 按子节点算子图大小,不考虑标题;Mermaid 事后把框撑到标题宽度,左右各外扩一截,
63/// 相邻子图就可能重叠。调用方据此加大 `nodeSpacing` 重新排版。
64pub(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
79/// 在每个 `<g class="clusters">` 内,把所有子图标题移到全部子图底框之后绘制,标题总在最上层。
80///
81/// 后画的子图底色会盖住先画的子图标题(Mermaid 原版同样如此)。子图 `<g>` 无 transform,
82/// 标题坐标相对所在 clusters 组不变,移动不改变位置;不跨组移动,因为不同 root 组可能各带
83/// transform。外包一层同 class 的 `<g>` 以保留 `.cluster span` 等样式。
84pub(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
99/// 处理一个完整的 clusters 组;解析失败时原样输出该组。
100fn 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
125/// 时序图自调用消息标签的上移量(px)。
126const SELF_LABEL_LIFT: f64 = 8.0;
127
128/// 时序图自调用消息的标签上移 [`SELF_LABEL_LIFT`]。
129///
130/// Mermaid 把自调用标签居中放在生命线上、紧贴自环起点,开了 `autonumber` 时序号圆点压住
131/// 标签下沿。标签的 `<text>`(多行时为连续多个)紧挨在自环 `<path data-from=X data-to=X>`
132/// 之前;只改它们的 `y`,横向不动,图的边界不变。
133fn lift_self_message_labels(svg: &str) -> String {
134    const PATH: &str = "<path ";
135    const TEXT: &str = "<text ";
136    const TEXT_END: &str = "</text>";
137    // 需要改写的 y 属性值区间 (start, end),按位置升序。
138    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
194/// 取开标签里 `name="…"` 的值(属性名前须为空格)。
195fn 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
202/// 最终修正:去掉末尾换行(`strip_dark_variants` 依赖 `</svg></div></figure>` 紧邻);
203/// 子图标题置顶;时序图自调用标签让开序号;HTML 标签的 foreignObject 允许溢出可见
204/// (字体实际宽度略超测量值时不裁掉文字,与 Mermaid 前端渲染时的修正一致);
205/// `extra_css` 追加到 SVG 自带的第一个 `<style>` 末尾。
206pub(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}