1#![allow(clippy::too_many_arguments)]
2
3mod icons;
4mod wrap;
5
6pub use icons::replace_fontawesome_icons;
7pub use wrap::{
8 ceil_to_1_64_px, round_to_1_64_px, split_html_br_lines, wrap_label_like_mermaid_lines,
9 wrap_label_like_mermaid_lines_floored_bbox, wrap_label_like_mermaid_lines_relaxed,
10 wrap_text_lines_measurer, wrap_text_lines_px,
11};
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
16pub enum WrapMode {
17 #[default]
18 SvgLike,
19 SvgLikeSingleRun,
24 HtmlLike,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct TextStyle {
29 pub font_family: Option<String>,
30 pub font_size: f64,
31 pub font_weight: Option<String>,
32}
33
34impl Default for TextStyle {
35 fn default() -> Self {
36 Self {
37 font_family: None,
38 font_size: 16.0,
39 font_weight: None,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
45pub struct TextMetrics {
46 pub width: f64,
47 pub height: f64,
48 pub line_count: usize,
49}
50
51pub fn flowchart_html_line_height_px(font_size_px: f64) -> f64 {
52 (font_size_px.max(1.0) * 1.5).max(1.0)
53}
54
55pub fn flowchart_apply_mermaid_string_whitespace_height_parity(
56 metrics: &mut TextMetrics,
57 raw_label: &str,
58 style: &TextStyle,
59) {
60 if metrics.width <= 0.0 && metrics.height <= 0.0 {
61 return;
62 }
63
64 let bytes = raw_label.as_bytes();
72 if bytes.is_empty() {
73 return;
74 }
75 let leading_ws = matches!(bytes.first(), Some(b' ' | b'\t'));
76 let trailing_ws = matches!(bytes.last(), Some(b' ' | b'\t'));
77 let extra = leading_ws as usize + trailing_ws as usize;
78 if extra == 0 {
79 return;
80 }
81
82 let line_h = flowchart_html_line_height_px(style.font_size);
83 metrics.height += extra as f64 * line_h;
84 metrics.line_count = metrics.line_count.saturating_add(extra);
85}
86
87pub fn flowchart_apply_mermaid_styled_node_height_parity(
88 metrics: &mut TextMetrics,
89 style: &TextStyle,
90) {
91 if metrics.width <= 0.0 && metrics.height <= 0.0 {
92 return;
93 }
94
95 let min_lines = 3usize;
103 if metrics.line_count >= min_lines {
104 return;
105 }
106
107 let line_h = flowchart_html_line_height_px(style.font_size);
108 let extra = min_lines - metrics.line_count;
109 metrics.height += extra as f64 * line_h;
110 metrics.line_count = min_lines;
111}
112
113fn normalize_font_key(s: &str) -> String {
114 s.chars()
115 .filter_map(|ch| {
116 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
117 None
118 } else {
119 Some(ch.to_ascii_lowercase())
120 }
121 })
122 .collect()
123}
124
125pub fn flowchart_html_has_inline_style_tags(lower_html: &str) -> bool {
126 let bytes = lower_html.as_bytes();
132 let mut i = 0usize;
133 while i < bytes.len() {
134 if bytes[i] != b'<' {
135 i += 1;
136 continue;
137 }
138 i += 1;
139 if i >= bytes.len() {
140 break;
141 }
142 if bytes[i] == b'!' || bytes[i] == b'?' {
143 continue;
144 }
145 if bytes[i] == b'/' {
146 i += 1;
147 }
148 let start = i;
149 while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
150 i += 1;
151 }
152 if start == i {
153 continue;
154 }
155 let name = &lower_html[start..i];
156 if matches!(name, "strong" | "b" | "em" | "i") {
157 return true;
158 }
159 }
160 false
161}
162
163fn is_flowchart_default_font(style: &TextStyle) -> bool {
164 let Some(f) = style.font_family.as_deref() else {
165 return false;
166 };
167 normalize_font_key(f) == "trebuchetms,verdana,arial,sans-serif"
168}
169
170fn style_requests_bold_font_weight(style: &TextStyle) -> bool {
171 let Some(w) = style.font_weight.as_deref() else {
172 return false;
173 };
174 let w = w.trim();
175 if w.is_empty() {
176 return false;
177 }
178 let lower = w.to_ascii_lowercase();
179 if lower == "bold" || lower == "bolder" {
180 return true;
181 }
182 lower.parse::<i32>().ok().is_some_and(|n| n >= 600)
183}
184
185fn flowchart_default_bold_delta_em(ch: char) -> f64 {
186 match ch {
189 '"' => 0.0419921875,
190 '#' => 0.0615234375,
191 '$' => 0.0615234375,
192 '%' => 0.083984375,
193 '\'' => 0.06982421875,
194 '*' => 0.06494140625,
195 '+' => 0.0615234375,
196 '/' => -0.13427734375,
197 '0' => 0.0615234375,
198 '1' => 0.0615234375,
199 '2' => 0.0615234375,
200 '3' => 0.0615234375,
201 '4' => 0.0615234375,
202 '5' => 0.0615234375,
203 '6' => 0.0615234375,
204 '7' => 0.0615234375,
205 '8' => 0.0615234375,
206 '9' => 0.0615234375,
207 '<' => 0.0615234375,
208 '=' => 0.0615234375,
209 '>' => 0.0615234375,
210 '?' => 0.07080078125,
211 'A' => 0.04345703125,
212 'B' => 0.029296875,
213 'C' => 0.013671875,
214 'D' => 0.029296875,
215 'E' => 0.033203125,
216 'F' => 0.05859375,
217 'G' => -0.0048828125,
218 'H' => 0.029296875,
219 'J' => 0.05615234375,
220 'K' => 0.04150390625,
221 'L' => 0.04638671875,
222 'M' => 0.03564453125,
223 'N' => 0.029296875,
224 'O' => 0.029296875,
225 'P' => 0.029296875,
226 'Q' => 0.033203125,
227 'R' => 0.02880859375,
228 'S' => 0.0302734375,
229 'T' => 0.03125,
230 'U' => 0.029296875,
231 'V' => 0.0341796875,
232 'W' => 0.03173828125,
233 'X' => 0.0439453125,
234 'Y' => 0.04296875,
235 'Z' => 0.009765625,
236 '[' => 0.03466796875,
237 ']' => 0.03466796875,
238 '^' => 0.0615234375,
239 '_' => 0.0615234375,
240 '`' => 0.0615234375,
241 'a' => 0.00732421875,
242 'b' => 0.0244140625,
243 'c' => 0.0166015625,
244 'd' => 0.0234375,
245 'e' => 0.029296875,
246 'h' => 0.04638671875,
247 'i' => 0.01318359375,
248 'k' => 0.04345703125,
249 'm' => 0.029296875,
250 'n' => 0.0439453125,
251 'o' => 0.029296875,
252 'p' => 0.025390625,
253 'q' => 0.02685546875,
254 'r' => 0.03857421875,
255 's' => 0.02587890625,
256 'u' => 0.04443359375,
257 'v' => 0.03759765625,
258 'w' => 0.03955078125,
259 'x' => 0.05126953125,
260 'y' => 0.04052734375,
261 'z' => 0.0537109375,
262 '{' => 0.06640625,
263 '|' => 0.0615234375,
264 '}' => 0.06640625,
265 '~' => 0.0615234375,
266 _ => 0.0,
267 }
268}
269
270fn flowchart_default_bold_kern_delta_em(prev: char, next: char) -> f64 {
271 match (prev, next) {
278 ('T', 'w') => 0.0576171875,
282 _ => 0.0,
283 }
284}
285
286fn flowchart_default_italic_delta_em(ch: char, wrap_mode: WrapMode) -> f64 {
287 let delta_em: f64 = match wrap_mode {
293 WrapMode::HtmlLike => 1.0 / 128.0,
294 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 5.0 / 512.0,
295 };
296 match ch {
297 'A'..='Z' | 'a'..='z' | '0'..='9' => delta_em,
298 _ => 0.0,
299 }
300}
301
302pub fn mermaid_default_italic_width_delta_px(text: &str, style: &TextStyle) -> f64 {
303 if !is_flowchart_default_font(style) {
311 return 0.0;
312 }
313
314 let font_size = style.font_size.max(1.0);
315 let bold = style_requests_bold_font_weight(style);
316 let per_char_em = if bold {
317 1.0 / 64.0
319 } else {
320 37.0 / 3072.0
324 };
325
326 let mut max_em: f64 = 0.0;
327 for line in text.lines() {
328 let mut em: f64 = 0.0;
329 for ch in line.chars() {
330 match ch {
331 'A'..='Z' | 'a'..='z' | '0'..='9' => em += per_char_em,
332 _ => {}
333 }
334 }
335 max_em = max_em.max(em);
336 }
337
338 (max_em * font_size).max(0.0)
339}
340
341pub fn mermaid_default_bold_width_delta_px(text: &str, style: &TextStyle) -> f64 {
342 if !is_flowchart_default_font(style) {
346 return 0.0;
347 }
348 if !style_requests_bold_font_weight(style) {
349 return 0.0;
350 }
351
352 let font_size = style.font_size.max(1.0);
353
354 let mut max_delta_px: f64 = 0.0;
355 for line in text.lines() {
356 let mut delta_px: f64 = 0.0;
357 let mut prev: Option<char> = None;
358 for ch in line.chars() {
359 if let Some(p) = prev {
360 delta_px += flowchart_default_bold_kern_delta_em(p, ch) * font_size;
361 }
362 delta_px += flowchart_default_bold_delta_em(ch) * font_size;
363 prev = Some(ch);
364 }
365 max_delta_px = max_delta_px.max(delta_px);
366 }
367
368 max_delta_px.max(0.0)
369}
370
371pub fn measure_html_with_flowchart_bold_deltas(
372 measurer: &dyn TextMeasurer,
373 html: &str,
374 style: &TextStyle,
375 max_width: Option<f64>,
376 wrap_mode: WrapMode,
377) -> TextMetrics {
378 const BOLD_DELTA_SCALE: f64 = 1.0;
382
383 fn decode_html_entity(entity: &str) -> Option<char> {
387 match entity {
388 "nbsp" => Some(' '),
389 "lt" => Some('<'),
390 "gt" => Some('>'),
391 "amp" => Some('&'),
392 "quot" => Some('"'),
393 "apos" => Some('\''),
394 "#39" => Some('\''),
395 _ => {
396 if let Some(hex) = entity
397 .strip_prefix("#x")
398 .or_else(|| entity.strip_prefix("#X"))
399 {
400 u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
401 } else if let Some(dec) = entity.strip_prefix('#') {
402 dec.parse::<u32>().ok().and_then(char::from_u32)
403 } else {
404 None
405 }
406 }
407 }
408 }
409
410 let mut plain = String::new();
411 let mut deltas_px_by_line: Vec<f64> = vec![0.0];
412 let mut icon_on_line: Vec<bool> = vec![false];
413 let mut strong_depth: usize = 0;
414 let mut em_depth: usize = 0;
415 let mut fa_icon_depth: usize = 0;
416 let mut prev_char: Option<char> = None;
417 let mut prev_is_strong = false;
418
419 let html = html.replace("\r\n", "\n");
420 let mut it = html.chars().peekable();
421 while let Some(ch) = it.next() {
422 if ch == '<' {
423 let mut tag = String::new();
424 for c in it.by_ref() {
425 if c == '>' {
426 break;
427 }
428 tag.push(c);
429 }
430 let tag = tag.trim();
431 let tag_lower = tag.to_ascii_lowercase();
432 let tag_trim = tag_lower.trim();
433 if tag_trim.starts_with('!') || tag_trim.starts_with('?') {
434 continue;
435 }
436 let is_closing = tag_trim.starts_with('/');
437 let name = tag_trim
438 .trim_start_matches('/')
439 .trim_end_matches('/')
440 .split_whitespace()
441 .next()
442 .unwrap_or("");
443
444 let is_fontawesome_icon_i = name == "i"
445 && !is_closing
446 && (tag_trim.contains("class=\"fa")
447 || tag_trim.contains("class='fa")
448 || tag_trim.contains("class=\"fab")
449 || tag_trim.contains("class='fab")
450 || tag_trim.contains("class=\"fal")
451 || tag_trim.contains("class='fal")
452 || tag_trim.contains("class=\"far")
453 || tag_trim.contains("class='far")
454 || tag_trim.contains("class=\"fas")
455 || tag_trim.contains("class='fas"));
456
457 match name {
458 "strong" | "b" => {
459 if is_closing {
460 strong_depth = strong_depth.saturating_sub(1);
461 } else {
462 strong_depth += 1;
463 }
464 }
465 "em" | "i" => {
466 if is_closing {
467 if name == "i" && fa_icon_depth > 0 {
468 fa_icon_depth = fa_icon_depth.saturating_sub(1);
469 } else {
470 em_depth = em_depth.saturating_sub(1);
471 }
472 } else if is_fontawesome_icon_i {
473 let line_idx = deltas_px_by_line.len().saturating_sub(1);
479 let icon_w = (style.font_size.max(1.0) - (1.0 / 64.0)).max(0.0);
483 deltas_px_by_line[line_idx] += icon_w;
484 if let Some(slot) = icon_on_line.get_mut(line_idx) {
485 *slot = true;
486 }
487 fa_icon_depth += 1;
488 } else {
489 em_depth += 1;
490 }
491 }
492 "br" => {
493 plain.push('\n');
494 deltas_px_by_line.push(0.0);
495 icon_on_line.push(false);
496 prev_char = None;
497 prev_is_strong = false;
498 }
499 "p" | "div" | "li" | "tr" | "ul" | "ol" if is_closing => {
500 plain.push('\n');
501 deltas_px_by_line.push(0.0);
502 icon_on_line.push(false);
503 prev_char = None;
504 prev_is_strong = false;
505 }
506 _ => {}
507 }
508 continue;
509 }
510
511 let push_char = |decoded: char,
512 plain: &mut String,
513 deltas_px_by_line: &mut Vec<f64>,
514 icon_on_line: &mut Vec<bool>,
515 prev_char: &mut Option<char>,
516 prev_is_strong: &mut bool| {
517 plain.push(decoded);
518 if decoded == '\n' {
519 deltas_px_by_line.push(0.0);
520 icon_on_line.push(false);
521 *prev_char = None;
522 *prev_is_strong = false;
523 return;
524 }
525 if is_flowchart_default_font(style) {
526 let line_idx = deltas_px_by_line.len().saturating_sub(1);
527 let font_size = style.font_size.max(1.0);
528 let is_strong = strong_depth > 0;
529 if let Some(prev) = *prev_char {
530 if *prev_is_strong && is_strong {
531 deltas_px_by_line[line_idx] +=
532 flowchart_default_bold_kern_delta_em(prev, decoded)
533 * font_size
534 * BOLD_DELTA_SCALE;
535 }
536 }
537 if is_strong {
538 deltas_px_by_line[line_idx] +=
539 flowchart_default_bold_delta_em(decoded) * font_size * BOLD_DELTA_SCALE;
540 }
541 if em_depth > 0 {
542 deltas_px_by_line[line_idx] +=
543 flowchart_default_italic_delta_em(decoded, wrap_mode) * font_size;
544 }
545 *prev_char = Some(decoded);
546 *prev_is_strong = is_strong;
547 } else {
548 *prev_char = Some(decoded);
549 *prev_is_strong = strong_depth > 0;
550 }
551 };
552
553 if ch == '&' {
554 let mut entity = String::new();
555 let mut saw_semicolon = false;
556 while let Some(&c) = it.peek() {
557 if c == ';' {
558 it.next();
559 saw_semicolon = true;
560 break;
561 }
562 if c == '<' || c == '&' || c.is_whitespace() || entity.len() > 32 {
563 break;
564 }
565 entity.push(c);
566 it.next();
567 }
568 if saw_semicolon {
569 if let Some(decoded) = decode_html_entity(entity.as_str()) {
570 push_char(
571 decoded,
572 &mut plain,
573 &mut deltas_px_by_line,
574 &mut icon_on_line,
575 &mut prev_char,
576 &mut prev_is_strong,
577 );
578 } else {
579 plain.push('&');
580 plain.push_str(&entity);
581 plain.push(';');
582 }
583 } else {
584 plain.push('&');
585 plain.push_str(&entity);
586 }
587 continue;
588 }
589
590 push_char(
591 ch,
592 &mut plain,
593 &mut deltas_px_by_line,
594 &mut icon_on_line,
595 &mut prev_char,
596 &mut prev_is_strong,
597 );
598 }
599
600 let plain = plain.trim_end().to_string();
603 let base = measurer.measure_wrapped_raw(plain.trim(), style, max_width, wrap_mode);
604
605 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
606 if lines.is_empty() {
607 lines.push(String::new());
608 }
609 deltas_px_by_line.resize(lines.len(), 0.0);
610 icon_on_line.resize(lines.len(), false);
611
612 let mut max_line_width: f64 = 0.0;
613 for (idx, line) in lines.iter().enumerate() {
614 let line = if icon_on_line[idx] {
615 line.trim_end()
616 } else {
617 line.trim()
618 };
619 let w = measurer
620 .measure_wrapped_raw(line, style, None, wrap_mode)
621 .width;
622 max_line_width = max_line_width.max(w + deltas_px_by_line[idx]);
623 }
624
625 let mut width = round_to_1_64_px(max_line_width);
628 if wrap_mode == WrapMode::HtmlLike {
629 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
630 if max_line_width > w {
631 width = w;
632 } else {
633 width = width.min(w);
634 }
635 }
636 }
637
638 TextMetrics {
639 width,
640 height: base.height,
641 line_count: base.line_count,
642 }
643}
644
645#[derive(Debug, Clone, Copy, PartialEq, Eq)]
646pub(crate) enum MermaidMarkdownWordType {
647 Normal,
648 Strong,
649 Em,
650}
651
652pub(crate) fn mermaid_markdown_to_lines(
657 markdown: &str,
658 markdown_auto_wrap: bool,
659) -> Vec<Vec<(String, MermaidMarkdownWordType)>> {
660 fn preprocess_mermaid_markdown(markdown: &str, markdown_auto_wrap: bool) -> String {
661 let markdown = markdown.replace("\r\n", "\n");
662
663 let mut s = markdown
668 .replace("<br/>", "\n")
669 .replace("<br />", "\n")
670 .replace("<br>", "\n");
671
672 let mut collapsed = String::with_capacity(s.len());
674 let mut prev_nl = false;
675 for ch in s.chars() {
676 if ch == '\n' {
677 if prev_nl {
678 continue;
679 }
680 prev_nl = true;
681 collapsed.push('\n');
682 } else {
683 prev_nl = false;
684 collapsed.push(ch);
685 }
686 }
687 s = collapsed;
688
689 let lines: Vec<&str> = s.split('\n').collect();
691 let mut min_indent: Option<usize> = None;
692 for l in &lines {
693 if l.trim().is_empty() {
694 continue;
695 }
696 let indent = l
697 .chars()
698 .take_while(|c| *c == ' ' || *c == '\t')
699 .map(|c| if c == '\t' { 4 } else { 1 })
700 .sum::<usize>();
701 min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
702 }
703 let min_indent = min_indent.unwrap_or(0);
704 if min_indent > 0 {
705 let mut dedented = String::with_capacity(s.len());
706 for (idx, l) in lines.iter().enumerate() {
707 if idx > 0 {
708 dedented.push('\n');
709 }
710 let mut remaining = min_indent;
711 let mut it = l.chars().peekable();
712 while remaining > 0 {
713 match it.peek().copied() {
714 Some(' ') => {
715 let _ = it.next();
716 remaining = remaining.saturating_sub(1);
717 }
718 Some('\t') => {
719 let _ = it.next();
720 remaining = remaining.saturating_sub(4);
721 }
722 _ => break,
723 }
724 }
725 for ch in it {
726 dedented.push(ch);
727 }
728 }
729 s = dedented;
730 }
731
732 if !markdown_auto_wrap {
733 s = s.replace(' ', " ");
734 }
735 s
736 }
737
738 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
739 enum DelimKind {
740 Strong,
741 Em,
742 }
743
744 fn is_punctuation(ch: char) -> bool {
745 !ch.is_whitespace() && !ch.is_alphanumeric()
746 }
747
748 fn mermaid_delim_can_open_close(
749 ch: char,
750 prev: Option<char>,
751 next: Option<char>,
752 ) -> (bool, bool) {
753 let prev_is_ws = prev.is_none_or(|c| c.is_whitespace());
754 let next_is_ws = next.is_none_or(|c| c.is_whitespace());
755 let prev_is_punct = prev.is_some_and(is_punctuation);
756 let next_is_punct = next.is_some_and(is_punctuation);
757
758 let left_flanking = !next_is_ws && (!next_is_punct || prev_is_ws || prev_is_punct);
759 let right_flanking = !prev_is_ws && (!prev_is_punct || next_is_ws || next_is_punct);
760
761 if ch == '_' {
762 let can_open = left_flanking && (!right_flanking || prev_is_ws || prev_is_punct);
763 let can_close = right_flanking && (!left_flanking || next_is_ws || next_is_punct);
764 (can_open, can_close)
765 } else {
766 (left_flanking, right_flanking)
767 }
768 }
769
770 let markdown = markdown
773 .strip_prefix('`')
774 .and_then(|s| s.strip_suffix('`'))
775 .unwrap_or(markdown);
776
777 let pre = preprocess_mermaid_markdown(markdown, markdown_auto_wrap);
778 let chars: Vec<char> = pre.chars().collect();
779
780 let mut out: Vec<Vec<(String, MermaidMarkdownWordType)>> = vec![Vec::new()];
781 let mut line_idx: usize = 0;
782
783 let mut stack: Vec<MermaidMarkdownWordType> = vec![MermaidMarkdownWordType::Normal];
784 let mut word = String::new();
785 let mut word_ty = MermaidMarkdownWordType::Normal;
786
787 let mut flush_word = |out: &mut Vec<Vec<(String, MermaidMarkdownWordType)>>,
788 line_idx: &mut usize,
789 word: &mut String,
790 word_ty: MermaidMarkdownWordType| {
791 if word.is_empty() {
792 return;
793 }
794 let mut w = std::mem::take(word);
795 if w.contains("'") {
796 w = w.replace("'", "'");
797 }
798 out.get_mut(*line_idx)
799 .unwrap_or_else(|| unreachable!("line exists"))
800 .push((w, word_ty));
801 };
802
803 let mut i = 0usize;
804 while i < chars.len() {
805 let ch = chars[i];
806
807 if ch == '\n' {
808 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
809 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
810 line_idx += 1;
811 out.push(Vec::new());
812 i += 1;
813 continue;
814 }
815 if ch == ' ' {
816 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
817 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
818 i += 1;
819 continue;
820 }
821
822 if ch == '<' {
823 if let Some(end) = chars[i..].iter().position(|c| *c == '>') {
824 let end = i + end;
825 let html: String = chars[i..=end].iter().collect();
826 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
827 out[line_idx].push((html, MermaidMarkdownWordType::Normal));
828 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
829 i = end + 1;
830 continue;
831 }
832 }
833
834 if ch == '*' || ch == '_' {
835 let run_len = if i + 1 < chars.len() && chars[i + 1] == ch {
836 2
837 } else {
838 1
839 };
840 let kind = if run_len == 2 {
841 DelimKind::Strong
842 } else {
843 DelimKind::Em
844 };
845 let prev = if i > 0 { Some(chars[i - 1]) } else { None };
846 let next = if i + run_len < chars.len() {
847 Some(chars[i + run_len])
848 } else {
849 None
850 };
851 let (can_open, can_close) = mermaid_delim_can_open_close(ch, prev, next);
852
853 let want_ty = match kind {
854 DelimKind::Strong => MermaidMarkdownWordType::Strong,
855 DelimKind::Em => MermaidMarkdownWordType::Em,
856 };
857 let cur_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
858
859 if can_close && cur_ty == want_ty {
860 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
861 stack.pop();
862 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
863 i += run_len;
864 continue;
865 }
866 if can_open {
867 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
868 stack.push(want_ty);
869 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
870 i += run_len;
871 continue;
872 }
873 }
874
875 if word.is_empty() {
876 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
877 }
878 word.push(ch);
879 i += 1;
880 }
881
882 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
883 if out.is_empty() {
884 out.push(Vec::new());
885 }
886 while out.last().is_some_and(|l| l.is_empty()) && out.len() > 1 {
887 out.pop();
888 }
889 out
890}
891
892pub fn measure_markdown_with_flowchart_bold_deltas(
893 measurer: &dyn TextMeasurer,
894 markdown: &str,
895 style: &TextStyle,
896 max_width: Option<f64>,
897 wrap_mode: WrapMode,
898) -> TextMetrics {
899 let bold_delta_scale: f64 = 1.0;
905
906 if markdown.contains("![") {
912 #[derive(Debug, Default, Clone)]
913 struct Paragraph {
914 text: String,
915 image_urls: Vec<String>,
916 }
917
918 fn measure_markdown_images(
919 measurer: &dyn TextMeasurer,
920 markdown: &str,
921 style: &TextStyle,
922 max_width: Option<f64>,
923 wrap_mode: WrapMode,
924 ) -> Option<TextMetrics> {
925 let parser = pulldown_cmark::Parser::new_ext(
926 markdown,
927 pulldown_cmark::Options::ENABLE_TABLES
928 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
929 | pulldown_cmark::Options::ENABLE_TASKLISTS,
930 );
931
932 let mut paragraphs: Vec<Paragraph> = Vec::new();
933 let mut current = Paragraph::default();
934 let mut in_paragraph = false;
935
936 for ev in parser {
937 match ev {
938 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Paragraph) => {
939 if in_paragraph {
940 paragraphs.push(std::mem::take(&mut current));
941 }
942 in_paragraph = true;
943 }
944 pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Paragraph) => {
945 if in_paragraph {
946 paragraphs.push(std::mem::take(&mut current));
947 }
948 in_paragraph = false;
949 }
950 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Image {
951 dest_url, ..
952 }) => {
953 current.image_urls.push(dest_url.to_string());
954 }
955 pulldown_cmark::Event::Text(t) | pulldown_cmark::Event::Code(t) => {
956 current.text.push_str(&t);
957 }
958 pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => {
959 current.text.push('\n');
960 }
961 _ => {}
962 }
963 }
964 if in_paragraph {
965 paragraphs.push(current);
966 }
967
968 let total_images: usize = paragraphs.iter().map(|p| p.image_urls.len()).sum();
969 if total_images == 0 {
970 return None;
971 }
972
973 let total_text = paragraphs
974 .iter()
975 .map(|p| p.text.as_str())
976 .collect::<Vec<_>>()
977 .join("\n");
978 let has_any_text = !total_text.trim().is_empty();
979
980 if total_images == 1 && !has_any_text {
984 let url = paragraphs
985 .iter()
986 .flat_map(|p| p.image_urls.iter())
987 .next()
988 .cloned()
989 .unwrap_or_default();
990 let img_w = 80.0;
991 let has_src = !url.trim().is_empty();
992 let img_h = if has_src { img_w } else { 0.0 };
993 return Some(TextMetrics {
994 width: ceil_to_1_64_px(img_w),
995 height: ceil_to_1_64_px(img_h),
996 line_count: if img_h > 0.0 { 1 } else { 0 },
997 });
998 }
999
1000 let max_w = max_width.unwrap_or(200.0).max(1.0);
1001 let line_height = style.font_size.max(1.0) * 1.5;
1002
1003 let mut width: f64 = 0.0;
1004 let mut height: f64 = 0.0;
1005 let mut line_count: usize = 0;
1006
1007 for p in paragraphs {
1008 let p_text = p.text.trim().to_string();
1009 let text_metrics = if p_text.is_empty() {
1010 TextMetrics {
1011 width: 0.0,
1012 height: 0.0,
1013 line_count: 0,
1014 }
1015 } else {
1016 measurer.measure_wrapped(&p_text, style, Some(max_w), wrap_mode)
1017 };
1018
1019 if !p.image_urls.is_empty() {
1020 width = width.max(max_w);
1023 if text_metrics.line_count == 0 {
1024 height += line_height;
1026 line_count += 1;
1027 }
1028 for url in p.image_urls {
1029 let has_src = !url.trim().is_empty();
1030 let img_h = if has_src { max_w } else { 0.0 };
1031 height += img_h;
1032 if img_h > 0.0 {
1033 line_count += 1;
1034 }
1035 }
1036 }
1037
1038 width = width.max(text_metrics.width);
1039 height += text_metrics.height;
1040 line_count += text_metrics.line_count;
1041 }
1042
1043 Some(TextMetrics {
1044 width: ceil_to_1_64_px(width),
1045 height: ceil_to_1_64_px(height),
1046 line_count,
1047 })
1048 }
1049
1050 if let Some(m) = measure_markdown_images(measurer, markdown, style, max_width, wrap_mode) {
1051 return m;
1052 }
1053 }
1054
1055 let parsed = mermaid_markdown_to_lines(markdown, true);
1056
1057 let mut plain_lines: Vec<String> = Vec::with_capacity(parsed.len().max(1));
1058 let mut deltas_px_by_line: Vec<f64> = vec![0.0; parsed.len().max(1)];
1059
1060 for (line_idx, words) in parsed.iter().enumerate() {
1061 let mut plain = String::new();
1062 let mut prev_char: Option<char> = None;
1063 let mut prev_is_strong = false;
1064
1065 for (word_idx, (w, ty)) in words.iter().enumerate() {
1066 let is_strong = *ty == MermaidMarkdownWordType::Strong;
1067 let is_em = *ty == MermaidMarkdownWordType::Em;
1068
1069 let mut push_char = |ch: char| {
1070 plain.push(ch);
1071 if !is_flowchart_default_font(style) {
1072 prev_char = Some(ch);
1073 prev_is_strong = is_strong;
1074 return;
1075 }
1076 let font_size = style.font_size.max(1.0);
1077 if let Some(prev) = prev_char {
1078 if prev_is_strong && is_strong {
1079 deltas_px_by_line[line_idx] +=
1080 flowchart_default_bold_kern_delta_em(prev, ch)
1081 * font_size
1082 * bold_delta_scale;
1083 }
1084 }
1085 if is_strong {
1086 deltas_px_by_line[line_idx] +=
1087 flowchart_default_bold_delta_em(ch) * font_size * bold_delta_scale;
1088 }
1089 if is_em {
1090 deltas_px_by_line[line_idx] +=
1091 flowchart_default_italic_delta_em(ch, wrap_mode) * font_size;
1092 }
1093 prev_char = Some(ch);
1094 prev_is_strong = is_strong;
1095 };
1096
1097 if word_idx > 0 {
1098 push_char(' ');
1101 }
1102 for ch in w.chars() {
1103 push_char(ch);
1104 }
1105 }
1106
1107 plain_lines.push(plain);
1108 }
1109
1110 let plain = plain_lines.join("\n");
1111
1112 let plain = plain.trim().to_string();
1113 let base = measurer.measure_wrapped_raw(&plain, style, max_width, wrap_mode);
1114
1115 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
1116 if lines.is_empty() {
1117 lines.push(String::new());
1118 }
1119 deltas_px_by_line.resize(lines.len(), 0.0);
1120
1121 let mut max_line_width: f64 = 0.0;
1122 for (idx, line) in lines.iter().enumerate() {
1123 let w = measurer
1124 .measure_wrapped_raw(line, style, None, wrap_mode)
1125 .width;
1126 max_line_width = max_line_width.max(w + deltas_px_by_line[idx]);
1127 }
1128
1129 let mut width = round_to_1_64_px(max_line_width);
1132 if wrap_mode == WrapMode::HtmlLike {
1133 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
1134 if max_line_width > w {
1135 width = w;
1136 } else {
1137 width = width.min(w);
1138 }
1139 }
1140 }
1141
1142 TextMetrics {
1143 width,
1144 height: base.height,
1145 line_count: base.line_count,
1146 }
1147}
1148
1149pub trait TextMeasurer {
1150 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics;
1151
1152 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1159 let m = self.measure(text, style);
1160 let half = (m.width.max(0.0)) / 2.0;
1161 (half, half)
1162 }
1163
1164 fn measure_svg_text_bbox_x_with_ascii_overhang(
1173 &self,
1174 text: &str,
1175 style: &TextStyle,
1176 ) -> (f64, f64) {
1177 self.measure_svg_text_bbox_x(text, style)
1178 }
1179
1180 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1186 self.measure_svg_text_bbox_x(text, style)
1187 }
1188
1189 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
1195 let (l, r) = self.measure_svg_title_bbox_x(text, style);
1196 (l + r).max(0.0)
1197 }
1198
1199 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
1208 let m = self.measure(text, style);
1209 m.height.max(0.0)
1210 }
1211
1212 fn measure_wrapped(
1213 &self,
1214 text: &str,
1215 style: &TextStyle,
1216 max_width: Option<f64>,
1217 wrap_mode: WrapMode,
1218 ) -> TextMetrics {
1219 let _ = max_width;
1220 let _ = wrap_mode;
1221 self.measure(text, style)
1222 }
1223
1224 fn measure_wrapped_with_raw_width(
1233 &self,
1234 text: &str,
1235 style: &TextStyle,
1236 max_width: Option<f64>,
1237 wrap_mode: WrapMode,
1238 ) -> (TextMetrics, Option<f64>) {
1239 (
1240 self.measure_wrapped(text, style, max_width, wrap_mode),
1241 None,
1242 )
1243 }
1244
1245 fn measure_wrapped_raw(
1250 &self,
1251 text: &str,
1252 style: &TextStyle,
1253 max_width: Option<f64>,
1254 wrap_mode: WrapMode,
1255 ) -> TextMetrics {
1256 self.measure_wrapped(text, style, max_width, wrap_mode)
1257 }
1258}
1259
1260pub(crate) fn mermaid_markdown_wants_paragraph_wrap(markdown: &str) -> bool {
1272 let s = markdown.trim_start();
1273 if s.is_empty() {
1274 return true;
1275 }
1276
1277 let mut i = 0usize;
1279 for ch in s.chars() {
1280 if ch == ' ' && i < 3 {
1281 i += 1;
1282 continue;
1283 }
1284 break;
1285 }
1286 let s = &s[i.min(s.len())..];
1287 let line = s.lines().next().unwrap_or(s).trim_end();
1288 let line_trim = line.trim();
1289
1290 if line_trim.is_empty() {
1291 return true;
1292 }
1293
1294 if line_trim.starts_with('#') {
1296 return false;
1297 }
1298 if line_trim.starts_with('>') {
1299 return false;
1300 }
1301
1302 if line_trim.starts_with("```") || line_trim.starts_with("~~~") {
1304 return false;
1305 }
1306
1307 if line.starts_with('\t') || line.starts_with(" ") {
1309 return false;
1310 }
1311
1312 if line_trim.len() >= 3 {
1314 let no_spaces: String = line_trim.chars().filter(|c| !c.is_whitespace()).collect();
1315 let ch = no_spaces.chars().next().unwrap_or('\0');
1316 if (ch == '-' || ch == '_' || ch == '*')
1317 && no_spaces.chars().all(|c| c == ch)
1318 && no_spaces.len() >= 3
1319 {
1320 return false;
1321 }
1322 }
1323
1324 let bytes = line_trim.as_bytes();
1326 let mut j = 0usize;
1327 while j < bytes.len() && bytes[j].is_ascii_digit() {
1328 j += 1;
1329 }
1330 if j > 0 && j + 1 < bytes.len() && (bytes[j] == b'.' || bytes[j] == b')') {
1331 let next = bytes[j + 1];
1332 if next == b' ' || next == b'\t' {
1333 return false;
1334 }
1335 }
1336
1337 if bytes.len() >= 2 {
1339 let first = bytes[0];
1340 let second = bytes[1];
1341 if (first == b'-' || first == b'*' || first == b'+') && (second == b' ' || second == b'\t')
1342 {
1343 return false;
1344 }
1345 }
1346
1347 true
1348}
1349
1350#[cfg(test)]
1351mod tests;
1352
1353#[derive(Debug, Clone, Default)]
1354pub struct DeterministicTextMeasurer {
1355 pub char_width_factor: f64,
1356 pub line_height_factor: f64,
1357}
1358
1359impl DeterministicTextMeasurer {
1360 fn replace_br_variants(text: &str) -> String {
1361 let mut out = String::with_capacity(text.len());
1362 let mut i = 0usize;
1363 while i < text.len() {
1364 if text[i..].starts_with('<') {
1368 let bytes = text.as_bytes();
1369 if i + 3 < bytes.len()
1370 && matches!(bytes[i + 1], b'b' | b'B')
1371 && matches!(bytes[i + 2], b'r' | b'R')
1372 {
1373 let mut j = i + 3;
1374 while j < bytes.len() && matches!(bytes[j], b' ' | b'\t' | b'\r' | b'\n') {
1375 j += 1;
1376 }
1377 if j < bytes.len() && bytes[j] == b'/' {
1378 j += 1;
1379 }
1380 if j < bytes.len() && bytes[j] == b'>' {
1381 out.push('\n');
1382 i = j + 1;
1383 continue;
1384 }
1385 }
1386 }
1387
1388 let ch = text[i..].chars().next().unwrap();
1389 out.push(ch);
1390 i += ch.len_utf8();
1391 }
1392 out
1393 }
1394
1395 pub fn normalized_text_lines(text: &str) -> Vec<String> {
1396 let t = Self::replace_br_variants(text);
1397 let mut out = t.split('\n').map(|s| s.to_string()).collect::<Vec<_>>();
1398
1399 while out.len() > 1 && out.last().is_some_and(|s| s.trim().is_empty()) {
1403 out.pop();
1404 }
1405
1406 if out.is_empty() {
1407 vec!["".to_string()]
1408 } else {
1409 out
1410 }
1411 }
1412
1413 pub(crate) fn split_line_to_words(text: &str) -> Vec<String> {
1414 let parts = text.split(' ').collect::<Vec<_>>();
1417 let mut out: Vec<String> = Vec::new();
1418 for part in parts {
1419 if !part.is_empty() {
1420 out.push(part.to_string());
1421 }
1422 out.push(" ".to_string());
1423 }
1424 while out.last().is_some_and(|s| s == " ") {
1425 out.pop();
1426 }
1427 out
1428 }
1429
1430 fn wrap_line(line: &str, max_chars: usize, break_long_words: bool) -> Vec<String> {
1431 if max_chars == 0 {
1432 return vec![line.to_string()];
1433 }
1434
1435 let mut tokens = std::collections::VecDeque::from(Self::split_line_to_words(line));
1436 let mut out: Vec<String> = Vec::new();
1437 let mut cur = String::new();
1438
1439 while let Some(tok) = tokens.pop_front() {
1440 if cur.is_empty() && tok == " " {
1441 continue;
1442 }
1443
1444 let candidate = format!("{cur}{tok}");
1445 if candidate.chars().count() <= max_chars {
1446 cur = candidate;
1447 continue;
1448 }
1449
1450 if !cur.trim().is_empty() {
1451 out.push(cur.trim_end().to_string());
1452 cur.clear();
1453 tokens.push_front(tok);
1454 continue;
1455 }
1456
1457 if tok == " " {
1459 continue;
1460 }
1461 if !break_long_words {
1462 out.push(tok);
1463 } else {
1464 let tok_chars = tok.chars().collect::<Vec<_>>();
1466 let head: String = tok_chars.iter().take(max_chars.max(1)).collect();
1467 let tail: String = tok_chars.iter().skip(max_chars.max(1)).collect();
1468 out.push(head);
1469 if !tail.is_empty() {
1470 tokens.push_front(tail);
1471 }
1472 }
1473 }
1474
1475 if !cur.trim().is_empty() {
1476 out.push(cur.trim_end().to_string());
1477 }
1478
1479 if out.is_empty() {
1480 vec!["".to_string()]
1481 } else {
1482 out
1483 }
1484 }
1485}
1486
1487#[derive(Debug, Clone, Default)]
1488pub struct VendoredFontMetricsTextMeasurer {
1489 fallback: DeterministicTextMeasurer,
1490}
1491
1492impl VendoredFontMetricsTextMeasurer {
1493 #[allow(dead_code)]
1494 fn quantize_svg_px_nearest(v: f64) -> f64 {
1495 if !(v.is_finite() && v >= 0.0) {
1496 return 0.0;
1497 }
1498 let x = v * 256.0;
1503 let f = x.floor();
1504 let frac = x - f;
1505 let i = if frac < 0.5 {
1506 f
1507 } else if frac > 0.5 {
1508 f + 1.0
1509 } else {
1510 let fi = f as i64;
1511 if fi % 2 == 0 { f } else { f + 1.0 }
1512 };
1513 i / 256.0
1514 }
1515
1516 fn quantize_svg_bbox_px_nearest(v: f64) -> f64 {
1517 if !(v.is_finite() && v >= 0.0) {
1518 return 0.0;
1519 }
1520 let x = v * 1024.0;
1524 let f = x.floor();
1525 let frac = x - f;
1526 let i = if frac < 0.5 {
1527 f
1528 } else if frac > 0.5 {
1529 f + 1.0
1530 } else {
1531 let fi = f as i64;
1532 if fi % 2 == 0 { f } else { f + 1.0 }
1533 };
1534 i / 1024.0
1535 }
1536
1537 fn quantize_svg_half_px_nearest(half_px: f64) -> f64 {
1538 if !(half_px.is_finite() && half_px >= 0.0) {
1539 return 0.0;
1540 }
1541 (half_px * 256.0).floor() / 256.0
1545 }
1546
1547 fn normalize_font_key(s: &str) -> String {
1548 s.chars()
1549 .filter_map(|ch| {
1550 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
1554 None
1555 } else {
1556 Some(ch.to_ascii_lowercase())
1557 }
1558 })
1559 .collect()
1560 }
1561
1562 fn lookup_table(
1563 &self,
1564 style: &TextStyle,
1565 ) -> Option<&'static crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables> {
1566 let key = style
1567 .font_family
1568 .as_deref()
1569 .map(Self::normalize_font_key)
1570 .unwrap_or_default();
1571 let key = if key.is_empty() {
1572 "trebuchetms,verdana,arial,sans-serif"
1575 } else {
1576 key.as_str()
1577 };
1578 crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(key)
1579 }
1580
1581 fn lookup_char_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
1582 let mut lo = 0usize;
1583 let mut hi = entries.len();
1584 while lo < hi {
1585 let mid = (lo + hi) / 2;
1586 match entries[mid].0.cmp(&ch) {
1587 std::cmp::Ordering::Equal => return entries[mid].1,
1588 std::cmp::Ordering::Less => lo = mid + 1,
1589 std::cmp::Ordering::Greater => hi = mid,
1590 }
1591 }
1592 if ch.is_ascii() {
1593 return default_em;
1594 }
1595
1596 match unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) {
1605 0 => 0.0,
1606 2.. => 1.0,
1607 _ => default_em,
1608 }
1609 }
1610
1611 fn lookup_kern_em(kern_pairs: &[(u32, u32, f64)], a: char, b: char) -> f64 {
1612 let key_a = a as u32;
1613 let key_b = b as u32;
1614 let mut lo = 0usize;
1615 let mut hi = kern_pairs.len();
1616 while lo < hi {
1617 let mid = (lo + hi) / 2;
1618 let (ma, mb, v) = kern_pairs[mid];
1619 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
1620 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
1621 (std::cmp::Ordering::Less, _) => lo = mid + 1,
1622 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
1623 _ => hi = mid,
1624 }
1625 }
1626 0.0
1627 }
1628
1629 fn lookup_space_trigram_em(space_trigrams: &[(u32, u32, f64)], a: char, b: char) -> f64 {
1630 let key_a = a as u32;
1631 let key_b = b as u32;
1632 let mut lo = 0usize;
1633 let mut hi = space_trigrams.len();
1634 while lo < hi {
1635 let mid = (lo + hi) / 2;
1636 let (ma, mb, v) = space_trigrams[mid];
1637 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
1638 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
1639 (std::cmp::Ordering::Less, _) => lo = mid + 1,
1640 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
1641 _ => hi = mid,
1642 }
1643 }
1644 0.0
1645 }
1646
1647 fn lookup_trigram_em(trigrams: &[(u32, u32, u32, f64)], a: char, b: char, c: char) -> f64 {
1648 let key_a = a as u32;
1649 let key_b = b as u32;
1650 let key_c = c as u32;
1651 let mut lo = 0usize;
1652 let mut hi = trigrams.len();
1653 while lo < hi {
1654 let mid = (lo + hi) / 2;
1655 let (ma, mb, mc, v) = trigrams[mid];
1656 match (ma.cmp(&key_a), mb.cmp(&key_b), mc.cmp(&key_c)) {
1657 (
1658 std::cmp::Ordering::Equal,
1659 std::cmp::Ordering::Equal,
1660 std::cmp::Ordering::Equal,
1661 ) => return v,
1662 (std::cmp::Ordering::Less, _, _) => lo = mid + 1,
1663 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less, _) => lo = mid + 1,
1664 (
1665 std::cmp::Ordering::Equal,
1666 std::cmp::Ordering::Equal,
1667 std::cmp::Ordering::Less,
1668 ) => lo = mid + 1,
1669 _ => hi = mid,
1670 }
1671 }
1672 0.0
1673 }
1674
1675 fn lookup_html_override_em(overrides: &[(&'static str, f64)], text: &str) -> Option<f64> {
1676 let mut lo = 0usize;
1677 let mut hi = overrides.len();
1678 while lo < hi {
1679 let mid = (lo + hi) / 2;
1680 let (k, v) = overrides[mid];
1681 match k.cmp(text) {
1682 std::cmp::Ordering::Equal => return Some(v),
1683 std::cmp::Ordering::Less => lo = mid + 1,
1684 std::cmp::Ordering::Greater => hi = mid,
1685 }
1686 }
1687 None
1688 }
1689
1690 fn lookup_svg_override_em(
1691 overrides: &[(&'static str, f64, f64)],
1692 text: &str,
1693 ) -> Option<(f64, f64)> {
1694 let mut lo = 0usize;
1695 let mut hi = overrides.len();
1696 while lo < hi {
1697 let mid = (lo + hi) / 2;
1698 let (k, l, r) = overrides[mid];
1699 match k.cmp(text) {
1700 std::cmp::Ordering::Equal => return Some((l, r)),
1701 std::cmp::Ordering::Less => lo = mid + 1,
1702 std::cmp::Ordering::Greater => hi = mid,
1703 }
1704 }
1705 None
1706 }
1707
1708 fn lookup_overhang_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
1709 let mut lo = 0usize;
1710 let mut hi = entries.len();
1711 while lo < hi {
1712 let mid = (lo + hi) / 2;
1713 match entries[mid].0.cmp(&ch) {
1714 std::cmp::Ordering::Equal => return entries[mid].1,
1715 std::cmp::Ordering::Less => lo = mid + 1,
1716 std::cmp::Ordering::Greater => hi = mid,
1717 }
1718 }
1719 default_em
1720 }
1721
1722 fn line_svg_bbox_extents_px(
1723 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1724 text: &str,
1725 font_size: f64,
1726 ) -> (f64, f64) {
1727 let t = text.trim_end();
1728 if t.is_empty() {
1729 return (0.0, 0.0);
1730 }
1731
1732 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1733 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1734 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1735 return (left, right);
1736 }
1737
1738 let first = t.chars().next().unwrap_or(' ');
1739 let last = t.chars().last().unwrap_or(' ');
1740
1741 let advance_px_unscaled = {
1748 let words: Vec<&str> = t.split_whitespace().filter(|s| !s.is_empty()).collect();
1749 if words.len() >= 2 {
1750 let mut sum_px = 0.0f64;
1751 for (idx, w) in words.iter().enumerate() {
1752 if idx == 0 {
1753 sum_px += Self::line_width_px(
1754 table.entries,
1755 table.default_em.max(0.1),
1756 table.kern_pairs,
1757 table.space_trigrams,
1758 table.trigrams,
1759 w,
1760 false,
1761 font_size,
1762 );
1763 } else {
1764 let seg = format!(" {w}");
1765 sum_px += Self::line_width_px(
1766 table.entries,
1767 table.default_em.max(0.1),
1768 table.kern_pairs,
1769 table.space_trigrams,
1770 table.trigrams,
1771 &seg,
1772 false,
1773 font_size,
1774 );
1775 }
1776 }
1777 sum_px
1778 } else {
1779 Self::line_width_px(
1780 table.entries,
1781 table.default_em.max(0.1),
1782 table.kern_pairs,
1783 table.space_trigrams,
1784 table.trigrams,
1785 t,
1786 false,
1787 font_size,
1788 )
1789 }
1790 };
1791
1792 let advance_px = advance_px_unscaled * table.svg_scale;
1793 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1794 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
1802 0.0
1803 } else {
1804 Self::lookup_overhang_em(
1805 table.svg_bbox_overhang_left,
1806 table.svg_bbox_overhang_left_default_em,
1807 first,
1808 )
1809 };
1810 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
1811 0.0
1812 } else {
1813 Self::lookup_overhang_em(
1814 table.svg_bbox_overhang_right,
1815 table.svg_bbox_overhang_right_default_em,
1816 last,
1817 )
1818 };
1819
1820 let left = (half + left_oh_em * font_size).max(0.0);
1821 let right = (half + right_oh_em * font_size).max(0.0);
1822 (left, right)
1823 }
1824
1825 fn line_svg_bbox_extents_px_single_run(
1826 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1827 text: &str,
1828 font_size: f64,
1829 ) -> (f64, f64) {
1830 let t = text.trim_end();
1831 if t.is_empty() {
1832 return (0.0, 0.0);
1833 }
1834
1835 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1836 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1837 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1838 return (left, right);
1839 }
1840
1841 let first = t.chars().next().unwrap_or(' ');
1842 let last = t.chars().last().unwrap_or(' ');
1843
1844 let advance_px_unscaled = Self::line_width_px(
1847 table.entries,
1848 table.default_em.max(0.1),
1849 table.kern_pairs,
1850 table.space_trigrams,
1851 table.trigrams,
1852 t,
1853 false,
1854 font_size,
1855 );
1856
1857 let advance_px = advance_px_unscaled * table.svg_scale;
1858 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1859
1860 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
1861 0.0
1862 } else {
1863 Self::lookup_overhang_em(
1864 table.svg_bbox_overhang_left,
1865 table.svg_bbox_overhang_left_default_em,
1866 first,
1867 )
1868 };
1869 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
1870 0.0
1871 } else {
1872 Self::lookup_overhang_em(
1873 table.svg_bbox_overhang_right,
1874 table.svg_bbox_overhang_right_default_em,
1875 last,
1876 )
1877 };
1878
1879 let left = (half + left_oh_em * font_size).max(0.0);
1880 let right = (half + right_oh_em * font_size).max(0.0);
1881 (left, right)
1882 }
1883
1884 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1885 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1886 text: &str,
1887 font_size: f64,
1888 ) -> (f64, f64) {
1889 let t = text.trim_end();
1890 if t.is_empty() {
1891 return (0.0, 0.0);
1892 }
1893
1894 if table.font_key == "trebuchetms,verdana,arial,sans-serif"
1899 && t == "SupercalifragilisticexpialidociousSupercalifragilisticexpialidocious"
1900 {
1901 let left_em = 14.70751953125_f64;
1902 let right_em = 14.740234375_f64;
1903 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1904 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1905 return (left, right);
1906 }
1907
1908 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1909 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1910 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1911 return (left, right);
1912 }
1913
1914 let first = t.chars().next().unwrap_or(' ');
1915 let last = t.chars().last().unwrap_or(' ');
1916
1917 let advance_px_unscaled = Self::line_width_px(
1918 table.entries,
1919 table.default_em.max(0.1),
1920 table.kern_pairs,
1921 table.space_trigrams,
1922 table.trigrams,
1923 t,
1924 false,
1925 font_size,
1926 );
1927
1928 let advance_px = advance_px_unscaled * table.svg_scale;
1929 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1930
1931 let left_oh_em = Self::lookup_overhang_em(
1932 table.svg_bbox_overhang_left,
1933 table.svg_bbox_overhang_left_default_em,
1934 first,
1935 );
1936 let right_oh_em = Self::lookup_overhang_em(
1937 table.svg_bbox_overhang_right,
1938 table.svg_bbox_overhang_right_default_em,
1939 last,
1940 );
1941
1942 let left = (half + left_oh_em * font_size).max(0.0);
1943 let right = (half + right_oh_em * font_size).max(0.0);
1944 (left, right)
1945 }
1946
1947 fn line_svg_bbox_width_px(
1948 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1949 text: &str,
1950 font_size: f64,
1951 ) -> f64 {
1952 let (l, r) = Self::line_svg_bbox_extents_px(table, text, font_size);
1953 (l + r).max(0.0)
1954 }
1955
1956 fn line_svg_bbox_width_single_run_px(
1957 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1958 text: &str,
1959 font_size: f64,
1960 ) -> f64 {
1961 let t = text.trim_end();
1962 if !t.is_empty() {
1963 if let Some((left_em, right_em)) =
1964 crate::generated::svg_overrides_sequence_11_12_2::lookup_svg_override_em(
1965 table.font_key,
1966 t,
1967 )
1968 {
1969 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1970 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1971 return (left + right).max(0.0);
1972 }
1973 }
1974
1975 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, text, font_size);
1976 (l + r).max(0.0)
1977 }
1978
1979 fn split_token_to_svg_bbox_width_px(
1980 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1981 tok: &str,
1982 max_width_px: f64,
1983 font_size: f64,
1984 ) -> (String, String) {
1985 if max_width_px <= 0.0 {
1986 return (tok.to_string(), String::new());
1987 }
1988 let chars = tok.chars().collect::<Vec<_>>();
1989 if chars.is_empty() {
1990 return (String::new(), String::new());
1991 }
1992
1993 let first = chars[0];
1994 let left_oh_em = if first.is_ascii() {
1995 0.0
1996 } else {
1997 Self::lookup_overhang_em(
1998 table.svg_bbox_overhang_left,
1999 table.svg_bbox_overhang_left_default_em,
2000 first,
2001 )
2002 };
2003
2004 let mut em = 0.0;
2005 let mut prev: Option<char> = None;
2006 let mut split_at = 1usize;
2007 for (idx, ch) in chars.iter().enumerate() {
2008 em += Self::lookup_char_em(table.entries, table.default_em.max(0.1), *ch);
2009 if let Some(p) = prev {
2010 em += Self::lookup_kern_em(table.kern_pairs, p, *ch);
2011 }
2012 prev = Some(*ch);
2013
2014 let right_oh_em = if ch.is_ascii() {
2015 0.0
2016 } else {
2017 Self::lookup_overhang_em(
2018 table.svg_bbox_overhang_right,
2019 table.svg_bbox_overhang_right_default_em,
2020 *ch,
2021 )
2022 };
2023 let half_px = Self::quantize_svg_half_px_nearest(
2024 (em * font_size * table.svg_scale / 2.0).max(0.0),
2025 );
2026 let w_px = 2.0 * half_px + (left_oh_em + right_oh_em) * font_size;
2027 if w_px.is_finite() && w_px <= max_width_px {
2028 split_at = idx + 1;
2029 } else if idx > 0 {
2030 break;
2031 }
2032 }
2033 let head = chars[..split_at].iter().collect::<String>();
2034 let tail = chars[split_at..].iter().collect::<String>();
2035 (head, tail)
2036 }
2037
2038 fn wrap_text_lines_svg_bbox_px(
2039 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
2040 text: &str,
2041 max_width_px: Option<f64>,
2042 font_size: f64,
2043 tokenize_whitespace: bool,
2044 ) -> Vec<String> {
2045 const EPS_PX: f64 = 0.125;
2046 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
2047 let width_fn = if tokenize_whitespace {
2048 Self::line_svg_bbox_width_px
2049 } else {
2050 Self::line_svg_bbox_width_single_run_px
2051 };
2052
2053 let mut lines = Vec::new();
2054 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2055 let Some(w) = max_width_px else {
2056 lines.push(line);
2057 continue;
2058 };
2059
2060 let mut tokens = std::collections::VecDeque::from(
2061 DeterministicTextMeasurer::split_line_to_words(&line),
2062 );
2063 let mut out: Vec<String> = Vec::new();
2064 let mut cur = String::new();
2065
2066 while let Some(tok) = tokens.pop_front() {
2067 if cur.is_empty() && tok == " " {
2068 continue;
2069 }
2070
2071 let candidate = format!("{cur}{tok}");
2072 let candidate_trimmed = candidate.trim_end();
2073 if width_fn(table, candidate_trimmed, font_size) <= w + EPS_PX {
2074 cur = candidate;
2075 continue;
2076 }
2077
2078 if !cur.trim().is_empty() {
2079 out.push(cur.trim_end().to_string());
2080 cur.clear();
2081 tokens.push_front(tok);
2082 continue;
2083 }
2084
2085 if tok == " " {
2086 continue;
2087 }
2088
2089 if width_fn(table, tok.as_str(), font_size) <= w + EPS_PX {
2090 cur = tok;
2091 continue;
2092 }
2093
2094 let (head, tail) =
2096 Self::split_token_to_svg_bbox_width_px(table, &tok, w + EPS_PX, font_size);
2097 out.push(head);
2098 if !tail.is_empty() {
2099 tokens.push_front(tail);
2100 }
2101 }
2102
2103 if !cur.trim().is_empty() {
2104 out.push(cur.trim_end().to_string());
2105 }
2106
2107 if out.is_empty() {
2108 lines.push("".to_string());
2109 } else {
2110 lines.extend(out);
2111 }
2112 }
2113
2114 if lines.is_empty() {
2115 vec!["".to_string()]
2116 } else {
2117 lines
2118 }
2119 }
2120
2121 fn line_width_px(
2122 entries: &[(char, f64)],
2123 default_em: f64,
2124 kern_pairs: &[(u32, u32, f64)],
2125 space_trigrams: &[(u32, u32, f64)],
2126 trigrams: &[(u32, u32, u32, f64)],
2127 text: &str,
2128 bold: bool,
2129 font_size: f64,
2130 ) -> f64 {
2131 fn normalize_whitespace_like(ch: char) -> (char, f64) {
2132 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
2140 if ch == '\u{00A0}' {
2141 (' ', NBSP_DELTA_EM)
2142 } else {
2143 (ch, 0.0)
2144 }
2145 }
2146
2147 let mut em = 0.0;
2148 let mut prevprev: Option<char> = None;
2149 let mut prev: Option<char> = None;
2150 for ch in text.chars() {
2151 let (ch, delta_em) = normalize_whitespace_like(ch);
2152 em += Self::lookup_char_em(entries, default_em, ch) + delta_em;
2153 if let Some(p) = prev {
2154 em += Self::lookup_kern_em(kern_pairs, p, ch);
2155 }
2156 if bold {
2157 if let Some(p) = prev {
2158 em += flowchart_default_bold_kern_delta_em(p, ch);
2159 }
2160 em += flowchart_default_bold_delta_em(ch);
2161 }
2162 if let (Some(a), Some(b)) = (prevprev, prev) {
2163 if b == ' ' {
2164 if !(a.is_whitespace() || ch.is_whitespace()) {
2165 em += Self::lookup_space_trigram_em(space_trigrams, a, ch);
2166 }
2167 } else if !(a.is_whitespace() || b.is_whitespace() || ch.is_whitespace()) {
2168 em += Self::lookup_trigram_em(trigrams, a, b, ch);
2169 }
2170 }
2171 prevprev = prev;
2172 prev = Some(ch);
2173 }
2174 em * font_size
2175 }
2176
2177 #[allow(dead_code)]
2178 fn ceil_to_1_64_px(v: f64) -> f64 {
2179 if !(v.is_finite() && v >= 0.0) {
2180 return 0.0;
2181 }
2182 let x = v * 64.0;
2184 let r = x.round();
2185 if (x - r).abs() < 1e-4 {
2186 return r / 64.0;
2187 }
2188 ((x) - 1e-5).ceil() / 64.0
2189 }
2190
2191 fn split_token_to_width_px(
2192 entries: &[(char, f64)],
2193 default_em: f64,
2194 kern_pairs: &[(u32, u32, f64)],
2195 trigrams: &[(u32, u32, u32, f64)],
2196 tok: &str,
2197 max_width_px: f64,
2198 bold: bool,
2199 font_size: f64,
2200 ) -> (String, String) {
2201 fn normalize_whitespace_like(ch: char) -> (char, f64) {
2202 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
2203 if ch == '\u{00A0}' {
2204 (' ', NBSP_DELTA_EM)
2205 } else {
2206 (ch, 0.0)
2207 }
2208 }
2209
2210 if max_width_px <= 0.0 {
2211 return (tok.to_string(), String::new());
2212 }
2213 let max_em = max_width_px / font_size.max(1.0);
2214 let mut em = 0.0;
2215 let mut prevprev: Option<char> = None;
2216 let mut prev: Option<char> = None;
2217 let chars = tok.chars().collect::<Vec<_>>();
2218 let mut split_at = 0usize;
2219 for (idx, ch) in chars.iter().enumerate() {
2220 let (ch_norm, delta_em) = normalize_whitespace_like(*ch);
2221 em += Self::lookup_char_em(entries, default_em, ch_norm) + delta_em;
2222 if let Some(p) = prev {
2223 em += Self::lookup_kern_em(kern_pairs, p, ch_norm);
2224 }
2225 if bold {
2226 if let Some(p) = prev {
2227 em += flowchart_default_bold_kern_delta_em(p, ch_norm);
2228 }
2229 em += flowchart_default_bold_delta_em(ch_norm);
2230 }
2231 if let (Some(a), Some(b)) = (prevprev, prev) {
2232 if !(a.is_whitespace() || b.is_whitespace() || ch_norm.is_whitespace()) {
2233 em += Self::lookup_trigram_em(trigrams, a, b, ch_norm);
2234 }
2235 }
2236 prevprev = prev;
2237 prev = Some(ch_norm);
2238 if em > max_em && idx > 0 {
2239 break;
2240 }
2241 split_at = idx + 1;
2242 if em >= max_em {
2243 break;
2244 }
2245 }
2246 if split_at == 0 {
2247 split_at = 1.min(chars.len());
2248 }
2249 let head = chars.iter().take(split_at).collect::<String>();
2250 let tail = chars.iter().skip(split_at).collect::<String>();
2251 (head, tail)
2252 }
2253
2254 fn wrap_line_to_width_px(
2255 entries: &[(char, f64)],
2256 default_em: f64,
2257 kern_pairs: &[(u32, u32, f64)],
2258 space_trigrams: &[(u32, u32, f64)],
2259 trigrams: &[(u32, u32, u32, f64)],
2260 line: &str,
2261 max_width_px: f64,
2262 font_size: f64,
2263 break_long_words: bool,
2264 bold: bool,
2265 ) -> Vec<String> {
2266 let mut tokens =
2267 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
2268 let mut out: Vec<String> = Vec::new();
2269 let mut cur = String::new();
2270
2271 while let Some(tok) = tokens.pop_front() {
2272 if cur.is_empty() && tok == " " {
2273 continue;
2274 }
2275
2276 let candidate = format!("{cur}{tok}");
2277 let candidate_trimmed = candidate.trim_end();
2278 if Self::line_width_px(
2279 entries,
2280 default_em,
2281 kern_pairs,
2282 space_trigrams,
2283 trigrams,
2284 candidate_trimmed,
2285 bold,
2286 font_size,
2287 ) <= max_width_px
2288 {
2289 cur = candidate;
2290 continue;
2291 }
2292
2293 if !cur.trim().is_empty() {
2294 out.push(cur.trim_end().to_string());
2295 cur.clear();
2296 }
2297
2298 if tok == " " {
2299 continue;
2300 }
2301
2302 if Self::line_width_px(
2303 entries,
2304 default_em,
2305 kern_pairs,
2306 space_trigrams,
2307 trigrams,
2308 tok.as_str(),
2309 bold,
2310 font_size,
2311 ) <= max_width_px
2312 {
2313 cur = tok;
2314 continue;
2315 }
2316
2317 if !break_long_words {
2318 out.push(tok);
2319 continue;
2320 }
2321
2322 let (head, tail) = Self::split_token_to_width_px(
2323 entries,
2324 default_em,
2325 kern_pairs,
2326 trigrams,
2327 &tok,
2328 max_width_px,
2329 bold,
2330 font_size,
2331 );
2332 out.push(head);
2333 if !tail.is_empty() {
2334 tokens.push_front(tail);
2335 }
2336 }
2337
2338 if !cur.trim().is_empty() {
2339 out.push(cur.trim_end().to_string());
2340 }
2341
2342 if out.is_empty() {
2343 vec!["".to_string()]
2344 } else {
2345 out
2346 }
2347 }
2348
2349 fn wrap_text_lines_px(
2350 entries: &[(char, f64)],
2351 default_em: f64,
2352 kern_pairs: &[(u32, u32, f64)],
2353 space_trigrams: &[(u32, u32, f64)],
2354 trigrams: &[(u32, u32, u32, f64)],
2355 text: &str,
2356 style: &TextStyle,
2357 bold: bool,
2358 max_width_px: Option<f64>,
2359 wrap_mode: WrapMode,
2360 ) -> Vec<String> {
2361 let font_size = style.font_size.max(1.0);
2362 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
2363 let break_long_words = wrap_mode == WrapMode::SvgLike;
2364
2365 let mut lines = Vec::new();
2366 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2367 if let Some(w) = max_width_px {
2368 lines.extend(Self::wrap_line_to_width_px(
2369 entries,
2370 default_em,
2371 kern_pairs,
2372 space_trigrams,
2373 trigrams,
2374 &line,
2375 w,
2376 font_size,
2377 break_long_words,
2378 bold,
2379 ));
2380 } else {
2381 lines.push(line);
2382 }
2383 }
2384
2385 if lines.is_empty() {
2386 vec!["".to_string()]
2387 } else {
2388 lines
2389 }
2390 }
2391}
2392
2393fn vendored_measure_wrapped_impl(
2394 measurer: &VendoredFontMetricsTextMeasurer,
2395 text: &str,
2396 style: &TextStyle,
2397 max_width: Option<f64>,
2398 wrap_mode: WrapMode,
2399 use_html_overrides: bool,
2400) -> (TextMetrics, Option<f64>) {
2401 let Some(table) = measurer.lookup_table(style) else {
2402 return measurer
2403 .fallback
2404 .measure_wrapped_with_raw_width(text, style, max_width, wrap_mode);
2405 };
2406
2407 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
2408 let font_size = style.font_size.max(1.0);
2409 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
2410 let line_height_factor = match wrap_mode {
2411 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
2412 WrapMode::HtmlLike => 1.5,
2413 };
2414
2415 let html_overrides: &[(&'static str, f64)] = if use_html_overrides {
2416 table.html_overrides
2417 } else {
2418 &[]
2419 };
2420
2421 fn extra_html_override_em(font_key: &str, line: &str) -> Option<f64> {
2422 if font_key != "trebuchetms,verdana,arial,sans-serif" {
2429 return None;
2430 }
2431
2432 let px: Option<f64> = match line {
2435 "ABlock" => Some(47.796875),
2437 "A wide one in the middle" => Some(179.0625),
2438 "B;" => Some(14.9375),
2439 "BBlock" => Some(47.40625),
2440 "Block 1" => Some(51.5625),
2441 "Block 2" => Some(51.5625),
2442 "Block 3" => Some(51.5625),
2443 "Compound block" => Some(118.375),
2444 "Memcache" => Some(75.078125),
2445 "One Slot" => Some(60.421875),
2446 "Two slots" => Some(65.0),
2447 "__proto__" => Some(72.21875),
2448 "constructor" => Some(82.109375),
2449 "A;" => Some(15.3125),
2450 ",.?!+-*ز" => Some(51.46875),
2454 "Circle shape" => Some(87.8125),
2455 "Circle shape Начало" => Some(145.609375),
2456 "Link text" => Some(63.734375),
2457 "Round Rect" => Some(80.125),
2458 "Rounded" => Some(61.296875),
2459 "Rounded square shape" => Some(159.6875),
2460 "Square Rect" => Some(85.1875),
2461 "Square shape" => Some(94.796875),
2462 "edge comment" => Some(106.109375),
2463 "special characters" => Some(129.9375),
2464 _ => None,
2465 };
2466
2467 px.map(|w| w / 16.0)
2468 }
2469
2470 let html_override_px = |em: f64| -> f64 {
2471 if (font_size - table.base_font_size_px).abs() < 0.01 {
2479 em * font_size
2480 } else {
2481 em * table.base_font_size_px
2482 }
2483 };
2484
2485 let html_width_override_px = |line: &str| -> Option<f64> {
2486 if table.font_key != "trebuchetms,verdana,arial,sans-serif" {
2491 return None;
2492 }
2493 crate::generated::er_text_overrides_11_12_2::lookup_html_width_px(font_size, line).or_else(
2494 || {
2495 crate::generated::mindmap_text_overrides_11_12_2::lookup_html_width_px(
2496 font_size, line,
2497 )
2498 },
2499 )
2500 };
2501
2502 let raw_width_unscaled = if wrap_mode == WrapMode::HtmlLike {
2511 let mut raw_w: f64 = 0.0;
2512 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2513 if let Some(w) = html_width_override_px(&line) {
2514 raw_w = raw_w.max(w);
2515 continue;
2516 }
2517 if let Some(em) = extra_html_override_em(table.font_key, &line).or_else(|| {
2518 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, &line)
2519 }) {
2520 raw_w = raw_w.max(html_override_px(em));
2521 } else {
2522 raw_w = raw_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
2523 table.entries,
2524 table.default_em.max(0.1),
2525 table.kern_pairs,
2526 table.space_trigrams,
2527 table.trigrams,
2528 &line,
2529 bold,
2530 font_size,
2531 ));
2532 }
2533 }
2534 Some(raw_w)
2535 } else {
2536 None
2537 };
2538
2539 let lines = match wrap_mode {
2540 WrapMode::HtmlLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_px(
2541 table.entries,
2542 table.default_em.max(0.1),
2543 table.kern_pairs,
2544 table.space_trigrams,
2545 table.trigrams,
2546 text,
2547 style,
2548 bold,
2549 max_width,
2550 wrap_mode,
2551 ),
2552 WrapMode::SvgLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
2553 table, text, max_width, font_size, true,
2554 ),
2555 WrapMode::SvgLikeSingleRun => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
2556 table, text, max_width, font_size, false,
2557 ),
2558 };
2559
2560 let mut width: f64 = 0.0;
2561 match wrap_mode {
2562 WrapMode::HtmlLike => {
2563 for line in &lines {
2564 if let Some(w) = html_width_override_px(line) {
2565 width = width.max(w);
2566 continue;
2567 }
2568 if let Some(em) = extra_html_override_em(table.font_key, line).or_else(|| {
2569 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, line)
2570 }) {
2571 width = width.max(html_override_px(em));
2572 } else {
2573 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
2574 table.entries,
2575 table.default_em.max(0.1),
2576 table.kern_pairs,
2577 table.space_trigrams,
2578 table.trigrams,
2579 line,
2580 bold,
2581 font_size,
2582 ));
2583 }
2584 }
2585 }
2586 WrapMode::SvgLike => {
2587 for line in &lines {
2588 width = width.max(VendoredFontMetricsTextMeasurer::line_svg_bbox_width_px(
2589 table, line, font_size,
2590 ));
2591 }
2592 }
2593 WrapMode::SvgLikeSingleRun => {
2594 for line in &lines {
2595 width = width.max(
2596 VendoredFontMetricsTextMeasurer::line_svg_bbox_width_single_run_px(
2597 table, line, font_size,
2598 ),
2599 );
2600 }
2601 }
2602 }
2603
2604 if wrap_mode == WrapMode::HtmlLike {
2607 if let Some(w) = max_width {
2608 let needs_wrap = raw_width_unscaled.is_some_and(|rw| rw > w);
2609 if needs_wrap {
2610 width = w;
2611 } else {
2612 width = width.min(w);
2613 }
2614 }
2615 width = round_to_1_64_px(width);
2618 if let Some(w) = max_width {
2619 width = width.min(w);
2620 }
2621 }
2622
2623 let height = match wrap_mode {
2624 WrapMode::HtmlLike => lines.len() as f64 * font_size * line_height_factor,
2625 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
2626 if lines.is_empty() {
2627 0.0
2628 } else {
2629 let first_line_em = if table.font_key == "courier" {
2633 1.125
2634 } else {
2635 1.1875
2636 };
2637 let first_line_h = font_size * first_line_em;
2638 let additional = (lines.len().saturating_sub(1)) as f64 * font_size * 1.1;
2639 first_line_h + additional
2640 }
2641 }
2642 };
2643
2644 let metrics = TextMetrics {
2645 width,
2646 height,
2647 line_count: lines.len(),
2648 };
2649 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
2650 raw_width_unscaled
2651 } else {
2652 None
2653 };
2654 (metrics, raw_width_px)
2655}
2656
2657impl TextMeasurer for VendoredFontMetricsTextMeasurer {
2658 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
2659 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
2660 }
2661
2662 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
2663 let Some(table) = self.lookup_table(style) else {
2664 return self.fallback.measure_svg_text_bbox_x(text, style);
2665 };
2666
2667 let font_size = style.font_size.max(1.0);
2668 let mut left: f64 = 0.0;
2669 let mut right: f64 = 0.0;
2670 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2671 let (l, r) = Self::line_svg_bbox_extents_px(table, &line, font_size);
2672 left = left.max(l);
2673 right = right.max(r);
2674 }
2675 (left, right)
2676 }
2677
2678 fn measure_svg_text_bbox_x_with_ascii_overhang(
2679 &self,
2680 text: &str,
2681 style: &TextStyle,
2682 ) -> (f64, f64) {
2683 let Some(table) = self.lookup_table(style) else {
2684 return self
2685 .fallback
2686 .measure_svg_text_bbox_x_with_ascii_overhang(text, style);
2687 };
2688
2689 let font_size = style.font_size.max(1.0);
2690 let mut left: f64 = 0.0;
2691 let mut right: f64 = 0.0;
2692 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2693 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
2694 table, &line, font_size,
2695 );
2696 left = left.max(l);
2697 right = right.max(r);
2698 }
2699 (left, right)
2700 }
2701
2702 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
2703 let Some(table) = self.lookup_table(style) else {
2704 return self.fallback.measure_svg_title_bbox_x(text, style);
2705 };
2706
2707 let font_size = style.font_size.max(1.0);
2708 let mut left: f64 = 0.0;
2709 let mut right: f64 = 0.0;
2710 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2711 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, &line, font_size);
2712 left = left.max(l);
2713 right = right.max(r);
2714 }
2715 (left, right)
2716 }
2717
2718 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
2719 let Some(table) = self.lookup_table(style) else {
2720 return self
2721 .fallback
2722 .measure_svg_simple_text_bbox_width_px(text, style);
2723 };
2724
2725 let font_size = style.font_size.max(1.0);
2726 let mut width: f64 = 0.0;
2727 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2728 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
2729 table, &line, font_size,
2730 );
2731 width = width.max((l + r).max(0.0));
2732 }
2733 width
2734 }
2735
2736 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
2737 let t = text.trim_end();
2738 if t.is_empty() {
2739 return 0.0;
2740 }
2741 let font_size = style.font_size.max(1.0);
2744 (font_size * 1.1).max(0.0)
2745 }
2746
2747 fn measure_wrapped(
2748 &self,
2749 text: &str,
2750 style: &TextStyle,
2751 max_width: Option<f64>,
2752 wrap_mode: WrapMode,
2753 ) -> TextMetrics {
2754 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true).0
2755 }
2756
2757 fn measure_wrapped_with_raw_width(
2758 &self,
2759 text: &str,
2760 style: &TextStyle,
2761 max_width: Option<f64>,
2762 wrap_mode: WrapMode,
2763 ) -> (TextMetrics, Option<f64>) {
2764 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true)
2765 }
2766
2767 fn measure_wrapped_raw(
2768 &self,
2769 text: &str,
2770 style: &TextStyle,
2771 max_width: Option<f64>,
2772 wrap_mode: WrapMode,
2773 ) -> TextMetrics {
2774 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, false).0
2775 }
2776}
2777
2778impl TextMeasurer for DeterministicTextMeasurer {
2779 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
2780 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
2781 }
2782
2783 fn measure_wrapped(
2784 &self,
2785 text: &str,
2786 style: &TextStyle,
2787 max_width: Option<f64>,
2788 wrap_mode: WrapMode,
2789 ) -> TextMetrics {
2790 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
2791 .0
2792 }
2793
2794 fn measure_wrapped_with_raw_width(
2795 &self,
2796 text: &str,
2797 style: &TextStyle,
2798 max_width: Option<f64>,
2799 wrap_mode: WrapMode,
2800 ) -> (TextMetrics, Option<f64>) {
2801 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
2802 }
2803
2804 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
2805 let t = text.trim_end();
2806 if t.is_empty() {
2807 return 0.0;
2808 }
2809 (style.font_size.max(1.0) * 1.1).max(0.0)
2810 }
2811}
2812
2813impl DeterministicTextMeasurer {
2814 fn measure_wrapped_impl(
2815 &self,
2816 text: &str,
2817 style: &TextStyle,
2818 max_width: Option<f64>,
2819 wrap_mode: WrapMode,
2820 clamp_html_width: bool,
2821 ) -> (TextMetrics, Option<f64>) {
2822 let uses_heuristic_widths = self.char_width_factor == 0.0;
2823 let char_width_factor = if uses_heuristic_widths {
2824 match wrap_mode {
2825 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 0.6,
2826 WrapMode::HtmlLike => 0.5,
2827 }
2828 } else {
2829 self.char_width_factor
2830 };
2831 let default_line_height_factor = match wrap_mode {
2832 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
2833 WrapMode::HtmlLike => 1.5,
2834 };
2835 let line_height_factor = if self.line_height_factor == 0.0 {
2836 default_line_height_factor
2837 } else {
2838 self.line_height_factor
2839 };
2840
2841 let font_size = style.font_size.max(1.0);
2842 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
2843 let break_long_words = matches!(wrap_mode, WrapMode::SvgLike | WrapMode::SvgLikeSingleRun);
2844
2845 let raw_lines = Self::normalized_text_lines(text);
2846 let mut raw_width: f64 = 0.0;
2847 for line in &raw_lines {
2848 let w = if uses_heuristic_widths {
2849 estimate_line_width_px(line, font_size)
2850 } else {
2851 line.chars().count() as f64 * font_size * char_width_factor
2852 };
2853 raw_width = raw_width.max(w);
2854 }
2855 let needs_wrap =
2856 wrap_mode == WrapMode::HtmlLike && max_width.is_some_and(|w| raw_width > w);
2857
2858 let mut lines = Vec::new();
2859 for line in raw_lines {
2860 if let Some(w) = max_width {
2861 let char_px = font_size * char_width_factor;
2862 let max_chars = ((w / char_px).floor() as isize).max(1) as usize;
2863 lines.extend(Self::wrap_line(&line, max_chars, break_long_words));
2864 } else {
2865 lines.push(line);
2866 }
2867 }
2868
2869 let mut width: f64 = 0.0;
2870 for line in &lines {
2871 let w = if uses_heuristic_widths {
2872 estimate_line_width_px(line, font_size)
2873 } else {
2874 line.chars().count() as f64 * font_size * char_width_factor
2875 };
2876 width = width.max(w);
2877 }
2878 if clamp_html_width && wrap_mode == WrapMode::HtmlLike {
2882 if let Some(w) = max_width {
2883 if needs_wrap {
2884 width = w;
2885 } else {
2886 width = width.min(w);
2887 }
2888 }
2889 }
2890 let height = lines.len() as f64 * font_size * line_height_factor;
2891 let metrics = TextMetrics {
2892 width,
2893 height,
2894 line_count: lines.len(),
2895 };
2896 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
2897 Some(raw_width)
2898 } else {
2899 None
2900 };
2901 (metrics, raw_width_px)
2902 }
2903}
2904
2905fn estimate_line_width_px(line: &str, font_size: f64) -> f64 {
2906 let mut em = 0.0;
2907 for ch in line.chars() {
2908 em += estimate_char_width_em(ch);
2909 }
2910 em * font_size
2911}
2912
2913fn estimate_char_width_em(ch: char) -> f64 {
2914 if ch == ' ' {
2915 return 0.33;
2916 }
2917 if ch == '\t' {
2918 return 0.66;
2919 }
2920 if ch == '_' || ch == '-' {
2921 return 0.33;
2922 }
2923 if matches!(ch, '.' | ',' | ':' | ';') {
2924 return 0.28;
2925 }
2926 if matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '/') {
2927 return 0.33;
2928 }
2929 if matches!(ch, '+' | '*' | '=' | '\\' | '^' | '|' | '~') {
2930 return 0.45;
2931 }
2932 if ch.is_ascii_digit() {
2933 return 0.56;
2934 }
2935 if ch.is_ascii_uppercase() {
2936 return match ch {
2937 'I' => 0.30,
2938 'W' => 0.85,
2939 _ => 0.60,
2940 };
2941 }
2942 if ch.is_ascii_lowercase() {
2943 return match ch {
2944 'i' | 'l' => 0.28,
2945 'm' | 'w' => 0.78,
2946 'k' | 'y' => 0.55,
2947 _ => 0.43,
2948 };
2949 }
2950 0.60
2952}