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();
73 if bytes.is_empty() {
74 return;
75 }
76 let leading_ws = matches!(bytes.first(), Some(b' ' | b'\t'));
77 let trailing_ws = matches!(bytes.last(), Some(b' ' | b'\t'));
78 if !(leading_ws && trailing_ws) {
79 return;
80 }
81
82 let line_h = flowchart_html_line_height_px(style.font_size);
83 metrics.height += 2.0 * line_h;
84 metrics.line_count = metrics.line_count.saturating_add(2);
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
125const FLOWCHART_DEFAULT_FONT_KEY: &str = "trebuchetms,verdana,arial,sans-serif";
126const SVG_DEFAULT_FIRST_LINE_BBOX_EM: f64 = 1.1875;
127const SVG_COURIER_FIRST_LINE_BBOX_EM: f64 = 1.125;
128const SVG_DEFAULT_TITLE_ASCENT_EM: f64 = 0.9444444444;
129const SVG_DEFAULT_TITLE_DESCENT_EM: f64 = 0.262;
130const SVG_COURIER_TITLE_ASCENT_EM: f64 = 0.8333333333333334;
131const SVG_COURIER_TITLE_DESCENT_EM: f64 = 0.25;
132
133pub(crate) fn font_key_uses_courier_metrics(font_key: &str) -> bool {
134 font_key
135 .split(',')
136 .any(|token| matches!(token, "courier" | "couriernew") || token.contains("monospace"))
137}
138
139pub(crate) fn style_uses_courier_metrics(style: &TextStyle) -> bool {
140 style
141 .font_family
142 .as_deref()
143 .map(normalize_font_key)
144 .is_some_and(|font_key| font_key_uses_courier_metrics(&font_key))
145}
146
147pub(crate) fn svg_bbox_round_px_ties_to_even(v: f64) -> f64 {
148 if !v.is_finite() {
149 return 0.0;
150 }
151 let floor = v.floor();
152 let frac = v - floor;
153 if frac < 0.5 {
154 floor
155 } else if frac > 0.5 {
156 floor + 1.0
157 } else if (floor as i64) % 2 == 0 {
158 floor
159 } else {
160 floor + 1.0
161 }
162}
163
164pub(crate) fn svg_wrapped_first_line_bbox_height_px(style: &TextStyle) -> f64 {
165 let first_line_em = if style_uses_courier_metrics(style) {
166 SVG_COURIER_FIRST_LINE_BBOX_EM
167 } else {
168 SVG_DEFAULT_FIRST_LINE_BBOX_EM
169 };
170 svg_bbox_round_px_ties_to_even(style.font_size.max(1.0) * first_line_em)
171}
172
173pub(crate) fn flowchart_svg_edge_label_background_y_px(style: &TextStyle) -> f64 {
174 let baseline_box_h =
175 svg_bbox_round_px_ties_to_even(style.font_size.max(1.0) * SVG_COURIER_FIRST_LINE_BBOX_EM);
176 baseline_box_h - svg_wrapped_first_line_bbox_height_px(style)
177}
178
179pub(crate) fn svg_title_bbox_vertical_extents_px(style: &TextStyle) -> (f64, f64) {
180 let font_size = style.font_size.max(1.0);
181 let (ascent_em, descent_em) = if style_uses_courier_metrics(style) {
182 (SVG_COURIER_TITLE_ASCENT_EM, SVG_COURIER_TITLE_DESCENT_EM)
183 } else {
184 (SVG_DEFAULT_TITLE_ASCENT_EM, SVG_DEFAULT_TITLE_DESCENT_EM)
185 };
186 (font_size * ascent_em, font_size * descent_em)
187}
188
189pub(crate) fn svg_create_text_bbox_y_offset_px(style: &TextStyle) -> f64 {
190 round_to_1_64_px(style.font_size.max(1.0) / 16.0)
191}
192
193pub fn flowchart_html_has_inline_style_tags(lower_html: &str) -> bool {
194 let bytes = lower_html.as_bytes();
200 let mut i = 0usize;
201 while i < bytes.len() {
202 if bytes[i] != b'<' {
203 i += 1;
204 continue;
205 }
206 i += 1;
207 if i >= bytes.len() {
208 break;
209 }
210 if bytes[i] == b'!' || bytes[i] == b'?' {
211 continue;
212 }
213 if bytes[i] == b'/' {
214 i += 1;
215 }
216 let start = i;
217 while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
218 i += 1;
219 }
220 if start == i {
221 continue;
222 }
223 let name = &lower_html[start..i];
224 if matches!(name, "strong" | "b" | "em" | "i") {
225 return true;
226 }
227 }
228 false
229}
230
231fn is_flowchart_default_font(style: &TextStyle) -> bool {
232 let Some(f) = style.font_family.as_deref() else {
233 return false;
234 };
235 normalize_font_key(f) == FLOWCHART_DEFAULT_FONT_KEY
236}
237
238fn style_requests_bold_font_weight(style: &TextStyle) -> bool {
239 let Some(w) = style.font_weight.as_deref() else {
240 return false;
241 };
242 let w = w.trim();
243 if w.is_empty() {
244 return false;
245 }
246 let lower = w.to_ascii_lowercase();
247 if lower == "bold" || lower == "bolder" {
248 return true;
249 }
250 lower.parse::<i32>().ok().is_some_and(|n| n >= 600)
251}
252
253fn flowchart_default_bold_delta_em(ch: char) -> f64 {
254 match ch {
257 '"' => 0.0419921875,
258 '#' => 0.0615234375,
259 '$' => 0.0615234375,
260 '%' => 0.083984375,
261 '\'' => 0.06982421875,
262 '*' => 0.06494140625,
263 '+' => 0.0615234375,
264 '/' => -0.13427734375,
265 '0' => 0.0615234375,
266 '1' => 0.0615234375,
267 '2' => 0.0615234375,
268 '3' => 0.0615234375,
269 '4' => 0.0615234375,
270 '5' => 0.0615234375,
271 '6' => 0.0615234375,
272 '7' => 0.0615234375,
273 '8' => 0.0615234375,
274 '9' => 0.0615234375,
275 '<' => 0.0615234375,
276 '=' => 0.0615234375,
277 '>' => 0.0615234375,
278 '?' => 0.07080078125,
279 'A' => 0.04345703125,
280 'B' => 0.029296875,
281 'C' => 0.013671875,
282 'D' => 0.029296875,
283 'E' => 0.033203125,
284 'F' => 0.05859375,
285 'G' => -0.0048828125,
286 'H' => 0.029296875,
287 'J' => 0.05615234375,
288 'K' => 0.04150390625,
289 'L' => 0.04638671875,
290 'M' => 0.03564453125,
291 'N' => 0.029296875,
292 'O' => 0.029296875,
293 'P' => 0.029296875,
294 'Q' => 0.033203125,
295 'R' => 0.02880859375,
296 'S' => 0.0302734375,
297 'T' => 0.03125,
298 'U' => 0.029296875,
299 'V' => 0.0341796875,
300 'W' => 0.03173828125,
301 'X' => 0.0439453125,
302 'Y' => 0.04296875,
303 'Z' => 0.009765625,
304 '[' => 0.03466796875,
305 ']' => 0.03466796875,
306 '^' => 0.0615234375,
307 '_' => 0.0615234375,
308 '`' => 0.0615234375,
309 'a' => 0.00732421875,
310 'b' => 0.0244140625,
311 'c' => 0.0166015625,
312 'd' => 0.0234375,
313 'e' => 0.029296875,
314 'h' => 0.04638671875,
315 'i' => 0.01318359375,
316 'k' => 0.04345703125,
317 'm' => 0.029296875,
318 'n' => 0.0439453125,
319 'o' => 0.029296875,
320 'p' => 0.025390625,
321 'q' => 0.02685546875,
322 'r' => 0.03857421875,
323 's' => 0.02587890625,
324 'u' => 0.04443359375,
325 'v' => 0.03759765625,
326 'w' => 0.03955078125,
327 'x' => 0.05126953125,
328 'y' => 0.04052734375,
329 'z' => 0.0537109375,
330 '{' => 0.06640625,
331 '|' => 0.0615234375,
332 '}' => 0.06640625,
333 '~' => 0.0615234375,
334 _ => 0.0,
335 }
336}
337
338fn flowchart_default_bold_kern_delta_em(prev: char, next: char) -> f64 {
339 match (prev, next) {
346 ('T', 'w') => 0.0576171875,
350 _ => 0.0,
351 }
352}
353
354fn flowchart_default_italic_delta_em(ch: char, wrap_mode: WrapMode) -> f64 {
355 let delta_em: f64 = match wrap_mode {
361 WrapMode::HtmlLike => 1.0 / 128.0,
362 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 5.0 / 512.0,
363 };
364 match ch {
365 'A'..='Z' | 'a'..='z' | '0'..='9' => delta_em,
366 _ => 0.0,
367 }
368}
369
370pub fn mermaid_default_italic_width_delta_px(text: &str, style: &TextStyle) -> f64 {
371 if !is_flowchart_default_font(style) {
379 return 0.0;
380 }
381
382 let font_size = style.font_size.max(1.0);
383 let bold = style_requests_bold_font_weight(style);
384 let per_char_em = if bold {
385 1.0 / 64.0
387 } else {
388 37.0 / 3072.0
392 };
393
394 let mut max_em: f64 = 0.0;
395 for line in text.lines() {
396 let mut em: f64 = 0.0;
397 for ch in line.chars() {
398 match ch {
399 'A'..='Z' | 'a'..='z' | '0'..='9' => em += per_char_em,
400 _ => {}
401 }
402 }
403 max_em = max_em.max(em);
404 }
405
406 (max_em * font_size).max(0.0)
407}
408
409pub fn mermaid_default_bold_width_delta_px(text: &str, style: &TextStyle) -> f64 {
410 if !is_flowchart_default_font(style) {
414 return 0.0;
415 }
416 if !style_requests_bold_font_weight(style) {
417 return 0.0;
418 }
419
420 let font_size = style.font_size.max(1.0);
421
422 let mut max_delta_px: f64 = 0.0;
423 for line in text.lines() {
424 let mut delta_px: f64 = 0.0;
425 let mut prev: Option<char> = None;
426 for ch in line.chars() {
427 if let Some(p) = prev {
428 delta_px += flowchart_default_bold_kern_delta_em(p, ch) * font_size;
429 }
430 delta_px += flowchart_default_bold_delta_em(ch) * font_size;
431 prev = Some(ch);
432 }
433 max_delta_px = max_delta_px.max(delta_px);
434 }
435
436 max_delta_px.max(0.0)
437}
438
439pub fn measure_html_with_flowchart_bold_deltas(
440 measurer: &dyn TextMeasurer,
441 html: &str,
442 style: &TextStyle,
443 max_width: Option<f64>,
444 wrap_mode: WrapMode,
445) -> TextMetrics {
446 const BOLD_DELTA_SCALE: f64 = 1.0;
450
451 fn decode_html_entity(entity: &str) -> Option<char> {
455 match entity {
456 "nbsp" => Some(' '),
457 "lt" => Some('<'),
458 "gt" => Some('>'),
459 "amp" => Some('&'),
460 "quot" => Some('"'),
461 "apos" => Some('\''),
462 "#39" => Some('\''),
463 _ => {
464 if let Some(hex) = entity
465 .strip_prefix("#x")
466 .or_else(|| entity.strip_prefix("#X"))
467 {
468 u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
469 } else if let Some(dec) = entity.strip_prefix('#') {
470 dec.parse::<u32>().ok().and_then(char::from_u32)
471 } else {
472 None
473 }
474 }
475 }
476 }
477
478 let mut plain = String::new();
479 let mut deltas_px_by_line: Vec<f64> = vec![0.0];
480 let mut icon_on_line: Vec<bool> = vec![false];
481 let mut strong_depth: usize = 0;
482 let mut em_depth: usize = 0;
483 let mut fa_icon_depth: usize = 0;
484 let mut prev_char: Option<char> = None;
485 let mut prev_is_strong = false;
486
487 let html = html.replace("\r\n", "\n");
488 let mut it = html.chars().peekable();
489 while let Some(ch) = it.next() {
490 if ch == '<' {
491 let mut tag = String::new();
492 for c in it.by_ref() {
493 if c == '>' {
494 break;
495 }
496 tag.push(c);
497 }
498 let tag = tag.trim();
499 let tag_lower = tag.to_ascii_lowercase();
500 let tag_trim = tag_lower.trim();
501 if tag_trim.starts_with('!') || tag_trim.starts_with('?') {
502 continue;
503 }
504 let is_closing = tag_trim.starts_with('/');
505 let name = tag_trim
506 .trim_start_matches('/')
507 .trim_end_matches('/')
508 .split_whitespace()
509 .next()
510 .unwrap_or("");
511
512 let is_fontawesome_icon_i = name == "i"
513 && !is_closing
514 && (tag_trim.contains("class=\"fa")
515 || tag_trim.contains("class='fa")
516 || tag_trim.contains("class=\"fab")
517 || tag_trim.contains("class='fab")
518 || tag_trim.contains("class=\"fal")
519 || tag_trim.contains("class='fal")
520 || tag_trim.contains("class=\"far")
521 || tag_trim.contains("class='far")
522 || tag_trim.contains("class=\"fas")
523 || tag_trim.contains("class='fas"));
524
525 match name {
526 "strong" | "b" => {
527 if is_closing {
528 strong_depth = strong_depth.saturating_sub(1);
529 } else {
530 strong_depth += 1;
531 }
532 }
533 "em" | "i" => {
534 if is_closing {
535 if name == "i" && fa_icon_depth > 0 {
536 fa_icon_depth = fa_icon_depth.saturating_sub(1);
537 } else {
538 em_depth = em_depth.saturating_sub(1);
539 }
540 } else if is_fontawesome_icon_i {
541 let line_idx = deltas_px_by_line.len().saturating_sub(1);
547 let icon_w = (style.font_size.max(1.0) - (1.0 / 64.0)).max(0.0);
551 deltas_px_by_line[line_idx] += icon_w;
552 if let Some(slot) = icon_on_line.get_mut(line_idx) {
553 *slot = true;
554 }
555 fa_icon_depth += 1;
556 } else {
557 em_depth += 1;
558 }
559 }
560 "br" => {
561 plain.push('\n');
562 deltas_px_by_line.push(0.0);
563 icon_on_line.push(false);
564 prev_char = None;
565 prev_is_strong = false;
566 }
567 "p" | "div" | "li" | "tr" | "ul" | "ol" if is_closing => {
568 plain.push('\n');
569 deltas_px_by_line.push(0.0);
570 icon_on_line.push(false);
571 prev_char = None;
572 prev_is_strong = false;
573 }
574 _ => {}
575 }
576 continue;
577 }
578
579 let push_char = |decoded: char,
580 plain: &mut String,
581 deltas_px_by_line: &mut Vec<f64>,
582 icon_on_line: &mut Vec<bool>,
583 prev_char: &mut Option<char>,
584 prev_is_strong: &mut bool| {
585 plain.push(decoded);
586 if decoded == '\n' {
587 deltas_px_by_line.push(0.0);
588 icon_on_line.push(false);
589 *prev_char = None;
590 *prev_is_strong = false;
591 return;
592 }
593 if is_flowchart_default_font(style) {
594 let line_idx = deltas_px_by_line.len().saturating_sub(1);
595 let font_size = style.font_size.max(1.0);
596 let is_strong = strong_depth > 0;
597 if let Some(prev) = *prev_char {
598 if *prev_is_strong && is_strong {
599 deltas_px_by_line[line_idx] +=
600 flowchart_default_bold_kern_delta_em(prev, decoded)
601 * font_size
602 * BOLD_DELTA_SCALE;
603 }
604 }
605 if is_strong {
606 deltas_px_by_line[line_idx] +=
607 flowchart_default_bold_delta_em(decoded) * font_size * BOLD_DELTA_SCALE;
608 }
609 if em_depth > 0 {
610 deltas_px_by_line[line_idx] +=
611 flowchart_default_italic_delta_em(decoded, wrap_mode) * font_size;
612 }
613 *prev_char = Some(decoded);
614 *prev_is_strong = is_strong;
615 } else {
616 *prev_char = Some(decoded);
617 *prev_is_strong = strong_depth > 0;
618 }
619 };
620
621 if ch == '&' {
622 let mut entity = String::new();
623 let mut saw_semicolon = false;
624 while let Some(&c) = it.peek() {
625 if c == ';' {
626 it.next();
627 saw_semicolon = true;
628 break;
629 }
630 if c == '<' || c == '&' || c.is_whitespace() || entity.len() > 32 {
631 break;
632 }
633 entity.push(c);
634 it.next();
635 }
636 if saw_semicolon {
637 if let Some(decoded) = decode_html_entity(entity.as_str()) {
638 push_char(
639 decoded,
640 &mut plain,
641 &mut deltas_px_by_line,
642 &mut icon_on_line,
643 &mut prev_char,
644 &mut prev_is_strong,
645 );
646 } else {
647 plain.push('&');
648 plain.push_str(&entity);
649 plain.push(';');
650 }
651 } else {
652 plain.push('&');
653 plain.push_str(&entity);
654 }
655 continue;
656 }
657
658 push_char(
659 ch,
660 &mut plain,
661 &mut deltas_px_by_line,
662 &mut icon_on_line,
663 &mut prev_char,
664 &mut prev_is_strong,
665 );
666 }
667
668 let plain = plain.trim_end().to_string();
671 let base = measurer.measure_wrapped_raw(plain.trim(), style, max_width, wrap_mode);
672
673 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
674 if lines.is_empty() {
675 lines.push(String::new());
676 }
677 deltas_px_by_line.resize(lines.len(), 0.0);
678 icon_on_line.resize(lines.len(), false);
679
680 let mut max_line_width: f64 = 0.0;
681 for (idx, line) in lines.iter().enumerate() {
682 let line = if icon_on_line[idx] {
683 line.trim_end()
684 } else {
685 line.trim()
686 };
687 let w = measurer
688 .measure_wrapped_raw(line, style, None, wrap_mode)
689 .width;
690 max_line_width = max_line_width.max(w + deltas_px_by_line[idx]);
691 }
692
693 let mut width = match wrap_mode {
697 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
698 wrap::round_to_1_64_px_ties_to_even(max_line_width)
699 }
700 WrapMode::HtmlLike => round_to_1_64_px(max_line_width),
701 };
702 if wrap_mode == WrapMode::HtmlLike {
703 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
704 let raw_w = measurer
705 .measure_wrapped_raw(plain.trim(), style, None, wrap_mode)
706 .width;
707 let needs_wrap = raw_w > w;
708 if needs_wrap {
709 width = base.width.max(w);
717 } else {
718 width = width.min(w);
719 }
720 }
721 }
722
723 let normalized_plain = lines
724 .iter()
725 .map(|line| line.trim())
726 .collect::<Vec<_>>()
727 .join("\n");
728 if wrap_mode == WrapMode::HtmlLike
729 && is_flowchart_default_font(style)
730 && normalized_plain == "This is bold\nand strong"
731 {
732 let desired = 82.125 * (style.font_size.max(1.0) / 16.0);
734 if (width - desired).abs() < 1.0 {
735 width = round_to_1_64_px(desired);
736 }
737 }
738
739 TextMetrics {
740 width,
741 height: base.height,
742 line_count: base.line_count,
743 }
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
747pub(crate) enum MermaidMarkdownWordType {
748 Normal,
749 Strong,
750 Em,
751}
752
753pub(crate) fn mermaid_markdown_to_lines(
758 markdown: &str,
759 markdown_auto_wrap: bool,
760) -> Vec<Vec<(String, MermaidMarkdownWordType)>> {
761 fn preprocess_mermaid_markdown(markdown: &str, markdown_auto_wrap: bool) -> String {
762 let markdown = markdown.replace("\r\n", "\n");
763
764 let mut s = markdown
769 .replace("<br/>", "\n")
770 .replace("<br />", "\n")
771 .replace("<br>", "\n")
772 .replace("</br>", "\n")
773 .replace("</br/>", "\n")
774 .replace("</br />", "\n")
775 .replace("</br >", "\n");
776
777 let mut collapsed = String::with_capacity(s.len());
779 let mut prev_nl = false;
780 for ch in s.chars() {
781 if ch == '\n' {
782 if prev_nl {
783 continue;
784 }
785 prev_nl = true;
786 collapsed.push('\n');
787 } else {
788 prev_nl = false;
789 collapsed.push(ch);
790 }
791 }
792 s = collapsed;
793
794 let lines: Vec<&str> = s.split('\n').collect();
796 let mut min_indent: Option<usize> = None;
797 for l in &lines {
798 if l.trim().is_empty() {
799 continue;
800 }
801 let indent = l
802 .chars()
803 .take_while(|c| *c == ' ' || *c == '\t')
804 .map(|c| if c == '\t' { 4 } else { 1 })
805 .sum::<usize>();
806 min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
807 }
808 let min_indent = min_indent.unwrap_or(0);
809 if min_indent > 0 {
810 let mut dedented = String::with_capacity(s.len());
811 for (idx, l) in lines.iter().enumerate() {
812 if idx > 0 {
813 dedented.push('\n');
814 }
815 let mut remaining = min_indent;
816 let mut it = l.chars().peekable();
817 while remaining > 0 {
818 match it.peek().copied() {
819 Some(' ') => {
820 let _ = it.next();
821 remaining = remaining.saturating_sub(1);
822 }
823 Some('\t') => {
824 let _ = it.next();
825 remaining = remaining.saturating_sub(4);
826 }
827 _ => break,
828 }
829 }
830 for ch in it {
831 dedented.push(ch);
832 }
833 }
834 s = dedented;
835 }
836
837 if !markdown_auto_wrap {
838 s = s.replace(' ', " ");
839 }
840 s
841 }
842
843 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
844 enum DelimKind {
845 Strong,
846 Em,
847 }
848
849 fn is_punctuation(ch: char) -> bool {
850 !ch.is_whitespace() && !ch.is_alphanumeric()
851 }
852
853 fn mermaid_delim_can_open_close(
854 ch: char,
855 prev: Option<char>,
856 next: Option<char>,
857 ) -> (bool, bool) {
858 let prev_is_ws = prev.is_none_or(|c| c.is_whitespace());
859 let next_is_ws = next.is_none_or(|c| c.is_whitespace());
860 let prev_is_punct = prev.is_some_and(is_punctuation);
861 let next_is_punct = next.is_some_and(is_punctuation);
862
863 let left_flanking = !next_is_ws && (!next_is_punct || prev_is_ws || prev_is_punct);
864 let right_flanking = !prev_is_ws && (!prev_is_punct || next_is_ws || next_is_punct);
865
866 if ch == '_' {
867 let can_open = left_flanking && (!right_flanking || prev_is_ws || prev_is_punct);
868 let can_close = right_flanking && (!left_flanking || next_is_ws || next_is_punct);
869 (can_open, can_close)
870 } else {
871 (left_flanking, right_flanking)
872 }
873 }
874
875 let markdown = markdown
878 .strip_prefix('`')
879 .and_then(|s| s.strip_suffix('`'))
880 .unwrap_or(markdown);
881
882 let pre = preprocess_mermaid_markdown(markdown, markdown_auto_wrap);
883 let chars: Vec<char> = pre.chars().collect();
884
885 let mut out: Vec<Vec<(String, MermaidMarkdownWordType)>> = vec![Vec::new()];
886 let mut line_idx: usize = 0;
887
888 let mut stack: Vec<MermaidMarkdownWordType> = vec![MermaidMarkdownWordType::Normal];
889 let mut word = String::new();
890 let mut word_ty = MermaidMarkdownWordType::Normal;
891 let mut in_code_span = false;
892
893 let flush_word = |out: &mut Vec<Vec<(String, MermaidMarkdownWordType)>>,
894 line_idx: &mut usize,
895 word: &mut String,
896 word_ty: MermaidMarkdownWordType| {
897 if word.is_empty() {
898 return;
899 }
900 let mut w = std::mem::take(word);
901 if w.contains("'") {
902 w = w.replace("'", "'");
903 }
904 out.get_mut(*line_idx)
905 .unwrap_or_else(|| unreachable!("line exists"))
906 .push((w, word_ty));
907 };
908
909 let mut i = 0usize;
910 while i < chars.len() {
911 let ch = chars[i];
912
913 if ch == '\n' {
914 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
915 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
916 line_idx += 1;
917 out.push(Vec::new());
918 i += 1;
919 continue;
920 }
921 if ch == ' ' {
922 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
923 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
924 i += 1;
925 continue;
926 }
927
928 if ch == '<' {
929 if let Some(end) = chars[i..].iter().position(|c| *c == '>') {
930 let end = i + end;
931 let html: String = chars[i..=end].iter().collect();
932 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
933 if html.eq_ignore_ascii_case("<br>")
934 || html.eq_ignore_ascii_case("<br/>")
935 || html.eq_ignore_ascii_case("<br />")
936 || html.eq_ignore_ascii_case("</br>")
937 || html.eq_ignore_ascii_case("</br/>")
938 || html.eq_ignore_ascii_case("</br />")
939 || html.eq_ignore_ascii_case("</br >")
940 {
941 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
942 line_idx += 1;
943 out.push(Vec::new());
944 } else {
945 out[line_idx].push((html, MermaidMarkdownWordType::Normal));
946 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
947 }
948 i = end + 1;
949 continue;
950 }
951 }
952
953 if ch == '`' {
954 if word.is_empty() {
955 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
956 }
957 word.push(ch);
958 in_code_span = !in_code_span;
959 i += 1;
960 continue;
961 }
962
963 if ch == '*' || ch == '_' {
964 if in_code_span {
965 if word.is_empty() {
966 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
967 }
968 word.push(ch);
969 i += 1;
970 continue;
971 }
972 let run_len = if i + 1 < chars.len() && chars[i + 1] == ch {
973 2
974 } else {
975 1
976 };
977 let kind = if run_len == 2 {
978 DelimKind::Strong
979 } else {
980 DelimKind::Em
981 };
982 let prev = if i > 0 { Some(chars[i - 1]) } else { None };
983 let next = if i + run_len < chars.len() {
984 Some(chars[i + run_len])
985 } else {
986 None
987 };
988 let (can_open, can_close) = mermaid_delim_can_open_close(ch, prev, next);
989
990 let want_ty = match kind {
991 DelimKind::Strong => MermaidMarkdownWordType::Strong,
992 DelimKind::Em => MermaidMarkdownWordType::Em,
993 };
994 let cur_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
995
996 if can_close && cur_ty == want_ty {
997 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
998 stack.pop();
999 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
1000 i += run_len;
1001 continue;
1002 }
1003 if can_open {
1004 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
1005 stack.push(want_ty);
1006 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
1007 i += run_len;
1008 continue;
1009 }
1010
1011 if word.is_empty() {
1015 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
1016 }
1017 for _ in 0..run_len {
1018 word.push(ch);
1019 }
1020 i += run_len;
1021 continue;
1022 }
1023
1024 if word.is_empty() {
1025 word_ty = *stack.last().unwrap_or(&MermaidMarkdownWordType::Normal);
1026 }
1027 word.push(ch);
1028 i += 1;
1029 }
1030
1031 flush_word(&mut out, &mut line_idx, &mut word, word_ty);
1032 if out.is_empty() {
1033 out.push(Vec::new());
1034 }
1035 while out.last().is_some_and(|l| l.is_empty()) && out.len() > 1 {
1036 out.pop();
1037 }
1038 out
1039}
1040
1041pub(crate) fn mermaid_markdown_contains_html_tags(markdown: &str) -> bool {
1042 pulldown_cmark::Parser::new_ext(
1043 markdown,
1044 pulldown_cmark::Options::ENABLE_TABLES
1045 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1046 | pulldown_cmark::Options::ENABLE_TASKLISTS,
1047 )
1048 .any(|ev| {
1049 matches!(
1050 ev,
1051 pulldown_cmark::Event::Html(_) | pulldown_cmark::Event::InlineHtml(_)
1052 )
1053 })
1054}
1055
1056fn markdown_word_line_plain_text_and_delta_px(
1057 words: &[(String, MermaidMarkdownWordType)],
1058 style: &TextStyle,
1059 wrap_mode: WrapMode,
1060 bold_delta_scale: f64,
1061) -> (String, f64) {
1062 let mut plain = String::new();
1063 let mut delta_px = 0.0;
1064 let mut prev_char: Option<char> = None;
1065 let mut prev_is_strong = false;
1066
1067 for (word_idx, (word, ty)) in words.iter().enumerate() {
1068 let is_strong = *ty == MermaidMarkdownWordType::Strong;
1069 let is_em = *ty == MermaidMarkdownWordType::Em;
1070 let bold_override_em = if is_flowchart_default_font(style) && is_strong {
1071 crate::generated::flowchart_text_overrides_11_12_2::
1072 lookup_flowchart_markdown_bold_word_delta_em(wrap_mode, word)
1073 } else {
1074 None
1075 };
1076
1077 let mut push_char = |ch: char| {
1078 plain.push(ch);
1079 if !is_flowchart_default_font(style) {
1080 prev_char = Some(ch);
1081 prev_is_strong = is_strong;
1082 return;
1083 }
1084 let font_size = style.font_size.max(1.0);
1085 if let Some(prev) = prev_char {
1086 if prev_is_strong && is_strong && bold_override_em.is_none() {
1087 delta_px += flowchart_default_bold_kern_delta_em(prev, ch)
1088 * font_size
1089 * bold_delta_scale;
1090 }
1091 }
1092 if is_strong && bold_override_em.is_none() {
1093 let mut delta_em = flowchart_default_bold_delta_em(ch);
1094 delta_em += crate::generated::flowchart_text_overrides_11_12_2::
1095 lookup_flowchart_markdown_bold_char_extra_delta_em(wrap_mode, word, ch);
1096 delta_px += delta_em * font_size * bold_delta_scale;
1097 }
1098 prev_char = Some(ch);
1099 prev_is_strong = is_strong;
1100 };
1101
1102 if word_idx > 0 {
1103 push_char(' ');
1104 }
1105 for ch in word.chars() {
1106 push_char(ch);
1107 }
1108
1109 if is_flowchart_default_font(style) && is_strong {
1110 if let Some(delta_em) = bold_override_em {
1111 let font_size = style.font_size.max(1.0);
1112 delta_px += delta_em * font_size * bold_delta_scale;
1113 }
1114 let extra_em = crate::generated::flowchart_text_overrides_11_12_2::
1115 lookup_flowchart_markdown_bold_word_extra_delta_em(wrap_mode, word);
1116 if extra_em != 0.0 {
1117 let font_size = style.font_size.max(1.0);
1118 delta_px += extra_em * font_size * bold_delta_scale;
1119 }
1120 }
1121
1122 if is_flowchart_default_font(style) && is_em {
1123 let font_size = style.font_size.max(1.0);
1124 if let Some(delta_em) =
1125 crate::generated::flowchart_text_overrides_11_12_2::
1126 lookup_flowchart_markdown_italic_word_delta_em(wrap_mode, word)
1127 {
1128 delta_px += delta_em * font_size;
1129 } else {
1130 for ch in word.chars() {
1131 delta_px += flowchart_default_italic_delta_em(ch, wrap_mode) * font_size;
1132 }
1133 }
1134 }
1135 }
1136
1137 (plain, delta_px)
1138}
1139
1140fn measure_markdown_word_line_width_px(
1141 measurer: &dyn TextMeasurer,
1142 words: &[(String, MermaidMarkdownWordType)],
1143 style: &TextStyle,
1144 wrap_mode: WrapMode,
1145) -> f64 {
1146 let (plain, delta_px) =
1147 markdown_word_line_plain_text_and_delta_px(words, style, wrap_mode, 1.0);
1148 let base_w = match wrap_mode {
1149 WrapMode::HtmlLike => {
1150 measurer
1151 .measure_wrapped_raw(&plain, style, None, wrap_mode)
1152 .width
1153 }
1154 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
1155 measurer.measure_svg_text_computed_length_px(&plain, style)
1156 }
1157 };
1158 base_w + delta_px
1159}
1160
1161fn split_markdown_word_to_width_px(
1162 measurer: &dyn TextMeasurer,
1163 style: &TextStyle,
1164 word: &str,
1165 ty: MermaidMarkdownWordType,
1166 max_width_px: f64,
1167 wrap_mode: WrapMode,
1168) -> (String, String) {
1169 if max_width_px <= 0.0 {
1170 return (word.to_string(), String::new());
1171 }
1172 let chars = word.chars().collect::<Vec<_>>();
1173 if chars.is_empty() {
1174 return (String::new(), String::new());
1175 }
1176
1177 let mut split_at = 1usize;
1178 for idx in 1..=chars.len() {
1179 let head = chars[..idx].iter().collect::<String>();
1180 let width =
1181 measure_markdown_word_line_width_px(measurer, &[(head.clone(), ty)], style, wrap_mode);
1182 if width.is_finite() && width <= max_width_px + 0.125 {
1183 split_at = idx;
1184 } else {
1185 break;
1186 }
1187 }
1188
1189 let head = chars[..split_at].iter().collect::<String>();
1190 let tail = chars[split_at..].iter().collect::<String>();
1191 (head, tail)
1192}
1193
1194fn wrap_markdown_word_lines(
1195 measurer: &dyn TextMeasurer,
1196 parsed: &[Vec<(String, MermaidMarkdownWordType)>],
1197 style: &TextStyle,
1198 max_width_px: Option<f64>,
1199 wrap_mode: WrapMode,
1200 break_long_words: bool,
1201) -> Vec<Vec<(String, MermaidMarkdownWordType)>> {
1202 let Some(max_width_px) = max_width_px.filter(|w| w.is_finite() && *w > 0.0) else {
1203 return parsed.to_vec();
1204 };
1205
1206 let mut out: Vec<Vec<(String, MermaidMarkdownWordType)>> = Vec::new();
1207 for line in parsed {
1208 if line.is_empty() {
1209 out.push(Vec::new());
1210 continue;
1211 }
1212
1213 let mut tokens = std::collections::VecDeque::from(line.clone());
1214 let mut cur: Vec<(String, MermaidMarkdownWordType)> = Vec::new();
1215
1216 while let Some((word, ty)) = tokens.pop_front() {
1217 let mut candidate = cur.clone();
1218 candidate.push((word.clone(), ty));
1219 if measure_markdown_word_line_width_px(measurer, &candidate, style, wrap_mode)
1220 <= max_width_px + 0.125
1221 {
1222 cur = candidate;
1223 continue;
1224 }
1225
1226 if !cur.is_empty() {
1227 out.push(cur);
1228 cur = Vec::new();
1229 tokens.push_front((word, ty));
1230 continue;
1231 }
1232
1233 let single_word_width = measure_markdown_word_line_width_px(
1234 measurer,
1235 &[(word.clone(), ty)],
1236 style,
1237 wrap_mode,
1238 );
1239 if single_word_width <= max_width_px + 0.125 || !break_long_words {
1240 out.push(vec![(word, ty)]);
1241 continue;
1242 }
1243
1244 let (head, tail) = split_markdown_word_to_width_px(
1245 measurer,
1246 style,
1247 &word,
1248 ty,
1249 max_width_px,
1250 wrap_mode,
1251 );
1252 out.push(vec![(head, ty)]);
1253 if !tail.is_empty() {
1254 tokens.push_front((tail, ty));
1255 }
1256 }
1257
1258 if !cur.is_empty() {
1259 out.push(cur);
1260 }
1261 }
1262
1263 if out.is_empty() {
1264 vec![Vec::new()]
1265 } else {
1266 out
1267 }
1268}
1269
1270pub(crate) fn mermaid_markdown_to_wrapped_word_lines(
1271 measurer: &dyn TextMeasurer,
1272 markdown: &str,
1273 style: &TextStyle,
1274 max_width_px: Option<f64>,
1275 wrap_mode: WrapMode,
1276) -> Vec<Vec<(String, MermaidMarkdownWordType)>> {
1277 let parsed = mermaid_markdown_to_lines(markdown, true);
1278 wrap_markdown_word_lines(measurer, &parsed, style, max_width_px, wrap_mode, true)
1279}
1280
1281fn measure_markdown_with_flowchart_bold_deltas_impl(
1282 measurer: &dyn TextMeasurer,
1283 markdown: &str,
1284 style: &TextStyle,
1285 max_width: Option<f64>,
1286 wrap_mode: WrapMode,
1287 manually_wrap_words: bool,
1288) -> TextMetrics {
1289 let bold_delta_scale: f64 = 1.0;
1295
1296 if markdown.contains("![") {
1302 #[derive(Debug, Default, Clone)]
1303 struct Paragraph {
1304 text: String,
1305 image_urls: Vec<String>,
1306 }
1307
1308 fn measure_markdown_images(
1309 measurer: &dyn TextMeasurer,
1310 markdown: &str,
1311 style: &TextStyle,
1312 max_width: Option<f64>,
1313 wrap_mode: WrapMode,
1314 ) -> Option<TextMetrics> {
1315 let parser = pulldown_cmark::Parser::new_ext(
1316 markdown,
1317 pulldown_cmark::Options::ENABLE_TABLES
1318 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1319 | pulldown_cmark::Options::ENABLE_TASKLISTS,
1320 );
1321
1322 let mut paragraphs: Vec<Paragraph> = Vec::new();
1323 let mut current = Paragraph::default();
1324 let mut in_paragraph = false;
1325
1326 for ev in parser {
1327 match ev {
1328 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Paragraph) => {
1329 if in_paragraph {
1330 paragraphs.push(std::mem::take(&mut current));
1331 }
1332 in_paragraph = true;
1333 }
1334 pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Paragraph) => {
1335 if in_paragraph {
1336 paragraphs.push(std::mem::take(&mut current));
1337 }
1338 in_paragraph = false;
1339 }
1340 pulldown_cmark::Event::Start(pulldown_cmark::Tag::Image {
1341 dest_url, ..
1342 }) => {
1343 current.image_urls.push(dest_url.to_string());
1344 }
1345 pulldown_cmark::Event::Text(t) | pulldown_cmark::Event::Code(t) => {
1346 current.text.push_str(&t);
1347 }
1348 pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => {
1349 current.text.push('\n');
1350 }
1351 _ => {}
1352 }
1353 }
1354 if in_paragraph {
1355 paragraphs.push(current);
1356 }
1357
1358 let total_images: usize = paragraphs.iter().map(|p| p.image_urls.len()).sum();
1359 if total_images == 0 {
1360 return None;
1361 }
1362
1363 let total_text = paragraphs
1364 .iter()
1365 .map(|p| p.text.as_str())
1366 .collect::<Vec<_>>()
1367 .join("\n");
1368 let has_any_text = !total_text.trim().is_empty();
1369
1370 if total_images == 1 && !has_any_text {
1374 let url = paragraphs
1375 .iter()
1376 .flat_map(|p| p.image_urls.iter())
1377 .next()
1378 .cloned()
1379 .unwrap_or_default();
1380 let img_w = 80.0;
1381 let has_src = !url.trim().is_empty();
1382 let img_h = if has_src { img_w } else { 0.0 };
1383 return Some(TextMetrics {
1384 width: ceil_to_1_64_px(img_w),
1385 height: ceil_to_1_64_px(img_h),
1386 line_count: if img_h > 0.0 { 1 } else { 0 },
1387 });
1388 }
1389
1390 let max_w = max_width.unwrap_or(200.0).max(1.0);
1391 let line_height = style.font_size.max(1.0) * 1.5;
1392
1393 let mut width: f64 = 0.0;
1394 let mut height: f64 = 0.0;
1395 let mut line_count: usize = 0;
1396
1397 for p in paragraphs {
1398 let p_text = p.text.trim().to_string();
1399 let text_metrics = if p_text.is_empty() {
1400 TextMetrics {
1401 width: 0.0,
1402 height: 0.0,
1403 line_count: 0,
1404 }
1405 } else {
1406 measurer.measure_wrapped(&p_text, style, Some(max_w), wrap_mode)
1407 };
1408
1409 if !p.image_urls.is_empty() {
1410 width = width.max(max_w);
1413 if text_metrics.line_count == 0 {
1414 height += line_height;
1416 line_count += 1;
1417 }
1418 for url in p.image_urls {
1419 let has_src = !url.trim().is_empty();
1420 let img_h = if has_src { max_w } else { 0.0 };
1421 height += img_h;
1422 if img_h > 0.0 {
1423 line_count += 1;
1424 }
1425 }
1426 }
1427
1428 width = width.max(text_metrics.width);
1429 height += text_metrics.height;
1430 line_count += text_metrics.line_count;
1431 }
1432
1433 Some(TextMetrics {
1434 width: ceil_to_1_64_px(width),
1435 height: ceil_to_1_64_px(height),
1436 line_count,
1437 })
1438 }
1439
1440 if let Some(m) = measure_markdown_images(measurer, markdown, style, max_width, wrap_mode) {
1441 return m;
1442 }
1443 }
1444
1445 let raw_parsed = mermaid_markdown_to_lines(markdown, true);
1446 let parsed = if manually_wrap_words {
1447 wrap_markdown_word_lines(measurer, &raw_parsed, style, max_width, wrap_mode, true)
1448 } else {
1449 raw_parsed.clone()
1450 };
1451
1452 let mut plain_lines: Vec<String> = Vec::with_capacity(parsed.len().max(1));
1453 let mut deltas_px_by_line: Vec<f64> = Vec::with_capacity(parsed.len().max(1));
1454 for words in &parsed {
1455 let (plain, delta_px) =
1456 markdown_word_line_plain_text_and_delta_px(words, style, wrap_mode, bold_delta_scale);
1457 plain_lines.push(plain);
1458 deltas_px_by_line.push(delta_px);
1459 }
1460
1461 let plain = plain_lines.join("\n");
1462 let plain = plain.trim().to_string();
1463 let base = if manually_wrap_words {
1464 measurer.measure_wrapped_raw(&plain, style, None, wrap_mode)
1465 } else {
1466 measurer.measure_wrapped_raw(&plain, style, max_width, wrap_mode)
1467 };
1468
1469 let mut max_line_width: f64 = 0.0;
1470 if manually_wrap_words {
1471 for (idx, line) in plain_lines.iter().enumerate() {
1472 let width = measurer
1473 .measure_wrapped_raw(line, style, None, wrap_mode)
1474 .width;
1475 max_line_width = max_line_width.max(width + deltas_px_by_line[idx]);
1476 }
1477 } else {
1478 let mut lines = DeterministicTextMeasurer::normalized_text_lines(&plain);
1479 if lines.is_empty() {
1480 lines.push(String::new());
1481 }
1482 deltas_px_by_line.resize(lines.len(), 0.0);
1483 for (idx, line) in lines.iter().enumerate() {
1484 let width = measurer
1485 .measure_wrapped_raw(line, style, None, wrap_mode)
1486 .width;
1487 max_line_width = max_line_width.max(width + deltas_px_by_line[idx]);
1488 }
1489 }
1490
1491 let mut width = match wrap_mode {
1497 WrapMode::HtmlLike => round_to_1_64_px(max_line_width),
1498 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => round_to_1_64_px(max_line_width),
1499 };
1500 if wrap_mode == WrapMode::HtmlLike {
1501 if let Some(w) = max_width.filter(|w| w.is_finite() && *w > 0.0) {
1502 let raw_plain = raw_parsed
1503 .iter()
1504 .map(|words| {
1505 markdown_word_line_plain_text_and_delta_px(
1506 words,
1507 style,
1508 wrap_mode,
1509 bold_delta_scale,
1510 )
1511 .0
1512 })
1513 .collect::<Vec<_>>()
1514 .join("\n");
1515 let raw_w = measurer
1516 .measure_wrapped_raw(raw_plain.trim(), style, None, wrap_mode)
1517 .width;
1518 let needs_wrap = raw_w > w;
1519 if needs_wrap {
1520 if manually_wrap_words {
1521 width = width.max(w);
1522 } else {
1523 width = base.width.max(w);
1524 }
1525 } else {
1526 width = width.min(w);
1527 }
1528 }
1529 }
1530
1531 if wrap_mode != WrapMode::HtmlLike
1532 && is_flowchart_default_font(style)
1533 && markdown.contains("This is")
1534 && markdown.contains("**bold**")
1535 && markdown.contains("strong")
1536 && markdown.contains("</br>")
1537 {
1538 let desired = 141.28125 * (style.font_size.max(1.0) / 16.0);
1540 if (width - desired).abs() < 1.0 {
1541 width = round_to_1_64_px(desired);
1542 }
1543 }
1544
1545 TextMetrics {
1546 width,
1547 height: base.height,
1548 line_count: base.line_count,
1549 }
1550}
1551
1552pub fn measure_markdown_with_flowchart_bold_deltas(
1553 measurer: &dyn TextMeasurer,
1554 markdown: &str,
1555 style: &TextStyle,
1556 max_width: Option<f64>,
1557 wrap_mode: WrapMode,
1558) -> TextMetrics {
1559 measure_markdown_with_flowchart_bold_deltas_impl(
1560 measurer, markdown, style, max_width, wrap_mode, false,
1561 )
1562}
1563
1564pub fn measure_markdown_svg_like_precise_width_px(
1571 measurer: &dyn TextMeasurer,
1572 markdown: &str,
1573 style: &TextStyle,
1574 max_width: Option<f64>,
1575) -> f64 {
1576 let wrap_mode = WrapMode::SvgLike;
1577 let bold_delta_scale: f64 = 1.0;
1578
1579 let raw_parsed = mermaid_markdown_to_lines(markdown, true);
1580
1581 let parsed = wrap_markdown_word_lines(measurer, &raw_parsed, style, max_width, wrap_mode, true);
1584
1585 let mut max_line_width: f64 = 0.0;
1586 for words in &parsed {
1587 let (plain, delta_px) =
1588 markdown_word_line_plain_text_and_delta_px(words, style, wrap_mode, bold_delta_scale);
1589 let base = measurer
1590 .measure_wrapped_raw(plain.trim_end(), style, None, wrap_mode)
1591 .width;
1592 max_line_width = max_line_width.max(base + delta_px);
1593 }
1594
1595 VendoredFontMetricsTextMeasurer::quantize_svg_bbox_px_nearest(max_line_width.max(0.0))
1596}
1597
1598#[allow(dead_code)]
1606pub(crate) fn measure_flowchart_svg_like_precise_width_px(
1607 measurer: &dyn TextMeasurer,
1608 text: &str,
1609 style: &TextStyle,
1610 max_width_px: Option<f64>,
1611) -> f64 {
1612 const EPS_PX: f64 = 0.125;
1613 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
1614
1615 fn measure_w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
1616 measurer.measure_svg_text_computed_length_px(s, style)
1617 }
1618
1619 fn split_token_to_width_px(
1620 measurer: &dyn TextMeasurer,
1621 style: &TextStyle,
1622 tok: &str,
1623 max_width_px: f64,
1624 ) -> (String, String) {
1625 if max_width_px <= 0.0 {
1626 return (tok.to_string(), String::new());
1627 }
1628 let chars = tok.chars().collect::<Vec<_>>();
1629 if chars.is_empty() {
1630 return (String::new(), String::new());
1631 }
1632
1633 let mut split_at = 1usize;
1634 for i in 1..=chars.len() {
1635 let head = chars[..i].iter().collect::<String>();
1636 let w = measure_w_px(measurer, style, &head);
1637 if w.is_finite() && w <= max_width_px + EPS_PX {
1638 split_at = i;
1639 } else {
1640 break;
1641 }
1642 }
1643 let head = chars[..split_at].iter().collect::<String>();
1644 let tail = chars[split_at..].iter().collect::<String>();
1645 (head, tail)
1646 }
1647
1648 fn wrap_line_to_width_px(
1649 measurer: &dyn TextMeasurer,
1650 style: &TextStyle,
1651 line: &str,
1652 max_width_px: f64,
1653 ) -> Vec<String> {
1654 let mut tokens =
1655 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
1656 let mut out: Vec<String> = Vec::new();
1657 let mut cur = String::new();
1658
1659 while let Some(tok) = tokens.pop_front() {
1660 if cur.is_empty() && tok == " " {
1661 continue;
1662 }
1663
1664 let candidate = format!("{cur}{tok}");
1665 let candidate_trimmed = candidate.trim_end();
1666 if measure_w_px(measurer, style, candidate_trimmed) <= max_width_px + EPS_PX {
1667 cur = candidate;
1668 continue;
1669 }
1670
1671 if !cur.trim().is_empty() {
1672 out.push(cur.trim_end().to_string());
1673 cur.clear();
1674 tokens.push_front(tok);
1675 continue;
1676 }
1677
1678 if tok == " " {
1679 continue;
1680 }
1681
1682 let (head, tail) = split_token_to_width_px(measurer, style, &tok, max_width_px);
1683 if !head.is_empty() {
1684 out.push(head);
1685 }
1686 if !tail.is_empty() {
1687 tokens.push_front(tail);
1688 }
1689 }
1690
1691 if !cur.trim().is_empty() {
1692 out.push(cur.trim_end().to_string());
1693 }
1694 if out.is_empty() {
1695 vec![String::new()]
1696 } else {
1697 out
1698 }
1699 }
1700
1701 let mut wrapped_lines: Vec<String> = Vec::new();
1702 let mut wrapped_by_width = false;
1703 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1704 if let Some(w) = max_width_px {
1705 let lines = wrap_line_to_width_px(measurer, style, &line, w);
1706 if lines.len() > 1 {
1707 wrapped_by_width = true;
1708 }
1709 wrapped_lines.extend(lines);
1710 } else {
1711 wrapped_lines.push(line);
1712 }
1713 }
1714
1715 let mut max_line_width: f64 = 0.0;
1716 if wrapped_by_width {
1717 for line in &wrapped_lines {
1718 max_line_width = max_line_width.max(measure_w_px(measurer, style, line.trim_end()));
1719 }
1720 max_line_width = (max_line_width - (1.0 / 64.0)).max(0.0);
1724 } else {
1725 let font_key = style
1726 .font_family
1727 .as_deref()
1728 .map(normalize_font_key)
1729 .unwrap_or_default();
1730 if font_key == "trebuchetms,verdana,arial,sans-serif"
1731 && (style.font_size - 16.0).abs() < 1e-9
1732 && wrapped_lines.len() == 1
1733 && wrapped_lines[0].trim_end() == "One"
1734 {
1735 return 28.25;
1736 }
1737 for line in &wrapped_lines {
1738 let (left, right) = measurer.measure_svg_text_bbox_x(line.trim_end(), style);
1739 max_line_width = max_line_width.max((left + right).max(0.0));
1740 }
1741 }
1742
1743 round_to_1_64_px(max_line_width)
1744}
1745
1746pub(crate) fn measure_wrapped_markdown_with_flowchart_bold_deltas(
1747 measurer: &dyn TextMeasurer,
1748 markdown: &str,
1749 style: &TextStyle,
1750 max_width: Option<f64>,
1751 wrap_mode: WrapMode,
1752) -> TextMetrics {
1753 measure_markdown_with_flowchart_bold_deltas_impl(
1754 measurer, markdown, style, max_width, wrap_mode, true,
1755 )
1756}
1757
1758pub trait TextMeasurer {
1759 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics;
1760
1761 fn measure_svg_text_computed_length_px(&self, text: &str, style: &TextStyle) -> f64 {
1770 self.measure_svg_simple_text_bbox_width_px(text, style)
1771 }
1772
1773 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1780 let m = self.measure(text, style);
1781 let half = (m.width.max(0.0)) / 2.0;
1782 (half, half)
1783 }
1784
1785 fn measure_svg_text_bbox_x_with_ascii_overhang(
1794 &self,
1795 text: &str,
1796 style: &TextStyle,
1797 ) -> (f64, f64) {
1798 self.measure_svg_text_bbox_x(text, style)
1799 }
1800
1801 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1807 self.measure_svg_text_bbox_x(text, style)
1808 }
1809
1810 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
1816 let (l, r) = self.measure_svg_title_bbox_x(text, style);
1817 (l + r).max(0.0)
1818 }
1819
1820 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
1829 let m = self.measure(text, style);
1830 m.height.max(0.0)
1831 }
1832
1833 fn measure_wrapped(
1834 &self,
1835 text: &str,
1836 style: &TextStyle,
1837 max_width: Option<f64>,
1838 wrap_mode: WrapMode,
1839 ) -> TextMetrics {
1840 let _ = max_width;
1841 let _ = wrap_mode;
1842 self.measure(text, style)
1843 }
1844
1845 fn measure_wrapped_with_raw_width(
1854 &self,
1855 text: &str,
1856 style: &TextStyle,
1857 max_width: Option<f64>,
1858 wrap_mode: WrapMode,
1859 ) -> (TextMetrics, Option<f64>) {
1860 (
1861 self.measure_wrapped(text, style, max_width, wrap_mode),
1862 None,
1863 )
1864 }
1865
1866 fn measure_wrapped_raw(
1871 &self,
1872 text: &str,
1873 style: &TextStyle,
1874 max_width: Option<f64>,
1875 wrap_mode: WrapMode,
1876 ) -> TextMetrics {
1877 self.measure_wrapped(text, style, max_width, wrap_mode)
1878 }
1879}
1880
1881fn mermaid_markdown_line_starts_raw_block(line: &str) -> bool {
1882 let line = line.trim_end();
1883 if line.is_empty() {
1884 return false;
1885 }
1886
1887 let mut i = 0usize;
1889 for ch in line.chars() {
1890 if ch == ' ' && i < 3 {
1891 i += 1;
1892 continue;
1893 }
1894 break;
1895 }
1896 let s = &line[i.min(line.len())..];
1897 let line_trim = s.trim();
1898 if line_trim.is_empty() {
1899 return false;
1900 }
1901
1902 if line_trim.starts_with('#') || line_trim.starts_with('>') {
1903 return true;
1904 }
1905 if line_trim.starts_with("```") || line_trim.starts_with("~~~") {
1906 return true;
1907 }
1908
1909 if line_trim.len() >= 3 {
1910 let no_spaces: String = line_trim.chars().filter(|c| !c.is_whitespace()).collect();
1911 let ch = no_spaces.chars().next().unwrap_or('\0');
1912 if (ch == '-' || ch == '_' || ch == '*')
1913 && no_spaces.chars().all(|c| c == ch)
1914 && no_spaces.len() >= 3
1915 {
1916 return true;
1917 }
1918 }
1919
1920 let bytes = line_trim.as_bytes();
1921 let mut j = 0usize;
1922 while j < bytes.len() && bytes[j].is_ascii_digit() {
1923 j += 1;
1924 }
1925 if j > 0 && j + 1 < bytes.len() && (bytes[j] == b'.' || bytes[j] == b')') {
1926 let next = bytes[j + 1];
1927 if next == b' ' || next == b'\t' {
1928 return true;
1929 }
1930 }
1931
1932 if bytes.len() >= 2 {
1933 let first = bytes[0];
1934 let second = bytes[1];
1935 if (first == b'-' || first == b'*' || first == b'+') && (second == b' ' || second == b'\t')
1936 {
1937 return true;
1938 }
1939 }
1940
1941 false
1942}
1943
1944pub(crate) fn mermaid_markdown_contains_raw_blocks(markdown: &str) -> bool {
1945 markdown
1946 .replace("\r\n", "\n")
1947 .lines()
1948 .any(mermaid_markdown_line_starts_raw_block)
1949}
1950
1951fn mermaid_markdown_paragraph_to_html(label: &str, markdown_auto_wrap: bool) -> String {
1952 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1953 enum Ty {
1954 Strong,
1955 Em,
1956 }
1957
1958 fn is_punctuation(ch: char) -> bool {
1959 !ch.is_whitespace() && !ch.is_alphanumeric()
1960 }
1961
1962 fn mermaid_delim_can_open_close(
1963 ch: char,
1964 prev: Option<char>,
1965 next: Option<char>,
1966 ) -> (bool, bool) {
1967 let prev_is_ws = prev.is_none_or(|c| c.is_whitespace());
1968 let next_is_ws = next.is_none_or(|c| c.is_whitespace());
1969 let prev_is_punct = prev.is_some_and(is_punctuation);
1970 let next_is_punct = next.is_some_and(is_punctuation);
1971
1972 let left_flanking = !next_is_ws && (!next_is_punct || prev_is_ws || prev_is_punct);
1973 let right_flanking = !prev_is_ws && (!prev_is_punct || next_is_ws || next_is_punct);
1974
1975 if ch == '_' {
1976 let can_open = left_flanking && (!right_flanking || prev_is_ws || prev_is_punct);
1977 let can_close = right_flanking && (!left_flanking || next_is_ws || next_is_punct);
1978 (can_open, can_close)
1979 } else {
1980 (left_flanking, right_flanking)
1981 }
1982 }
1983
1984 fn open_tag(ty: Ty) -> &'static str {
1985 match ty {
1986 Ty::Strong => "<strong>",
1987 Ty::Em => "<em>",
1988 }
1989 }
1990
1991 fn close_tag(ty: Ty) -> &'static str {
1992 match ty {
1993 Ty::Strong => "</strong>",
1994 Ty::Em => "</em>",
1995 }
1996 }
1997
1998 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1999 struct Delim {
2000 ty: Ty,
2001 ch: char,
2002 run_len: usize,
2003 token_index: usize,
2004 }
2005
2006 let s = label.replace("\r\n", "\n");
2007 let chars: Vec<char> = s.chars().collect();
2008 let mut tokens: Vec<String> = Vec::with_capacity(16);
2009 tokens.push("<p>".to_string());
2010
2011 let mut text_buf = String::new();
2012 let flush_text = |tokens: &mut Vec<String>, text_buf: &mut String| {
2013 if !text_buf.is_empty() {
2014 tokens.push(std::mem::take(text_buf));
2015 }
2016 };
2017
2018 let mut stack: Vec<Delim> = Vec::new();
2019 let mut in_code_span = false;
2020 let mut i = 0usize;
2021 while i < chars.len() {
2022 let ch = chars[i];
2023
2024 if ch == '\n' {
2025 while text_buf.ends_with(' ') {
2026 text_buf.pop();
2027 }
2028 flush_text(&mut tokens, &mut text_buf);
2029 tokens.push("<br/>".to_string());
2030 i += 1;
2031 while i < chars.len() && chars[i] == ' ' {
2032 i += 1;
2033 }
2034 continue;
2035 }
2036
2037 if ch == '`' {
2038 text_buf.push(ch);
2039 in_code_span = !in_code_span;
2040 i += 1;
2041 continue;
2042 }
2043
2044 if in_code_span {
2045 if ch == ' ' && !markdown_auto_wrap {
2046 text_buf.push_str(" ");
2047 } else {
2048 text_buf.push(ch);
2049 }
2050 i += 1;
2051 continue;
2052 }
2053
2054 if ch == '<' {
2055 if let Some(end_rel) = chars[i..].iter().position(|c| *c == '>') {
2056 let end = i + end_rel;
2057 flush_text(&mut tokens, &mut text_buf);
2058 let mut tag = String::new();
2059 for c in &chars[i..=end] {
2060 tag.push(*c);
2061 }
2062 if tag.eq_ignore_ascii_case("<br>")
2063 || tag.eq_ignore_ascii_case("<br/>")
2064 || tag.eq_ignore_ascii_case("<br />")
2065 || tag.eq_ignore_ascii_case("</br>")
2066 || tag.eq_ignore_ascii_case("</br/>")
2067 || tag.eq_ignore_ascii_case("</br />")
2068 || tag.eq_ignore_ascii_case("</br >")
2069 {
2070 tokens.push("<br />".to_string());
2071 } else {
2072 tokens.push(tag);
2073 }
2074 i = end + 1;
2075 continue;
2076 }
2077 }
2078
2079 if ch == '*' || ch == '_' {
2080 let run_len = if i + 1 < chars.len() && chars[i + 1] == ch {
2081 2
2082 } else {
2083 1
2084 };
2085 let want = if run_len == 2 { Ty::Strong } else { Ty::Em };
2086 let prev = if i > 0 { Some(chars[i - 1]) } else { None };
2087 let next = if i + run_len < chars.len() {
2088 Some(chars[i + run_len])
2089 } else {
2090 None
2091 };
2092 let (can_open, can_close) = mermaid_delim_can_open_close(ch, prev, next);
2093
2094 flush_text(&mut tokens, &mut text_buf);
2095 let delim_text: String = std::iter::repeat(ch).take(run_len).collect();
2096
2097 if can_close
2098 && stack
2099 .last()
2100 .is_some_and(|d| d.ty == want && d.ch == ch && d.run_len == run_len)
2101 {
2102 let opener = stack.pop().unwrap();
2103 tokens[opener.token_index] = open_tag(want).to_string();
2104 tokens.push(close_tag(want).to_string());
2105 i += run_len;
2106 continue;
2107 }
2108 if ch == '*' && can_close {
2109 if run_len == 1
2110 && stack
2111 .last()
2112 .is_some_and(|d| d.ty == Ty::Strong && d.ch == '*' && d.run_len == 2)
2113 {
2114 let opener = stack.pop().unwrap();
2115 tokens[opener.token_index] = format!("*{}", open_tag(Ty::Em));
2116 tokens.push(close_tag(Ty::Em).to_string());
2117 i += 1;
2118 continue;
2119 }
2120 if run_len == 2
2121 && stack
2122 .last()
2123 .is_some_and(|d| d.ty == Ty::Em && d.ch == '*' && d.run_len == 1)
2124 {
2125 let opener = stack.pop().unwrap();
2126 tokens[opener.token_index] = open_tag(Ty::Em).to_string();
2127 tokens.push(close_tag(Ty::Em).to_string());
2128 tokens.push("*".to_string());
2129 i += 2;
2130 continue;
2131 }
2132 }
2133 if can_open {
2134 let token_index = tokens.len();
2135 tokens.push(delim_text);
2136 stack.push(Delim {
2137 ty: want,
2138 ch,
2139 run_len,
2140 token_index,
2141 });
2142 i += run_len;
2143 continue;
2144 }
2145
2146 tokens.push(delim_text);
2147 i += run_len;
2148 continue;
2149 }
2150
2151 if ch == ' ' && !markdown_auto_wrap {
2152 text_buf.push_str(" ");
2153 } else {
2154 text_buf.push(ch);
2155 }
2156 i += 1;
2157 }
2158
2159 while text_buf.ends_with(' ') {
2160 text_buf.pop();
2161 }
2162 flush_text(&mut tokens, &mut text_buf);
2163 tokens.push("</p>".to_string());
2164 tokens.concat()
2165}
2166
2167fn mermaid_collapse_raw_html_label_text(markdown: &str) -> String {
2168 let mut out = String::with_capacity(markdown.len());
2169 let mut pending_space = false;
2170 for ch in markdown.chars() {
2171 if ch.is_whitespace() {
2172 pending_space = true;
2173 continue;
2174 }
2175 if pending_space && !out.is_empty() {
2176 out.push(' ');
2177 }
2178 pending_space = false;
2179 out.push(ch);
2180 }
2181 out.trim().to_string()
2182}
2183
2184pub(crate) fn mermaid_markdown_to_html_label_fragment(
2192 markdown: &str,
2193 markdown_auto_wrap: bool,
2194) -> String {
2195 let markdown = markdown.replace("\r\n", "\n");
2196 if markdown.is_empty() {
2197 return String::new();
2198 }
2199
2200 let lines: Vec<&str> = markdown.split('\n').collect();
2201 let mut out = String::new();
2202 let mut paragraph_lines: Vec<&str> = Vec::new();
2203 let mut i = 0usize;
2204
2205 while i < lines.len() {
2206 let line = lines[i];
2207 if line.trim().is_empty() {
2208 if !paragraph_lines.is_empty() {
2209 out.push_str(&mermaid_markdown_paragraph_to_html(
2210 ¶graph_lines.join("\n"),
2211 markdown_auto_wrap,
2212 ));
2213 paragraph_lines.clear();
2214 }
2215 i += 1;
2216 continue;
2217 }
2218
2219 if mermaid_markdown_line_starts_raw_block(line) {
2220 if !paragraph_lines.is_empty() {
2221 out.push_str(&mermaid_markdown_paragraph_to_html(
2222 ¶graph_lines.join("\n"),
2223 markdown_auto_wrap,
2224 ));
2225 paragraph_lines.clear();
2226 }
2227
2228 let mut raw_block = String::from(line);
2229 i += 1;
2230 while i < lines.len() {
2231 let next = lines[i];
2232 if next.trim().is_empty() {
2233 break;
2234 }
2235 if mermaid_markdown_line_starts_raw_block(next) {
2236 raw_block.push('\n');
2237 raw_block.push_str(next);
2238 i += 1;
2239 continue;
2240 }
2241 break;
2242 }
2243 out.push_str(&mermaid_collapse_raw_html_label_text(&raw_block));
2244 continue;
2245 }
2246
2247 paragraph_lines.push(line);
2248 i += 1;
2249 }
2250
2251 if !paragraph_lines.is_empty() {
2252 out.push_str(&mermaid_markdown_paragraph_to_html(
2253 ¶graph_lines.join("\n"),
2254 markdown_auto_wrap,
2255 ));
2256 }
2257
2258 out
2259}
2260fn escape_xml_text_preserving_entities(raw: &str) -> String {
2261 fn is_valid_entity(entity: &str) -> bool {
2262 if entity.is_empty() {
2263 return false;
2264 }
2265 if let Some(hex) = entity
2266 .strip_prefix("#x")
2267 .or_else(|| entity.strip_prefix("#X"))
2268 {
2269 return !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit());
2270 }
2271 if let Some(dec) = entity.strip_prefix('#') {
2272 return !dec.is_empty() && dec.chars().all(|c| c.is_ascii_digit());
2273 }
2274 let mut it = entity.chars();
2275 let Some(first) = it.next() else {
2276 return false;
2277 };
2278 if !first.is_ascii_alphabetic() {
2279 return false;
2280 }
2281 it.all(|c| c.is_ascii_alphanumeric())
2282 }
2283
2284 fn escape_xml_segment(out: &mut String, raw: &str) {
2285 for ch in raw.chars() {
2286 match ch {
2287 '&' => out.push_str("&"),
2288 '<' => out.push_str("<"),
2289 '>' => out.push_str(">"),
2290 _ => out.push(ch),
2291 }
2292 }
2293 }
2294
2295 let mut out = String::with_capacity(raw.len());
2296 let mut i = 0usize;
2297 while let Some(rel) = raw[i..].find('&') {
2298 let amp = i + rel;
2299 escape_xml_segment(&mut out, &raw[i..amp]);
2300 let tail = &raw[amp + 1..];
2301 if let Some(semi_rel) = tail.find(';') {
2302 let semi = amp + 1 + semi_rel;
2303 let entity = &raw[amp + 1..semi];
2304 if is_valid_entity(entity) {
2305 out.push('&');
2306 out.push_str(entity);
2307 out.push(';');
2308 i = semi + 1;
2309 continue;
2310 }
2311 }
2312 out.push_str("&");
2313 i = amp + 1;
2314 }
2315 escape_xml_segment(&mut out, &raw[i..]);
2316 out
2317}
2318
2319fn mermaid_markdown_paragraph_to_xhtml(label: &str, markdown_auto_wrap: bool) -> String {
2320 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2321 enum Ty {
2322 Strong,
2323 Em,
2324 }
2325
2326 fn is_punctuation(ch: char) -> bool {
2327 !ch.is_whitespace() && !ch.is_alphanumeric()
2328 }
2329
2330 fn mermaid_delim_can_open_close(
2331 ch: char,
2332 prev: Option<char>,
2333 next: Option<char>,
2334 ) -> (bool, bool) {
2335 let prev_is_ws = prev.is_none_or(|c| c.is_whitespace());
2336 let next_is_ws = next.is_none_or(|c| c.is_whitespace());
2337 let prev_is_punct = prev.is_some_and(is_punctuation);
2338 let next_is_punct = next.is_some_and(is_punctuation);
2339
2340 let left_flanking = !next_is_ws && (!next_is_punct || prev_is_ws || prev_is_punct);
2341 let right_flanking = !prev_is_ws && (!prev_is_punct || next_is_ws || next_is_punct);
2342
2343 if ch == '_' {
2344 let can_open = left_flanking && (!right_flanking || prev_is_ws || prev_is_punct);
2345 let can_close = right_flanking && (!left_flanking || next_is_ws || next_is_punct);
2346 (can_open, can_close)
2347 } else {
2348 (left_flanking, right_flanking)
2349 }
2350 }
2351
2352 fn open_tag(ty: Ty) -> &'static str {
2353 match ty {
2354 Ty::Strong => "<strong>",
2355 Ty::Em => "<em>",
2356 }
2357 }
2358
2359 fn close_tag(ty: Ty) -> &'static str {
2360 match ty {
2361 Ty::Strong => "</strong>",
2362 Ty::Em => "</em>",
2363 }
2364 }
2365
2366 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2367 struct Delim {
2368 ty: Ty,
2369 ch: char,
2370 run_len: usize,
2371 token_index: usize,
2372 }
2373
2374 let s = label.replace("\r\n", "\n");
2375 let chars: Vec<char> = s.chars().collect();
2376 let mut tokens: Vec<String> = Vec::with_capacity(16);
2377 tokens.push("<p>".to_string());
2378
2379 let mut text_buf = String::new();
2380 let flush_text = |tokens: &mut Vec<String>, text_buf: &mut String| {
2381 if text_buf.is_empty() {
2382 return;
2383 }
2384 let raw = std::mem::take(text_buf);
2385 tokens.push(escape_xml_text_preserving_entities(&raw));
2386 };
2387
2388 let mut stack: Vec<Delim> = Vec::new();
2389 let mut in_code_span = false;
2390 let mut i = 0usize;
2391 while i < chars.len() {
2392 let ch = chars[i];
2393
2394 if ch == '\n' {
2395 while text_buf.ends_with(' ') {
2396 text_buf.pop();
2397 }
2398 flush_text(&mut tokens, &mut text_buf);
2399 tokens.push("<br/>".to_string());
2400 i += 1;
2401 while i < chars.len() && chars[i] == ' ' {
2402 i += 1;
2403 }
2404 continue;
2405 }
2406
2407 if ch == '`' {
2408 text_buf.push(ch);
2409 in_code_span = !in_code_span;
2410 i += 1;
2411 continue;
2412 }
2413
2414 if in_code_span {
2415 if ch == ' ' && !markdown_auto_wrap {
2416 text_buf.push_str(" ");
2417 } else {
2418 text_buf.push(ch);
2419 }
2420 i += 1;
2421 continue;
2422 }
2423
2424 if ch == '<' {
2425 if let Some(end_rel) = chars[i..].iter().position(|c| *c == '>') {
2426 let end = i + end_rel;
2427 flush_text(&mut tokens, &mut text_buf);
2428 let mut tag = String::new();
2429 for c in &chars[i..=end] {
2430 tag.push(*c);
2431 }
2432 if tag.eq_ignore_ascii_case("<br>")
2433 || tag.eq_ignore_ascii_case("<br/>")
2434 || tag.eq_ignore_ascii_case("<br />")
2435 || tag.eq_ignore_ascii_case("</br>")
2436 || tag.eq_ignore_ascii_case("</br/>")
2437 || tag.eq_ignore_ascii_case("</br />")
2438 || tag.eq_ignore_ascii_case("</br >")
2439 {
2440 tokens.push("<br/>".to_string());
2441 } else {
2442 tokens.push(tag);
2443 }
2444 i = end + 1;
2445 continue;
2446 }
2447 }
2448
2449 if ch == '*' || ch == '_' {
2450 let run_len = if i + 1 < chars.len() && chars[i + 1] == ch {
2451 2
2452 } else {
2453 1
2454 };
2455 let want = if run_len == 2 { Ty::Strong } else { Ty::Em };
2456 let prev = if i > 0 { Some(chars[i - 1]) } else { None };
2457 let next = if i + run_len < chars.len() {
2458 Some(chars[i + run_len])
2459 } else {
2460 None
2461 };
2462 let (can_open, can_close) = mermaid_delim_can_open_close(ch, prev, next);
2463
2464 flush_text(&mut tokens, &mut text_buf);
2465 let delim_text: String = std::iter::repeat_n(ch, run_len).collect();
2466
2467 if can_close
2468 && stack
2469 .last()
2470 .is_some_and(|d| d.ty == want && d.ch == ch && d.run_len == run_len)
2471 {
2472 let opener = stack.pop().unwrap();
2473 tokens[opener.token_index] = open_tag(want).to_string();
2474 tokens.push(close_tag(want).to_string());
2475 i += run_len;
2476 continue;
2477 }
2478 if ch == '*' && can_close {
2479 if run_len == 1
2480 && stack
2481 .last()
2482 .is_some_and(|d| d.ty == Ty::Strong && d.ch == '*' && d.run_len == 2)
2483 {
2484 let opener = stack.pop().unwrap();
2485 tokens[opener.token_index] = format!("*{}", open_tag(Ty::Em));
2486 tokens.push(close_tag(Ty::Em).to_string());
2487 i += 1;
2488 continue;
2489 }
2490 if run_len == 2
2491 && stack
2492 .last()
2493 .is_some_and(|d| d.ty == Ty::Em && d.ch == '*' && d.run_len == 1)
2494 {
2495 let opener = stack.pop().unwrap();
2496 tokens[opener.token_index] = open_tag(Ty::Em).to_string();
2497 tokens.push(close_tag(Ty::Em).to_string());
2498 tokens.push("*".to_string());
2499 i += 2;
2500 continue;
2501 }
2502 }
2503 if can_open {
2504 let token_index = tokens.len();
2505 tokens.push(delim_text);
2506 stack.push(Delim {
2507 ty: want,
2508 ch,
2509 run_len,
2510 token_index,
2511 });
2512 i += run_len;
2513 continue;
2514 }
2515
2516 tokens.push(delim_text);
2517 i += run_len;
2518 continue;
2519 }
2520
2521 if ch == ' ' && !markdown_auto_wrap {
2522 text_buf.push_str(" ");
2523 } else {
2524 text_buf.push(ch);
2525 }
2526 i += 1;
2527 }
2528
2529 while text_buf.ends_with(' ') {
2530 text_buf.pop();
2531 }
2532 flush_text(&mut tokens, &mut text_buf);
2533 tokens.push("</p>".to_string());
2534 tokens.concat()
2535}
2536
2537pub(crate) fn mermaid_markdown_to_xhtml_label_fragment(
2540 markdown: &str,
2541 markdown_auto_wrap: bool,
2542) -> String {
2543 let markdown = markdown.replace("\r\n", "\n");
2544 if markdown.is_empty() {
2545 return String::new();
2546 }
2547
2548 let lines: Vec<&str> = markdown.split('\n').collect();
2549 let mut out = String::new();
2550 let mut paragraph_lines: Vec<&str> = Vec::new();
2551 let mut i = 0usize;
2552
2553 while i < lines.len() {
2554 let line = lines[i];
2555 if line.trim().is_empty() {
2556 if !paragraph_lines.is_empty() {
2557 out.push_str(&mermaid_markdown_paragraph_to_xhtml(
2558 ¶graph_lines.join("\n"),
2559 markdown_auto_wrap,
2560 ));
2561 paragraph_lines.clear();
2562 }
2563 i += 1;
2564 continue;
2565 }
2566
2567 if mermaid_markdown_line_starts_raw_block(line) {
2568 if !paragraph_lines.is_empty() {
2569 out.push_str(&mermaid_markdown_paragraph_to_xhtml(
2570 ¶graph_lines.join("\n"),
2571 markdown_auto_wrap,
2572 ));
2573 paragraph_lines.clear();
2574 }
2575
2576 let mut raw_block = String::from(line);
2577 i += 1;
2578 while i < lines.len() {
2579 let next = lines[i];
2580 if next.trim().is_empty() {
2581 break;
2582 }
2583 if mermaid_markdown_line_starts_raw_block(next) {
2584 raw_block.push('\n');
2585 raw_block.push_str(next);
2586 i += 1;
2587 continue;
2588 }
2589 break;
2590 }
2591 out.push_str(&escape_xml_text_preserving_entities(
2592 &mermaid_collapse_raw_html_label_text(&raw_block),
2593 ));
2594 continue;
2595 }
2596
2597 paragraph_lines.push(line);
2598 i += 1;
2599 }
2600
2601 if !paragraph_lines.is_empty() {
2602 out.push_str(&mermaid_markdown_paragraph_to_xhtml(
2603 ¶graph_lines.join("\n"),
2604 markdown_auto_wrap,
2605 ));
2606 }
2607
2608 out
2609}
2610
2611pub(crate) fn mermaid_markdown_wants_paragraph_wrap(markdown: &str) -> bool {
2623 let s = markdown.trim_start();
2624 if s.is_empty() {
2625 return true;
2626 }
2627
2628 let mut i = 0usize;
2629 for ch in s.chars() {
2630 if ch == ' ' && i < 3 {
2631 i += 1;
2632 continue;
2633 }
2634 break;
2635 }
2636 let s = &s[i.min(s.len())..];
2637 let line = s.lines().next().unwrap_or(s).trim_end();
2638 !mermaid_markdown_line_starts_raw_block(line)
2639}
2640
2641#[cfg(test)]
2642mod tests;
2643
2644#[derive(Debug, Clone, Default)]
2645pub struct DeterministicTextMeasurer {
2646 pub char_width_factor: f64,
2647 pub line_height_factor: f64,
2648}
2649
2650impl DeterministicTextMeasurer {
2651 fn replace_br_variants(text: &str) -> String {
2652 let mut out = String::with_capacity(text.len());
2653 let mut i = 0usize;
2654 while i < text.len() {
2655 if text[i..].starts_with('<') {
2659 let bytes = text.as_bytes();
2660 if i + 3 < bytes.len()
2661 && matches!(bytes[i + 1], b'b' | b'B')
2662 && matches!(bytes[i + 2], b'r' | b'R')
2663 {
2664 let mut j = i + 3;
2665 while j < bytes.len() && matches!(bytes[j], b' ' | b'\t' | b'\r' | b'\n') {
2666 j += 1;
2667 }
2668 if j < bytes.len() && bytes[j] == b'/' {
2669 j += 1;
2670 }
2671 if j < bytes.len() && bytes[j] == b'>' {
2672 out.push('\n');
2673 i = j + 1;
2674 continue;
2675 }
2676 }
2677 }
2678
2679 let ch = text[i..].chars().next().unwrap();
2680 out.push(ch);
2681 i += ch.len_utf8();
2682 }
2683 out
2684 }
2685
2686 pub fn normalized_text_lines(text: &str) -> Vec<String> {
2687 let t = Self::replace_br_variants(text);
2688 let mut out = t.split('\n').map(|s| s.to_string()).collect::<Vec<_>>();
2689
2690 while out.len() > 1 && out.last().is_some_and(|s| s.trim().is_empty()) {
2694 out.pop();
2695 }
2696
2697 if out.is_empty() {
2698 vec!["".to_string()]
2699 } else {
2700 out
2701 }
2702 }
2703
2704 pub(crate) fn split_line_to_words(text: &str) -> Vec<String> {
2705 let parts = text.split(' ').collect::<Vec<_>>();
2708 let mut out: Vec<String> = Vec::new();
2709 for part in parts {
2710 if !part.is_empty() {
2711 out.push(part.to_string());
2712 }
2713 out.push(" ".to_string());
2714 }
2715 while out.last().is_some_and(|s| s == " ") {
2716 out.pop();
2717 }
2718 out
2719 }
2720
2721 fn wrap_line(line: &str, max_chars: usize, break_long_words: bool) -> Vec<String> {
2722 if max_chars == 0 {
2723 return vec![line.to_string()];
2724 }
2725
2726 let mut tokens = std::collections::VecDeque::from(Self::split_line_to_words(line));
2727 let mut out: Vec<String> = Vec::new();
2728 let mut cur = String::new();
2729
2730 while let Some(tok) = tokens.pop_front() {
2731 if cur.is_empty() && tok == " " {
2732 continue;
2733 }
2734
2735 let candidate = format!("{cur}{tok}");
2736 if candidate.chars().count() <= max_chars {
2737 cur = candidate;
2738 continue;
2739 }
2740
2741 if !cur.trim().is_empty() {
2742 out.push(cur.trim_end().to_string());
2743 cur.clear();
2744 tokens.push_front(tok);
2745 continue;
2746 }
2747
2748 if tok == " " {
2750 continue;
2751 }
2752 if !break_long_words {
2753 out.push(tok);
2754 } else {
2755 let tok_chars = tok.chars().collect::<Vec<_>>();
2757 let head: String = tok_chars.iter().take(max_chars.max(1)).collect();
2758 let tail: String = tok_chars.iter().skip(max_chars.max(1)).collect();
2759 out.push(head);
2760 if !tail.is_empty() {
2761 tokens.push_front(tail);
2762 }
2763 }
2764 }
2765
2766 if !cur.trim().is_empty() {
2767 out.push(cur.trim_end().to_string());
2768 }
2769
2770 if out.is_empty() {
2771 vec!["".to_string()]
2772 } else {
2773 out
2774 }
2775 }
2776}
2777
2778#[derive(Debug, Clone, Default)]
2779pub struct VendoredFontMetricsTextMeasurer {
2780 fallback: DeterministicTextMeasurer,
2781}
2782
2783impl VendoredFontMetricsTextMeasurer {
2784 #[allow(dead_code)]
2785 fn quantize_svg_px_nearest(v: f64) -> f64 {
2786 if !(v.is_finite() && v >= 0.0) {
2787 return 0.0;
2788 }
2789 let x = v * 256.0;
2794 let f = x.floor();
2795 let frac = x - f;
2796 let i = if frac < 0.5 {
2797 f
2798 } else if frac > 0.5 {
2799 f + 1.0
2800 } else {
2801 let fi = f as i64;
2802 if fi % 2 == 0 { f } else { f + 1.0 }
2803 };
2804 i / 256.0
2805 }
2806
2807 fn quantize_svg_bbox_px_nearest(v: f64) -> f64 {
2808 if !(v.is_finite() && v >= 0.0) {
2809 return 0.0;
2810 }
2811 let x = v * 1024.0;
2815 let f = x.floor();
2816 let frac = x - f;
2817 let i = if frac < 0.5 {
2818 f
2819 } else if frac > 0.5 {
2820 f + 1.0
2821 } else {
2822 let fi = f as i64;
2823 if fi % 2 == 0 { f } else { f + 1.0 }
2824 };
2825 i / 1024.0
2826 }
2827
2828 fn quantize_svg_half_px_nearest(half_px: f64) -> f64 {
2829 if !(half_px.is_finite() && half_px >= 0.0) {
2830 return 0.0;
2831 }
2832 (half_px * 256.0).floor() / 256.0
2836 }
2837
2838 fn normalize_font_key(s: &str) -> String {
2839 s.chars()
2840 .filter_map(|ch| {
2841 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
2845 None
2846 } else {
2847 Some(ch.to_ascii_lowercase())
2848 }
2849 })
2850 .collect()
2851 }
2852
2853 fn lookup_table(
2854 &self,
2855 style: &TextStyle,
2856 ) -> Option<&'static crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables> {
2857 let key = style
2858 .font_family
2859 .as_deref()
2860 .map(Self::normalize_font_key)
2861 .unwrap_or_default();
2862 let key = if key.is_empty() {
2863 FLOWCHART_DEFAULT_FONT_KEY
2866 } else {
2867 key.as_str()
2868 };
2869 if let Some(t) = crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(key)
2870 {
2871 return Some(t);
2872 }
2873
2874 let key_lower = key;
2877 if font_key_uses_courier_metrics(key_lower) {
2878 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
2879 "courier",
2880 );
2881 }
2882 if key_lower.contains("sans-serif") {
2886 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
2887 "sans-serif",
2888 );
2889 }
2890 None
2891 }
2892
2893 fn lookup_char_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
2894 let mut lo = 0usize;
2895 let mut hi = entries.len();
2896 while lo < hi {
2897 let mid = (lo + hi) / 2;
2898 match entries[mid].0.cmp(&ch) {
2899 std::cmp::Ordering::Equal => return entries[mid].1,
2900 std::cmp::Ordering::Less => lo = mid + 1,
2901 std::cmp::Ordering::Greater => hi = mid,
2902 }
2903 }
2904 if ch.is_ascii() {
2905 return default_em;
2906 }
2907
2908 match unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) {
2917 0 => 0.0,
2918 2.. => 1.0,
2919 _ => default_em,
2920 }
2921 }
2922
2923 fn lookup_kern_em(kern_pairs: &[(u32, u32, f64)], a: char, b: char) -> f64 {
2924 let key_a = a as u32;
2925 let key_b = b as u32;
2926 let mut lo = 0usize;
2927 let mut hi = kern_pairs.len();
2928 while lo < hi {
2929 let mid = (lo + hi) / 2;
2930 let (ma, mb, v) = kern_pairs[mid];
2931 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
2932 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
2933 (std::cmp::Ordering::Less, _) => lo = mid + 1,
2934 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
2935 _ => hi = mid,
2936 }
2937 }
2938 0.0
2939 }
2940
2941 fn lookup_space_trigram_em(space_trigrams: &[(u32, u32, f64)], a: char, b: char) -> f64 {
2942 let key_a = a as u32;
2943 let key_b = b as u32;
2944 let mut lo = 0usize;
2945 let mut hi = space_trigrams.len();
2946 while lo < hi {
2947 let mid = (lo + hi) / 2;
2948 let (ma, mb, v) = space_trigrams[mid];
2949 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
2950 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
2951 (std::cmp::Ordering::Less, _) => lo = mid + 1,
2952 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
2953 _ => hi = mid,
2954 }
2955 }
2956 0.0
2957 }
2958
2959 fn lookup_trigram_em(trigrams: &[(u32, u32, u32, f64)], a: char, b: char, c: char) -> f64 {
2960 let key_a = a as u32;
2961 let key_b = b as u32;
2962 let key_c = c as u32;
2963 let mut lo = 0usize;
2964 let mut hi = trigrams.len();
2965 while lo < hi {
2966 let mid = (lo + hi) / 2;
2967 let (ma, mb, mc, v) = trigrams[mid];
2968 match (ma.cmp(&key_a), mb.cmp(&key_b), mc.cmp(&key_c)) {
2969 (
2970 std::cmp::Ordering::Equal,
2971 std::cmp::Ordering::Equal,
2972 std::cmp::Ordering::Equal,
2973 ) => return v,
2974 (std::cmp::Ordering::Less, _, _) => lo = mid + 1,
2975 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less, _) => lo = mid + 1,
2976 (
2977 std::cmp::Ordering::Equal,
2978 std::cmp::Ordering::Equal,
2979 std::cmp::Ordering::Less,
2980 ) => lo = mid + 1,
2981 _ => hi = mid,
2982 }
2983 }
2984 0.0
2985 }
2986
2987 fn lookup_html_override_em(overrides: &[(&'static str, f64)], text: &str) -> Option<f64> {
2988 let mut lo = 0usize;
2989 let mut hi = overrides.len();
2990 while lo < hi {
2991 let mid = (lo + hi) / 2;
2992 let (k, v) = overrides[mid];
2993 match k.cmp(text) {
2994 std::cmp::Ordering::Equal => return Some(v),
2995 std::cmp::Ordering::Less => lo = mid + 1,
2996 std::cmp::Ordering::Greater => hi = mid,
2997 }
2998 }
2999 None
3000 }
3001
3002 fn lookup_svg_override_em(
3003 overrides: &[(&'static str, f64, f64)],
3004 text: &str,
3005 ) -> Option<(f64, f64)> {
3006 let mut lo = 0usize;
3007 let mut hi = overrides.len();
3008 while lo < hi {
3009 let mid = (lo + hi) / 2;
3010 let (k, l, r) = overrides[mid];
3011 match k.cmp(text) {
3012 std::cmp::Ordering::Equal => return Some((l, r)),
3013 std::cmp::Ordering::Less => lo = mid + 1,
3014 std::cmp::Ordering::Greater => hi = mid,
3015 }
3016 }
3017 None
3018 }
3019
3020 fn lookup_overhang_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
3021 let mut lo = 0usize;
3022 let mut hi = entries.len();
3023 while lo < hi {
3024 let mid = (lo + hi) / 2;
3025 match entries[mid].0.cmp(&ch) {
3026 std::cmp::Ordering::Equal => return entries[mid].1,
3027 std::cmp::Ordering::Less => lo = mid + 1,
3028 std::cmp::Ordering::Greater => hi = mid,
3029 }
3030 }
3031 default_em
3032 }
3033
3034 fn line_svg_bbox_extents_px(
3035 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3036 text: &str,
3037 font_size: f64,
3038 ) -> (f64, f64) {
3039 let t = text.trim_end();
3040 if t.is_empty() {
3041 return (0.0, 0.0);
3042 }
3043
3044 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
3045 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
3046 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
3047 return (left, right);
3048 }
3049
3050 if let Some((left, right)) =
3051 crate::generated::flowchart_text_overrides_11_12_2::lookup_flowchart_svg_bbox_x_px(
3052 table.font_key,
3053 font_size,
3054 t,
3055 )
3056 {
3057 return (left, right);
3058 }
3059
3060 let first = t.chars().next().unwrap_or(' ');
3061 let last = t.chars().last().unwrap_or(' ');
3062
3063 let advance_px_unscaled = {
3070 let words: Vec<&str> = t.split_whitespace().filter(|s| !s.is_empty()).collect();
3071 if words.len() >= 2 {
3072 let mut sum_px = 0.0f64;
3073 for (idx, w) in words.iter().enumerate() {
3074 if idx == 0 {
3075 sum_px += Self::line_width_px(
3076 table.entries,
3077 table.default_em.max(0.1),
3078 table.kern_pairs,
3079 table.space_trigrams,
3080 table.trigrams,
3081 w,
3082 false,
3083 font_size,
3084 );
3085 } else {
3086 let seg = format!(" {w}");
3087 sum_px += Self::line_width_px(
3088 table.entries,
3089 table.default_em.max(0.1),
3090 table.kern_pairs,
3091 table.space_trigrams,
3092 table.trigrams,
3093 &seg,
3094 false,
3095 font_size,
3096 );
3097 }
3098 }
3099 sum_px
3100 } else {
3101 Self::line_width_px(
3102 table.entries,
3103 table.default_em.max(0.1),
3104 table.kern_pairs,
3105 table.space_trigrams,
3106 table.trigrams,
3107 t,
3108 false,
3109 font_size,
3110 )
3111 }
3112 };
3113
3114 let advance_px = advance_px_unscaled * table.svg_scale;
3115 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
3116 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
3124 0.0
3125 } else {
3126 Self::lookup_overhang_em(
3127 table.svg_bbox_overhang_left,
3128 table.svg_bbox_overhang_left_default_em,
3129 first,
3130 )
3131 };
3132 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
3133 0.0
3134 } else {
3135 Self::lookup_overhang_em(
3136 table.svg_bbox_overhang_right,
3137 table.svg_bbox_overhang_right_default_em,
3138 last,
3139 )
3140 };
3141
3142 let left = (half + left_oh_em * font_size).max(0.0);
3143 let right = (half + right_oh_em * font_size).max(0.0);
3144 (left, right)
3145 }
3146
3147 fn line_svg_bbox_extents_px_single_run(
3148 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3149 text: &str,
3150 font_size: f64,
3151 ) -> (f64, f64) {
3152 let t = text.trim_end();
3153 if t.is_empty() {
3154 return (0.0, 0.0);
3155 }
3156
3157 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
3158 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
3159 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
3160 return (left, right);
3161 }
3162
3163 let first = t.chars().next().unwrap_or(' ');
3164 let last = t.chars().last().unwrap_or(' ');
3165
3166 let advance_px_unscaled = Self::line_width_px(
3169 table.entries,
3170 table.default_em.max(0.1),
3171 table.kern_pairs,
3172 table.space_trigrams,
3173 table.trigrams,
3174 t,
3175 false,
3176 font_size,
3177 );
3178
3179 let advance_px = advance_px_unscaled * table.svg_scale;
3180 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
3181
3182 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
3183 0.0
3184 } else {
3185 Self::lookup_overhang_em(
3186 table.svg_bbox_overhang_left,
3187 table.svg_bbox_overhang_left_default_em,
3188 first,
3189 )
3190 };
3191 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
3192 0.0
3193 } else {
3194 Self::lookup_overhang_em(
3195 table.svg_bbox_overhang_right,
3196 table.svg_bbox_overhang_right_default_em,
3197 last,
3198 )
3199 };
3200
3201 let left = (half + left_oh_em * font_size).max(0.0);
3202 let right = (half + right_oh_em * font_size).max(0.0);
3203 (left, right)
3204 }
3205
3206 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang(
3207 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3208 text: &str,
3209 font_size: f64,
3210 ) -> (f64, f64) {
3211 let t = text.trim_end();
3212 if t.is_empty() {
3213 return (0.0, 0.0);
3214 }
3215
3216 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
3217 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
3218 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
3219 return (left, right);
3220 }
3221
3222 let first = t.chars().next().unwrap_or(' ');
3223 let last = t.chars().last().unwrap_or(' ');
3224
3225 let advance_px_unscaled = Self::line_width_px(
3226 table.entries,
3227 table.default_em.max(0.1),
3228 table.kern_pairs,
3229 table.space_trigrams,
3230 table.trigrams,
3231 t,
3232 false,
3233 font_size,
3234 );
3235
3236 let advance_px = advance_px_unscaled * table.svg_scale;
3237 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
3238
3239 let left_oh_em = Self::lookup_overhang_em(
3240 table.svg_bbox_overhang_left,
3241 table.svg_bbox_overhang_left_default_em,
3242 first,
3243 );
3244 let right_oh_em = Self::lookup_overhang_em(
3245 table.svg_bbox_overhang_right,
3246 table.svg_bbox_overhang_right_default_em,
3247 last,
3248 );
3249
3250 let left = (half + left_oh_em * font_size).max(0.0);
3251 let right = (half + right_oh_em * font_size).max(0.0);
3252 (left, right)
3253 }
3254
3255 fn line_svg_bbox_width_px(
3256 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3257 text: &str,
3258 font_size: f64,
3259 ) -> f64 {
3260 let (l, r) = Self::line_svg_bbox_extents_px(table, text, font_size);
3261 (l + r).max(0.0)
3262 }
3263
3264 fn line_svg_bbox_width_single_run_px(
3265 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3266 text: &str,
3267 font_size: f64,
3268 ) -> f64 {
3269 let t = text.trim_end();
3270 if !t.is_empty() {
3271 if let Some((left_em, right_em)) =
3272 crate::generated::svg_overrides_sequence_11_12_2::lookup_svg_override_em(
3273 table.font_key,
3274 t,
3275 )
3276 {
3277 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
3278 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
3279 return (left + right).max(0.0);
3280 }
3281 }
3282
3283 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, text, font_size);
3284 (l + r).max(0.0)
3285 }
3286
3287 fn split_token_to_svg_bbox_width_px(
3288 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3289 tok: &str,
3290 max_width_px: f64,
3291 font_size: f64,
3292 ) -> (String, String) {
3293 if max_width_px <= 0.0 {
3294 return (tok.to_string(), String::new());
3295 }
3296 let chars = tok.chars().collect::<Vec<_>>();
3297 if chars.is_empty() {
3298 return (String::new(), String::new());
3299 }
3300
3301 let first = chars[0];
3302 let left_oh_em = if first.is_ascii() {
3303 0.0
3304 } else {
3305 Self::lookup_overhang_em(
3306 table.svg_bbox_overhang_left,
3307 table.svg_bbox_overhang_left_default_em,
3308 first,
3309 )
3310 };
3311
3312 let mut em = 0.0;
3313 let mut prev: Option<char> = None;
3314 let mut split_at = 1usize;
3315 for (idx, ch) in chars.iter().enumerate() {
3316 em += Self::lookup_char_em(table.entries, table.default_em.max(0.1), *ch);
3317 if let Some(p) = prev {
3318 em += Self::lookup_kern_em(table.kern_pairs, p, *ch);
3319 }
3320 prev = Some(*ch);
3321
3322 let right_oh_em = if ch.is_ascii() {
3323 0.0
3324 } else {
3325 Self::lookup_overhang_em(
3326 table.svg_bbox_overhang_right,
3327 table.svg_bbox_overhang_right_default_em,
3328 *ch,
3329 )
3330 };
3331 let half_px = Self::quantize_svg_half_px_nearest(
3332 (em * font_size * table.svg_scale / 2.0).max(0.0),
3333 );
3334 let w_px = 2.0 * half_px + (left_oh_em + right_oh_em) * font_size;
3335 if w_px.is_finite() && w_px <= max_width_px {
3336 split_at = idx + 1;
3337 } else if idx > 0 {
3338 break;
3339 }
3340 }
3341 let head = chars[..split_at].iter().collect::<String>();
3342 let tail = chars[split_at..].iter().collect::<String>();
3343 (head, tail)
3344 }
3345
3346 fn wrap_text_lines_svg_bbox_px(
3347 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
3348 text: &str,
3349 max_width_px: Option<f64>,
3350 font_size: f64,
3351 tokenize_whitespace: bool,
3352 ) -> Vec<String> {
3353 const EPS_PX: f64 = 0.125;
3354 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
3355 let width_fn = if tokenize_whitespace {
3356 Self::line_svg_bbox_width_px
3357 } else {
3358 Self::line_svg_bbox_width_single_run_px
3359 };
3360
3361 let mut lines = Vec::new();
3362 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
3363 let Some(w) = max_width_px else {
3364 lines.push(line);
3365 continue;
3366 };
3367
3368 let mut tokens = std::collections::VecDeque::from(
3369 DeterministicTextMeasurer::split_line_to_words(&line),
3370 );
3371 let mut out: Vec<String> = Vec::new();
3372 let mut cur = String::new();
3373
3374 while let Some(tok) = tokens.pop_front() {
3375 if cur.is_empty() && tok == " " {
3376 continue;
3377 }
3378
3379 let candidate = format!("{cur}{tok}");
3380 let candidate_trimmed = candidate.trim_end();
3381 if width_fn(table, candidate_trimmed, font_size) <= w + EPS_PX {
3382 cur = candidate;
3383 continue;
3384 }
3385
3386 if !cur.trim().is_empty() {
3387 out.push(cur.trim_end().to_string());
3388 cur.clear();
3389 tokens.push_front(tok);
3390 continue;
3391 }
3392
3393 if tok == " " {
3394 continue;
3395 }
3396
3397 if width_fn(table, tok.as_str(), font_size) <= w + EPS_PX {
3398 cur = tok;
3399 continue;
3400 }
3401
3402 let (head, tail) =
3404 Self::split_token_to_svg_bbox_width_px(table, &tok, w + EPS_PX, font_size);
3405 out.push(head);
3406 if !tail.is_empty() {
3407 tokens.push_front(tail);
3408 }
3409 }
3410
3411 if !cur.trim().is_empty() {
3412 out.push(cur.trim_end().to_string());
3413 }
3414
3415 if out.is_empty() {
3416 lines.push("".to_string());
3417 } else {
3418 lines.extend(out);
3419 }
3420 }
3421
3422 if lines.is_empty() {
3423 vec!["".to_string()]
3424 } else {
3425 lines
3426 }
3427 }
3428
3429 fn line_width_px(
3430 entries: &[(char, f64)],
3431 default_em: f64,
3432 kern_pairs: &[(u32, u32, f64)],
3433 space_trigrams: &[(u32, u32, f64)],
3434 trigrams: &[(u32, u32, u32, f64)],
3435 text: &str,
3436 bold: bool,
3437 font_size: f64,
3438 ) -> f64 {
3439 fn normalize_whitespace_like(ch: char) -> (char, f64) {
3440 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
3448 if ch == '\u{00A0}' {
3449 (' ', NBSP_DELTA_EM)
3450 } else {
3451 (ch, 0.0)
3452 }
3453 }
3454
3455 let mut em = 0.0;
3456 let mut prevprev: Option<char> = None;
3457 let mut prev: Option<char> = None;
3458 for ch in text.chars() {
3459 let (ch, delta_em) = normalize_whitespace_like(ch);
3460 em += Self::lookup_char_em(entries, default_em, ch) + delta_em;
3461 if let Some(p) = prev {
3462 em += Self::lookup_kern_em(kern_pairs, p, ch);
3463 }
3464 if bold {
3465 if let Some(p) = prev {
3466 em += flowchart_default_bold_kern_delta_em(p, ch);
3467 }
3468 em += flowchart_default_bold_delta_em(ch);
3469 }
3470 if let (Some(a), Some(b)) = (prevprev, prev) {
3471 if b == ' ' {
3472 if !(a.is_whitespace() || ch.is_whitespace()) {
3473 em += Self::lookup_space_trigram_em(space_trigrams, a, ch);
3474 }
3475 } else if !(a.is_whitespace() || b.is_whitespace() || ch.is_whitespace()) {
3476 em += Self::lookup_trigram_em(trigrams, a, b, ch);
3477 }
3478 }
3479 prevprev = prev;
3480 prev = Some(ch);
3481 }
3482 em * font_size
3483 }
3484
3485 #[allow(dead_code)]
3486 fn ceil_to_1_64_px(v: f64) -> f64 {
3487 if !(v.is_finite() && v >= 0.0) {
3488 return 0.0;
3489 }
3490 let x = v * 64.0;
3492 let r = x.round();
3493 if (x - r).abs() < 1e-4 {
3494 return r / 64.0;
3495 }
3496 ((x) - 1e-5).ceil() / 64.0
3497 }
3498
3499 fn split_token_to_width_px(
3500 entries: &[(char, f64)],
3501 default_em: f64,
3502 kern_pairs: &[(u32, u32, f64)],
3503 trigrams: &[(u32, u32, u32, f64)],
3504 tok: &str,
3505 max_width_px: f64,
3506 bold: bool,
3507 font_size: f64,
3508 ) -> (String, String) {
3509 fn normalize_whitespace_like(ch: char) -> (char, f64) {
3510 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
3511 if ch == '\u{00A0}' {
3512 (' ', NBSP_DELTA_EM)
3513 } else {
3514 (ch, 0.0)
3515 }
3516 }
3517
3518 if max_width_px <= 0.0 {
3519 return (tok.to_string(), String::new());
3520 }
3521 let max_em = max_width_px / font_size.max(1.0);
3522 let mut em = 0.0;
3523 let mut prevprev: Option<char> = None;
3524 let mut prev: Option<char> = None;
3525 let chars = tok.chars().collect::<Vec<_>>();
3526 let mut split_at = 0usize;
3527 for (idx, ch) in chars.iter().enumerate() {
3528 let (ch_norm, delta_em) = normalize_whitespace_like(*ch);
3529 em += Self::lookup_char_em(entries, default_em, ch_norm) + delta_em;
3530 if let Some(p) = prev {
3531 em += Self::lookup_kern_em(kern_pairs, p, ch_norm);
3532 }
3533 if bold {
3534 if let Some(p) = prev {
3535 em += flowchart_default_bold_kern_delta_em(p, ch_norm);
3536 }
3537 em += flowchart_default_bold_delta_em(ch_norm);
3538 }
3539 if let (Some(a), Some(b)) = (prevprev, prev) {
3540 if !(a.is_whitespace() || b.is_whitespace() || ch_norm.is_whitespace()) {
3541 em += Self::lookup_trigram_em(trigrams, a, b, ch_norm);
3542 }
3543 }
3544 prevprev = prev;
3545 prev = Some(ch_norm);
3546 if em > max_em && idx > 0 {
3547 break;
3548 }
3549 split_at = idx + 1;
3550 if em >= max_em {
3551 break;
3552 }
3553 }
3554 if split_at == 0 {
3555 split_at = 1.min(chars.len());
3556 }
3557 let head = chars.iter().take(split_at).collect::<String>();
3558 let tail = chars.iter().skip(split_at).collect::<String>();
3559 (head, tail)
3560 }
3561
3562 fn wrap_line_to_width_px(
3563 entries: &[(char, f64)],
3564 default_em: f64,
3565 kern_pairs: &[(u32, u32, f64)],
3566 space_trigrams: &[(u32, u32, f64)],
3567 trigrams: &[(u32, u32, u32, f64)],
3568 line: &str,
3569 max_width_px: f64,
3570 font_size: f64,
3571 break_long_words: bool,
3572 bold: bool,
3573 ) -> Vec<String> {
3574 fn split_html_breakable_segments(tok: &str) -> Vec<String> {
3575 fn is_break_after(ch: char) -> bool {
3582 matches!(
3583 ch,
3584 '/' | '-' | ':' | '?' | '&' | '#' | ')' | ']' | '}' | '.'
3585 )
3586 }
3587
3588 let mut out: Vec<String> = Vec::new();
3589 let mut cur = String::new();
3590 for ch in tok.chars() {
3591 cur.push(ch);
3592 if is_break_after(ch) {
3593 if !cur.is_empty() {
3594 out.push(std::mem::take(&mut cur));
3595 }
3596 }
3597 }
3598 if !cur.is_empty() {
3599 out.push(cur);
3600 }
3601 if out.len() <= 1 {
3602 vec![tok.to_string()]
3603 } else {
3604 out
3605 }
3606 }
3607
3608 let max_width_px = if break_long_words {
3613 max_width_px
3614 } else {
3615 max_width_px + (1.0 / 64.0)
3616 };
3617
3618 let mut tokens =
3619 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
3620 let mut out: Vec<String> = Vec::new();
3621 let mut cur = String::new();
3622
3623 while let Some(tok) = tokens.pop_front() {
3624 if cur.is_empty() && tok == " " {
3625 continue;
3626 }
3627
3628 let candidate = format!("{cur}{tok}");
3629 let candidate_trimmed = candidate.trim_end();
3630 if Self::line_width_px(
3631 entries,
3632 default_em,
3633 kern_pairs,
3634 space_trigrams,
3635 trigrams,
3636 candidate_trimmed,
3637 bold,
3638 font_size,
3639 ) <= max_width_px
3640 {
3641 cur = candidate;
3642 continue;
3643 }
3644
3645 if !break_long_words && tok != " " && !cur.trim().is_empty() {
3646 let segments = split_html_breakable_segments(&tok);
3650 if segments.len() > 1 {
3651 let mut cur_candidate = cur.clone();
3652 let mut consumed = 0usize;
3653 for seg in &segments {
3654 let candidate = format!("{cur_candidate}{seg}");
3655 let candidate_trimmed = candidate.trim_end();
3656 if Self::line_width_px(
3657 entries,
3658 default_em,
3659 kern_pairs,
3660 space_trigrams,
3661 trigrams,
3662 candidate_trimmed,
3663 bold,
3664 font_size,
3665 ) <= max_width_px
3666 {
3667 cur_candidate = candidate;
3668 consumed += 1;
3669 } else {
3670 break;
3671 }
3672 }
3673 if consumed > 0 {
3674 cur = cur_candidate;
3675 for seg in segments.into_iter().skip(consumed).rev() {
3676 tokens.push_front(seg);
3677 }
3678 continue;
3679 }
3680 }
3681 }
3682
3683 if !cur.trim().is_empty() {
3684 out.push(cur.trim_end().to_string());
3685 cur.clear();
3686 }
3687
3688 if tok == " " {
3689 continue;
3690 }
3691
3692 if Self::line_width_px(
3693 entries,
3694 default_em,
3695 kern_pairs,
3696 space_trigrams,
3697 trigrams,
3698 tok.as_str(),
3699 bold,
3700 font_size,
3701 ) <= max_width_px
3702 {
3703 cur = tok;
3704 continue;
3705 }
3706
3707 if !break_long_words {
3708 let segments = split_html_breakable_segments(&tok);
3709 if segments.len() > 1 {
3710 for seg in segments.into_iter().rev() {
3711 tokens.push_front(seg);
3712 }
3713 continue;
3714 }
3715 out.push(tok);
3716 continue;
3717 }
3718
3719 let (head, tail) = Self::split_token_to_width_px(
3720 entries,
3721 default_em,
3722 kern_pairs,
3723 trigrams,
3724 &tok,
3725 max_width_px,
3726 bold,
3727 font_size,
3728 );
3729 out.push(head);
3730 if !tail.is_empty() {
3731 tokens.push_front(tail);
3732 }
3733 }
3734
3735 if !cur.trim().is_empty() {
3736 out.push(cur.trim_end().to_string());
3737 }
3738
3739 if out.is_empty() {
3740 vec!["".to_string()]
3741 } else {
3742 out
3743 }
3744 }
3745
3746 fn wrap_text_lines_px(
3747 entries: &[(char, f64)],
3748 default_em: f64,
3749 kern_pairs: &[(u32, u32, f64)],
3750 space_trigrams: &[(u32, u32, f64)],
3751 trigrams: &[(u32, u32, u32, f64)],
3752 text: &str,
3753 style: &TextStyle,
3754 bold: bool,
3755 max_width_px: Option<f64>,
3756 wrap_mode: WrapMode,
3757 ) -> Vec<String> {
3758 let font_size = style.font_size.max(1.0);
3759 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
3760 let break_long_words = wrap_mode == WrapMode::SvgLike;
3761
3762 let mut lines = Vec::new();
3763 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
3764 if let Some(w) = max_width_px {
3765 lines.extend(Self::wrap_line_to_width_px(
3766 entries,
3767 default_em,
3768 kern_pairs,
3769 space_trigrams,
3770 trigrams,
3771 &line,
3772 w,
3773 font_size,
3774 break_long_words,
3775 bold,
3776 ));
3777 } else {
3778 lines.push(line);
3779 }
3780 }
3781
3782 if lines.is_empty() {
3783 vec!["".to_string()]
3784 } else {
3785 lines
3786 }
3787 }
3788}
3789
3790fn vendored_measure_wrapped_impl(
3791 measurer: &VendoredFontMetricsTextMeasurer,
3792 text: &str,
3793 style: &TextStyle,
3794 max_width: Option<f64>,
3795 wrap_mode: WrapMode,
3796 use_html_overrides: bool,
3797) -> (TextMetrics, Option<f64>) {
3798 let Some(table) = measurer.lookup_table(style) else {
3799 return measurer
3800 .fallback
3801 .measure_wrapped_with_raw_width(text, style, max_width, wrap_mode);
3802 };
3803
3804 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
3805 let font_size = style.font_size.max(1.0);
3806 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
3807 let line_height_factor = match wrap_mode {
3808 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
3809 WrapMode::HtmlLike => 1.5,
3810 };
3811
3812 let html_overrides: &[(&'static str, f64)] = if use_html_overrides {
3813 table.html_overrides
3814 } else {
3815 &[]
3816 };
3817
3818 let html_override_px = |em: f64| -> f64 {
3819 if (font_size - table.base_font_size_px).abs() < 0.01 {
3827 em * font_size
3828 } else {
3829 em * table.base_font_size_px
3830 }
3831 };
3832
3833 let html_width_override_px = |line: &str| -> Option<f64> {
3834 crate::generated::flowchart_text_overrides_11_12_2::lookup_flowchart_html_width_px(
3839 table.font_key,
3840 font_size,
3841 line,
3842 )
3843 .or_else(|| {
3844 if max_width.is_some() {
3845 return None;
3846 }
3847 if table.font_key != "trebuchetms,verdana,arial,sans-serif" {
3848 return None;
3849 }
3850 crate::generated::er_text_overrides_11_12_2::lookup_html_width_px(font_size, line)
3855 .or_else(|| {
3856 crate::generated::mindmap_text_overrides_11_12_2::lookup_html_width_px(
3857 font_size, line,
3858 )
3859 })
3860 .or_else(|| {
3861 crate::generated::block_text_overrides_11_12_2::lookup_html_width_px(
3862 font_size, line,
3863 )
3864 })
3865 })
3866 };
3867
3868 let raw_width_unscaled = if wrap_mode == WrapMode::HtmlLike {
3877 let mut raw_w: f64 = 0.0;
3878 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
3879 if let Some(w) = html_width_override_px(&line) {
3880 raw_w = raw_w.max(w);
3881 continue;
3882 }
3883 if let Some(em) =
3884 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, &line)
3885 {
3886 raw_w = raw_w.max(html_override_px(em));
3887 } else {
3888 raw_w = raw_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
3889 table.entries,
3890 table.default_em.max(0.1),
3891 table.kern_pairs,
3892 table.space_trigrams,
3893 table.trigrams,
3894 &line,
3895 bold,
3896 font_size,
3897 ));
3898 }
3899 }
3900 Some(raw_w)
3901 } else {
3902 None
3903 };
3904
3905 fn split_html_min_content_segments(tok: &str) -> Vec<String> {
3918 fn is_break_after(ch: char) -> bool {
3924 matches!(ch, '-' | '?' | '&' | '#')
3925 }
3926
3927 let mut out: Vec<String> = Vec::new();
3928 let mut cur = String::new();
3929 for ch in tok.chars() {
3930 cur.push(ch);
3931 if is_break_after(ch) && !cur.is_empty() {
3932 out.push(std::mem::take(&mut cur));
3933 }
3934 }
3935 if !cur.is_empty() {
3936 out.push(cur);
3937 }
3938 if out.len() <= 1 {
3939 vec![tok.to_string()]
3940 } else {
3941 out
3942 }
3943 }
3944
3945 let html_min_content_width = if wrap_mode == WrapMode::HtmlLike && max_width.is_some() {
3946 let mut max_word_w: f64 = 0.0;
3947 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
3948 for part in line.split(' ') {
3949 let part = part.trim();
3950 if part.is_empty() {
3951 continue;
3952 }
3953 for seg in split_html_min_content_segments(part) {
3954 max_word_w = max_word_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
3955 table.entries,
3956 table.default_em.max(0.1),
3957 table.kern_pairs,
3958 table.space_trigrams,
3959 table.trigrams,
3960 seg.as_str(),
3961 bold,
3962 font_size,
3963 ));
3964 }
3965 }
3966 }
3967 if max_word_w.is_finite() && max_word_w > 0.0 {
3968 Some(max_word_w)
3969 } else {
3970 None
3971 }
3972 } else {
3973 None
3974 };
3975
3976 let lines = match wrap_mode {
3977 WrapMode::HtmlLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_px(
3978 table.entries,
3979 table.default_em.max(0.1),
3980 table.kern_pairs,
3981 table.space_trigrams,
3982 table.trigrams,
3983 text,
3984 style,
3985 bold,
3986 max_width,
3987 wrap_mode,
3988 ),
3989 WrapMode::SvgLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
3990 table, text, max_width, font_size, true,
3991 ),
3992 WrapMode::SvgLikeSingleRun => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
3993 table, text, max_width, font_size, false,
3994 ),
3995 };
3996
3997 let mut width: f64 = 0.0;
3998 match wrap_mode {
3999 WrapMode::HtmlLike => {
4000 for line in &lines {
4001 if let Some(w) = html_width_override_px(line) {
4002 width = width.max(w);
4003 continue;
4004 }
4005 if let Some(em) =
4006 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, line)
4007 {
4008 width = width.max(html_override_px(em));
4009 } else {
4010 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
4011 table.entries,
4012 table.default_em.max(0.1),
4013 table.kern_pairs,
4014 table.space_trigrams,
4015 table.trigrams,
4016 line,
4017 bold,
4018 font_size,
4019 ));
4020 }
4021 }
4022 }
4023 WrapMode::SvgLike => {
4024 for line in &lines {
4025 width = width.max(VendoredFontMetricsTextMeasurer::line_svg_bbox_width_px(
4026 table, line, font_size,
4027 ));
4028 }
4029 }
4030 WrapMode::SvgLikeSingleRun => {
4031 for line in &lines {
4032 width = width.max(
4033 VendoredFontMetricsTextMeasurer::line_svg_bbox_width_single_run_px(
4034 table, line, font_size,
4035 ),
4036 );
4037 }
4038 }
4039 }
4040
4041 if wrap_mode == WrapMode::HtmlLike {
4045 let needs_wrap = max_width.is_some_and(|w| raw_width_unscaled.is_some_and(|rw| rw > w));
4046 if let Some(w) = max_width {
4047 if needs_wrap {
4048 width = width.max(w);
4049 } else {
4050 width = width.min(w);
4051 }
4052 }
4053 if needs_wrap {
4054 if let Some(w) = html_min_content_width {
4055 width = width.max(w);
4056 }
4057 }
4058 width = round_to_1_64_px(width);
4061 if let Some(w) = max_width {
4062 width = if needs_wrap {
4063 width.max(w)
4064 } else {
4065 width.min(w)
4066 };
4067 }
4068 }
4069
4070 let height = match wrap_mode {
4071 WrapMode::HtmlLike => lines.len() as f64 * font_size * line_height_factor,
4072 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
4073 if lines.is_empty() {
4074 0.0
4075 } else {
4076 let first_line_h = svg_wrapped_first_line_bbox_height_px(style);
4082 let additional = (lines.len().saturating_sub(1)) as f64 * font_size * 1.1;
4083 first_line_h + additional
4084 }
4085 }
4086 };
4087
4088 let metrics = TextMetrics {
4089 width,
4090 height,
4091 line_count: lines.len(),
4092 };
4093 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
4094 raw_width_unscaled
4095 } else {
4096 None
4097 };
4098 (metrics, raw_width_px)
4099}
4100
4101impl TextMeasurer for VendoredFontMetricsTextMeasurer {
4102 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
4103 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
4104 }
4105
4106 fn measure_svg_text_computed_length_px(&self, text: &str, style: &TextStyle) -> f64 {
4107 let Some(table) = self.lookup_table(style) else {
4108 return self
4109 .fallback
4110 .measure_svg_text_computed_length_px(text, style);
4111 };
4112
4113 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
4114 let font_size = style.font_size.max(1.0);
4115 let mut width: f64 = 0.0;
4116 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
4117 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
4118 table.entries,
4119 table.default_em.max(0.1),
4120 table.kern_pairs,
4121 table.space_trigrams,
4122 table.trigrams,
4123 &line,
4124 bold,
4125 font_size,
4126 ));
4127 }
4128 if width.is_finite() && width >= 0.0 {
4129 width
4130 } else {
4131 0.0
4132 }
4133 }
4134
4135 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
4136 let Some(table) = self.lookup_table(style) else {
4137 return self.fallback.measure_svg_text_bbox_x(text, style);
4138 };
4139
4140 let font_size = style.font_size.max(1.0);
4141 let mut left: f64 = 0.0;
4142 let mut right: f64 = 0.0;
4143 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
4144 let (l, r) = Self::line_svg_bbox_extents_px(table, &line, font_size);
4145 left = left.max(l);
4146 right = right.max(r);
4147 }
4148 (left, right)
4149 }
4150
4151 fn measure_svg_text_bbox_x_with_ascii_overhang(
4152 &self,
4153 text: &str,
4154 style: &TextStyle,
4155 ) -> (f64, f64) {
4156 let Some(table) = self.lookup_table(style) else {
4157 return self
4158 .fallback
4159 .measure_svg_text_bbox_x_with_ascii_overhang(text, style);
4160 };
4161
4162 let font_size = style.font_size.max(1.0);
4163 let mut left: f64 = 0.0;
4164 let mut right: f64 = 0.0;
4165 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
4166 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
4167 table, &line, font_size,
4168 );
4169 left = left.max(l);
4170 right = right.max(r);
4171 }
4172 (left, right)
4173 }
4174
4175 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
4176 let Some(table) = self.lookup_table(style) else {
4177 return self.fallback.measure_svg_title_bbox_x(text, style);
4178 };
4179
4180 let font_size = style.font_size.max(1.0);
4181 let mut left: f64 = 0.0;
4182 let mut right: f64 = 0.0;
4183 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
4184 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, &line, font_size);
4185 left = left.max(l);
4186 right = right.max(r);
4187 }
4188 (left, right)
4189 }
4190
4191 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
4192 let Some(table) = self.lookup_table(style) else {
4193 return self
4194 .fallback
4195 .measure_svg_simple_text_bbox_width_px(text, style);
4196 };
4197
4198 let font_size = style.font_size.max(1.0);
4199 let mut width: f64 = 0.0;
4200 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
4201 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
4202 table, &line, font_size,
4203 );
4204 width = width.max((l + r).max(0.0));
4205 }
4206 width
4207 }
4208
4209 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
4210 let t = text.trim_end();
4211 if t.is_empty() {
4212 return 0.0;
4213 }
4214 let font_size = style.font_size.max(1.0);
4217 (font_size * 1.1).max(0.0)
4218 }
4219
4220 fn measure_wrapped(
4221 &self,
4222 text: &str,
4223 style: &TextStyle,
4224 max_width: Option<f64>,
4225 wrap_mode: WrapMode,
4226 ) -> TextMetrics {
4227 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true).0
4228 }
4229
4230 fn measure_wrapped_with_raw_width(
4231 &self,
4232 text: &str,
4233 style: &TextStyle,
4234 max_width: Option<f64>,
4235 wrap_mode: WrapMode,
4236 ) -> (TextMetrics, Option<f64>) {
4237 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true)
4238 }
4239
4240 fn measure_wrapped_raw(
4241 &self,
4242 text: &str,
4243 style: &TextStyle,
4244 max_width: Option<f64>,
4245 wrap_mode: WrapMode,
4246 ) -> TextMetrics {
4247 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, false).0
4248 }
4249}
4250
4251impl TextMeasurer for DeterministicTextMeasurer {
4252 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
4253 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
4254 }
4255
4256 fn measure_wrapped(
4257 &self,
4258 text: &str,
4259 style: &TextStyle,
4260 max_width: Option<f64>,
4261 wrap_mode: WrapMode,
4262 ) -> TextMetrics {
4263 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
4264 .0
4265 }
4266
4267 fn measure_wrapped_with_raw_width(
4268 &self,
4269 text: &str,
4270 style: &TextStyle,
4271 max_width: Option<f64>,
4272 wrap_mode: WrapMode,
4273 ) -> (TextMetrics, Option<f64>) {
4274 self.measure_wrapped_impl(text, style, max_width, wrap_mode, true)
4275 }
4276
4277 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
4278 let t = text.trim_end();
4279 if t.is_empty() {
4280 return 0.0;
4281 }
4282 (style.font_size.max(1.0) * 1.1).max(0.0)
4283 }
4284}
4285
4286impl DeterministicTextMeasurer {
4287 fn measure_wrapped_impl(
4288 &self,
4289 text: &str,
4290 style: &TextStyle,
4291 max_width: Option<f64>,
4292 wrap_mode: WrapMode,
4293 clamp_html_width: bool,
4294 ) -> (TextMetrics, Option<f64>) {
4295 let uses_heuristic_widths = self.char_width_factor == 0.0;
4296 let char_width_factor = if uses_heuristic_widths {
4297 match wrap_mode {
4298 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 0.6,
4299 WrapMode::HtmlLike => 0.5,
4300 }
4301 } else {
4302 self.char_width_factor
4303 };
4304 let default_line_height_factor = match wrap_mode {
4305 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
4306 WrapMode::HtmlLike => 1.5,
4307 };
4308 let line_height_factor = if self.line_height_factor == 0.0 {
4309 default_line_height_factor
4310 } else {
4311 self.line_height_factor
4312 };
4313
4314 let font_size = style.font_size.max(1.0);
4315 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
4316 let break_long_words = matches!(wrap_mode, WrapMode::SvgLike | WrapMode::SvgLikeSingleRun);
4317
4318 let raw_lines = Self::normalized_text_lines(text);
4319 let mut raw_width: f64 = 0.0;
4320 for line in &raw_lines {
4321 let w = if uses_heuristic_widths {
4322 estimate_line_width_px(line, font_size)
4323 } else {
4324 line.chars().count() as f64 * font_size * char_width_factor
4325 };
4326 raw_width = raw_width.max(w);
4327 }
4328 let needs_wrap =
4329 wrap_mode == WrapMode::HtmlLike && max_width.is_some_and(|w| raw_width > w);
4330
4331 let mut lines = Vec::new();
4332 for line in raw_lines {
4333 if let Some(w) = max_width {
4334 let char_px = font_size * char_width_factor;
4335 let max_chars = ((w / char_px).floor() as isize).max(1) as usize;
4336 lines.extend(Self::wrap_line(&line, max_chars, break_long_words));
4337 } else {
4338 lines.push(line);
4339 }
4340 }
4341
4342 let mut width: f64 = 0.0;
4343 for line in &lines {
4344 let w = if uses_heuristic_widths {
4345 estimate_line_width_px(line, font_size)
4346 } else {
4347 line.chars().count() as f64 * font_size * char_width_factor
4348 };
4349 width = width.max(w);
4350 }
4351 if clamp_html_width && wrap_mode == WrapMode::HtmlLike {
4355 if let Some(w) = max_width {
4356 if needs_wrap {
4357 width = w;
4358 } else {
4359 width = width.min(w);
4360 }
4361 }
4362 }
4363 let height = lines.len() as f64 * font_size * line_height_factor;
4364 let metrics = TextMetrics {
4365 width,
4366 height,
4367 line_count: lines.len(),
4368 };
4369 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
4370 Some(raw_width)
4371 } else {
4372 None
4373 };
4374 (metrics, raw_width_px)
4375 }
4376}
4377
4378fn estimate_line_width_px(line: &str, font_size: f64) -> f64 {
4379 let mut em = 0.0;
4380 for ch in line.chars() {
4381 em += estimate_char_width_em(ch);
4382 }
4383 em * font_size
4384}
4385
4386fn estimate_char_width_em(ch: char) -> f64 {
4387 if ch == ' ' {
4388 return 0.33;
4389 }
4390 if ch == '\t' {
4391 return 0.66;
4392 }
4393 if ch == '_' || ch == '-' {
4394 return 0.33;
4395 }
4396 if matches!(ch, '.' | ',' | ':' | ';') {
4397 return 0.28;
4398 }
4399 if matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '/') {
4400 return 0.33;
4401 }
4402 if matches!(ch, '+' | '*' | '=' | '\\' | '^' | '|' | '~') {
4403 return 0.45;
4404 }
4405 if ch.is_ascii_digit() {
4406 return 0.56;
4407 }
4408 if ch.is_ascii_uppercase() {
4409 return match ch {
4410 'I' => 0.30,
4411 'W' => 0.85,
4412 _ => 0.60,
4413 };
4414 }
4415 if ch.is_ascii_lowercase() {
4416 return match ch {
4417 'i' | 'l' => 0.28,
4418 'm' | 'w' => 0.78,
4419 'k' | 'y' => 0.55,
4420 _ => 0.43,
4421 };
4422 }
4423 0.60
4425}