1use serde_json::{json, Map, Value};
15
16struct Tones {
18 background: &'static str,
20 node: &'static str,
21 node_alt: &'static str,
22 node_soft: &'static str,
23 border: &'static str,
24 border_strong: &'static str,
25 text: &'static str,
26 text_secondary: &'static str,
27 text_tertiary: &'static str,
28 line: &'static str,
29 cluster: &'static str,
31 note_background: &'static str,
32 note_border: &'static str,
33 series: [&'static str; 8],
35 sections: [&'static str; 6],
37 on_series: &'static str,
39 crit_fill: &'static str,
40 crit_border: &'static str,
41}
42
43const LIGHT: Tones = Tones {
45 background: "#fafaf8",
46 node: "#ffffff",
47 node_alt: "#f5f5f3",
48 node_soft: "#efefec",
49 border: "#dcdbd7",
50 border_strong: "#c4c3be",
51 text: "#151515",
52 text_secondary: "#525252",
53 text_tertiary: "#6b6b69",
54 line: "#6b6b69",
55 cluster: "#f5f5f3",
56 note_background: "#f7e8e1",
57 note_border: "#d97757",
58 series: [
59 "#d97757", "#5f8f6e", "#5b7fa6", "#c49a3c", "#93689c", "#4a9a92", "#c46b7d", "#8a8f4a",
60 ],
61 sections: [
62 "#f3ddd3", "#dce9df", "#dbe4ef", "#f1e6cc", "#e9dcec", "#d5eae8",
63 ],
64 on_series: "#ffffff",
65 crit_fill: "#fbeaea",
66 crit_border: "#c93a3a",
67};
68
69const DARK: Tones = Tones {
71 background: "#161615",
72 node: "#20201e",
73 node_alt: "#262624",
74 node_soft: "#1a1a19",
75 border: "#3a3936",
76 border_strong: "#4f4e4a",
77 text: "#f2f1ee",
78 text_secondary: "#a9a7a1",
79 text_tertiary: "#8b8984",
80 line: "#a9a7a1",
81 cluster: "#1d1d1c",
82 note_background: "#33251f",
83 note_border: "#d97757",
84 series: [
85 "#e08a6c", "#8fbf9f", "#8fabd0", "#ddb965", "#b993c2", "#7cc4bb", "#e0939f", "#b4b874",
86 ],
87 sections: [
88 "#4a2e24", "#2b3d31", "#2a3443", "#463c25", "#3d2f42", "#233f3d",
89 ],
90 on_series: "#161615",
91 crit_fill: "#3d2222",
92 crit_border: "#f87171",
93};
94
95const SCALE_LEN: usize = 12;
97const GIT_LEN: usize = 8;
98
99const FONT_FAMILY: &str = "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Microsoft YaHei', sans-serif";
100
101fn tones(dark: bool) -> &'static Tones {
102 if dark {
103 &DARK
104 } else {
105 &LIGHT
106 }
107}
108
109fn theme_variables(t: &Tones, radar: bool) -> Value {
112 let mut v = json!({
113 "background": t.background,
114 "primaryColor": t.node,
116 "secondaryColor": t.node_alt,
117 "tertiaryColor": t.node_soft,
118 "mainBkg": t.node,
119 "nodeBkg": t.node,
120 "secondBkg": t.node_alt,
121 "primaryBorderColor": t.border,
122 "secondaryBorderColor": t.border,
123 "tertiaryBorderColor": t.border,
124 "nodeBorder": t.border,
125 "clusterBorder": t.border,
126 "labelBoxBorderColor": t.border,
127 "primaryTextColor": t.text,
128 "secondaryTextColor": t.text_secondary,
129 "tertiaryTextColor": t.text_tertiary,
130 "textColor": t.text,
131 "nodeTextColor": t.text,
132 "titleColor": t.text,
133 "classText": t.text,
134 "labelTextColor": t.text,
135 "lineColor": t.line,
136 "defaultLinkColor": t.line,
137 "arrowheadColor": t.line,
138 "actorBkg": t.node,
140 "actorBorder": t.border,
141 "actorTextColor": t.text,
142 "actorLineColor": t.line,
143 "signalColor": t.line,
144 "signalTextColor": t.text,
145 "loopTextColor": t.text,
146 "sequenceNumberColor": t.background,
147 "activationBkgColor": t.node_alt,
148 "activationBorderColor": t.border_strong,
149 "edgeLabelBackground": t.background,
151 "labelBoxBkgColor": t.background,
152 "clusterBkg": t.cluster,
153 "noteBkgColor": t.note_background,
155 "noteBorderColor": t.note_border,
156 "noteTextColor": t.text,
157 "fontFamily": FONT_FAMILY,
158 "fontSize": "16px",
159 });
160 let diagrams = json!({
162 "rowOdd": t.node,
164 "rowEven": t.node_alt,
165 "attributeBackgroundColorOdd": t.node,
166 "attributeBackgroundColorEven": t.node_alt,
167 "sectionBkgColor": t.cluster,
169 "sectionBkgColor2": t.cluster,
170 "altSectionBkgColor": t.background,
171 "excludeBkgColor": t.node_soft,
172 "gridColor": t.border,
173 "todayLineColor": t.series[0],
174 "vertLineColor": t.line,
175 "taskBkgColor": t.sections[2],
176 "taskBorderColor": t.series[2],
177 "activeTaskBkgColor": t.sections[0],
178 "activeTaskBorderColor": t.series[0],
179 "doneTaskBkgColor": t.node_alt,
180 "doneTaskBorderColor": t.border_strong,
181 "critBkgColor": t.crit_fill,
182 "critBorderColor": t.crit_border,
183 "taskTextColor": t.text,
184 "taskTextLightColor": t.text,
185 "taskTextDarkColor": t.text,
186 "taskTextOutsideColor": t.text,
187 "taskTextClickableColor": t.text,
188 "commitLabelColor": t.text_secondary,
190 "commitLabelBackground": t.background,
191 "tagLabelColor": t.text,
192 "tagLabelBackground": t.node,
193 "tagLabelBorder": t.border_strong,
194 "pieStrokeColor": t.background,
196 "pieOuterStrokeColor": t.background,
197 "pieOpacity": "1",
198 "pieSectionTextColor": t.on_series,
199 "pieTitleTextColor": t.text,
200 "pieLegendTextColor": t.text,
201 "quadrant1Fill": t.node,
203 "quadrant2Fill": t.node,
204 "quadrant3Fill": t.node,
205 "quadrant4Fill": t.node,
206 "quadrantPointFill": t.series[0],
207 "quadrantPointTextFill": t.text,
208 "quadrantInternalBorderStrokeFill": t.border,
209 "quadrantExternalBorderStrokeFill": t.border_strong,
210 "archEdgeColor": t.line,
212 "archEdgeArrowColor": t.line,
213 "archEdgeWidth": "2",
214 "archGroupBorderColor": t.border_strong,
215 "xyChart": {
216 "backgroundColor": t.background,
217 "titleColor": t.text,
218 "dataLabelColor": t.text,
219 "legendTextColor": t.text,
220 "xAxisTitleColor": t.text_secondary,
221 "xAxisLabelColor": t.text_secondary,
222 "xAxisTickColor": t.border_strong,
223 "xAxisLineColor": t.border_strong,
224 "yAxisTitleColor": t.text_secondary,
225 "yAxisLabelColor": t.text_secondary,
226 "yAxisTickColor": t.border_strong,
227 "yAxisLineColor": t.border_strong,
228 "plotColorPalette": t.series.join(","),
229 },
230 "radar": {
231 "axisColor": t.line,
232 "graticuleColor": t.node_alt,
233 "graticuleOpacity": 0.5,
234 "curveOpacity": 0.3,
235 },
236 });
237 let vars = v.as_object_mut().expect("themeVariables 是对象");
238 if let Value::Object(diagrams) = diagrams {
239 vars.extend(diagrams);
240 }
241 for i in 0..SCALE_LEN {
243 let fill = if radar {
244 t.series[i % t.series.len()]
245 } else {
246 t.sections[i % t.sections.len()]
247 };
248 let accent = t.series[i % t.sections.len()];
249 set(vars, format!("cScale{i}"), fill);
250 set(vars, format!("cScalePeer{i}"), accent);
251 set(vars, format!("cScaleInv{i}"), accent);
252 set(vars, format!("cScaleLabel{i}"), t.text);
253 set(vars, format!("pie{}", i + 1), t.series[i % t.series.len()]);
254 }
255 for i in 0..GIT_LEN {
256 set(vars, format!("git{i}"), t.series[i]);
257 set(vars, format!("gitInv{i}"), t.background);
259 set(vars, format!("gitBranchLabel{i}"), t.on_series);
260 }
261 v
262}
263
264fn set(vars: &mut Map<String, Value>, key: String, color: &str) {
265 vars.insert(key, Value::from(color));
266}
267
268pub(super) fn site_config(dark: bool, node_spacing: Option<u32>, source: &str) -> Value {
273 let t = tones(dark);
274 let mut flowchart = json!({
275 "curve": "basis",
276 "diagramPadding": 16,
277 "useMaxWidth": true,
278 "htmlLabels": true,
279 });
280 if let Some(spacing) = node_spacing {
281 flowchart["nodeSpacing"] = json!(spacing);
282 }
283 json!({
284 "layout": "dagre",
285 "look": "classic",
286 "theme": "base",
287 "darkMode": dark,
288 "securityLevel": "strict",
289 "flowchart": flowchart,
290 "c4": {
294 "c4ShapeMargin": 70,
295 "relationship_text_color": t.text_secondary,
296 "relationship_line_color": t.line,
297 "boundary_border_color": t.border_strong,
298 "boundary_font_color": t.text_secondary,
299 },
300 "packet": {
301 "startByteColor": t.text_tertiary,
302 "endByteColor": t.text_tertiary,
303 "labelColor": t.text,
304 "titleColor": t.text,
305 "blockStrokeColor": t.border_strong,
306 "blockFillColor": t.node,
307 },
308 "themeVariables": theme_variables(t, is_radar(source)),
309 })
310}
311
312fn is_radar(source: &str) -> bool {
314 let mut lines = source
315 .lines()
316 .map(str::trim)
317 .filter(|l| !l.is_empty() && !l.starts_with("%%"));
318 let mut first = lines.next();
319 if first == Some("---") {
320 first = lines.by_ref().skip_while(|l| *l != "---").nth(1);
321 }
322 first
323 .and_then(|l| l.split_whitespace().next())
324 .is_some_and(|kind| kind == "radar-beta" || kind == "radar")
325}
326
327pub(super) fn fixup_css(svg_id: &str, dark: bool) -> String {
337 let background = tones(dark).background;
338 let mut css = format!(
339 "#{svg_id} .grid .tick line{{stroke:inherit;}}\
340 #{svg_id} .marker.er circle{{fill:{background};}}\
341 #{svg_id} .messageText{{paint-order:stroke;stroke:{background};stroke-width:4px;stroke-linejoin:round;}}"
342 );
343 if dark {
344 css.push_str(&format!(
345 "#{svg_id} .link{{mix-blend-mode:screen!important;}}"
346 ));
347 }
348 css
349}
350
351#[cfg(all(test, feature = "server"))]
352mod tests {
353 use super::is_radar;
354
355 #[test]
356 fn radar_detection_skips_comments_and_frontmatter() {
357 assert!(is_radar("radar-beta\n axis a"));
358 assert!(is_radar("\n%% 注释\n%%{init: {}}%%\n radar-beta\n"));
359 assert!(is_radar("---\ntitle: 雷达\n---\nradar-beta\n"));
360 assert!(!is_radar("flowchart LR\n radar --> b"));
361 assert!(!is_radar("---\nradar-beta\n---\npie\n"));
362 assert!(!is_radar(""));
363 }
364}