Skip to main content

merman_render/
block.rs

1use crate::config::config_f64;
2use crate::model::{BlockDiagramLayout, Bounds, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint};
3use crate::text::{TextMeasurer, TextStyle, WrapMode};
4use crate::{Error, Result};
5use serde_json::{Map, Value};
6use std::collections::HashMap;
7
8pub(crate) type BlockDiagramModel = merman_core::diagrams::block::BlockDiagramRenderModel;
9pub(crate) type BlockNode = merman_core::diagrams::block::BlockNodeRenderModel;
10
11#[derive(Debug, Clone)]
12struct SizedBlock {
13    id: String,
14    block_type: String,
15    children: Vec<SizedBlock>,
16    columns: i64,
17    width_in_columns: i64,
18    width: f64,
19    height: f64,
20    label_width: f64,
21    label_height: f64,
22    x: f64,
23    y: f64,
24}
25
26fn decode_block_label_html(raw: &str) -> String {
27    raw.replace("&nbsp;", "\u{00A0}")
28}
29
30pub(crate) fn block_label_is_effectively_empty(text: &str) -> bool {
31    !text.is_empty()
32        && text
33            .chars()
34            .all(|ch| ch != '\u{00A0}' && ch.is_whitespace())
35}
36
37fn block_html_label_metrics_px(
38    text: &str,
39    measurer: &dyn TextMeasurer,
40    style: &TextStyle,
41) -> (f64, f64) {
42    let html_metrics = measurer.measure_wrapped(text, style, None, WrapMode::HtmlLike);
43    let width =
44        crate::generated::block_text_overrides_11_12_2::lookup_html_width_px(style.font_size, text)
45            .unwrap_or(html_metrics.width)
46            .max(0.0);
47    let height = html_metrics.height.max(0.0);
48    (width, height)
49}
50
51#[derive(Debug, Clone, Copy)]
52pub(crate) struct BlockArrowPoint {
53    pub(crate) x: f64,
54    pub(crate) y: f64,
55}
56
57pub(crate) fn block_arrow_points(
58    directions: &[String],
59    bbox_w: f64,
60    bbox_h: f64,
61    node_padding: f64,
62) -> Vec<BlockArrowPoint> {
63    fn expand_and_dedup(directions: &[String]) -> std::collections::BTreeSet<String> {
64        let mut out = std::collections::BTreeSet::new();
65        for d in directions {
66            match d.trim() {
67                "x" => {
68                    out.insert("right".to_string());
69                    out.insert("left".to_string());
70                }
71                "y" => {
72                    out.insert("up".to_string());
73                    out.insert("down".to_string());
74                }
75                other if !other.is_empty() => {
76                    out.insert(other.to_string());
77                }
78                _ => {}
79            }
80        }
81        out
82    }
83
84    let dirs = expand_and_dedup(directions);
85    let height = bbox_h + 2.0 * node_padding;
86    let midpoint = height / 2.0;
87    let width = bbox_w + 2.0 * midpoint + node_padding;
88    let pad = node_padding / 2.0;
89
90    let has = |name: &str| dirs.contains(name);
91
92    if has("right") && has("left") && has("up") && has("down") {
93        return vec![
94            BlockArrowPoint { x: 0.0, y: 0.0 },
95            BlockArrowPoint {
96                x: midpoint,
97                y: 0.0,
98            },
99            BlockArrowPoint {
100                x: width / 2.0,
101                y: 2.0 * pad,
102            },
103            BlockArrowPoint {
104                x: width - midpoint,
105                y: 0.0,
106            },
107            BlockArrowPoint { x: width, y: 0.0 },
108            BlockArrowPoint {
109                x: width,
110                y: -height / 3.0,
111            },
112            BlockArrowPoint {
113                x: width + 2.0 * pad,
114                y: -height / 2.0,
115            },
116            BlockArrowPoint {
117                x: width,
118                y: (-2.0 * height) / 3.0,
119            },
120            BlockArrowPoint {
121                x: width,
122                y: -height,
123            },
124            BlockArrowPoint {
125                x: width - midpoint,
126                y: -height,
127            },
128            BlockArrowPoint {
129                x: width / 2.0,
130                y: -height - 2.0 * pad,
131            },
132            BlockArrowPoint {
133                x: midpoint,
134                y: -height,
135            },
136            BlockArrowPoint { x: 0.0, y: -height },
137            BlockArrowPoint {
138                x: 0.0,
139                y: (-2.0 * height) / 3.0,
140            },
141            BlockArrowPoint {
142                x: -2.0 * pad,
143                y: -height / 2.0,
144            },
145            BlockArrowPoint {
146                x: 0.0,
147                y: -height / 3.0,
148            },
149        ];
150    }
151    if has("right") && has("left") && has("up") {
152        return vec![
153            BlockArrowPoint {
154                x: midpoint,
155                y: 0.0,
156            },
157            BlockArrowPoint {
158                x: width - midpoint,
159                y: 0.0,
160            },
161            BlockArrowPoint {
162                x: width,
163                y: -height / 2.0,
164            },
165            BlockArrowPoint {
166                x: width - midpoint,
167                y: -height,
168            },
169            BlockArrowPoint {
170                x: midpoint,
171                y: -height,
172            },
173            BlockArrowPoint {
174                x: 0.0,
175                y: -height / 2.0,
176            },
177        ];
178    }
179    if has("right") && has("left") && has("down") {
180        return vec![
181            BlockArrowPoint { x: 0.0, y: 0.0 },
182            BlockArrowPoint {
183                x: midpoint,
184                y: -height,
185            },
186            BlockArrowPoint {
187                x: width - midpoint,
188                y: -height,
189            },
190            BlockArrowPoint { x: width, y: 0.0 },
191        ];
192    }
193    if has("right") && has("up") && has("down") {
194        return vec![
195            BlockArrowPoint { x: 0.0, y: 0.0 },
196            BlockArrowPoint {
197                x: width,
198                y: -midpoint,
199            },
200            BlockArrowPoint {
201                x: width,
202                y: -height + midpoint,
203            },
204            BlockArrowPoint { x: 0.0, y: -height },
205        ];
206    }
207    if has("left") && has("up") && has("down") {
208        return vec![
209            BlockArrowPoint { x: width, y: 0.0 },
210            BlockArrowPoint {
211                x: 0.0,
212                y: -midpoint,
213            },
214            BlockArrowPoint {
215                x: 0.0,
216                y: -height + midpoint,
217            },
218            BlockArrowPoint {
219                x: width,
220                y: -height,
221            },
222        ];
223    }
224    if has("right") && has("left") {
225        return vec![
226            BlockArrowPoint {
227                x: midpoint,
228                y: 0.0,
229            },
230            BlockArrowPoint {
231                x: midpoint,
232                y: -pad,
233            },
234            BlockArrowPoint {
235                x: width - midpoint,
236                y: -pad,
237            },
238            BlockArrowPoint {
239                x: width - midpoint,
240                y: 0.0,
241            },
242            BlockArrowPoint {
243                x: width,
244                y: -height / 2.0,
245            },
246            BlockArrowPoint {
247                x: width - midpoint,
248                y: -height,
249            },
250            BlockArrowPoint {
251                x: width - midpoint,
252                y: -height + pad,
253            },
254            BlockArrowPoint {
255                x: midpoint,
256                y: -height + pad,
257            },
258            BlockArrowPoint {
259                x: midpoint,
260                y: -height,
261            },
262            BlockArrowPoint {
263                x: 0.0,
264                y: -height / 2.0,
265            },
266        ];
267    }
268    if has("up") && has("down") {
269        return vec![
270            BlockArrowPoint {
271                x: width / 2.0,
272                y: 0.0,
273            },
274            BlockArrowPoint { x: 0.0, y: -pad },
275            BlockArrowPoint {
276                x: midpoint,
277                y: -pad,
278            },
279            BlockArrowPoint {
280                x: midpoint,
281                y: -height + pad,
282            },
283            BlockArrowPoint {
284                x: 0.0,
285                y: -height + pad,
286            },
287            BlockArrowPoint {
288                x: width / 2.0,
289                y: -height,
290            },
291            BlockArrowPoint {
292                x: width,
293                y: -height + pad,
294            },
295            BlockArrowPoint {
296                x: width - midpoint,
297                y: -height + pad,
298            },
299            BlockArrowPoint {
300                x: width - midpoint,
301                y: -pad,
302            },
303            BlockArrowPoint { x: width, y: -pad },
304        ];
305    }
306    if has("right") && has("up") {
307        return vec![
308            BlockArrowPoint { x: 0.0, y: 0.0 },
309            BlockArrowPoint {
310                x: width,
311                y: -midpoint,
312            },
313            BlockArrowPoint { x: 0.0, y: -height },
314        ];
315    }
316    if has("right") && has("down") {
317        return vec![
318            BlockArrowPoint { x: 0.0, y: 0.0 },
319            BlockArrowPoint { x: width, y: 0.0 },
320            BlockArrowPoint { x: 0.0, y: -height },
321        ];
322    }
323    if has("left") && has("up") {
324        return vec![
325            BlockArrowPoint { x: width, y: 0.0 },
326            BlockArrowPoint {
327                x: 0.0,
328                y: -midpoint,
329            },
330            BlockArrowPoint {
331                x: width,
332                y: -height,
333            },
334        ];
335    }
336    if has("left") && has("down") {
337        return vec![
338            BlockArrowPoint { x: width, y: 0.0 },
339            BlockArrowPoint { x: 0.0, y: 0.0 },
340            BlockArrowPoint {
341                x: width,
342                y: -height,
343            },
344        ];
345    }
346    if has("right") {
347        return vec![
348            BlockArrowPoint {
349                x: midpoint,
350                y: -pad,
351            },
352            BlockArrowPoint {
353                x: midpoint,
354                y: -pad,
355            },
356            BlockArrowPoint {
357                x: width - midpoint,
358                y: -pad,
359            },
360            BlockArrowPoint {
361                x: width - midpoint,
362                y: 0.0,
363            },
364            BlockArrowPoint {
365                x: width,
366                y: -height / 2.0,
367            },
368            BlockArrowPoint {
369                x: width - midpoint,
370                y: -height,
371            },
372            BlockArrowPoint {
373                x: width - midpoint,
374                y: -height + pad,
375            },
376            BlockArrowPoint {
377                x: midpoint,
378                y: -height + pad,
379            },
380            BlockArrowPoint {
381                x: midpoint,
382                y: -height + pad,
383            },
384        ];
385    }
386    if has("left") {
387        return vec![
388            BlockArrowPoint {
389                x: midpoint,
390                y: 0.0,
391            },
392            BlockArrowPoint {
393                x: midpoint,
394                y: -pad,
395            },
396            BlockArrowPoint {
397                x: width - midpoint,
398                y: -pad,
399            },
400            BlockArrowPoint {
401                x: width - midpoint,
402                y: -height + pad,
403            },
404            BlockArrowPoint {
405                x: midpoint,
406                y: -height + pad,
407            },
408            BlockArrowPoint {
409                x: midpoint,
410                y: -height,
411            },
412            BlockArrowPoint {
413                x: 0.0,
414                y: -height / 2.0,
415            },
416        ];
417    }
418    if has("up") {
419        return vec![
420            BlockArrowPoint {
421                x: midpoint,
422                y: -pad,
423            },
424            BlockArrowPoint {
425                x: midpoint,
426                y: -height + pad,
427            },
428            BlockArrowPoint {
429                x: 0.0,
430                y: -height + pad,
431            },
432            BlockArrowPoint {
433                x: width / 2.0,
434                y: -height,
435            },
436            BlockArrowPoint {
437                x: width,
438                y: -height + pad,
439            },
440            BlockArrowPoint {
441                x: width - midpoint,
442                y: -height + pad,
443            },
444            BlockArrowPoint {
445                x: width - midpoint,
446                y: -pad,
447            },
448        ];
449    }
450    if has("down") {
451        return vec![
452            BlockArrowPoint {
453                x: width / 2.0,
454                y: 0.0,
455            },
456            BlockArrowPoint { x: 0.0, y: -pad },
457            BlockArrowPoint {
458                x: midpoint,
459                y: -pad,
460            },
461            BlockArrowPoint {
462                x: midpoint,
463                y: -height + pad,
464            },
465            BlockArrowPoint {
466                x: width - midpoint,
467                y: -height + pad,
468            },
469            BlockArrowPoint {
470                x: width - midpoint,
471                y: -pad,
472            },
473            BlockArrowPoint { x: width, y: -pad },
474        ];
475    }
476
477    vec![BlockArrowPoint { x: 0.0, y: 0.0 }]
478}
479
480fn polygon_bounds(points: &[BlockArrowPoint]) -> (f64, f64) {
481    if points.is_empty() {
482        return (0.0, 0.0);
483    }
484
485    let mut min_x = points[0].x;
486    let mut max_x = points[0].x;
487    let mut min_y = points[0].y;
488    let mut max_y = points[0].y;
489    for point in &points[1..] {
490        min_x = min_x.min(point.x);
491        max_x = max_x.max(point.x);
492        min_y = min_y.min(point.y);
493        max_y = max_y.max(point.y);
494    }
495
496    ((max_x - min_x).max(0.0), (max_y - min_y).max(0.0))
497}
498
499fn block_shape_size(
500    block_type: &str,
501    directions: &[String],
502    label_width: f64,
503    label_height: f64,
504    padding: f64,
505    has_label: bool,
506) -> Option<(f64, f64)> {
507    let rect_w = (label_width + padding).max(1.0);
508    let rect_h = (label_height + padding).max(1.0);
509
510    match block_type {
511        "composite" => has_label.then(|| (label_width.max(1.0), (label_height + padding).max(1.0))),
512        "group" => has_label.then_some((rect_w, rect_h)),
513        "space" => None,
514        "circle" => Some((rect_w, rect_w)),
515        "doublecircle" => {
516            let outer_diameter = rect_w + 10.0;
517            Some((outer_diameter, outer_diameter))
518        }
519        "stadium" => Some(((label_width + rect_h / 4.0 + padding).max(1.0), rect_h)),
520        "cylinder" => {
521            let rx = rect_w / 2.0;
522            let ry = rx / (2.5 + rect_w / 50.0);
523            let body_h = (label_height + ry + padding).max(1.0);
524            Some((rect_w, body_h + 2.0 * ry))
525        }
526        "diamond" => {
527            let side = (rect_w + rect_h).max(1.0);
528            Some((side, side))
529        }
530        "hexagon" => {
531            let shoulder = rect_h / 4.0;
532            Some(((label_width + 2.0 * shoulder + padding).max(1.0), rect_h))
533        }
534        "rect_left_inv_arrow" => Some((rect_w + rect_h / 2.0, rect_h)),
535        "subroutine" => Some((rect_w + 16.0, rect_h)),
536        "lean_right" | "trapezoid" | "inv_trapezoid" => {
537            Some((rect_w + (2.0 * rect_h) / 3.0, rect_h))
538        }
539        "lean_left" => Some((rect_w + rect_h / 3.0, rect_h)),
540        "block_arrow" => Some(polygon_bounds(&block_arrow_points(
541            directions,
542            label_width,
543            label_height,
544            padding,
545        ))),
546        _ => Some((rect_w, rect_h)),
547    }
548}
549
550fn to_sized_block_shallow(
551    node: &BlockNode,
552    padding: f64,
553    measurer: &dyn TextMeasurer,
554    text_style: &TextStyle,
555    children: Vec<SizedBlock>,
556) -> SizedBlock {
557    let columns = node.columns.unwrap_or(-1);
558    let width_in_columns = node.width_in_columns.unwrap_or(1).max(1);
559
560    let mut width = 0.0;
561    let mut height = 0.0;
562
563    // Mermaid renders block diagram labels via `labelHelper(...)`, which decodes HTML entities
564    // and measures the resulting HTML content (`getBoundingClientRect()` for width/height).
565    //
566    // Block diagrams frequently use `&nbsp;` placeholders (notably for block arrows), so we must
567    // decode those before measuring; otherwise node widths drift drastically.
568    let label_decoded = decode_block_label_html(&node.label);
569    let label_effectively_empty = block_label_is_effectively_empty(&label_decoded);
570    let (label_width, label_height) = if label_effectively_empty {
571        (0.0, 0.0)
572    } else {
573        block_html_label_metrics_px(&label_decoded, measurer, text_style)
574    };
575    let shape_label_height = label_height;
576
577    if let Some((computed_width, computed_height)) = block_shape_size(
578        node.block_type.as_str(),
579        &node.directions,
580        label_width,
581        shape_label_height,
582        padding,
583        !label_effectively_empty && !label_decoded.trim().is_empty(),
584    ) {
585        width = computed_width;
586        height = computed_height;
587    }
588
589    SizedBlock {
590        id: node.id.clone(),
591        block_type: node.block_type.clone(),
592        children,
593        columns,
594        width_in_columns,
595        width,
596        height,
597        label_width,
598        label_height,
599        x: 0.0,
600        y: 0.0,
601    }
602}
603
604fn to_sized_block(
605    node: &BlockNode,
606    padding: f64,
607    measurer: &dyn TextMeasurer,
608    text_style: &TextStyle,
609) -> SizedBlock {
610    let mut stack: Vec<(&BlockNode, bool)> = vec![(node, false)];
611    let mut completed: HashMap<*const BlockNode, SizedBlock> = HashMap::new();
612
613    while let Some((block, visited)) = stack.pop() {
614        if visited {
615            let children = block
616                .children
617                .iter()
618                .filter_map(|child| completed.remove(&(child as *const BlockNode)))
619                .collect();
620            completed.insert(
621                block as *const BlockNode,
622                to_sized_block_shallow(block, padding, measurer, text_style, children),
623            );
624        } else {
625            stack.push((block, true));
626            for child in block.children.iter().rev() {
627                stack.push((child, false));
628            }
629        }
630    }
631
632    completed
633        .remove(&(node as *const BlockNode))
634        .unwrap_or_else(|| to_sized_block_shallow(node, padding, measurer, text_style, Vec::new()))
635}
636
637fn get_max_child_size(block: &SizedBlock) -> (f64, f64) {
638    let mut max_width = 0.0;
639    let mut max_height = 0.0;
640    for child in &block.children {
641        if child.block_type == "space" {
642            continue;
643        }
644        if child.width > max_width {
645            max_width = child.width / (block.width_in_columns as f64);
646        }
647        if child.height > max_height {
648            max_height = child.height;
649        }
650    }
651    (max_width, max_height)
652}
653
654fn block_ref_at_path<'a>(root: &'a SizedBlock, path: &[usize]) -> &'a SizedBlock {
655    let mut block = root;
656    for &index in path {
657        block = &block.children[index];
658    }
659    block
660}
661
662fn block_mut_at_path<'a>(root: &'a mut SizedBlock, path: &[usize]) -> &'a mut SizedBlock {
663    let mut block = root;
664    for &index in path {
665        block = &mut block.children[index];
666    }
667    block
668}
669
670fn set_block_sizes_shallow(block: &mut SizedBlock, padding: f64) {
671    if block.width <= 0.0 {
672        block.width = 0.0;
673        block.height = 0.0;
674        block.x = 0.0;
675        block.y = 0.0;
676    }
677
678    if block.children.is_empty() {
679        return;
680    }
681
682    let (mut max_width, mut max_height) = get_max_child_size(block);
683
684    for child in &mut block.children {
685        child.width = max_width * (child.width_in_columns as f64)
686            + padding * ((child.width_in_columns as f64) - 1.0);
687        child.height = max_height;
688        child.x = 0.0;
689        child.y = 0.0;
690    }
691
692    for child in &mut block.children {
693        child.x = 0.0;
694        child.y = 0.0;
695    }
696
697    let (x_size, y_size) = block_grid_size(block);
698
699    let mut width = (x_size as f64) * (max_width + padding) + padding;
700    let height = (y_size as f64) * (max_height + padding) + padding;
701
702    if width < block.width {
703        width = block.width;
704        let num = if block.columns > 0 {
705            (block.children.len() as i64).min(block.columns)
706        } else {
707            block.children.len() as i64
708        };
709        if num > 0 {
710            let child_width = (width - (num as f64) * padding - padding) / (num as f64);
711            for child in &mut block.children {
712                child.width = child_width;
713            }
714        }
715    }
716
717    block.width = width;
718    block.height = height;
719    block.x = 0.0;
720    block.y = 0.0;
721
722    // Keep behavior consistent with Mermaid even when all children were `space`.
723    max_width = max_width.max(0.0);
724    max_height = max_height.max(0.0);
725    let _ = (max_width, max_height);
726}
727
728fn block_grid_size(block: &SizedBlock) -> (i64, i64) {
729    let columns = block.columns;
730    let mut num_items = 0i64;
731    for child in &block.children {
732        num_items += child.width_in_columns.max(1);
733    }
734
735    let mut x_size = block.children.len() as i64;
736    if columns > 0 && columns < num_items {
737        x_size = columns;
738    }
739    let y_size = ((num_items as f64) / (x_size.max(1) as f64)).ceil() as i64;
740    (x_size, y_size)
741}
742
743fn propagate_parent_size_to_children(block: &mut SizedBlock, padding: f64) {
744    if block.children.is_empty() {
745        return;
746    }
747
748    let (max_width, _max_height) = get_max_child_size(block);
749    let (x_size, y_size) = block_grid_size(block);
750    let grid_width = (x_size as f64) * (max_width + padding) + padding;
751
752    if grid_width < block.width {
753        let child_width = (block.width - (x_size as f64) * padding - padding) / (x_size as f64);
754        let child_height = (block.height - (y_size as f64) * padding - padding) / (y_size as f64);
755        for child in &mut block.children {
756            child.width = child_width;
757            child.height = child_height;
758            child.x = 0.0;
759            child.y = 0.0;
760        }
761    }
762}
763
764fn set_block_sizes(block: &mut SizedBlock, padding: f64) {
765    let mut stack: Vec<(Vec<usize>, bool)> = vec![(Vec::new(), false)];
766    while let Some((path, visited)) = stack.pop() {
767        if visited {
768            let block = block_mut_at_path(block, &path);
769            set_block_sizes_shallow(block, padding);
770            continue;
771        }
772
773        let child_count = block_ref_at_path(block, &path).children.len();
774        stack.push((path.clone(), true));
775        for index in (0..child_count).rev() {
776            let mut child_path = path.clone();
777            child_path.push(index);
778            stack.push((child_path, false));
779        }
780    }
781
782    let mut stack: Vec<Vec<usize>> = vec![Vec::new()];
783    while let Some(path) = stack.pop() {
784        let child_count = {
785            let block = block_mut_at_path(block, &path);
786            propagate_parent_size_to_children(block, padding);
787            block.children.len()
788        };
789
790        for index in (0..child_count).rev() {
791            let mut child_path = path.clone();
792            child_path.push(index);
793            stack.push(child_path);
794        }
795    }
796}
797
798fn calculate_block_position(columns: i64, position: i64) -> (i64, i64) {
799    if columns < 0 {
800        return (position, 0);
801    }
802    if columns == 1 {
803        return (0, position);
804    }
805    (position % columns, position / columns)
806}
807
808fn layout_blocks(block: &mut SizedBlock, padding: f64) {
809    let mut stack: Vec<Vec<usize>> = vec![Vec::new()];
810    while let Some(path) = stack.pop() {
811        let child_count = {
812            let block = block_mut_at_path(block, &path);
813            if block.children.is_empty() {
814                0
815            } else {
816                let columns = block.columns;
817                let mut column_pos = 0i64;
818
819                // JS truthiness: treat `0` as falsy (Mermaid uses `block?.size?.x ? ... : -padding`).
820                let mut starting_pos_x = if block.x != 0.0 {
821                    block.x + (-block.width / 2.0)
822                } else {
823                    -padding
824                };
825                let mut row_pos = 0i64;
826
827                for child in &mut block.children {
828                    let (px, py) = calculate_block_position(columns, column_pos);
829
830                    if py != row_pos {
831                        row_pos = py;
832                        starting_pos_x = if block.x != 0.0 {
833                            block.x + (-block.width / 2.0)
834                        } else {
835                            -padding
836                        };
837                    }
838
839                    let half_width = child.width / 2.0;
840                    child.x = starting_pos_x + padding + half_width;
841                    starting_pos_x = child.x + half_width;
842
843                    child.y = block.y - block.height / 2.0
844                        + (py as f64) * (child.height + padding)
845                        + child.height / 2.0
846                        + padding;
847
848                    let mut columns_filled = child.width_in_columns.max(1);
849                    if columns > 0 {
850                        let rem = columns - (column_pos % columns);
851                        columns_filled = columns_filled.min(rem.max(1));
852                    }
853                    column_pos += columns_filled;
854
855                    let _ = px;
856                }
857                block.children.len()
858            }
859        };
860
861        for index in (0..child_count).rev() {
862            let mut child_path = path.clone();
863            child_path.push(index);
864            stack.push(child_path);
865        }
866    }
867}
868
869fn find_bounds(block: &SizedBlock, b: &mut Bounds) {
870    let mut stack = vec![block];
871    while let Some(block) = stack.pop() {
872        if block.id != "root" {
873            b.min_x = b.min_x.min(block.x - block.width / 2.0);
874            b.min_y = b.min_y.min(block.y - block.height / 2.0);
875            b.max_x = b.max_x.max(block.x + block.width / 2.0);
876            b.max_y = b.max_y.max(block.y + block.height / 2.0);
877        }
878        for child in block.children.iter().rev() {
879            stack.push(child);
880        }
881    }
882}
883
884fn collect_nodes(block: &SizedBlock, out: &mut Vec<LayoutNode>) {
885    let mut stack = vec![block];
886    while let Some(block) = stack.pop() {
887        if block.id != "root" && block.block_type != "space" {
888            out.push(LayoutNode {
889                id: block.id.clone(),
890                x: block.x,
891                y: block.y,
892                width: block.width,
893                height: block.height,
894                is_cluster: false,
895                label_width: Some(block.label_width.max(0.0)),
896                label_height: Some(block.label_height.max(0.0)),
897            });
898        }
899        for child in block.children.iter().rev() {
900            stack.push(child);
901        }
902    }
903}
904
905fn invalid_block_model(message: impl Into<String>) -> Error {
906    Error::InvalidModel {
907        message: message.into(),
908    }
909}
910
911fn required_string_field(obj: &Map<String, Value>, key: &str) -> Result<String> {
912    match obj.get(key) {
913        Some(Value::String(value)) => Ok(value.clone()),
914        Some(other) => Err(invalid_block_model(format!(
915            "block node field `{key}` must be a string, got {other:?}"
916        ))),
917        None => Err(invalid_block_model(format!(
918            "block node missing required field `{key}`"
919        ))),
920    }
921}
922
923fn optional_string_field(obj: &Map<String, Value>, key: &str) -> Result<String> {
924    match obj.get(key) {
925        Some(Value::String(value)) => Ok(value.clone()),
926        Some(other) => Err(invalid_block_model(format!(
927            "block node field `{key}` must be a string, got {other:?}"
928        ))),
929        None => Ok(String::new()),
930    }
931}
932
933fn optional_i64_field(obj: &Map<String, Value>, key: &str) -> Result<Option<i64>> {
934    match obj.get(key) {
935        Some(Value::Number(value)) => value.as_i64().map(Some).ok_or_else(|| {
936            invalid_block_model(format!("block node field `{key}` must be an integer"))
937        }),
938        Some(Value::Null) | None => Ok(None),
939        Some(other) => Err(invalid_block_model(format!(
940            "block node field `{key}` must be an integer, got {other:?}"
941        ))),
942    }
943}
944
945fn string_array_field(obj: &Map<String, Value>, key: &str) -> Result<Vec<String>> {
946    let Some(value) = obj.get(key) else {
947        return Ok(Vec::new());
948    };
949    let Value::Array(items) = value else {
950        return Err(invalid_block_model(format!(
951            "block node field `{key}` must be an array"
952        )));
953    };
954
955    items
956        .iter()
957        .map(|item| match item {
958            Value::String(value) => Ok(value.clone()),
959            other => Err(invalid_block_model(format!(
960                "block node field `{key}` must contain strings, got {other:?}"
961            ))),
962        })
963        .collect()
964}
965
966fn block_children_values<'a>(value: &'a Value) -> Result<&'a [Value]> {
967    let obj = value
968        .as_object()
969        .ok_or_else(|| invalid_block_model("block node must be an object"))?;
970    match obj.get("children") {
971        Some(Value::Array(children)) => Ok(children),
972        Some(other) => Err(invalid_block_model(format!(
973            "block node field `children` must be an array, got {other:?}"
974        ))),
975        None => Ok(&[]),
976    }
977}
978
979fn block_node_from_value_nonrecursive(value: &Value) -> Result<BlockNode> {
980    let mut stack = vec![(value, false)];
981    let mut completed: HashMap<*const Value, BlockNode> = HashMap::new();
982
983    while let Some((current, visited)) = stack.pop() {
984        if visited {
985            let obj = current
986                .as_object()
987                .ok_or_else(|| invalid_block_model("block node must be an object"))?;
988            let children = block_children_values(current)?
989                .iter()
990                .map(|child| {
991                    completed.remove(&std::ptr::from_ref(child)).ok_or_else(|| {
992                        invalid_block_model("block child node was not completed before parent")
993                    })
994                })
995                .collect::<Result<Vec<_>>>()?;
996
997            completed.insert(
998                std::ptr::from_ref(current),
999                BlockNode {
1000                    id: required_string_field(obj, "id")?,
1001                    label: optional_string_field(obj, "label")?,
1002                    block_type: optional_string_field(obj, "type")?,
1003                    children,
1004                    columns: optional_i64_field(obj, "columns")?,
1005                    width_in_columns: optional_i64_field(obj, "widthInColumns")?,
1006                    width: optional_i64_field(obj, "width")?,
1007                    classes: string_array_field(obj, "classes")?,
1008                    styles: string_array_field(obj, "styles")?,
1009                    directions: string_array_field(obj, "directions")?,
1010                },
1011            );
1012        } else {
1013            stack.push((current, true));
1014            for child in block_children_values(current)?.iter().rev() {
1015                stack.push((child, false));
1016            }
1017        }
1018    }
1019
1020    completed
1021        .remove(&std::ptr::from_ref(value))
1022        .ok_or_else(|| invalid_block_model("block root node was not completed"))
1023}
1024
1025pub(crate) fn block_model_from_semantic(semantic: &Value) -> Result<BlockDiagramModel> {
1026    let blocks_flat = match semantic.get("blocksFlat") {
1027        Some(Value::Array(items)) => items
1028            .iter()
1029            .map(block_node_from_value_nonrecursive)
1030            .collect::<Result<Vec<_>>>()?,
1031        Some(other) => {
1032            return Err(invalid_block_model(format!(
1033                "block semantic field `blocksFlat` must be an array, got {other:?}"
1034            )));
1035        }
1036        None => Vec::new(),
1037    };
1038    let edges = semantic
1039        .get("edges")
1040        .map(crate::json::from_value_ref)
1041        .transpose()?
1042        .unwrap_or_default();
1043
1044    Ok(BlockDiagramModel { blocks_flat, edges })
1045}
1046
1047pub fn layout_block_diagram(
1048    semantic: &Value,
1049    effective_config: &Value,
1050    measurer: &dyn TextMeasurer,
1051) -> Result<BlockDiagramLayout> {
1052    let model = block_model_from_semantic(semantic)?;
1053    layout_block_diagram_typed(&model, effective_config, measurer)
1054}
1055
1056pub fn layout_block_diagram_typed(
1057    model: &merman_core::diagrams::block::BlockDiagramRenderModel,
1058    effective_config: &Value,
1059    measurer: &dyn TextMeasurer,
1060) -> Result<BlockDiagramLayout> {
1061    let padding = config_f64(effective_config, &["block", "padding"]).unwrap_or(8.0);
1062    let text_style = crate::text::TextStyle {
1063        font_family: Some(crate::config::config_font_family_or_first_array_css(
1064            effective_config,
1065        )),
1066        font_size: crate::config::config_theme_or_root_font_size_px(effective_config, 16.0)
1067            .max(1.0),
1068        font_weight: None,
1069    };
1070
1071    let root = model
1072        .blocks_flat
1073        .iter()
1074        .find(|b| b.id == "root" && b.block_type == "composite")
1075        .ok_or_else(|| Error::InvalidModel {
1076            message: "missing block root composite".to_string(),
1077        })?;
1078
1079    let mut root = to_sized_block(root, padding, measurer, &text_style);
1080    set_block_sizes(&mut root, padding);
1081    layout_blocks(&mut root, padding);
1082
1083    let mut nodes: Vec<LayoutNode> = Vec::new();
1084    collect_nodes(&root, &mut nodes);
1085
1086    let mut bounds = Bounds {
1087        min_x: 0.0,
1088        min_y: 0.0,
1089        max_x: 0.0,
1090        max_y: 0.0,
1091    };
1092    find_bounds(&root, &mut bounds);
1093    let bounds = if nodes.is_empty() { None } else { Some(bounds) };
1094
1095    let nodes_by_id: HashMap<String, LayoutNode> =
1096        nodes.iter().cloned().map(|n| (n.id.clone(), n)).collect();
1097
1098    let mut edges: Vec<LayoutEdge> = Vec::new();
1099    for e in &model.edges {
1100        let Some(from) = nodes_by_id.get(&e.start) else {
1101            continue;
1102        };
1103        let Some(to) = nodes_by_id.get(&e.end) else {
1104            continue;
1105        };
1106
1107        let start = LayoutPoint {
1108            x: from.x,
1109            y: from.y,
1110        };
1111        let end = LayoutPoint { x: to.x, y: to.y };
1112        let mid = LayoutPoint {
1113            x: start.x + (end.x - start.x) / 2.0,
1114            y: start.y + (end.y - start.y) / 2.0,
1115        };
1116
1117        let label = if e.label.trim().is_empty() {
1118            None
1119        } else {
1120            let edge_label = decode_block_label_html(&e.label);
1121            let (label_width, label_height) =
1122                block_html_label_metrics_px(&edge_label, measurer, &text_style);
1123            Some(LayoutLabel {
1124                x: mid.x,
1125                y: mid.y,
1126                width: label_width.max(1.0),
1127                height: label_height.max(1.0),
1128            })
1129        };
1130
1131        edges.push(LayoutEdge {
1132            id: e.id.clone(),
1133            from: e.start.clone(),
1134            to: e.end.clone(),
1135            from_cluster: None,
1136            to_cluster: None,
1137            points: vec![start, mid, end],
1138            label,
1139            start_label_left: None,
1140            start_label_right: None,
1141            end_label_left: None,
1142            end_label_right: None,
1143            start_marker: e.arrow_type_start.clone(),
1144            end_marker: e.arrow_type_end.clone(),
1145            stroke_dasharray: None,
1146        });
1147    }
1148
1149    Ok(BlockDiagramLayout {
1150        nodes,
1151        edges,
1152        bounds,
1153    })
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158    use crate::text::{TextStyle, VendoredFontMetricsTextMeasurer};
1159
1160    fn default_style(font_size: f64) -> TextStyle {
1161        TextStyle {
1162            font_family: Some("\"trebuchet ms\", verdana, arial, sans-serif".to_string()),
1163            font_size,
1164            font_weight: None,
1165        }
1166    }
1167
1168    #[test]
1169    fn block_label_metrics_use_block_owned_width_and_height_overrides() {
1170        let measurer = VendoredFontMetricsTextMeasurer::default();
1171        let style = default_style(24.0);
1172
1173        let (width, height) = super::block_html_label_metrics_px(
1174            "Font size precedence should widen this block",
1175            &measurer,
1176            &style,
1177        );
1178
1179        assert_eq!(width, 487.890625);
1180        assert_eq!(height, 36.0);
1181    }
1182}