merman_render/text/measure.rs
1//! Text measurement trait shared by renderers and wrapping helpers.
2
3use super::{TextMetrics, TextStyle, WrapMode};
4
5pub trait TextMeasurer {
6 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics;
7
8 /// Measures SVG `<tspan>.getComputedTextLength()`-like widths (advance length along the
9 /// baseline).
10 ///
11 /// Mermaid's Timeline diagram uses `getComputedTextLength()` to decide when to wrap tokens
12 /// into additional `<tspan>` lines. This length can differ meaningfully from `getBBox().width`
13 /// (which includes glyph overhang), especially near wrapping boundaries.
14 ///
15 /// Default implementation falls back to bbox-derived widths.
16 fn measure_svg_text_computed_length_px(&self, text: &str, style: &TextStyle) -> f64 {
17 self.measure_svg_simple_text_bbox_width_px(text, style)
18 }
19
20 /// Measures the horizontal extents of an SVG `<text>` element relative to its anchor `x`.
21 ///
22 /// Mermaid's flowchart-v2 viewport sizing uses `getBBox()` on the rendered SVG. For `<text>`
23 /// elements this bbox can be slightly asymmetric around the anchor due to glyph overhangs.
24 ///
25 /// Default implementation assumes a symmetric bbox: `left = right = width/2`.
26 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
27 let m = self.measure(text, style);
28 let half = (m.width.max(0.0)) / 2.0;
29 (half, half)
30 }
31
32 /// Measures SVG `<text>.getBBox()` horizontal extents while including ASCII overhang.
33 ///
34 /// Upstream Mermaid bbox behavior can be asymmetric even for ASCII strings due to glyph
35 /// outlines and hinting. Most diagrams in this codebase intentionally ignore ASCII overhang
36 /// to avoid systematic `viewBox` drift, but some diagrams (notably `timeline`) rely on the
37 /// actual `getBBox()` extents when labels can overflow node shapes.
38 ///
39 /// Default implementation falls back to the symmetric bbox measurement.
40 fn measure_svg_text_bbox_x_with_ascii_overhang(
41 &self,
42 text: &str,
43 style: &TextStyle,
44 ) -> (f64, f64) {
45 self.measure_svg_text_bbox_x(text, style)
46 }
47
48 /// Measures the horizontal extents for Mermaid diagram titles rendered as a single `<text>`
49 /// node (no whitespace-tokenized `<tspan>` runs).
50 ///
51 /// Mermaid flowchart-v2 uses this style for `flowchartTitleText`, and the bbox impacts the
52 /// final `viewBox` / `max-width` computed via `getBBox()`.
53 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
54 self.measure_svg_text_bbox_x(text, style)
55 }
56
57 /// Measures the bbox width for Mermaid `drawSimpleText(...).getBBox().width`-style probes
58 /// (used by upstream `calculateTextWidth`).
59 ///
60 /// This should reflect actual glyph outline extents (including ASCII overhang where present),
61 /// rather than the symmetric/center-anchored title bbox approximation.
62 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
63 let (l, r) = self.measure_svg_title_bbox_x(text, style);
64 (l + r).max(0.0)
65 }
66
67 /// Measures the bbox height for Mermaid `drawSimpleText(...).getBBox().height`-style probes.
68 ///
69 /// Upstream Mermaid uses `<text>.getBBox()` for some diagrams (notably `gitGraph` commit/tag
70 /// labels). Those `<text>` nodes are not split into `<tspan>` runs, and empirically their
71 /// bbox height behaves closer to ~`1.1em` than the slightly taller first-line heuristic used
72 /// by `measure_wrapped(..., WrapMode::SvgLike)`.
73 ///
74 /// Default implementation falls back to `measure(...).height`.
75 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
76 let m = self.measure(text, style);
77 m.height.max(0.0)
78 }
79
80 fn measure_wrapped(
81 &self,
82 text: &str,
83 style: &TextStyle,
84 max_width: Option<f64>,
85 wrap_mode: WrapMode,
86 ) -> TextMetrics {
87 let _ = max_width;
88 let _ = wrap_mode;
89 self.measure(text, style)
90 }
91
92 /// Measures wrapped text and (optionally) returns the unwrapped width for the same payload.
93 ///
94 /// This exists mainly to avoid redundant measurement passes in diagrams that need both:
95 /// - wrapped metrics (for height/line breaks), and
96 /// - a raw "overflow width" probe (for sizing containers that can visually overflow).
97 ///
98 /// Default implementation returns `None` for `raw_width_px` and callers may fall back to an
99 /// explicit second measurement if needed.
100 fn measure_wrapped_with_raw_width(
101 &self,
102 text: &str,
103 style: &TextStyle,
104 max_width: Option<f64>,
105 wrap_mode: WrapMode,
106 ) -> (TextMetrics, Option<f64>) {
107 (
108 self.measure_wrapped(text, style, max_width, wrap_mode),
109 None,
110 )
111 }
112
113 /// Measures wrapped text while disabling any implementation-specific HTML overrides.
114 ///
115 /// This is primarily used for Markdown labels measured via DOM in upstream Mermaid, where we
116 /// want a raw regular-weight baseline before applying `<strong>/<em>` deltas.
117 fn measure_wrapped_raw(
118 &self,
119 text: &str,
120 style: &TextStyle,
121 max_width: Option<f64>,
122 wrap_mode: WrapMode,
123 ) -> TextMetrics {
124 self.measure_wrapped(text, style, max_width, wrap_mode)
125 }
126}