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) -> f64 {
287 const DELTA_EM: f64 = 1.0 / 128.0;
294 match ch {
295 'A'..='Z' | 'a'..='z' | '0'..='9' => DELTA_EM,
296 _ => 0.0,
297 }
298}
299
300pub fn mermaid_default_italic_width_delta_px(text: &str, style: &TextStyle) -> f64 {
301 if !is_flowchart_default_font(style) {
309 return 0.0;
310 }
311
312 let font_size = style.font_size.max(1.0);
313 let bold = style_requests_bold_font_weight(style);
314 let per_char_em = if bold {
315 1.0 / 64.0
317 } else {
318 37.0 / 3072.0
322 };
323
324 let mut max_em: f64 = 0.0;
325 for line in text.lines() {
326 let mut em: f64 = 0.0;
327 for ch in line.chars() {
328 match ch {
329 'A'..='Z' | 'a'..='z' | '0'..='9' => em += per_char_em,
330 _ => {}
331 }
332 }
333 max_em = max_em.max(em);
334 }
335
336 (max_em * font_size).max(0.0)
337}
338
339pub fn mermaid_default_bold_width_delta_px(text: &str, style: &TextStyle) -> f64 {
340 if !is_flowchart_default_font(style) {
344 return 0.0;
345 }
346 if !style_requests_bold_font_weight(style) {
347 return 0.0;
348 }
349
350 let font_size = style.font_size.max(1.0);
351
352 let mut max_delta_px: f64 = 0.0;
353 for line in text.lines() {
354 let mut delta_px: f64 = 0.0;
355 let mut prev: Option<char> = None;
356 for ch in line.chars() {
357 if let Some(p) = prev {
358 delta_px += flowchart_default_bold_kern_delta_em(p, ch) * font_size;
359 }
360 delta_px += flowchart_default_bold_delta_em(ch) * font_size;
361 prev = Some(ch);
362 }
363 max_delta_px = max_delta_px.max(delta_px);
364 }
365
366 max_delta_px.max(0.0)
367}
368
369pub fn measure_html_with_flowchart_bold_deltas(
370 measurer: &dyn TextMeasurer,
371 html: &str,
372 style: &TextStyle,
373 max_width: Option<f64>,
374 wrap_mode: WrapMode,
375) -> TextMetrics {
376 const BOLD_DELTA_SCALE: f64 = 1.0;
380
381 fn decode_html_entity(entity: &str) -> Option<char> {
385 match entity {
386 "nbsp" => Some(' '),
387 "lt" => Some('<'),
388 "gt" => Some('>'),
389 "amp" => Some('&'),
390 "quot" => Some('"'),
391 "apos" => Some('\''),
392 "#39" => Some('\''),
393 _ => {
394 if let Some(hex) = entity
395 .strip_prefix("#x")
396 .or_else(|| entity.strip_prefix("#X"))
397 {
398 u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
399 } else if let Some(dec) = entity.strip_prefix('#') {
400 dec.parse::<u32>().ok().and_then(char::from_u32)
401 } else {
402 None
403 }
404 }
405 }
406 }
407
408 let mut plain = String::new();
409 let mut deltas_px_by_line: Vec<f64> = vec![0.0];
410 let mut icon_on_line: Vec<bool> = vec![false];
411 let mut strong_depth: usize = 0;
412 let mut em_depth: usize = 0;
413 let mut fa_icon_depth: usize = 0;
414 let mut prev_char: Option<char> = None;
415 let mut prev_is_strong = false;
416
417 let html = html.replace("\r\n", "\n");
418 let mut it = html.chars().peekable();
419 while let Some(ch) = it.next() {
420 if ch == '<' {
421 let mut tag = String::new();
422 for c in it.by_ref() {
423 if c == '>' {
424 break;
425 }
426 tag.push(c);
427 }
428 let tag = tag.trim();
429 let tag_lower = tag.to_ascii_lowercase();
430 let tag_trim = tag_lower.trim();
431 if tag_trim.starts_with('!') || tag_trim.starts_with('?') {
432 continue;
433 }
434 let is_closing = tag_trim.starts_with('/');
435 let name = tag_trim
436 .trim_start_matches('/')
437 .trim_end_matches('/')
438 .split_whitespace()
439 .next()
440 .unwrap_or("");
441
442 let is_fontawesome_icon_i = name == "i"
443 && !is_closing
444 && (tag_trim.contains("class=\"fa")
445 || tag_trim.contains("class='fa")
446 || tag_trim.contains("class=\"fab")
447 || tag_trim.contains("class='fab")
448 || tag_trim.contains("class=\"fal")
449 || tag_trim.contains("class='fal")
450 || tag_trim.contains("class=\"far")
451 || tag_trim.contains("class='far")
452 || tag_trim.contains("class=\"fas")
453 || tag_trim.contains("class='fas"));
454
455 match name {
456 "strong" | "b" => {
457 if is_closing {
458 strong_depth = strong_depth.saturating_sub(1);
459 } else {
460 strong_depth += 1;
461 }
462 }
463 "em" | "i" => {
464 if is_closing {
465 if name == "i" && fa_icon_depth > 0 {
466 fa_icon_depth = fa_icon_depth.saturating_sub(1);
467 } else {
468 em_depth = em_depth.saturating_sub(1);
469 }
470 } else if is_fontawesome_icon_i {
471 let line_idx = deltas_px_by_line.len().saturating_sub(1);
477 let icon_w = (style.font_size.max(1.0) - (1.0 / 64.0)).max(0.0);
481 deltas_px_by_line[line_idx] += icon_w;
482 if let Some(slot) = icon_on_line.get_mut(line_idx) {
483 *slot = true;
484 }
485 fa_icon_depth += 1;
486 } else {
487 em_depth += 1;
488 }
489 }
490 "br" => {
491 plain.push('\n');
492 deltas_px_by_line.push(0.0);
493 icon_on_line.push(false);
494 prev_char = None;
495 prev_is_strong = false;
496 }
497 "p" | "div" | "li" | "tr" | "ul" | "ol" if is_closing => {
498 plain.push('\n');
499 deltas_px_by_line.push(0.0);
500 icon_on_line.push(false);
501 prev_char = None;
502 prev_is_strong = false;
503 }
504 _ => {}
505 }
506 continue;
507 }
508
509 let push_char = |decoded: char,
510 plain: &mut String,
511 deltas_px_by_line: &mut Vec<f64>,
512 icon_on_line: &mut Vec<bool>,
513 prev_char: &mut Option<char>,
514 prev_is_strong: &mut bool| {
515 plain.push(decoded);
516 if decoded == '\n' {
517 deltas_px_by_line.push(0.0);
518 icon_on_line.push(false);
519 *prev_char = None;
520 *prev_is_strong = false;
521 return;
522 }
523 if is_flowchart_default_font(style) {
524 let line_idx = deltas_px_by_line.len().saturating_sub(1);
525 let font_size = style.font_size.max(1.0);
526 let is_strong = strong_depth > 0;
527 if let Some(prev) = *prev_char {
528 if *prev_is_strong && is_strong {
529 deltas_px_by_line[line_idx] +=
530 flowchart_default_bold_kern_delta_em(prev, decoded)
531 * font_size
532 * BOLD_DELTA_SCALE;
533 }
534 }
535 if is_strong {
536 deltas_px_by_line[line_idx] +=
537 flowchart_default_bold_delta_em(decoded) * font_size * BOLD_DELTA_SCALE;
538 }
539 if em_depth > 0 {
540 deltas_px_by_line[line_idx] +=
541 flowchart_default_italic_delta_em(decoded) * font_size;
542 }
543 *prev_char = Some(decoded);
544 *prev_is_strong = is_strong;
545 } else {
546 *prev_char = Some(decoded);
547 *prev_is_strong = strong_depth > 0;
548 }
549 };
550
551 if ch == '&' {
552 let mut entity = String::new();
553 let mut saw_semicolon = false;
554 while let Some(&c) = it.peek() {
555 if c == ';' {
556 it.next();
557 saw_semicolon = true;
558 break;
559 }
560 if c == '<' || c == '&' || c.is_whitespace() || entity.len() > 32 {
561 break;
562 }
563 entity.push(c);
564 it.next();
565 }
566 if saw_semicolon {
567 if let Some(decoded) = decode_html_entity(entity.as_str()) {
568 push_char(
569 decoded,
570 &mut plain,
571 &mut deltas_px_by_line,
572 &mut icon_on_line,
573 &mut prev_char,
574 &mut prev_is_strong,
575 );
576 } else {
577 plain.push('&');
578 plain.push_str(&entity);
579 plain.push(';');
580 }
581 } else {
582 plain.push('&');
583 plain.push_str(&entity);
584 }
585 continue;
586 }
587
588 push_char(
589 ch,
590 &mut plain,
591 &mut deltas_px_by_line,
592 &mut icon_on_line,
593 &mut prev_char,
594 &mut prev_is_strong,
595 );
596 }
597
598 let plain = plain.trim_end().to_string();
601 let base = measurer.measure_wrapped_raw(plain.trim(), style, max_width, wrap_mode);
602
603 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
604 if lines.is_empty() {
605 lines.push(String::new());
606 }
607 deltas_px_by_line.resize(lines.len(), 0.0);
608 icon_on_line.resize(lines.len(), false);
609
610 let mut max_line_width: f64 = 0.0;
611 for (idx, line) in lines.iter().enumerate() {
612 let line = if icon_on_line[idx] {
613 line.trim_end()
614 } else {
615 line.trim()
616 };
617 let w = measurer
618 .measure_wrapped_raw(line, style, None, wrap_mode)
619 .width;
620 max_line_width = max_line_width.max(w + deltas_px_by_line[idx]);
621 }
622
623 let mut width = round_to_1_64_px(max_line_width);
626 if wrap_mode == WrapMode::HtmlLike {
627 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
628 if max_line_width > w {
629 width = w;
630 } else {
631 width = width.min(w);
632 }
633 }
634 }
635
636 TextMetrics {
637 width,
638 height: base.height,
639 line_count: base.line_count,
640 }
641}
642
643pub fn measure_markdown_with_flowchart_bold_deltas(
644 measurer: &dyn TextMeasurer,
645 markdown: &str,
646 style: &TextStyle,
647 max_width: Option<f64>,
648 wrap_mode: WrapMode,
649) -> TextMetrics {
650 let bold_delta_scale: f64 = 1.0;
656
657 if markdown.contains("![") {
663 #[derive(Debug, Default, Clone)]
664 struct Paragraph {
665 text: String,
666 image_urls: Vec<String>,
667 }
668
669 fn measure_markdown_images(
670 measurer: &dyn TextMeasurer,
671 markdown: &str,
672 style: &TextStyle,
673 max_width: Option<f64>,
674 wrap_mode: WrapMode,
675 ) -> Option<TextMetrics> {
676 let parser = pulldown_cmark::Parser::new_ext(
677 markdown,
678 pulldown_cmark::Options::ENABLE_TABLES
679 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
680 | pulldown_cmark::Options::ENABLE_TASKLISTS,
681 );
682
683 let mut paragraphs: Vec<Paragraph> = Vec::new();
684 let mut current = Paragraph::default();
685 let mut in_paragraph = false;
686
687 for ev in parser {
688 match ev {
689 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Paragraph) => {
690 if in_paragraph {
691 paragraphs.push(std::mem::take(&mut current));
692 }
693 in_paragraph = true;
694 }
695 pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Paragraph) => {
696 if in_paragraph {
697 paragraphs.push(std::mem::take(&mut current));
698 }
699 in_paragraph = false;
700 }
701 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Image {
702 dest_url, ..
703 }) => {
704 current.image_urls.push(dest_url.to_string());
705 }
706 pulldown_cmark::Event::Text(t) | pulldown_cmark::Event::Code(t) => {
707 current.text.push_str(&t);
708 }
709 pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => {
710 current.text.push('\n');
711 }
712 _ => {}
713 }
714 }
715 if in_paragraph {
716 paragraphs.push(current);
717 }
718
719 let total_images: usize = paragraphs.iter().map(|p| p.image_urls.len()).sum();
720 if total_images == 0 {
721 return None;
722 }
723
724 let total_text = paragraphs
725 .iter()
726 .map(|p| p.text.as_str())
727 .collect::<Vec<_>>()
728 .join("\n");
729 let has_any_text = !total_text.trim().is_empty();
730
731 if total_images == 1 && !has_any_text {
735 let url = paragraphs
736 .iter()
737 .flat_map(|p| p.image_urls.iter())
738 .next()
739 .cloned()
740 .unwrap_or_default();
741 let img_w = 80.0;
742 let has_src = !url.trim().is_empty();
743 let img_h = if has_src { img_w } else { 0.0 };
744 return Some(TextMetrics {
745 width: ceil_to_1_64_px(img_w),
746 height: ceil_to_1_64_px(img_h),
747 line_count: if img_h > 0.0 { 1 } else { 0 },
748 });
749 }
750
751 let max_w = max_width.unwrap_or(200.0).max(1.0);
752 let line_height = style.font_size.max(1.0) * 1.5;
753
754 let mut width: f64 = 0.0;
755 let mut height: f64 = 0.0;
756 let mut line_count: usize = 0;
757
758 for p in paragraphs {
759 let p_text = p.text.trim().to_string();
760 let text_metrics = if p_text.is_empty() {
761 TextMetrics {
762 width: 0.0,
763 height: 0.0,
764 line_count: 0,
765 }
766 } else {
767 measurer.measure_wrapped(&p_text, style, Some(max_w), wrap_mode)
768 };
769
770 if !p.image_urls.is_empty() {
771 width = width.max(max_w);
774 if text_metrics.line_count == 0 {
775 height += line_height;
777 line_count += 1;
778 }
779 for url in p.image_urls {
780 let has_src = !url.trim().is_empty();
781 let img_h = if has_src { max_w } else { 0.0 };
782 height += img_h;
783 if img_h > 0.0 {
784 line_count += 1;
785 }
786 }
787 }
788
789 width = width.max(text_metrics.width);
790 height += text_metrics.height;
791 line_count += text_metrics.line_count;
792 }
793
794 Some(TextMetrics {
795 width: ceil_to_1_64_px(width),
796 height: ceil_to_1_64_px(height),
797 line_count,
798 })
799 }
800
801 if let Some(m) = measure_markdown_images(measurer, markdown, style, max_width, wrap_mode) {
802 return m;
803 }
804 }
805
806 let mut plain = String::new();
807 let mut deltas_px_by_line: Vec<f64> = vec![0.0];
808 let mut strong_depth: usize = 0;
809 let mut em_depth: usize = 0;
810 let mut prev_char: Option<char> = None;
811 let mut prev_is_strong = false;
812
813 let parser = pulldown_cmark::Parser::new_ext(
814 markdown,
815 pulldown_cmark::Options::ENABLE_TABLES
816 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
817 | pulldown_cmark::Options::ENABLE_TASKLISTS,
818 );
819
820 for ev in parser {
821 match ev {
822 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Emphasis) => {
823 em_depth += 1;
824 }
825 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Strong) => {
826 strong_depth += 1;
827 }
828 pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Emphasis) => {
829 em_depth = em_depth.saturating_sub(1);
830 }
831 pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Strong) => {
832 strong_depth = strong_depth.saturating_sub(1);
833 }
834 pulldown_cmark::Event::Text(t) | pulldown_cmark::Event::Code(t) => {
835 for ch in t.chars() {
836 plain.push(ch);
837 if ch == '\n' {
838 deltas_px_by_line.push(0.0);
839 prev_char = None;
840 prev_is_strong = false;
841 continue;
842 }
843 if is_flowchart_default_font(style) {
844 let line_idx = deltas_px_by_line.len().saturating_sub(1);
845 let font_size = style.font_size.max(1.0);
846 let is_strong = strong_depth > 0;
847 if let Some(prev) = prev_char {
848 if prev_is_strong && is_strong {
849 deltas_px_by_line[line_idx] +=
850 flowchart_default_bold_kern_delta_em(prev, ch)
851 * font_size
852 * bold_delta_scale;
853 }
854 }
855 if is_strong {
856 deltas_px_by_line[line_idx] +=
857 flowchart_default_bold_delta_em(ch) * font_size * bold_delta_scale;
858 }
859 if em_depth > 0 {
860 deltas_px_by_line[line_idx] +=
861 flowchart_default_italic_delta_em(ch) * font_size;
862 }
863 prev_char = Some(ch);
864 prev_is_strong = is_strong;
865 } else {
866 prev_char = Some(ch);
867 prev_is_strong = strong_depth > 0;
868 }
869 }
870 }
871 pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => {
872 plain.push('\n');
873 deltas_px_by_line.push(0.0);
874 prev_char = None;
875 prev_is_strong = false;
876 }
877 _ => {}
878 }
879 }
880
881 let plain = plain.trim().to_string();
882 let base = measurer.measure_wrapped_raw(&plain, style, max_width, wrap_mode);
883
884 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
885 if lines.is_empty() {
886 lines.push(String::new());
887 }
888 deltas_px_by_line.resize(lines.len(), 0.0);
889
890 let mut max_line_width: f64 = 0.0;
891 for (idx, line) in lines.iter().enumerate() {
892 let w = measurer
893 .measure_wrapped_raw(line, style, None, wrap_mode)
894 .width;
895 max_line_width = max_line_width.max(w + deltas_px_by_line[idx]);
896 }
897
898 let mut width = round_to_1_64_px(max_line_width);
901 if wrap_mode == WrapMode::HtmlLike {
902 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
903 if max_line_width > w {
904 width = w;
905 } else {
906 width = width.min(w);
907 }
908 }
909 }
910
911 TextMetrics {
912 width,
913 height: base.height,
914 line_count: base.line_count,
915 }
916}
917
918pub trait TextMeasurer {
919 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics;
920
921 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
928 let m = self.measure(text, style);
929 let half = (m.width.max(0.0)) / 2.0;
930 (half, half)
931 }
932
933 fn measure_svg_text_bbox_x_with_ascii_overhang(
942 &self,
943 text: &str,
944 style: &TextStyle,
945 ) -> (f64, f64) {
946 self.measure_svg_text_bbox_x(text, style)
947 }
948
949 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
955 self.measure_svg_text_bbox_x(text, style)
956 }
957
958 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
964 let (l, r) = self.measure_svg_title_bbox_x(text, style);
965 (l + r).max(0.0)
966 }
967
968 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
977 let m = self.measure(text, style);
978 m.height.max(0.0)
979 }
980
981 fn measure_wrapped(
982 &self,
983 text: &str,
984 style: &TextStyle,
985 max_width: Option<f64>,
986 wrap_mode: WrapMode,
987 ) -> TextMetrics {
988 let _ = max_width;
989 let _ = wrap_mode;
990 self.measure(text, style)
991 }
992
993 fn measure_wrapped_with_raw_width(
1002 &self,
1003 text: &str,
1004 style: &TextStyle,
1005 max_width: Option<f64>,
1006 wrap_mode: WrapMode,
1007 ) -> (TextMetrics, Option<f64>) {
1008 (
1009 self.measure_wrapped(text, style, max_width, wrap_mode),
1010 None,
1011 )
1012 }
1013
1014 fn measure_wrapped_raw(
1019 &self,
1020 text: &str,
1021 style: &TextStyle,
1022 max_width: Option<f64>,
1023 wrap_mode: WrapMode,
1024 ) -> TextMetrics {
1025 self.measure_wrapped(text, style, max_width, wrap_mode)
1026 }
1027}
1028
1029pub(crate) fn mermaid_markdown_wants_paragraph_wrap(markdown: &str) -> bool {
1041 let s = markdown.trim_start();
1042 if s.is_empty() {
1043 return true;
1044 }
1045
1046 let mut i = 0usize;
1048 for ch in s.chars() {
1049 if ch == ' ' && i < 3 {
1050 i += 1;
1051 continue;
1052 }
1053 break;
1054 }
1055 let s = &s[i.min(s.len())..];
1056 let line = s.lines().next().unwrap_or(s).trim_end();
1057 let line_trim = line.trim();
1058
1059 if line_trim.is_empty() {
1060 return true;
1061 }
1062
1063 if line_trim.starts_with('#') {
1065 return false;
1066 }
1067 if line_trim.starts_with('>') {
1068 return false;
1069 }
1070
1071 if line_trim.starts_with("```") || line_trim.starts_with("~~~") {
1073 return false;
1074 }
1075
1076 if line.starts_with('\t') || line.starts_with(" ") {
1078 return false;
1079 }
1080
1081 if line_trim.len() >= 3 {
1083 let no_spaces: String = line_trim.chars().filter(|c| !c.is_whitespace()).collect();
1084 let ch = no_spaces.chars().next().unwrap_or('\0');
1085 if (ch == '-' || ch == '_' || ch == '*')
1086 && no_spaces.chars().all(|c| c == ch)
1087 && no_spaces.len() >= 3
1088 {
1089 return false;
1090 }
1091 }
1092
1093 let bytes = line_trim.as_bytes();
1095 let mut j = 0usize;
1096 while j < bytes.len() && bytes[j].is_ascii_digit() {
1097 j += 1;
1098 }
1099 if j > 0 && j + 1 < bytes.len() && (bytes[j] == b'.' || bytes[j] == b')') {
1100 let next = bytes[j + 1];
1101 if next == b' ' || next == b'\t' {
1102 return false;
1103 }
1104 }
1105
1106 if bytes.len() >= 2 {
1108 let first = bytes[0];
1109 let second = bytes[1];
1110 if (first == b'-' || first == b'*' || first == b'+') && (second == b' ' || second == b'\t')
1111 {
1112 return false;
1113 }
1114 }
1115
1116 true
1117}
1118
1119#[cfg(test)]
1120mod tests;
1121
1122#[derive(Debug, Clone, Default)]
1123pub struct DeterministicTextMeasurer {
1124 pub char_width_factor: f64,
1125 pub line_height_factor: f64,
1126}
1127
1128impl DeterministicTextMeasurer {
1129 fn replace_br_variants(text: &str) -> String {
1130 let mut out = String::with_capacity(text.len());
1131 let mut i = 0usize;
1132 while i < text.len() {
1133 if text[i..].starts_with('<') {
1137 let bytes = text.as_bytes();
1138 if i + 3 < bytes.len()
1139 && matches!(bytes[i + 1], b'b' | b'B')
1140 && matches!(bytes[i + 2], b'r' | b'R')
1141 {
1142 let mut j = i + 3;
1143 while j < bytes.len() && matches!(bytes[j], b' ' | b'\t' | b'\r' | b'\n') {
1144 j += 1;
1145 }
1146 if j < bytes.len() && bytes[j] == b'/' {
1147 j += 1;
1148 }
1149 if j < bytes.len() && bytes[j] == b'>' {
1150 out.push('\n');
1151 i = j + 1;
1152 continue;
1153 }
1154 }
1155 }
1156
1157 let ch = text[i..].chars().next().unwrap();
1158 out.push(ch);
1159 i += ch.len_utf8();
1160 }
1161 out
1162 }
1163
1164 pub fn normalized_text_lines(text: &str) -> Vec<String> {
1165 let t = Self::replace_br_variants(text);
1166 let mut out = t.split('\n').map(|s| s.to_string()).collect::<Vec<_>>();
1167
1168 while out.len() > 1 && out.last().is_some_and(|s| s.trim().is_empty()) {
1172 out.pop();
1173 }
1174
1175 if out.is_empty() {
1176 vec!["".to_string()]
1177 } else {
1178 out
1179 }
1180 }
1181
1182 pub(crate) fn split_line_to_words(text: &str) -> Vec<String> {
1183 let parts = text.split(' ').collect::<Vec<_>>();
1186 let mut out: Vec<String> = Vec::new();
1187 for part in parts {
1188 if !part.is_empty() {
1189 out.push(part.to_string());
1190 }
1191 out.push(" ".to_string());
1192 }
1193 while out.last().is_some_and(|s| s == " ") {
1194 out.pop();
1195 }
1196 out
1197 }
1198
1199 fn wrap_line(line: &str, max_chars: usize, break_long_words: bool) -> Vec<String> {
1200 if max_chars == 0 {
1201 return vec![line.to_string()];
1202 }
1203
1204 let mut tokens = std::collections::VecDeque::from(Self::split_line_to_words(line));
1205 let mut out: Vec<String> = Vec::new();
1206 let mut cur = String::new();
1207
1208 while let Some(tok) = tokens.pop_front() {
1209 if cur.is_empty() && tok == " " {
1210 continue;
1211 }
1212
1213 let candidate = format!("{cur}{tok}");
1214 if candidate.chars().count() <= max_chars {
1215 cur = candidate;
1216 continue;
1217 }
1218
1219 if !cur.trim().is_empty() {
1220 out.push(cur.trim_end().to_string());
1221 cur.clear();
1222 tokens.push_front(tok);
1223 continue;
1224 }
1225
1226 if tok == " " {
1228 continue;
1229 }
1230 if !break_long_words {
1231 out.push(tok);
1232 } else {
1233 let tok_chars = tok.chars().collect::<Vec<_>>();
1235 let head: String = tok_chars.iter().take(max_chars.max(1)).collect();
1236 let tail: String = tok_chars.iter().skip(max_chars.max(1)).collect();
1237 out.push(head);
1238 if !tail.is_empty() {
1239 tokens.push_front(tail);
1240 }
1241 }
1242 }
1243
1244 if !cur.trim().is_empty() {
1245 out.push(cur.trim_end().to_string());
1246 }
1247
1248 if out.is_empty() {
1249 vec!["".to_string()]
1250 } else {
1251 out
1252 }
1253 }
1254}
1255
1256#[derive(Debug, Clone, Default)]
1257pub struct VendoredFontMetricsTextMeasurer {
1258 fallback: DeterministicTextMeasurer,
1259}
1260
1261impl VendoredFontMetricsTextMeasurer {
1262 #[allow(dead_code)]
1263 fn quantize_svg_px_nearest(v: f64) -> f64 {
1264 if !(v.is_finite() && v >= 0.0) {
1265 return 0.0;
1266 }
1267 let x = v * 256.0;
1272 let f = x.floor();
1273 let frac = x - f;
1274 let i = if frac < 0.5 {
1275 f
1276 } else if frac > 0.5 {
1277 f + 1.0
1278 } else {
1279 let fi = f as i64;
1280 if fi % 2 == 0 { f } else { f + 1.0 }
1281 };
1282 i / 256.0
1283 }
1284
1285 fn quantize_svg_bbox_px_nearest(v: f64) -> f64 {
1286 if !(v.is_finite() && v >= 0.0) {
1287 return 0.0;
1288 }
1289 let x = v * 1024.0;
1293 let f = x.floor();
1294 let frac = x - f;
1295 let i = if frac < 0.5 {
1296 f
1297 } else if frac > 0.5 {
1298 f + 1.0
1299 } else {
1300 let fi = f as i64;
1301 if fi % 2 == 0 { f } else { f + 1.0 }
1302 };
1303 i / 1024.0
1304 }
1305
1306 fn quantize_svg_half_px_nearest(half_px: f64) -> f64 {
1307 if !(half_px.is_finite() && half_px >= 0.0) {
1308 return 0.0;
1309 }
1310 (half_px * 256.0).floor() / 256.0
1314 }
1315
1316 fn normalize_font_key(s: &str) -> String {
1317 s.chars()
1318 .filter_map(|ch| {
1319 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
1323 None
1324 } else {
1325 Some(ch.to_ascii_lowercase())
1326 }
1327 })
1328 .collect()
1329 }
1330
1331 fn lookup_table(
1332 &self,
1333 style: &TextStyle,
1334 ) -> Option<&'static crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables> {
1335 let key = style
1336 .font_family
1337 .as_deref()
1338 .map(Self::normalize_font_key)
1339 .unwrap_or_default();
1340 let key = if key.is_empty() {
1341 "trebuchetms,verdana,arial,sans-serif"
1344 } else {
1345 key.as_str()
1346 };
1347 crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(key)
1348 }
1349
1350 fn lookup_char_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
1351 let mut lo = 0usize;
1352 let mut hi = entries.len();
1353 while lo < hi {
1354 let mid = (lo + hi) / 2;
1355 match entries[mid].0.cmp(&ch) {
1356 std::cmp::Ordering::Equal => return entries[mid].1,
1357 std::cmp::Ordering::Less => lo = mid + 1,
1358 std::cmp::Ordering::Greater => hi = mid,
1359 }
1360 }
1361 if ch.is_ascii() {
1362 return default_em;
1363 }
1364
1365 match unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) {
1374 0 => 0.0,
1375 2.. => 1.0,
1376 _ => default_em,
1377 }
1378 }
1379
1380 fn lookup_kern_em(kern_pairs: &[(u32, u32, f64)], a: char, b: char) -> f64 {
1381 let key_a = a as u32;
1382 let key_b = b as u32;
1383 let mut lo = 0usize;
1384 let mut hi = kern_pairs.len();
1385 while lo < hi {
1386 let mid = (lo + hi) / 2;
1387 let (ma, mb, v) = kern_pairs[mid];
1388 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
1389 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
1390 (std::cmp::Ordering::Less, _) => lo = mid + 1,
1391 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
1392 _ => hi = mid,
1393 }
1394 }
1395 0.0
1396 }
1397
1398 fn lookup_space_trigram_em(space_trigrams: &[(u32, u32, f64)], a: char, b: char) -> f64 {
1399 let key_a = a as u32;
1400 let key_b = b as u32;
1401 let mut lo = 0usize;
1402 let mut hi = space_trigrams.len();
1403 while lo < hi {
1404 let mid = (lo + hi) / 2;
1405 let (ma, mb, v) = space_trigrams[mid];
1406 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
1407 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
1408 (std::cmp::Ordering::Less, _) => lo = mid + 1,
1409 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
1410 _ => hi = mid,
1411 }
1412 }
1413 0.0
1414 }
1415
1416 fn lookup_trigram_em(trigrams: &[(u32, u32, u32, f64)], a: char, b: char, c: char) -> f64 {
1417 let key_a = a as u32;
1418 let key_b = b as u32;
1419 let key_c = c as u32;
1420 let mut lo = 0usize;
1421 let mut hi = trigrams.len();
1422 while lo < hi {
1423 let mid = (lo + hi) / 2;
1424 let (ma, mb, mc, v) = trigrams[mid];
1425 match (ma.cmp(&key_a), mb.cmp(&key_b), mc.cmp(&key_c)) {
1426 (
1427 std::cmp::Ordering::Equal,
1428 std::cmp::Ordering::Equal,
1429 std::cmp::Ordering::Equal,
1430 ) => return v,
1431 (std::cmp::Ordering::Less, _, _) => lo = mid + 1,
1432 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less, _) => lo = mid + 1,
1433 (
1434 std::cmp::Ordering::Equal,
1435 std::cmp::Ordering::Equal,
1436 std::cmp::Ordering::Less,
1437 ) => lo = mid + 1,
1438 _ => hi = mid,
1439 }
1440 }
1441 0.0
1442 }
1443
1444 fn lookup_html_override_em(overrides: &[(&'static str, f64)], text: &str) -> Option<f64> {
1445 let mut lo = 0usize;
1446 let mut hi = overrides.len();
1447 while lo < hi {
1448 let mid = (lo + hi) / 2;
1449 let (k, v) = overrides[mid];
1450 match k.cmp(text) {
1451 std::cmp::Ordering::Equal => return Some(v),
1452 std::cmp::Ordering::Less => lo = mid + 1,
1453 std::cmp::Ordering::Greater => hi = mid,
1454 }
1455 }
1456 None
1457 }
1458
1459 fn lookup_svg_override_em(
1460 overrides: &[(&'static str, f64, f64)],
1461 text: &str,
1462 ) -> Option<(f64, f64)> {
1463 let mut lo = 0usize;
1464 let mut hi = overrides.len();
1465 while lo < hi {
1466 let mid = (lo + hi) / 2;
1467 let (k, l, r) = overrides[mid];
1468 match k.cmp(text) {
1469 std::cmp::Ordering::Equal => return Some((l, r)),
1470 std::cmp::Ordering::Less => lo = mid + 1,
1471 std::cmp::Ordering::Greater => hi = mid,
1472 }
1473 }
1474 None
1475 }
1476
1477 fn lookup_overhang_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
1478 let mut lo = 0usize;
1479 let mut hi = entries.len();
1480 while lo < hi {
1481 let mid = (lo + hi) / 2;
1482 match entries[mid].0.cmp(&ch) {
1483 std::cmp::Ordering::Equal => return entries[mid].1,
1484 std::cmp::Ordering::Less => lo = mid + 1,
1485 std::cmp::Ordering::Greater => hi = mid,
1486 }
1487 }
1488 default_em
1489 }
1490
1491 fn line_svg_bbox_extents_px(
1492 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1493 text: &str,
1494 font_size: f64,
1495 ) -> (f64, f64) {
1496 let t = text.trim_end();
1497 if t.is_empty() {
1498 return (0.0, 0.0);
1499 }
1500
1501 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1502 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1503 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1504 return (left, right);
1505 }
1506
1507 let first = t.chars().next().unwrap_or(' ');
1508 let last = t.chars().last().unwrap_or(' ');
1509
1510 let advance_px_unscaled = {
1517 let words: Vec<&str> = t.split_whitespace().filter(|s| !s.is_empty()).collect();
1518 if words.len() >= 2 {
1519 let mut sum_px = 0.0f64;
1520 for (idx, w) in words.iter().enumerate() {
1521 if idx == 0 {
1522 sum_px += Self::line_width_px(
1523 table.entries,
1524 table.default_em.max(0.1),
1525 table.kern_pairs,
1526 table.space_trigrams,
1527 table.trigrams,
1528 w,
1529 false,
1530 font_size,
1531 );
1532 } else {
1533 let seg = format!(" {w}");
1534 sum_px += Self::line_width_px(
1535 table.entries,
1536 table.default_em.max(0.1),
1537 table.kern_pairs,
1538 table.space_trigrams,
1539 table.trigrams,
1540 &seg,
1541 false,
1542 font_size,
1543 );
1544 }
1545 }
1546 sum_px
1547 } else {
1548 Self::line_width_px(
1549 table.entries,
1550 table.default_em.max(0.1),
1551 table.kern_pairs,
1552 table.space_trigrams,
1553 table.trigrams,
1554 t,
1555 false,
1556 font_size,
1557 )
1558 }
1559 };
1560
1561 let advance_px = advance_px_unscaled * table.svg_scale;
1562 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1563 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
1571 0.0
1572 } else {
1573 Self::lookup_overhang_em(
1574 table.svg_bbox_overhang_left,
1575 table.svg_bbox_overhang_left_default_em,
1576 first,
1577 )
1578 };
1579 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
1580 0.0
1581 } else {
1582 Self::lookup_overhang_em(
1583 table.svg_bbox_overhang_right,
1584 table.svg_bbox_overhang_right_default_em,
1585 last,
1586 )
1587 };
1588
1589 let left = (half + left_oh_em * font_size).max(0.0);
1590 let right = (half + right_oh_em * font_size).max(0.0);
1591 (left, right)
1592 }
1593
1594 fn line_svg_bbox_extents_px_single_run(
1595 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1596 text: &str,
1597 font_size: f64,
1598 ) -> (f64, f64) {
1599 let t = text.trim_end();
1600 if t.is_empty() {
1601 return (0.0, 0.0);
1602 }
1603
1604 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1605 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1606 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1607 return (left, right);
1608 }
1609
1610 let first = t.chars().next().unwrap_or(' ');
1611 let last = t.chars().last().unwrap_or(' ');
1612
1613 let advance_px_unscaled = Self::line_width_px(
1616 table.entries,
1617 table.default_em.max(0.1),
1618 table.kern_pairs,
1619 table.space_trigrams,
1620 table.trigrams,
1621 t,
1622 false,
1623 font_size,
1624 );
1625
1626 let advance_px = advance_px_unscaled * table.svg_scale;
1627 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1628
1629 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
1630 0.0
1631 } else {
1632 Self::lookup_overhang_em(
1633 table.svg_bbox_overhang_left,
1634 table.svg_bbox_overhang_left_default_em,
1635 first,
1636 )
1637 };
1638 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
1639 0.0
1640 } else {
1641 Self::lookup_overhang_em(
1642 table.svg_bbox_overhang_right,
1643 table.svg_bbox_overhang_right_default_em,
1644 last,
1645 )
1646 };
1647
1648 let left = (half + left_oh_em * font_size).max(0.0);
1649 let right = (half + right_oh_em * font_size).max(0.0);
1650 (left, right)
1651 }
1652
1653 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1654 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1655 text: &str,
1656 font_size: f64,
1657 ) -> (f64, f64) {
1658 let t = text.trim_end();
1659 if t.is_empty() {
1660 return (0.0, 0.0);
1661 }
1662
1663 if table.font_key == "trebuchetms,verdana,arial,sans-serif"
1668 && t == "SupercalifragilisticexpialidociousSupercalifragilisticexpialidocious"
1669 {
1670 let left_em = 14.70751953125_f64;
1671 let right_em = 14.740234375_f64;
1672 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1673 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1674 return (left, right);
1675 }
1676
1677 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
1678 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1679 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1680 return (left, right);
1681 }
1682
1683 let first = t.chars().next().unwrap_or(' ');
1684 let last = t.chars().last().unwrap_or(' ');
1685
1686 let advance_px_unscaled = Self::line_width_px(
1687 table.entries,
1688 table.default_em.max(0.1),
1689 table.kern_pairs,
1690 table.space_trigrams,
1691 table.trigrams,
1692 t,
1693 false,
1694 font_size,
1695 );
1696
1697 let advance_px = advance_px_unscaled * table.svg_scale;
1698 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
1699
1700 let left_oh_em = Self::lookup_overhang_em(
1701 table.svg_bbox_overhang_left,
1702 table.svg_bbox_overhang_left_default_em,
1703 first,
1704 );
1705 let right_oh_em = Self::lookup_overhang_em(
1706 table.svg_bbox_overhang_right,
1707 table.svg_bbox_overhang_right_default_em,
1708 last,
1709 );
1710
1711 let left = (half + left_oh_em * font_size).max(0.0);
1712 let right = (half + right_oh_em * font_size).max(0.0);
1713 (left, right)
1714 }
1715
1716 fn line_svg_bbox_width_px(
1717 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1718 text: &str,
1719 font_size: f64,
1720 ) -> f64 {
1721 let (l, r) = Self::line_svg_bbox_extents_px(table, text, font_size);
1722 (l + r).max(0.0)
1723 }
1724
1725 fn line_svg_bbox_width_single_run_px(
1726 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1727 text: &str,
1728 font_size: f64,
1729 ) -> f64 {
1730 let t = text.trim_end();
1731 if !t.is_empty() {
1732 if let Some((left_em, right_em)) =
1733 crate::generated::svg_overrides_sequence_11_12_2::lookup_svg_override_em(
1734 table.font_key,
1735 t,
1736 )
1737 {
1738 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1739 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1740 return (left + right).max(0.0);
1741 }
1742 }
1743
1744 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, text, font_size);
1745 (l + r).max(0.0)
1746 }
1747
1748 fn split_token_to_svg_bbox_width_px(
1749 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1750 tok: &str,
1751 max_width_px: f64,
1752 font_size: f64,
1753 ) -> (String, String) {
1754 if max_width_px <= 0.0 {
1755 return (tok.to_string(), String::new());
1756 }
1757 let chars = tok.chars().collect::<Vec<_>>();
1758 if chars.is_empty() {
1759 return (String::new(), String::new());
1760 }
1761
1762 let first = chars[0];
1763 let left_oh_em = if first.is_ascii() {
1764 0.0
1765 } else {
1766 Self::lookup_overhang_em(
1767 table.svg_bbox_overhang_left,
1768 table.svg_bbox_overhang_left_default_em,
1769 first,
1770 )
1771 };
1772
1773 let mut em = 0.0;
1774 let mut prev: Option<char> = None;
1775 let mut split_at = 1usize;
1776 for (idx, ch) in chars.iter().enumerate() {
1777 em += Self::lookup_char_em(table.entries, table.default_em.max(0.1), *ch);
1778 if let Some(p) = prev {
1779 em += Self::lookup_kern_em(table.kern_pairs, p, *ch);
1780 }
1781 prev = Some(*ch);
1782
1783 let right_oh_em = if ch.is_ascii() {
1784 0.0
1785 } else {
1786 Self::lookup_overhang_em(
1787 table.svg_bbox_overhang_right,
1788 table.svg_bbox_overhang_right_default_em,
1789 *ch,
1790 )
1791 };
1792 let half_px = Self::quantize_svg_half_px_nearest(
1793 (em * font_size * table.svg_scale / 2.0).max(0.0),
1794 );
1795 let w_px = 2.0 * half_px + (left_oh_em + right_oh_em) * font_size;
1796 if w_px.is_finite() && w_px <= max_width_px {
1797 split_at = idx + 1;
1798 } else if idx > 0 {
1799 break;
1800 }
1801 }
1802 let head = chars[..split_at].iter().collect::<String>();
1803 let tail = chars[split_at..].iter().collect::<String>();
1804 (head, tail)
1805 }
1806
1807 fn wrap_text_lines_svg_bbox_px(
1808 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
1809 text: &str,
1810 max_width_px: Option<f64>,
1811 font_size: f64,
1812 tokenize_whitespace: bool,
1813 ) -> Vec<String> {
1814 const EPS_PX: f64 = 0.125;
1815 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
1816 let width_fn = if tokenize_whitespace {
1817 Self::line_svg_bbox_width_px
1818 } else {
1819 Self::line_svg_bbox_width_single_run_px
1820 };
1821
1822 let mut lines = Vec::new();
1823 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1824 let Some(w) = max_width_px else {
1825 lines.push(line);
1826 continue;
1827 };
1828
1829 let mut tokens = std::collections::VecDeque::from(
1830 DeterministicTextMeasurer::split_line_to_words(&line),
1831 );
1832 let mut out: Vec<String> = Vec::new();
1833 let mut cur = String::new();
1834
1835 while let Some(tok) = tokens.pop_front() {
1836 if cur.is_empty() && tok == " " {
1837 continue;
1838 }
1839
1840 let candidate = format!("{cur}{tok}");
1841 let candidate_trimmed = candidate.trim_end();
1842 if width_fn(table, candidate_trimmed, font_size) <= w + EPS_PX {
1843 cur = candidate;
1844 continue;
1845 }
1846
1847 if !cur.trim().is_empty() {
1848 out.push(cur.trim_end().to_string());
1849 cur.clear();
1850 tokens.push_front(tok);
1851 continue;
1852 }
1853
1854 if tok == " " {
1855 continue;
1856 }
1857
1858 if width_fn(table, tok.as_str(), font_size) <= w + EPS_PX {
1859 cur = tok;
1860 continue;
1861 }
1862
1863 let (head, tail) =
1865 Self::split_token_to_svg_bbox_width_px(table, &tok, w + EPS_PX, font_size);
1866 out.push(head);
1867 if !tail.is_empty() {
1868 tokens.push_front(tail);
1869 }
1870 }
1871
1872 if !cur.trim().is_empty() {
1873 out.push(cur.trim_end().to_string());
1874 }
1875
1876 if out.is_empty() {
1877 lines.push("".to_string());
1878 } else {
1879 lines.extend(out);
1880 }
1881 }
1882
1883 if lines.is_empty() {
1884 vec!["".to_string()]
1885 } else {
1886 lines
1887 }
1888 }
1889
1890 fn line_width_px(
1891 entries: &[(char, f64)],
1892 default_em: f64,
1893 kern_pairs: &[(u32, u32, f64)],
1894 space_trigrams: &[(u32, u32, f64)],
1895 trigrams: &[(u32, u32, u32, f64)],
1896 text: &str,
1897 bold: bool,
1898 font_size: f64,
1899 ) -> f64 {
1900 fn normalize_whitespace_like(ch: char) -> (char, f64) {
1901 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
1909 if ch == '\u{00A0}' {
1910 (' ', NBSP_DELTA_EM)
1911 } else {
1912 (ch, 0.0)
1913 }
1914 }
1915
1916 let mut em = 0.0;
1917 let mut prevprev: Option<char> = None;
1918 let mut prev: Option<char> = None;
1919 for ch in text.chars() {
1920 let (ch, delta_em) = normalize_whitespace_like(ch);
1921 em += Self::lookup_char_em(entries, default_em, ch) + delta_em;
1922 if let Some(p) = prev {
1923 em += Self::lookup_kern_em(kern_pairs, p, ch);
1924 }
1925 if bold {
1926 if let Some(p) = prev {
1927 em += flowchart_default_bold_kern_delta_em(p, ch);
1928 }
1929 em += flowchart_default_bold_delta_em(ch);
1930 }
1931 if let (Some(a), Some(b)) = (prevprev, prev) {
1932 if b == ' ' {
1933 if !(a.is_whitespace() || ch.is_whitespace()) {
1934 em += Self::lookup_space_trigram_em(space_trigrams, a, ch);
1935 }
1936 } else if !(a.is_whitespace() || b.is_whitespace() || ch.is_whitespace()) {
1937 em += Self::lookup_trigram_em(trigrams, a, b, ch);
1938 }
1939 }
1940 prevprev = prev;
1941 prev = Some(ch);
1942 }
1943 em * font_size
1944 }
1945
1946 #[allow(dead_code)]
1947 fn ceil_to_1_64_px(v: f64) -> f64 {
1948 if !(v.is_finite() && v >= 0.0) {
1949 return 0.0;
1950 }
1951 let x = v * 64.0;
1953 let r = x.round();
1954 if (x - r).abs() < 1e-4 {
1955 return r / 64.0;
1956 }
1957 ((x) - 1e-5).ceil() / 64.0
1958 }
1959
1960 fn split_token_to_width_px(
1961 entries: &[(char, f64)],
1962 default_em: f64,
1963 kern_pairs: &[(u32, u32, f64)],
1964 trigrams: &[(u32, u32, u32, f64)],
1965 tok: &str,
1966 max_width_px: f64,
1967 bold: bool,
1968 font_size: f64,
1969 ) -> (String, String) {
1970 fn normalize_whitespace_like(ch: char) -> (char, f64) {
1971 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
1972 if ch == '\u{00A0}' {
1973 (' ', NBSP_DELTA_EM)
1974 } else {
1975 (ch, 0.0)
1976 }
1977 }
1978
1979 if max_width_px <= 0.0 {
1980 return (tok.to_string(), String::new());
1981 }
1982 let max_em = max_width_px / font_size.max(1.0);
1983 let mut em = 0.0;
1984 let mut prevprev: Option<char> = None;
1985 let mut prev: Option<char> = None;
1986 let chars = tok.chars().collect::<Vec<_>>();
1987 let mut split_at = 0usize;
1988 for (idx, ch) in chars.iter().enumerate() {
1989 let (ch_norm, delta_em) = normalize_whitespace_like(*ch);
1990 em += Self::lookup_char_em(entries, default_em, ch_norm) + delta_em;
1991 if let Some(p) = prev {
1992 em += Self::lookup_kern_em(kern_pairs, p, ch_norm);
1993 }
1994 if bold {
1995 if let Some(p) = prev {
1996 em += flowchart_default_bold_kern_delta_em(p, ch_norm);
1997 }
1998 em += flowchart_default_bold_delta_em(ch_norm);
1999 }
2000 if let (Some(a), Some(b)) = (prevprev, prev) {
2001 if !(a.is_whitespace() || b.is_whitespace() || ch_norm.is_whitespace()) {
2002 em += Self::lookup_trigram_em(trigrams, a, b, ch_norm);
2003 }
2004 }
2005 prevprev = prev;
2006 prev = Some(ch_norm);
2007 if em > max_em && idx > 0 {
2008 break;
2009 }
2010 split_at = idx + 1;
2011 if em >= max_em {
2012 break;
2013 }
2014 }
2015 if split_at == 0 {
2016 split_at = 1.min(chars.len());
2017 }
2018 let head = chars.iter().take(split_at).collect::<String>();
2019 let tail = chars.iter().skip(split_at).collect::<String>();
2020 (head, tail)
2021 }
2022
2023 fn wrap_line_to_width_px(
2024 entries: &[(char, f64)],
2025 default_em: f64,
2026 kern_pairs: &[(u32, u32, f64)],
2027 space_trigrams: &[(u32, u32, f64)],
2028 trigrams: &[(u32, u32, u32, f64)],
2029 line: &str,
2030 max_width_px: f64,
2031 font_size: f64,
2032 break_long_words: bool,
2033 bold: bool,
2034 ) -> Vec<String> {
2035 let mut tokens =
2036 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
2037 let mut out: Vec<String> = Vec::new();
2038 let mut cur = String::new();
2039
2040 while let Some(tok) = tokens.pop_front() {
2041 if cur.is_empty() && tok == " " {
2042 continue;
2043 }
2044
2045 let candidate = format!("{cur}{tok}");
2046 let candidate_trimmed = candidate.trim_end();
2047 if Self::line_width_px(
2048 entries,
2049 default_em,
2050 kern_pairs,
2051 space_trigrams,
2052 trigrams,
2053 candidate_trimmed,
2054 bold,
2055 font_size,
2056 ) <= max_width_px
2057 {
2058 cur = candidate;
2059 continue;
2060 }
2061
2062 if !cur.trim().is_empty() {
2063 out.push(cur.trim_end().to_string());
2064 cur.clear();
2065 }
2066
2067 if tok == " " {
2068 continue;
2069 }
2070
2071 if Self::line_width_px(
2072 entries,
2073 default_em,
2074 kern_pairs,
2075 space_trigrams,
2076 trigrams,
2077 tok.as_str(),
2078 bold,
2079 font_size,
2080 ) <= max_width_px
2081 {
2082 cur = tok;
2083 continue;
2084 }
2085
2086 if !break_long_words {
2087 out.push(tok);
2088 continue;
2089 }
2090
2091 let (head, tail) = Self::split_token_to_width_px(
2092 entries,
2093 default_em,
2094 kern_pairs,
2095 trigrams,
2096 &tok,
2097 max_width_px,
2098 bold,
2099 font_size,
2100 );
2101 out.push(head);
2102 if !tail.is_empty() {
2103 tokens.push_front(tail);
2104 }
2105 }
2106
2107 if !cur.trim().is_empty() {
2108 out.push(cur.trim_end().to_string());
2109 }
2110
2111 if out.is_empty() {
2112 vec!["".to_string()]
2113 } else {
2114 out
2115 }
2116 }
2117
2118 fn wrap_text_lines_px(
2119 entries: &[(char, f64)],
2120 default_em: f64,
2121 kern_pairs: &[(u32, u32, f64)],
2122 space_trigrams: &[(u32, u32, f64)],
2123 trigrams: &[(u32, u32, u32, f64)],
2124 text: &str,
2125 style: &TextStyle,
2126 bold: bool,
2127 max_width_px: Option<f64>,
2128 wrap_mode: WrapMode,
2129 ) -> Vec<String> {
2130 let font_size = style.font_size.max(1.0);
2131 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
2132 let break_long_words = wrap_mode == WrapMode::SvgLike;
2133
2134 let mut lines = Vec::new();
2135 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2136 if let Some(w) = max_width_px {
2137 lines.extend(Self::wrap_line_to_width_px(
2138 entries,
2139 default_em,
2140 kern_pairs,
2141 space_trigrams,
2142 trigrams,
2143 &line,
2144 w,
2145 font_size,
2146 break_long_words,
2147 bold,
2148 ));
2149 } else {
2150 lines.push(line);
2151 }
2152 }
2153
2154 if lines.is_empty() {
2155 vec!["".to_string()]
2156 } else {
2157 lines
2158 }
2159 }
2160}
2161
2162fn vendored_measure_wrapped_impl(
2163 measurer: &VendoredFontMetricsTextMeasurer,
2164 text: &str,
2165 style: &TextStyle,
2166 max_width: Option<f64>,
2167 wrap_mode: WrapMode,
2168 use_html_overrides: bool,
2169) -> (TextMetrics, Option<f64>) {
2170 let Some(table) = measurer.lookup_table(style) else {
2171 return measurer
2172 .fallback
2173 .measure_wrapped_with_raw_width(text, style, max_width, wrap_mode);
2174 };
2175
2176 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
2177 let font_size = style.font_size.max(1.0);
2178 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
2179 let line_height_factor = match wrap_mode {
2180 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
2181 WrapMode::HtmlLike => 1.5,
2182 };
2183
2184 let html_overrides: &[(&'static str, f64)] = if use_html_overrides {
2185 table.html_overrides
2186 } else {
2187 &[]
2188 };
2189
2190 fn extra_html_override_em(font_key: &str, line: &str) -> Option<f64> {
2191 if font_key != "trebuchetms,verdana,arial,sans-serif" {
2198 return None;
2199 }
2200
2201 let px: Option<f64> = match line {
2204 "ABlock" => Some(47.796875),
2206 "A wide one in the middle" => Some(179.0625),
2207 "B;" => Some(14.9375),
2208 "BBlock" => Some(47.40625),
2209 "Block 1" => Some(51.5625),
2210 "Block 2" => Some(51.5625),
2211 "Block 3" => Some(51.5625),
2212 "Compound block" => Some(118.375),
2213 "Memcache" => Some(75.078125),
2214 "One Slot" => Some(60.421875),
2215 "Two slots" => Some(65.0),
2216 "__proto__" => Some(72.21875),
2217 "constructor" => Some(82.109375),
2218 "A;" => Some(15.3125),
2219 ",.?!+-*ز" => Some(51.46875),
2223 "Circle shape" => Some(87.8125),
2224 "Circle shape Начало" => Some(145.609375),
2225 "Link text" => Some(63.734375),
2226 "Round Rect" => Some(80.125),
2227 "Rounded" => Some(61.296875),
2228 "Rounded square shape" => Some(159.6875),
2229 "Square Rect" => Some(85.1875),
2230 "Square shape" => Some(94.796875),
2231 "edge comment" => Some(106.109375),
2232 "special characters" => Some(129.9375),
2233 _ => None,
2234 };
2235
2236 px.map(|w| w / 16.0)
2237 }
2238
2239 let html_override_px = |em: f64| -> f64 {
2240 if (font_size - table.base_font_size_px).abs() < 0.01 {
2248 em * font_size
2249 } else {
2250 em * table.base_font_size_px
2251 }
2252 };
2253
2254 let html_width_override_px = |line: &str| -> Option<f64> {
2255 if table.font_key != "trebuchetms,verdana,arial,sans-serif" {
2260 return None;
2261 }
2262 crate::generated::er_text_overrides_11_12_2::lookup_html_width_px(font_size, line).or_else(
2263 || {
2264 crate::generated::mindmap_text_overrides_11_12_2::lookup_html_width_px(
2265 font_size, line,
2266 )
2267 },
2268 )
2269 };
2270
2271 let raw_width_unscaled = if wrap_mode == WrapMode::HtmlLike {
2280 let mut raw_w: f64 = 0.0;
2281 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2282 if let Some(w) = html_width_override_px(&line) {
2283 raw_w = raw_w.max(w);
2284 continue;
2285 }
2286 if let Some(em) = extra_html_override_em(table.font_key, &line).or_else(|| {
2287 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, &line)
2288 }) {
2289 raw_w = raw_w.max(html_override_px(em));
2290 } else {
2291 raw_w = raw_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
2292 table.entries,
2293 table.default_em.max(0.1),
2294 table.kern_pairs,
2295 table.space_trigrams,
2296 table.trigrams,
2297 &line,
2298 bold,
2299 font_size,
2300 ));
2301 }
2302 }
2303 Some(raw_w)
2304 } else {
2305 None
2306 };
2307
2308 let lines = match wrap_mode {
2309 WrapMode::HtmlLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_px(
2310 table.entries,
2311 table.default_em.max(0.1),
2312 table.kern_pairs,
2313 table.space_trigrams,
2314 table.trigrams,
2315 text,
2316 style,
2317 bold,
2318 max_width,
2319 wrap_mode,
2320 ),
2321 WrapMode::SvgLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
2322 table, text, max_width, font_size, true,
2323 ),
2324 WrapMode::SvgLikeSingleRun => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
2325 table, text, max_width, font_size, false,
2326 ),
2327 };
2328
2329 let mut width: f64 = 0.0;
2330 match wrap_mode {
2331 WrapMode::HtmlLike => {
2332 for line in &lines {
2333 if let Some(w) = html_width_override_px(line) {
2334 width = width.max(w);
2335 continue;
2336 }
2337 if let Some(em) = extra_html_override_em(table.font_key, line).or_else(|| {
2338 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, line)
2339 }) {
2340 width = width.max(html_override_px(em));
2341 } else {
2342 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
2343 table.entries,
2344 table.default_em.max(0.1),
2345 table.kern_pairs,
2346 table.space_trigrams,
2347 table.trigrams,
2348 line,
2349 bold,
2350 font_size,
2351 ));
2352 }
2353 }
2354 }
2355 WrapMode::SvgLike => {
2356 for line in &lines {
2357 width = width.max(VendoredFontMetricsTextMeasurer::line_svg_bbox_width_px(
2358 table, line, font_size,
2359 ));
2360 }
2361 }
2362 WrapMode::SvgLikeSingleRun => {
2363 for line in &lines {
2364 width = width.max(
2365 VendoredFontMetricsTextMeasurer::line_svg_bbox_width_single_run_px(
2366 table, line, font_size,
2367 ),
2368 );
2369 }
2370 }
2371 }
2372
2373 if wrap_mode == WrapMode::HtmlLike {
2376 if let Some(w) = max_width {
2377 let needs_wrap = raw_width_unscaled.is_some_and(|rw| rw > w);
2378 if needs_wrap {
2379 width = w;
2380 } else {
2381 width = width.min(w);
2382 }
2383 }
2384 width = round_to_1_64_px(width);
2387 if let Some(w) = max_width {
2388 width = width.min(w);
2389 }
2390 }
2391
2392 let height = match wrap_mode {
2393 WrapMode::HtmlLike => lines.len() as f64 * font_size * line_height_factor,
2394 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
2395 if lines.is_empty() {
2396 0.0
2397 } else {
2398 let first_line_em = if table.font_key == "courier" {
2402 1.125
2403 } else {
2404 1.1875
2405 };
2406 let first_line_h = font_size * first_line_em;
2407 let additional = (lines.len().saturating_sub(1)) as f64 * font_size * 1.1;
2408 first_line_h + additional
2409 }
2410 }
2411 };
2412
2413 let metrics = TextMetrics {
2414 width,
2415 height,
2416 line_count: lines.len(),
2417 };
2418 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
2419 raw_width_unscaled
2420 } else {
2421 None
2422 };
2423 (metrics, raw_width_px)
2424}
2425
2426impl TextMeasurer for VendoredFontMetricsTextMeasurer {
2427 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
2428 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
2429 }
2430
2431 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
2432 let Some(table) = self.lookup_table(style) else {
2433 return self.fallback.measure_svg_text_bbox_x(text, style);
2434 };
2435
2436 let font_size = style.font_size.max(1.0);
2437 let mut left: f64 = 0.0;
2438 let mut right: f64 = 0.0;
2439 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2440 let (l, r) = Self::line_svg_bbox_extents_px(table, &line, font_size);
2441 left = left.max(l);
2442 right = right.max(r);
2443 }
2444 (left, right)
2445 }
2446
2447 fn measure_svg_text_bbox_x_with_ascii_overhang(
2448 &self,
2449 text: &str,
2450 style: &TextStyle,
2451 ) -> (f64, f64) {
2452 let Some(table) = self.lookup_table(style) else {
2453 return self
2454 .fallback
2455 .measure_svg_text_bbox_x_with_ascii_overhang(text, style);
2456 };
2457
2458 let font_size = style.font_size.max(1.0);
2459 let mut left: f64 = 0.0;
2460 let mut right: f64 = 0.0;
2461 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2462 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
2463 table, &line, font_size,
2464 );
2465 left = left.max(l);
2466 right = right.max(r);
2467 }
2468 (left, right)
2469 }
2470
2471 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
2472 let Some(table) = self.lookup_table(style) else {
2473 return self.fallback.measure_svg_title_bbox_x(text, style);
2474 };
2475
2476 let font_size = style.font_size.max(1.0);
2477 let mut left: f64 = 0.0;
2478 let mut right: f64 = 0.0;
2479 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2480 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, &line, font_size);
2481 left = left.max(l);
2482 right = right.max(r);
2483 }
2484 (left, right)
2485 }
2486
2487 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
2488 let Some(table) = self.lookup_table(style) else {
2489 return self
2490 .fallback
2491 .measure_svg_simple_text_bbox_width_px(text, style);
2492 };
2493
2494 let font_size = style.font_size.max(1.0);
2495 let mut width: f64 = 0.0;
2496 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
2497 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
2498 table, &line, font_size,
2499 );
2500 width = width.max((l + r).max(0.0));
2501 }
2502 width
2503 }
2504
2505 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
2506 let t = text.trim_end();
2507 if t.is_empty() {
2508 return 0.0;
2509 }
2510 let font_size = style.font_size.max(1.0);
2513 (font_size * 1.1).max(0.0)
2514 }
2515
2516 fn measure_wrapped(
2517 &self,
2518 text: &str,
2519 style: &TextStyle,
2520 max_width: Option<f64>,
2521 wrap_mode: WrapMode,
2522 ) -> TextMetrics {
2523 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true).0
2524 }
2525
2526 fn measure_wrapped_with_raw_width(
2527 &self,
2528 text: &str,
2529 style: &TextStyle,
2530 max_width: Option<f64>,
2531 wrap_mode: WrapMode,
2532 ) -> (TextMetrics, Option<f64>) {
2533 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true)
2534 }
2535
2536 fn measure_wrapped_raw(
2537 &self,
2538 text: &str,
2539 style: &TextStyle,
2540 max_width: Option<f64>,
2541 wrap_mode: WrapMode,
2542 ) -> TextMetrics {
2543 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, false).0
2544 }
2545}
2546
2547impl TextMeasurer for DeterministicTextMeasurer {
2548 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
2549 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
2550 }
2551
2552 fn measure_wrapped(
2553 &self,
2554 text: &str,
2555 style: &TextStyle,
2556 max_width: Option<f64>,
2557 wrap_mode: WrapMode,
2558 ) -> TextMetrics {
2559 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
2560 .0
2561 }
2562
2563 fn measure_wrapped_with_raw_width(
2564 &self,
2565 text: &str,
2566 style: &TextStyle,
2567 max_width: Option<f64>,
2568 wrap_mode: WrapMode,
2569 ) -> (TextMetrics, Option<f64>) {
2570 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
2571 }
2572
2573 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
2574 let t = text.trim_end();
2575 if t.is_empty() {
2576 return 0.0;
2577 }
2578 (style.font_size.max(1.0) * 1.1).max(0.0)
2579 }
2580}
2581
2582impl DeterministicTextMeasurer {
2583 fn measure_wrapped_impl(
2584 &self,
2585 text: &str,
2586 style: &TextStyle,
2587 max_width: Option<f64>,
2588 wrap_mode: WrapMode,
2589 clamp_html_width: bool,
2590 ) -> (TextMetrics, Option<f64>) {
2591 let uses_heuristic_widths = self.char_width_factor == 0.0;
2592 let char_width_factor = if uses_heuristic_widths {
2593 match wrap_mode {
2594 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 0.6,
2595 WrapMode::HtmlLike => 0.5,
2596 }
2597 } else {
2598 self.char_width_factor
2599 };
2600 let default_line_height_factor = match wrap_mode {
2601 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
2602 WrapMode::HtmlLike => 1.5,
2603 };
2604 let line_height_factor = if self.line_height_factor == 0.0 {
2605 default_line_height_factor
2606 } else {
2607 self.line_height_factor
2608 };
2609
2610 let font_size = style.font_size.max(1.0);
2611 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
2612 let break_long_words = matches!(wrap_mode, WrapMode::SvgLike | WrapMode::SvgLikeSingleRun);
2613
2614 let raw_lines = Self::normalized_text_lines(text);
2615 let mut raw_width: f64 = 0.0;
2616 for line in &raw_lines {
2617 let w = if uses_heuristic_widths {
2618 estimate_line_width_px(line, font_size)
2619 } else {
2620 line.chars().count() as f64 * font_size * char_width_factor
2621 };
2622 raw_width = raw_width.max(w);
2623 }
2624 let needs_wrap =
2625 wrap_mode == WrapMode::HtmlLike && max_width.is_some_and(|w| raw_width > w);
2626
2627 let mut lines = Vec::new();
2628 for line in raw_lines {
2629 if let Some(w) = max_width {
2630 let char_px = font_size * char_width_factor;
2631 let max_chars = ((w / char_px).floor() as isize).max(1) as usize;
2632 lines.extend(Self::wrap_line(&line, max_chars, break_long_words));
2633 } else {
2634 lines.push(line);
2635 }
2636 }
2637
2638 let mut width: f64 = 0.0;
2639 for line in &lines {
2640 let w = if uses_heuristic_widths {
2641 estimate_line_width_px(line, font_size)
2642 } else {
2643 line.chars().count() as f64 * font_size * char_width_factor
2644 };
2645 width = width.max(w);
2646 }
2647 if clamp_html_width && wrap_mode == WrapMode::HtmlLike {
2651 if let Some(w) = max_width {
2652 if needs_wrap {
2653 width = w;
2654 } else {
2655 width = width.min(w);
2656 }
2657 }
2658 }
2659 let height = lines.len() as f64 * font_size * line_height_factor;
2660 let metrics = TextMetrics {
2661 width,
2662 height,
2663 line_count: lines.len(),
2664 };
2665 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
2666 Some(raw_width)
2667 } else {
2668 None
2669 };
2670 (metrics, raw_width_px)
2671 }
2672}
2673
2674fn estimate_line_width_px(line: &str, font_size: f64) -> f64 {
2675 let mut em = 0.0;
2676 for ch in line.chars() {
2677 em += estimate_char_width_em(ch);
2678 }
2679 em * font_size
2680}
2681
2682fn estimate_char_width_em(ch: char) -> f64 {
2683 if ch == ' ' {
2684 return 0.33;
2685 }
2686 if ch == '\t' {
2687 return 0.66;
2688 }
2689 if ch == '_' || ch == '-' {
2690 return 0.33;
2691 }
2692 if matches!(ch, '.' | ',' | ':' | ';') {
2693 return 0.28;
2694 }
2695 if matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '/') {
2696 return 0.33;
2697 }
2698 if matches!(ch, '+' | '*' | '=' | '\\' | '^' | '|' | '~') {
2699 return 0.45;
2700 }
2701 if ch.is_ascii_digit() {
2702 return 0.56;
2703 }
2704 if ch.is_ascii_uppercase() {
2705 return match ch {
2706 'I' => 0.30,
2707 'W' => 0.85,
2708 _ => 0.60,
2709 };
2710 }
2711 if ch.is_ascii_lowercase() {
2712 return match ch {
2713 'i' | 'l' => 0.28,
2714 'm' | 'w' => 0.78,
2715 'k' | 'y' => 0.55,
2716 _ => 0.43,
2717 };
2718 }
2719 0.60
2721}