1mod css;
24mod error;
25mod render;
26
27pub use error::RenderError;
28pub use render::render_html;
29
30pub fn version() -> &'static str {
32 env!("CARGO_PKG_VERSION")
33}
34
35#[cfg(test)]
36mod tests {
37 use dendryform_core::{
38 Color, Connector, ConnectorStyle, Container, ContainerBorder, Diagram, DiagramHeader,
39 FlowLabels, Layer, LegendEntry, Node, NodeId, NodeKind, RawDiagram, Theme, Tier,
40 TierLayout, Title,
41 };
42 use dendryform_layout::compute_layout;
43
44 use super::*;
45
46 fn test_node(id: &str, color: Color) -> Node {
47 Node::builder()
48 .id(NodeId::new(id).unwrap())
49 .kind(NodeKind::System)
50 .color(color)
51 .icon("\u{25c7}")
52 .title(id)
53 .description("test node")
54 .build()
55 .unwrap()
56 }
57
58 fn make_diagram(layers: Vec<Layer>, legend: Vec<LegendEntry>) -> Diagram {
59 let raw = RawDiagram {
60 diagram: DiagramHeader::new(Title::new("test", "accent"), "subtitle", "dark"),
61 layers,
62 legend,
63 edges: vec![],
64 };
65 Diagram::try_from(raw).unwrap()
66 }
67
68 #[test]
69 fn test_version_is_set() {
70 assert_eq!(version(), "0.1.0");
71 }
72
73 #[test]
74 fn test_render_minimal() {
75 let diagram = make_diagram(
76 vec![Layer::Tier(Tier::new(
77 NodeId::new("main").unwrap(),
78 vec![test_node("app", Color::Blue)],
79 ))],
80 vec![],
81 );
82 let plan = compute_layout(&diagram).unwrap();
83 let html = render_html(&plan, &Theme::dark()).unwrap();
84
85 assert!(html.contains("<!DOCTYPE html>"));
86 assert!(html.contains("</html>"));
87 assert!(html.contains("accent"));
88 assert!(html.contains("test"));
89 }
90
91 #[test]
92 fn test_render_contains_css_variables() {
93 let diagram = make_diagram(
94 vec![Layer::Tier(Tier::new(
95 NodeId::new("main").unwrap(),
96 vec![test_node("app", Color::Blue)],
97 ))],
98 vec![],
99 );
100 let plan = compute_layout(&diagram).unwrap();
101 let html = render_html(&plan, &Theme::dark()).unwrap();
102
103 assert!(html.contains("--bg: #0a0e14"));
104 assert!(html.contains("--text: #c4cdd9"));
105 assert!(html.contains("--accent-blue: #4fc3f7"));
106 }
107
108 #[test]
109 fn test_render_contains_node() {
110 let diagram = make_diagram(
111 vec![Layer::Tier(Tier::new(
112 NodeId::new("main").unwrap(),
113 vec![test_node("myapp", Color::Green)],
114 ))],
115 vec![],
116 );
117 let plan = compute_layout(&diagram).unwrap();
118 let html = render_html(&plan, &Theme::dark()).unwrap();
119
120 assert!(html.contains("class=\"node green"));
121 assert!(html.contains("myapp"));
122 }
123
124 #[test]
125 fn test_render_contains_connector() {
126 let diagram = make_diagram(
127 vec![
128 Layer::Tier(Tier::new(
129 NodeId::new("top").unwrap(),
130 vec![test_node("a", Color::Blue)],
131 )),
132 Layer::Connector(Connector::with_label(ConnectorStyle::Line, "HTTPS")),
133 Layer::Tier(Tier::new(
134 NodeId::new("bottom").unwrap(),
135 vec![test_node("b", Color::Green)],
136 )),
137 ],
138 vec![],
139 );
140 let plan = compute_layout(&diagram).unwrap();
141 let html = render_html(&plan, &Theme::dark()).unwrap();
142
143 assert!(html.contains("protocol-label"));
144 assert!(html.contains("HTTPS"));
145 }
146
147 #[test]
148 fn test_render_contains_container() {
149 let container = Container::new(
150 "server",
151 ContainerBorder::Solid,
152 Color::Green,
153 vec![Layer::Tier(Tier::new(
154 NodeId::new("inner").unwrap(),
155 vec![test_node("api", Color::Green)],
156 ))],
157 );
158 let diagram = make_diagram(
159 vec![Layer::Tier(Tier::with_container(
160 NodeId::new("server").unwrap(),
161 container,
162 ))],
163 vec![],
164 );
165 let plan = compute_layout(&diagram).unwrap();
166 let html = render_html(&plan, &Theme::dark()).unwrap();
167
168 assert!(html.contains("container-solid"));
169 assert!(html.contains("container-label"));
170 assert!(html.contains("server"));
171 }
172
173 #[test]
174 fn test_render_contains_legend() {
175 let diagram = make_diagram(
176 vec![Layer::Tier(Tier::new(
177 NodeId::new("main").unwrap(),
178 vec![test_node("a", Color::Blue)],
179 ))],
180 vec![
181 LegendEntry::new(Color::Blue, "Clients"),
182 LegendEntry::new(Color::Green, "Servers"),
183 ],
184 );
185 let plan = compute_layout(&diagram).unwrap();
186 let html = render_html(&plan, &Theme::dark()).unwrap();
187
188 assert!(html.contains("legend"));
189 assert!(html.contains("swatch blue"));
190 assert!(html.contains("Clients"));
191 }
192
193 #[test]
194 fn test_render_contains_flow_labels() {
195 let diagram = make_diagram(
196 vec![
197 Layer::Tier(Tier::new(
198 NodeId::new("top").unwrap(),
199 vec![test_node("a", Color::Blue)],
200 )),
201 Layer::FlowLabels(FlowLabels::new(vec![
202 "SQL queries".to_owned(),
203 "cache reads".to_owned(),
204 ])),
205 Layer::Tier(Tier::new(
206 NodeId::new("bottom").unwrap(),
207 vec![test_node("b", Color::Red)],
208 )),
209 ],
210 vec![],
211 );
212 let plan = compute_layout(&diagram).unwrap();
213 let html = render_html(&plan, &Theme::dark()).unwrap();
214
215 assert!(html.contains("flow-labels"));
216 assert!(html.contains("SQL queries"));
217 assert!(html.contains("\u{2193}")); }
219
220 #[test]
221 fn test_render_grid_layout() {
222 let mut tier = Tier::new(
223 NodeId::new("grid").unwrap(),
224 vec![
225 test_node("a", Color::Blue),
226 test_node("b", Color::Blue),
227 test_node("c", Color::Blue),
228 test_node("d", Color::Blue),
229 ],
230 );
231 tier.set_layout(TierLayout::Grid { columns: 4 });
232 let diagram = make_diagram(vec![Layer::Tier(tier)], vec![]);
233 let plan = compute_layout(&diagram).unwrap();
234 let html = render_html(&plan, &Theme::dark()).unwrap();
235
236 assert!(html.contains("grid-4"));
237 }
238
239 #[test]
240 fn test_render_responsive_css() {
241 let diagram = make_diagram(
242 vec![Layer::Tier(Tier::new(
243 NodeId::new("main").unwrap(),
244 vec![test_node("a", Color::Blue)],
245 ))],
246 vec![],
247 );
248 let plan = compute_layout(&diagram).unwrap();
249 let html = render_html(&plan, &Theme::dark()).unwrap();
250
251 assert!(html.contains("@media (max-width: 800px)"));
252 }
253
254 #[test]
255 fn test_render_animations() {
256 let diagram = make_diagram(
257 vec![Layer::Tier(Tier::new(
258 NodeId::new("main").unwrap(),
259 vec![test_node("a", Color::Blue)],
260 ))],
261 vec![],
262 );
263 let plan = compute_layout(&diagram).unwrap();
264 let html = render_html(&plan, &Theme::dark()).unwrap();
265
266 assert!(html.contains("@keyframes fadeIn"));
267 assert!(html.contains("@keyframes slideUp"));
268 }
269
270 #[test]
271 fn test_render_no_animations_when_disabled() {
272 let diagram = make_diagram(
273 vec![Layer::Tier(Tier::new(
274 NodeId::new("main").unwrap(),
275 vec![test_node("a", Color::Blue)],
276 ))],
277 vec![],
278 );
279 let plan = compute_layout(&diagram).unwrap();
280 let mut theme = Theme::dark();
281 let overrides = dendryform_core::ThemeOverrides {
282 animate: Some(false),
283 ..Default::default()
284 };
285 theme = theme.merge(overrides);
286 let html = render_html(&plan, &theme).unwrap();
287
288 assert!(html.contains("animation: none !important"));
289 assert!(!html.contains("@keyframes fadeIn"));
290 }
291
292 #[test]
293 fn test_render_taproot_full() {
294 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
295 let diagram = dendryform_parse::parse_yaml(yaml).unwrap();
296 let plan = compute_layout(&diagram).unwrap();
297 let html = render_html(&plan, &Theme::dark()).unwrap();
298
299 assert!(html.contains("<!DOCTYPE html>"));
301 assert!(html.contains("taproot"));
302 assert!(html.contains("system architecture"));
303 assert!(html.contains("Claude Desktop"));
304 assert!(html.contains("Streamable HTTP + SSE"));
305 assert!(html.contains("container-solid"));
306 assert!(html.contains("container-dashed"));
307 assert!(html.contains("knowledge engine"));
308 assert!(html.contains("BigQuery"));
309 assert!(html.contains("flow-labels"));
310 assert!(html.contains("SQL queries"));
311 assert!(html.contains("legend"));
312 assert!(html.contains("Client / Auth"));
313 }
314
315 #[test]
316 fn test_render_ai_kasu_full() {
317 let yaml = include_str!("../../../examples/ai-kasu/architecture.yaml");
318 let diagram = dendryform_parse::parse_yaml(yaml).unwrap();
319 let plan = compute_layout(&diagram).unwrap();
320 let html = render_html(&plan, &Theme::dark()).unwrap();
321
322 assert!(html.contains("<!DOCTYPE html>"));
323 assert!(html.contains("ai-kasu"));
324 assert!(html.contains("MCP server architecture"));
325 assert!(html.contains("Claude Code"));
326 assert!(html.contains("KnowledgeEngine"));
327 assert!(html.contains("container-solid"));
328 assert!(html.contains("container-dashed"));
329 assert!(html.contains("knowledge engine"));
330 assert!(html.contains("flow-labels"));
331 assert!(html.contains("legend"));
332 assert!(html.contains("Tool Registries"));
333 }
334
335 #[test]
336 fn test_render_oxur_lisp_full() {
337 let yaml = include_str!("../../../examples/oxur-lisp/architecture.yaml");
338 let diagram = dendryform_parse::parse_yaml(yaml).unwrap();
339 let plan = compute_layout(&diagram).unwrap();
340 let html = render_html(&plan, &Theme::dark()).unwrap();
341
342 assert!(html.contains("<!DOCTYPE html>"));
343 assert!(html.contains("oxur"));
344 assert!(html.contains("language architecture"));
345 assert!(html.contains("Stage 1: Parse"));
346 assert!(html.contains("container-solid"));
347 assert!(html.contains("container-dashed"));
348 assert!(html.contains("oxur compilation pipeline"));
349 assert!(html.contains("flow-labels"));
350 assert!(html.contains("legend"));
351 assert!(html.contains("Frontend (oxur-lang)"));
352 }
353
354 #[test]
355 fn test_html_escaping() {
356 let node = Node::builder()
357 .id(NodeId::new("test").unwrap())
358 .kind(NodeKind::System)
359 .color(Color::Blue)
360 .icon("<>")
361 .title("A & B")
362 .description("x < y > z")
363 .build()
364 .unwrap();
365 let diagram = make_diagram(
366 vec![Layer::Tier(Tier::new(
367 NodeId::new("main").unwrap(),
368 vec![node],
369 ))],
370 vec![],
371 );
372 let plan = compute_layout(&diagram).unwrap();
373 let html = render_html(&plan, &Theme::dark()).unwrap();
374
375 assert!(html.contains("&"));
376 assert!(html.contains("<"));
377 assert!(html.contains(">"));
378 assert!(!html.contains("A & B"));
380 assert!(!html.contains("x < y"));
381 }
382}