Skip to main content

merman_render/
error.rs

1use crate::Result;
2use crate::model::ErrorDiagramLayout;
3use crate::text::TextMeasurer;
4
5pub const UPSTREAM_MERMAID_VERSION: &str = merman_core::baseline::PINNED_MERMAID_BASELINE_VERSION;
6
7pub(crate) fn layout_error_diagram_typed(
8    semantic: &merman_core::diagrams::error_diagram::ErrorDiagramRenderModel,
9    _effective_config: &serde_json::Value,
10    _measurer: &dyn TextMeasurer,
11) -> Result<ErrorDiagramLayout> {
12    let lines = wrap_error_message(semantic.error_message.as_deref().unwrap_or_default());
13    let height = if lines.is_empty() {
14        512.0
15    } else {
16        500.0 + lines.len() as f64 * 56.0
17    };
18    Ok(ErrorDiagramLayout {
19        viewbox_width: 2412.0,
20        viewbox_height: height,
21        max_width_px: height,
22    })
23}
24
25/// Wraps the upstream error text at 75 Unicode code points and at most four lines.
26pub(crate) fn wrap_error_message(message: &str) -> Vec<String> {
27    const MAX_LINE_LENGTH: usize = 75;
28    const MAX_LINES: usize = 4;
29    let mut lines = Vec::new();
30    let mut current = String::new();
31    let mut current_length = 0;
32    for token in
33        message.split(|ch: char| (ch.is_whitespace() && ch != '\u{0085}') || ch == '\u{feff}')
34    {
35        let mut chars = token.chars().peekable();
36        while chars.peek().is_some() {
37            let word: String = chars.by_ref().take(MAX_LINE_LENGTH).collect();
38            let word_length = word.chars().count();
39            let separator_length = usize::from(!current.is_empty());
40            if current_length + separator_length + word_length > MAX_LINE_LENGTH {
41                lines.push(std::mem::take(&mut current));
42                if lines.len() == MAX_LINES {
43                    let last = &mut lines[MAX_LINES - 1];
44                    *last = last.chars().take(MAX_LINE_LENGTH - 3).collect();
45                    last.push_str("...");
46                    return lines;
47                }
48                current_length = 0;
49            }
50            if !current.is_empty() {
51                current.push(' ');
52                current_length += 1;
53            }
54            current.push_str(&word);
55            current_length += word_length;
56        }
57    }
58    if !current.is_empty() {
59        lines.push(current);
60    }
61    lines
62}
63
64#[cfg(test)]
65mod tests {
66    use super::wrap_error_message;
67
68    #[test]
69    fn wraps_error_text_using_upstream_word_and_code_point_boundaries() {
70        assert!(wrap_error_message("\t \n\u{feff}").is_empty());
71        assert_eq!(wrap_error_message("A short error"), ["A short error"]);
72        assert_eq!(
73            wrap_error_message(&format!(
74                "a{}{} c{}",
75                " ".repeat(40),
76                "b".repeat(40),
77                "d".repeat(39)
78            )),
79            [
80                format!("a {}", "b".repeat(40)),
81                format!("c{}", "d".repeat(39))
82            ],
83        );
84        assert_eq!(
85            wrap_error_message(&"x".repeat(160)),
86            ["x".repeat(75), "x".repeat(75), "x".repeat(10)]
87        );
88        assert_eq!(
89            wrap_error_message(&"\u{1f600}".repeat(80)),
90            ["\u{1f600}".repeat(75), "\u{1f600}".repeat(5)]
91        );
92        assert_eq!(wrap_error_message("a\u{0085}b"), ["a\u{0085}b"]);
93    }
94
95    #[test]
96    fn error_text_is_capped_only_when_more_than_four_lines_are_needed() {
97        assert_eq!(
98            wrap_error_message(&"x".repeat(300)),
99            vec!["x".repeat(75); 4]
100        );
101        let lines = wrap_error_message(&"x".repeat(301));
102        assert_eq!(lines.len(), 4);
103        assert_eq!(lines[3], format!("{}...", "x".repeat(72)));
104        let lines = wrap_error_message(&vec!["\u{1f600}".repeat(70); 6].join(" "));
105        assert_eq!(lines.len(), 4);
106        assert_eq!(lines[3], format!("{}...", "\u{1f600}".repeat(70)));
107    }
108}