1use rustc_hash::FxHashSet as HashSet;
17
18use crate::document::NodeData;
19
20const NOBREAK: i32 = 99999999;
22const POORBREAK_FACTOR: i32 = 20;
23const BADBREAK_FACTOR: i32 = 100;
24const PENALTY_OK: i32 = 5;
25const PENALTY_LIMIT: i32 = 1000;
26const CONVERSION_FACTOR: i32 = 2;
27
28fn break_before_ops() -> HashSet<&'static str> {
30 ["+", "-", "\u{00B1}", "\u{2212}", "\u{2213}"]
31 .into_iter()
32 .collect()
33}
34
35fn break_after_ops() -> HashSet<&'static str> { [","].into_iter().collect() }
37
38fn relation_ops() -> HashSet<&'static str> {
40 [
41 "=", "<", ">", "\u{2264}", "\u{2265}", "\u{2260}", "\u{226A}", "\u{2261}", "\u{223C}",
42 "\u{2243}", "\u{224D}", "\u{2248}", "\u{221D}",
43 ]
44 .into_iter()
45 .collect()
46}
47
48fn fence_ops() -> HashSet<&'static str> {
50 [
51 "(", ")", "[", "]", "{", "}", "|", "||", "\u{2308}", "\u{2309}", "\u{230A}", "\u{230B}",
52 "\u{27E8}", "\u{27E9}", "\u{27EA}", "\u{27EB}", "\u{27EE}", "\u{27EF}",
53 ]
54 .into_iter()
55 .collect()
56}
57
58fn separator_ops() -> HashSet<&'static str> { [",", ";", ".", "\u{2063}"].into_iter().collect() }
60
61fn convert_ops() -> Vec<(&'static str, &'static str)> {
63 vec![("\u{2062}", "\u{00D7}")] }
65
66#[derive(Debug, Clone)]
68pub struct Layout {
69 pub width: f64,
71 pub penalty: i32,
73 pub has_break: bool,
75 pub breaks: Vec<usize>,
77 pub indent: f64,
79}
80
81impl Layout {
82 fn no_break(width: f64) -> Self {
83 Layout {
84 width,
85 penalty: 0,
86 has_break: false,
87 breaks: vec![],
88 indent: 0.0,
89 }
90 }
91}
92
93pub struct Linebreaker {
95 pub target_width: f64,
97}
98
99impl Linebreaker {
100 pub fn new(target_width: f64) -> Self { Linebreaker { target_width } }
101
102 pub fn best_fit_to_width(&self, node: &NodeData) -> Layout {
106 let layouts = self.find_layouts(node, 0);
107 let mut best = Layout::no_break(self.estimate_width(node));
109 for layout in &layouts {
110 if layout.width <= self.target_width && (!best.has_break || layout.penalty < best.penalty) {
111 best = layout.clone();
112 }
113 }
114 best
115 }
116
117 fn find_layouts(&self, node: &NodeData, depth: usize) -> Vec<Layout> {
121 match node {
122 NodeData::Text(s) => {
123 vec![Layout::no_break(estimate_text_width(s))]
124 },
125 NodeData::Element { tag, children, .. } => {
126 if tag.starts_with("m:msub")
128 || tag.starts_with("m:msup")
129 || tag == "m:mfrac"
130 || tag == "m:msqrt"
131 || tag == "m:mroot"
132 || tag == "m:munder"
133 || tag == "m:mover"
134 || tag == "m:munderover"
135 {
136 let w: f64 = children.iter().map(|c| self.estimate_width(c)).sum();
137 return vec![Layout::no_break(w)];
138 }
139
140 if tag == "m:mrow" || tag == "m:math" {
142 return self.find_mrow_layouts(children, depth);
143 }
144
145 let w: f64 = children.iter().map(|c| self.estimate_width(c)).sum();
147 vec![Layout::no_break(w)]
148 },
149 NodeData::XmlNode(_) => vec![Layout::no_break(1.0)],
150 }
151 }
152
153 fn find_mrow_layouts(&self, children: &[NodeData], _depth: usize) -> Vec<Layout> {
155 let total_width: f64 = children.iter().map(|c| self.estimate_width(c)).sum();
156
157 if total_width <= self.target_width {
159 return vec![Layout::no_break(total_width)];
160 }
161
162 let break_before = break_before_ops();
163 let break_after = break_after_ops();
164 let relation = relation_ops();
165
166 let mut layouts = vec![Layout::no_break(total_width)];
168
169 for (i, child) in children.iter().enumerate() {
170 if let NodeData::Element { tag, children: inner, .. } = child {
171 if tag == "m:mo" {
172 if let Some(NodeData::Text(text)) = inner.first() {
173 let penalty = if relation.contains(text.as_str()) {
174 PENALTY_OK
175 } else if break_before.contains(text.as_str()) {
176 PENALTY_OK * POORBREAK_FACTOR
177 } else if break_after.contains(text.as_str()) {
178 PENALTY_OK * 2
179 } else {
180 PENALTY_OK * BADBREAK_FACTOR
181 };
182
183 let indent = 2.0; let width_after = children[i + 1..]
186 .iter()
187 .map(|c| self.estimate_width(c))
188 .sum::<f64>()
189 + indent;
190 let width_before: f64 = children[..i].iter().map(|c| self.estimate_width(c)).sum();
191 let max_line = width_before.max(width_after);
192
193 layouts.push(Layout {
194 width: max_line,
195 penalty,
196 has_break: true,
197 breaks: vec![i],
198 indent,
199 });
200 }
201 }
202 }
203 }
204
205 layouts.sort_by(|a, b| {
207 a.width
208 .partial_cmp(&b.width)
209 .unwrap()
210 .then(a.penalty.cmp(&b.penalty))
211 });
212
213 let mut pruned = Vec::new();
215 let mut best_penalty = i32::MAX;
216 for layout in layouts {
217 if layout.penalty < best_penalty || layout.width <= self.target_width {
218 best_penalty = best_penalty.min(layout.penalty);
219 pruned.push(layout);
220 }
221 }
222
223 pruned
224 }
225
226 fn estimate_width(&self, node: &NodeData) -> f64 {
228 match node {
229 NodeData::Text(s) => estimate_text_width(s),
230 NodeData::Element { children, .. } => children
231 .iter()
232 .map(|c| self.estimate_width(c))
233 .sum::<f64>()
234 .max(0.5),
235 NodeData::XmlNode(_) => 1.0,
236 }
237 }
238
239 pub fn apply_layout(&self, node: &NodeData, layout: &Layout) -> NodeData {
243 if !layout.has_break {
244 return node.clone();
245 }
246 match node {
248 NodeData::Element { tag, attributes, children } => {
249 let mut new_children = Vec::new();
250 for (i, child) in children.iter().enumerate() {
251 new_children.push(child.clone());
252 if layout.breaks.contains(&i) {
253 new_children.push(NodeData::Element {
254 tag: "m:mspace".to_string(),
255 attributes: Some(rustc_hash::FxHashMap::from_iter([(
256 "linebreak".to_string(),
257 "newline".to_string(),
258 )])),
259 children: vec![],
260 });
261 }
262 }
263 NodeData::Element {
264 tag: tag.clone(),
265 attributes: attributes.clone(),
266 children: new_children,
267 }
268 },
269 _ => node.clone(),
270 }
271 }
272}
273
274fn estimate_text_width(s: &str) -> f64 { s.chars().count() as f64 * 0.6 }
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn penalty_constants_ordering() {
283 const {
285 assert!(NOBREAK > PENALTY_LIMIT);
286 assert!(PENALTY_LIMIT > BADBREAK_FACTOR);
287 assert!(BADBREAK_FACTOR > POORBREAK_FACTOR);
288 assert!(POORBREAK_FACTOR > PENALTY_OK);
289 assert!(PENALTY_OK > 0);
290 }
291 }
292
293 #[test]
294 fn break_before_ops_contains_plus_minus() {
295 let ops = break_before_ops();
296 assert!(ops.contains("+"));
297 assert!(ops.contains("-"));
298 assert!(ops.contains("\u{00B1}")); assert!(ops.contains("\u{2212}")); }
301
302 #[test]
303 fn break_after_ops_contains_comma() {
304 let ops = break_after_ops();
305 assert!(ops.contains(","));
306 }
307
308 #[test]
309 fn relation_ops_contains_common() {
310 let ops = relation_ops();
311 assert!(ops.contains("="));
312 assert!(ops.contains("<"));
313 assert!(ops.contains(">"));
314 assert!(ops.contains("\u{2264}")); assert!(ops.contains("\u{2265}")); assert!(ops.contains("\u{2260}")); }
318
319 #[test]
320 fn fence_ops_contains_parens_brackets_braces() {
321 let ops = fence_ops();
322 for c in ["(", ")", "[", "]", "{", "}"] {
323 assert!(ops.contains(c), "missing {c}");
324 }
325 }
326
327 #[test]
328 fn separator_ops_distinct_from_relation() {
329 let sep = separator_ops();
330 let rel = relation_ops();
331 for s in &sep {
333 assert!(
334 !rel.contains(s),
335 "{s:?} should not be both separator and relation"
336 );
337 }
338 assert!(sep.contains(","));
340 assert!(sep.contains(";"));
341 }
342
343 #[test]
344 fn convert_ops_invisible_to_visible_times() {
345 let pairs = convert_ops();
346 assert_eq!(pairs.len(), 1);
347 assert_eq!(
348 pairs[0],
349 ("\u{2062}", "\u{00D7}"),
350 "INVISIBLE TIMES → MULTIPLICATION SIGN"
351 );
352 }
353
354 #[test]
355 fn layout_no_break_has_zero_penalty() {
356 let l = Layout::no_break(5.0);
357 assert_eq!(l.width, 5.0);
358 assert_eq!(l.penalty, 0);
359 assert!(!l.has_break);
360 assert!(l.breaks.is_empty());
361 assert_eq!(l.indent, 0.0);
362 }
363
364 #[test]
365 fn estimate_text_width_proportional_to_length() {
366 assert!((estimate_text_width("") - 0.0).abs() < 1e-6);
368 assert!((estimate_text_width("a") - 0.6).abs() < 1e-6);
369 assert!((estimate_text_width("abcde") - 3.0).abs() < 1e-6);
370 }
371
372 #[test]
373 fn estimate_text_width_counts_chars_not_bytes() {
374 assert!((estimate_text_width("αβγ") - 1.8).abs() < 1e-6);
377 }
378}