1use std::collections::HashMap;
2
3use ratex_font::{get_char_metrics, get_global_metrics, FontId};
4use ratex_parser::parse_node::{ArrayTag, AtomFamily, Mode, ParseNode};
5use ratex_types::color::Color;
6use ratex_types::math_style::MathStyle;
7use ratex_types::path_command::PathCommand;
8
9use crate::hbox::make_hbox;
10use crate::layout_box::{BoxContent, LayoutBox};
11use crate::layout_options::LayoutOptions;
12
13use crate::katex_svg::parse_svg_path_data;
14use crate::spacing::{atom_spacing, mu_to_em, MathClass};
15use crate::stacked_delim::make_stacked_delim_if_needed;
16
17const NULL_DELIMITER_SPACE: f64 = 0.12;
20
21pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
23 layout_expression(nodes, options, true)
24}
25
26fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
29 let n = raw.len();
30 let mut eff = raw.to_vec();
31 for i in 0..n {
32 if raw[i] != Some(MathClass::Bin) {
33 continue;
34 }
35 let prev = if i == 0 { None } else { raw[i - 1] };
36 let left_cancel = matches!(
37 prev,
38 None
39 | Some(MathClass::Bin)
40 | Some(MathClass::Open)
41 | Some(MathClass::Rel)
42 | Some(MathClass::Op)
43 | Some(MathClass::Punct)
44 );
45 if left_cancel {
46 eff[i] = Some(MathClass::Ord);
47 }
48 }
49 for i in 0..n {
50 if raw[i] != Some(MathClass::Bin) {
51 continue;
52 }
53 let next = if i + 1 < n { raw[i + 1] } else { None };
54 let right_cancel = matches!(
55 next,
56 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
57 );
58 if right_cancel {
59 eff[i] = Some(MathClass::Ord);
60 }
61 }
62 eff
63}
64
65fn node_is_middle_fence(node: &ParseNode) -> bool {
70 matches!(node, ParseNode::Middle { .. })
71}
72
73fn layout_expression(
75 nodes: &[ParseNode],
76 options: &LayoutOptions,
77 is_real_group: bool,
78) -> LayoutBox {
79 if nodes.is_empty() {
80 return LayoutBox::new_empty();
81 }
82
83 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
85 if has_cr {
86 return layout_multiline(nodes, options, is_real_group);
87 }
88
89 let raw_classes: Vec<Option<MathClass>> =
90 nodes.iter().map(node_math_class).collect();
91 let eff_classes = apply_bin_cancellation(&raw_classes);
92
93 let mut children = Vec::new();
94 let mut prev_class: Option<MathClass> = None;
95 let mut prev_class_node_idx: Option<usize> = None;
97
98 for (i, node) in nodes.iter().enumerate() {
99 let lbox = layout_node(node, options);
100 let cur_class = eff_classes.get(i).copied().flatten();
101
102 if is_real_group {
103 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
104 let prev_middle = prev_class_node_idx
105 .is_some_and(|j| node_is_middle_fence(&nodes[j]));
106 let cur_middle = node_is_middle_fence(node);
107 let mu = if prev_middle || cur_middle {
108 0.0
109 } else {
110 atom_spacing(prev, cur, options.style.is_tight())
111 };
112 let mu = if let Some(cap) = options.align_relation_spacing {
113 if prev == MathClass::Rel || cur == MathClass::Rel {
114 mu.min(cap)
115 } else {
116 mu
117 }
118 } else {
119 mu
120 };
121 if mu > 0.0 {
122 let em = mu_to_em(mu, options.metrics().quad);
123 children.push(LayoutBox::new_kern(em));
124 }
125 }
126 }
127
128 if cur_class.is_some() {
129 prev_class = cur_class;
130 prev_class_node_idx = Some(i);
131 }
132
133 children.push(lbox);
134 }
135
136 make_hbox(children)
137}
138
139fn layout_multiline(
141 nodes: &[ParseNode],
142 options: &LayoutOptions,
143 is_real_group: bool,
144) -> LayoutBox {
145 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
146 let metrics = options.metrics();
147 let pt = 1.0 / metrics.pt_per_em;
148 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
153 let mut start = 0;
154 for (i, node) in nodes.iter().enumerate() {
155 if matches!(node, ParseNode::Cr { .. }) {
156 rows.push(&nodes[start..i]);
157 start = i + 1;
158 }
159 }
160 rows.push(&nodes[start..]);
161
162 let row_boxes: Vec<LayoutBox> = rows
163 .iter()
164 .map(|row| layout_expression(row, options, is_real_group))
165 .collect();
166
167 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
168
169 let mut vchildren: Vec<VBoxChild> = Vec::new();
170 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
171 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
172 for (i, row) in row_boxes.iter().enumerate() {
173 if i > 0 {
174 let prev_depth = row_boxes[i - 1].depth;
176 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
177 vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
178 h += gap + row.height + prev_depth;
179 }
180 vchildren.push(VBoxChild {
181 kind: VBoxChildKind::Box(Box::new(row.clone())),
182 shift: 0.0,
183 });
184 }
185
186 LayoutBox {
187 width: total_width,
188 height: h,
189 depth: d,
190 content: BoxContent::VBox(vchildren),
191 color: options.color,
192 }
193}
194
195
196fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
198 match node {
199 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
200 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
201 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
202 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
203
204 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
205
206 ParseNode::SupSub {
207 base, sup, sub, ..
208 } => {
209 if let Some(base_node) = base.as_deref() {
210 if should_use_op_limits(base_node, options) {
211 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
212 }
213 }
214 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
215 }
216
217 ParseNode::GenFrac {
218 numer,
219 denom,
220 has_bar_line,
221 bar_size,
222 left_delim,
223 right_delim,
224 continued,
225 ..
226 } => {
227 let bar_thickness = if *has_bar_line {
228 bar_size
229 .as_ref()
230 .map(|m| measurement_to_em(m, options))
231 .unwrap_or(options.metrics().default_rule_thickness)
232 } else {
233 0.0
234 };
235 let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
236
237 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
238 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
239
240 if has_left || has_right {
241 let total_h = genfrac_delim_target_height(options);
242 let left_d = left_delim.as_deref().unwrap_or(".");
243 let right_d = right_delim.as_deref().unwrap_or(".");
244 let left_box = make_stretchy_delim(left_d, total_h, options);
245 let right_box = make_stretchy_delim(right_d, total_h, options);
246
247 let width = left_box.width + frac.width + right_box.width;
248 let height = frac.height.max(left_box.height).max(right_box.height);
249 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
250
251 LayoutBox {
252 width,
253 height,
254 depth,
255 content: BoxContent::LeftRight {
256 left: Box::new(left_box),
257 right: Box::new(right_box),
258 inner: Box::new(frac),
259 },
260 color: options.color,
261 }
262 } else {
263 let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE };
264 make_hbox(vec![
265 LayoutBox::new_kern(NULL_DELIMITER_SPACE),
266 frac,
267 LayoutBox::new_kern(right_nds),
268 ])
269 }
270 }
271
272 ParseNode::Sqrt { body, index, .. } => {
273 layout_radical(body, index.as_deref(), options)
274 }
275
276 ParseNode::Op {
277 name,
278 symbol,
279 body,
280 limits,
281 suppress_base_shift,
282 ..
283 } => layout_op(
284 name.as_deref(),
285 *symbol,
286 body.as_deref(),
287 *limits,
288 suppress_base_shift.unwrap_or(false),
289 options,
290 ),
291
292 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
293
294 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
295
296 ParseNode::Kern { dimension, .. } => {
297 let em = measurement_to_em(dimension, options);
298 LayoutBox::new_kern(em)
299 }
300
301 ParseNode::Color { color, body, .. } => {
302 let new_color = Color::parse(color).unwrap_or(options.color);
303 let new_opts = options.with_color(new_color);
304 let mut lbox = layout_expression(body, &new_opts, true);
305 lbox.color = new_color;
306 lbox
307 }
308
309 ParseNode::Styling { style, body, .. } => {
310 let new_style = match style {
311 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
312 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
313 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
314 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
315 };
316 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
317 let new_opts = options.with_style(new_style);
318 let inner = layout_expression(body, &new_opts, true);
319 if (ratio - 1.0).abs() < 0.001 {
320 inner
321 } else {
322 LayoutBox {
323 width: inner.width * ratio,
324 height: inner.height * ratio,
325 depth: inner.depth * ratio,
326 content: BoxContent::Scaled {
327 body: Box::new(inner),
328 child_scale: ratio,
329 },
330 color: options.color,
331 }
332 }
333 }
334
335 ParseNode::Accent {
336 label, base, is_stretchy, is_shifty, ..
337 } => {
338 let is_below = matches!(label.as_str(), "\\c");
340 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
341 }
342
343 ParseNode::AccentUnder {
344 label, base, is_stretchy, ..
345 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
346
347 ParseNode::LeftRight {
348 body, left, right, ..
349 } => layout_left_right(body, left, right, options),
350
351 ParseNode::DelimSizing {
352 size, delim, ..
353 } => layout_delim_sizing(*size, delim, options),
354
355 ParseNode::Array {
356 body,
357 cols,
358 arraystretch,
359 add_jot,
360 row_gaps,
361 hlines_before_row,
362 col_separation_type,
363 hskip_before_and_after,
364 is_cd,
365 tags,
366 leqno,
367 ..
368 } => {
369 if is_cd.unwrap_or(false) {
370 layout_cd(body, options)
371 } else {
372 layout_array(
373 body,
374 cols.as_deref(),
375 *arraystretch,
376 add_jot.unwrap_or(false),
377 row_gaps,
378 hlines_before_row,
379 col_separation_type.as_deref(),
380 hskip_before_and_after.unwrap_or(false),
381 tags.as_deref(),
382 leqno.unwrap_or(false),
383 options,
384 )
385 }
386 }
387
388 ParseNode::CdArrow {
389 direction,
390 label_above,
391 label_below,
392 ..
393 } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
394
395 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
396
397 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
398 Some(f) => {
399 let group = ParseNode::OrdGroup {
400 mode: *mode,
401 body: body.clone(),
402 semisimple: None,
403 loc: None,
404 };
405 layout_font(f, &group, options)
406 }
407 None => layout_text(body, options),
408 },
409
410 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
411
412 ParseNode::Href { body, .. } => layout_href(body, options),
413
414 ParseNode::Overline { body, .. } => layout_overline(body, options),
415 ParseNode::Underline { body, .. } => layout_underline(body, options),
416
417 ParseNode::Rule {
418 width: w,
419 height: h,
420 shift,
421 ..
422 } => {
423 let width = measurement_to_em(w, options);
424 let ink_h = measurement_to_em(h, options);
425 let raise = shift
426 .as_ref()
427 .map(|s| measurement_to_em(s, options))
428 .unwrap_or(0.0);
429 let box_height = (raise + ink_h).max(0.0);
430 let box_depth = (-raise).max(0.0);
431 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
432 }
433
434 ParseNode::Phantom { body, .. } => {
435 let inner = layout_expression(body, options, true);
436 LayoutBox {
437 width: inner.width,
438 height: inner.height,
439 depth: inner.depth,
440 content: BoxContent::Empty,
441 color: Color::BLACK,
442 }
443 }
444
445 ParseNode::VPhantom { body, .. } => {
446 let inner = layout_node(body, options);
447 LayoutBox {
448 width: 0.0,
449 height: inner.height,
450 depth: inner.depth,
451 content: BoxContent::Empty,
452 color: Color::BLACK,
453 }
454 }
455
456 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
457 let mut inner = layout_node(body, options);
458 if *smash_height { inner.height = 0.0; }
459 if *smash_depth { inner.depth = 0.0; }
460 inner
461 }
462
463 ParseNode::Middle { delim, .. } => {
464 match options.leftright_delim_height {
465 Some(h) => make_stretchy_delim(delim, h, options),
466 None => {
467 let placeholder = make_stretchy_delim(delim, 1.0, options);
469 LayoutBox {
470 width: placeholder.width,
471 height: 0.0,
472 depth: 0.0,
473 content: BoxContent::Empty,
474 color: options.color,
475 }
476 }
477 }
478 }
479
480 ParseNode::HtmlMathMl { html, .. } => {
481 layout_expression(html, options, true)
482 }
483
484 ParseNode::Html { attributes, body, .. } => layout_html(attributes, body, options),
485
486 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
487
488 ParseNode::MathChoice {
489 display, text, script, scriptscript, ..
490 } => {
491 let branch = match options.style {
492 MathStyle::Display | MathStyle::DisplayCramped => display,
493 MathStyle::Text | MathStyle::TextCramped => text,
494 MathStyle::Script | MathStyle::ScriptCramped => script,
495 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
496 };
497 layout_expression(branch, options, true)
498 }
499
500 ParseNode::Lap { alignment, body, .. } => {
501 let inner = layout_node(body, options);
502 let shift = match alignment.as_str() {
503 "llap" => -inner.width,
504 "clap" => -inner.width / 2.0,
505 _ => 0.0, };
507 let mut children = Vec::new();
508 if shift != 0.0 {
509 children.push(LayoutBox::new_kern(shift));
510 }
511 let h = inner.height;
512 let d = inner.depth;
513 children.push(inner);
514 LayoutBox {
515 width: 0.0,
516 height: h,
517 depth: d,
518 content: BoxContent::HBox(children),
519 color: options.color,
520 }
521 }
522
523 ParseNode::HorizBrace {
524 base,
525 is_over,
526 label,
527 ..
528 } => layout_horiz_brace(base, *is_over, label, options),
529
530 ParseNode::XArrow {
531 label, body, below, ..
532 } => layout_xarrow(label, body, below.as_deref(), options),
533
534 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
535
536 ParseNode::HBox { body, .. } => layout_text(body, options),
537
538 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
539 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
540 }
541
542 ParseNode::RaiseBox { dy, body, .. } => {
543 let shift = measurement_to_em(dy, options);
544 layout_raisebox(shift, body, options)
545 }
546
547 ParseNode::VCenter { body, .. } => {
548 let inner = layout_node(body, options);
550 let axis = options.metrics().axis_height;
551 let total = inner.height + inner.depth;
552 let height = total / 2.0 + axis;
553 let depth = total - height;
554 LayoutBox {
555 width: inner.width,
556 height,
557 depth,
558 content: inner.content,
559 color: inner.color,
560 }
561 }
562
563 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
564
565 ParseNode::Tag { tag, .. } => {
566 let text_opts = options.with_style(options.style.text());
567 layout_expression(tag, &text_opts, true)
568 },
569
570 _ => LayoutBox::new_empty(),
572 }
573}
574
575fn missing_glyph_width_em(ch: char) -> f64 {
585 match ch as u32 {
586 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
588 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
590 0xAC00..=0xD7AF => 1.0,
592 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
594 0x1F000..=0x1FAFF => 1.0,
597 0x2700..=0x27BF => 1.0,
599 0x2600..=0x26FF => 1.0,
601 0x2B00..=0x2BFF => 1.0,
603 _ => 0.5,
604 }
605}
606
607fn missing_glyph_height_em(ch: char, m: &ratex_font::MathConstants) -> f64 {
608 let ru = ch as u32;
609 if (0x1F000..=0x1FAFF).contains(&ru) {
610 (m.quad * 0.74).max(m.x_height)
615 } else {
616 (m.quad * 0.92).max(m.x_height)
617 }
618}
619
620fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
621 let m = get_global_metrics(options.style.size_index());
622 let w = missing_glyph_width_em(ch);
623 if w >= 0.99 {
624 let h = missing_glyph_height_em(ch, m);
625 (w, h, 0.0)
626 } else {
627 (w, m.x_height, 0.0)
628 }
629}
630
631#[inline]
633fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
634 if mode == Mode::Math {
635 m.width + m.italic
636 } else {
637 m.width
638 }
639}
640
641fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
642 let ch = resolve_symbol_char(text, mode);
643
644 match ch as u32 {
646 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
649 }
650
651 let char_code = ch as u32;
652
653 if let Some((font_id, metric_cp)) =
654 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
655 {
656 let m = get_char_metrics(font_id, metric_cp);
657 let (width, height, depth) = match m {
658 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
659 None => missing_glyph_metrics_fallback(ch, options),
660 };
661 return LayoutBox {
662 width,
663 height,
664 depth,
665 content: BoxContent::Glyph {
666 font_id,
667 char_code,
668 },
669 color: options.color,
670 };
671 }
672
673 let mut font_id = select_font(text, ch, mode, options);
674 let mut metrics = get_char_metrics(font_id, char_code);
675
676 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
677 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
678 font_id = FontId::MathItalic;
679 metrics = Some(m);
680 }
681 }
682
683 let (width, height, depth) = if let Some(m) = metrics {
689 (math_glyph_advance_em(&m, mode), m.height, m.depth)
690 } else if mode == Mode::Math {
691 let size_font = if options.style.is_display() {
692 FontId::Size2Regular
693 } else {
694 FontId::Size1Regular
695 };
696 match get_char_metrics(size_font, char_code)
697 .or_else(|| get_char_metrics(FontId::Size1Regular, char_code))
698 {
699 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
700 None => missing_glyph_metrics_fallback(ch, options),
701 }
702 } else {
703 missing_glyph_metrics_fallback(ch, options)
704 };
705
706 if metrics.is_none() && missing_glyph_width_em(ch) >= 0.99 {
709 font_id = FontId::CjkRegular;
710 }
711
712 LayoutBox {
713 width,
714 height,
715 depth,
716 content: BoxContent::Glyph {
717 font_id,
718 char_code,
719 },
720 color: options.color,
721 }
722}
723
724fn resolve_symbol_char(text: &str, mode: Mode) -> char {
726 let font_mode = match mode {
727 Mode::Math => ratex_font::Mode::Math,
728 Mode::Text => ratex_font::Mode::Text,
729 };
730
731 if let Some(raw) = text.chars().next() {
732 let ru = raw as u32;
733 if (0x1D400..=0x1D7FF).contains(&ru) {
734 return raw;
735 }
736 }
737
738 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
739 if let Some(cp) = info.codepoint {
740 return cp;
741 }
742 }
743
744 text.chars().next().unwrap_or('?')
745}
746
747fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
751 let font_mode = match mode {
752 Mode::Math => ratex_font::Mode::Math,
753 Mode::Text => ratex_font::Mode::Text,
754 };
755
756 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
757 if info.font == ratex_font::SymbolFont::Ams {
758 return FontId::AmsRegular;
759 }
760 }
761
762 match mode {
763 Mode::Math => {
764 if resolved_char.is_ascii_lowercase()
765 || resolved_char.is_ascii_uppercase()
766 || is_math_italic_greek(resolved_char)
767 {
768 FontId::MathItalic
769 } else {
770 FontId::MainRegular
771 }
772 }
773 Mode::Text => FontId::MainRegular,
774 }
775}
776
777fn is_math_italic_greek(ch: char) -> bool {
780 matches!(ch,
781 '\u{03B1}'..='\u{03C9}' |
782 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
783 '\u{03F1}' | '\u{03F5}'
784 )
785}
786
787fn is_arrow_accent(label: &str) -> bool {
788 matches!(
789 label,
790 "\\overrightarrow"
791 | "\\overleftarrow"
792 | "\\Overrightarrow"
793 | "\\overleftrightarrow"
794 | "\\underrightarrow"
795 | "\\underleftarrow"
796 | "\\underleftrightarrow"
797 | "\\overleftharpoon"
798 | "\\overrightharpoon"
799 | "\\overlinesegment"
800 | "\\underlinesegment"
801 )
802}
803
804fn layout_fraction(
809 numer: &ParseNode,
810 denom: &ParseNode,
811 bar_thickness: f64,
812 continued: bool,
813 options: &LayoutOptions,
814) -> LayoutBox {
815 let numer_s = options.style.numerator();
816 let denom_s = options.style.denominator();
817 let numer_style = options.with_style(numer_s);
818 let denom_style = options.with_style(denom_s);
819
820 let mut numer_box = layout_node(numer, &numer_style);
821 if continued {
823 let pt = options.metrics().pt_per_em;
824 let h_min = 8.5 / pt;
825 let d_min = 3.5 / pt;
826 if numer_box.height < h_min {
827 numer_box.height = h_min;
828 }
829 if numer_box.depth < d_min {
830 numer_box.depth = d_min;
831 }
832 }
833 let denom_box = layout_node(denom, &denom_style);
834
835 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
837 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
838
839 let numer_height = numer_box.height * numer_ratio;
840 let numer_depth = numer_box.depth * numer_ratio;
841 let denom_height = denom_box.height * denom_ratio;
842 let denom_depth = denom_box.depth * denom_ratio;
843 let numer_width = numer_box.width * numer_ratio;
844 let denom_width = denom_box.width * denom_ratio;
845
846 let metrics = options.metrics();
847 let axis = metrics.axis_height;
848 let rule = bar_thickness;
849
850 let (mut num_shift, mut den_shift) = if options.style.is_display() {
852 (metrics.num1, metrics.denom1)
853 } else if bar_thickness > 0.0 {
854 (metrics.num2, metrics.denom2)
855 } else {
856 (metrics.num3, metrics.denom2)
857 };
858
859 if bar_thickness > 0.0 {
860 let min_clearance = if options.style.is_display() {
861 3.0 * rule
862 } else {
863 rule
864 };
865
866 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
867 if num_clearance < min_clearance {
868 num_shift += min_clearance - num_clearance;
869 }
870
871 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
872 if den_clearance < min_clearance {
873 den_shift += min_clearance - den_clearance;
874 }
875 } else {
876 let min_gap = if options.style.is_display() {
877 7.0 * metrics.default_rule_thickness
878 } else {
879 3.0 * metrics.default_rule_thickness
880 };
881
882 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
883 if gap < min_gap {
884 let adjust = (min_gap - gap) / 2.0;
885 num_shift += adjust;
886 den_shift += adjust;
887 }
888 }
889
890 let total_width = numer_width.max(denom_width);
891 let height = numer_height + num_shift;
892 let depth = denom_depth + den_shift;
893
894 LayoutBox {
895 width: total_width,
896 height,
897 depth,
898 content: BoxContent::Fraction {
899 numer: Box::new(numer_box),
900 denom: Box::new(denom_box),
901 numer_shift: num_shift,
902 denom_shift: den_shift,
903 bar_thickness: rule,
904 numer_scale: numer_ratio,
905 denom_scale: denom_ratio,
906 },
907 color: options.color,
908 }
909}
910
911fn layout_supsub(
916 base: Option<&ParseNode>,
917 sup: Option<&ParseNode>,
918 sub: Option<&ParseNode>,
919 options: &LayoutOptions,
920 inherited_font: Option<FontId>,
921) -> LayoutBox {
922 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
923 Some(fid) => layout_with_font(n, fid, opts),
924 None => layout_node(n, opts),
925 };
926
927 let horiz_brace_over = matches!(
928 base,
929 Some(ParseNode::HorizBrace {
930 is_over: true,
931 ..
932 })
933 );
934 let horiz_brace_under = matches!(
935 base,
936 Some(ParseNode::HorizBrace {
937 is_over: false,
938 ..
939 })
940 );
941 let center_scripts = horiz_brace_over || horiz_brace_under;
942
943 let base_box = base
944 .map(|b| layout_child(b, options))
945 .unwrap_or_else(LayoutBox::new_empty);
946
947 let is_char_box = base.is_some_and(is_character_box);
948 let metrics = options.metrics();
949 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
953
954 let sup_style = options.style.superscript();
955 let sub_style = options.style.subscript();
956
957 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
958 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
959
960 let sup_box = sup.map(|s| {
961 let sup_opts = options.with_style(sup_style);
962 layout_child(s, &sup_opts)
963 });
964
965 let sub_box = sub.map(|s| {
966 let sub_opts = options.with_style(sub_style);
967 layout_child(s, &sub_opts)
968 });
969
970 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
971 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
972 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
973 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
974
975 let sup_style_metrics = get_global_metrics(sup_style.size_index());
977 let sub_style_metrics = get_global_metrics(sub_style.size_index());
978
979 let mut sup_shift = if !is_char_box && sup_box.is_some() {
982 base_box.height - sup_style_metrics.sup_drop * sup_ratio
983 } else {
984 0.0
985 };
986
987 let mut sub_shift = if !is_char_box && sub_box.is_some() {
988 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
989 } else {
990 0.0
991 };
992
993 let min_sup_shift = if options.style.is_cramped() {
994 metrics.sup3
995 } else if options.style.is_display() {
996 metrics.sup1
997 } else {
998 metrics.sup2
999 };
1000
1001 if sup_box.is_some() && sub_box.is_some() {
1002 sup_shift = sup_shift
1004 .max(min_sup_shift)
1005 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1006 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
1009 let max_width = 4.0 * rule_width;
1010 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
1011 if gap < max_width {
1012 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
1013 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
1014 if psi > 0.0 {
1015 sup_shift += psi;
1016 sub_shift -= psi;
1017 }
1018 }
1019 } else if sub_box.is_some() {
1020 sub_shift = sub_shift
1022 .max(metrics.sub1)
1023 .max(sub_height_scaled - 0.8 * metrics.x_height);
1024 } else if sup_box.is_some() {
1025 sup_shift = sup_shift
1027 .max(min_sup_shift)
1028 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1029 }
1030
1031 if horiz_brace_over && sup_box.is_some() {
1035 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
1036 }
1037 if horiz_brace_under && sub_box.is_some() {
1038 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
1039 }
1040
1041 let italic_correction = 0.0;
1044
1045 let sub_h_kern = if sub_box.is_some() && !center_scripts {
1048 -glyph_italic(&base_box)
1049 } else {
1050 0.0
1051 };
1052
1053 let mut height = base_box.height;
1055 let mut depth = base_box.depth;
1056 let mut total_width = base_box.width;
1057
1058 if let Some(ref sup_b) = sup_box {
1059 height = height.max(sup_shift + sup_height_scaled);
1060 if center_scripts {
1061 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1062 } else {
1063 total_width = total_width.max(
1064 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1065 );
1066 }
1067 }
1068 if let Some(ref sub_b) = sub_box {
1069 depth = depth.max(sub_shift + sub_depth_scaled);
1070 if center_scripts {
1071 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1072 } else {
1073 total_width = total_width.max(
1074 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1075 );
1076 }
1077 }
1078
1079 LayoutBox {
1080 width: total_width,
1081 height,
1082 depth,
1083 content: BoxContent::SupSub {
1084 base: Box::new(base_box),
1085 sup: sup_box.map(Box::new),
1086 sub: sub_box.map(Box::new),
1087 sup_shift,
1088 sub_shift,
1089 sup_scale: sup_ratio,
1090 sub_scale: sub_ratio,
1091 center_scripts,
1092 italic_correction,
1093 sub_h_kern,
1094 },
1095 color: options.color,
1096 }
1097}
1098
1099fn layout_radical(
1104 body: &ParseNode,
1105 index: Option<&ParseNode>,
1106 options: &LayoutOptions,
1107) -> LayoutBox {
1108 let cramped = options.style.cramped();
1109 let cramped_opts = options.with_style(cramped);
1110 let mut body_box = layout_node(body, &cramped_opts);
1111
1112 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1114 body_box.height *= body_ratio;
1115 body_box.depth *= body_ratio;
1116 body_box.width *= body_ratio;
1117
1118 if body_box.height == 0.0 {
1120 body_box.height = options.metrics().x_height;
1121 }
1122
1123 let metrics = options.metrics();
1124 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1129 metrics.x_height
1130 } else {
1131 theta
1132 };
1133
1134 let mut line_clearance = theta + phi / 4.0;
1135
1136 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1138
1139 let tex_height = select_surd_height(min_delim_height);
1142 let rule_width = theta;
1143 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1144 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1145 .map(|m| m.width)
1146 .unwrap_or(0.833);
1147
1148 let delim_depth = tex_height - rule_width;
1150 if delim_depth > body_box.height + body_box.depth + line_clearance {
1151 line_clearance =
1152 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1153 }
1154
1155 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1156
1157 let height = tex_height + rule_width - img_shift;
1160 let depth = if img_shift > body_box.depth {
1161 img_shift
1162 } else {
1163 body_box.depth
1164 };
1165
1166 const INDEX_KERN: f64 = 0.05;
1168 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1169 let root_style = options.style.superscript().superscript();
1170 let root_opts = options.with_style(root_style);
1171 let idx = layout_node(index_node, &root_opts);
1172 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1173 let offset = idx.width * index_ratio + INDEX_KERN;
1174 (Some(Box::new(idx)), offset, index_ratio)
1175 } else {
1176 (None, 0.0, 1.0)
1177 };
1178
1179 let width = index_offset + advance_width + body_box.width;
1180
1181 LayoutBox {
1182 width,
1183 height,
1184 depth,
1185 content: BoxContent::Radical {
1186 body: Box::new(body_box),
1187 index: index_box,
1188 index_offset,
1189 index_scale,
1190 rule_thickness: rule_width,
1191 inner_height: tex_height,
1192 },
1193 color: options.color,
1194 }
1195}
1196
1197fn select_surd_height(min_height: f64) -> f64 {
1200 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1201 for &h in &SURD_HEIGHTS {
1202 if h >= min_height {
1203 return h;
1204 }
1205 }
1206 SURD_HEIGHTS[4].max(min_height)
1208}
1209
1210const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1215
1216fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1218 match base {
1219 ParseNode::Op {
1220 limits,
1221 always_handle_sup_sub,
1222 ..
1223 } => {
1224 *limits
1225 && (options.style.is_display()
1226 || always_handle_sup_sub.unwrap_or(false))
1227 }
1228 ParseNode::OperatorName {
1229 always_handle_sup_sub,
1230 limits,
1231 ..
1232 } => {
1233 *always_handle_sup_sub
1234 && (options.style.is_display() || *limits)
1235 }
1236 _ => false,
1237 }
1238}
1239
1240fn layout_op(
1246 name: Option<&str>,
1247 symbol: bool,
1248 body: Option<&[ParseNode]>,
1249 _limits: bool,
1250 suppress_base_shift: bool,
1251 options: &LayoutOptions,
1252) -> LayoutBox {
1253 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1254
1255 if symbol && !suppress_base_shift {
1257 let axis = options.metrics().axis_height;
1258 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1259 if shift.abs() > 0.001 {
1260 base_box.height -= shift;
1261 base_box.depth += shift;
1262 }
1263 }
1264
1265 if !suppress_base_shift && !symbol && body.is_some() {
1270 let axis = options.metrics().axis_height;
1271 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1272 if delta.abs() > 0.001 {
1273 let w = base_box.width;
1274 let raise = -delta;
1276 base_box = LayoutBox {
1277 width: w,
1278 height: (base_box.height + raise).max(0.0),
1279 depth: (base_box.depth - raise).max(0.0),
1280 content: BoxContent::RaiseBox {
1281 body: Box::new(base_box),
1282 shift: raise,
1283 },
1284 color: options.color,
1285 };
1286 }
1287 }
1288
1289 base_box
1290}
1291
1292fn build_op_base(
1295 name: Option<&str>,
1296 symbol: bool,
1297 body: Option<&[ParseNode]>,
1298 options: &LayoutOptions,
1299) -> (LayoutBox, f64) {
1300 if symbol {
1301 let large = options.style.is_display()
1302 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1303 let font_id = if large {
1304 FontId::Size2Regular
1305 } else {
1306 FontId::Size1Regular
1307 };
1308
1309 let op_name = name.unwrap_or("");
1310 let ch = resolve_op_char(op_name);
1311 let char_code = ch as u32;
1312
1313 let metrics = get_char_metrics(font_id, char_code);
1314 let (width, height, depth, italic) = match metrics {
1315 Some(m) => (m.width, m.height, m.depth, m.italic),
1316 None => (1.0, 0.75, 0.25, 0.0),
1317 };
1318 let width_with_italic = width + italic;
1321
1322 let base = LayoutBox {
1323 width: width_with_italic,
1324 height,
1325 depth,
1326 content: BoxContent::Glyph {
1327 font_id,
1328 char_code,
1329 },
1330 color: options.color,
1331 };
1332
1333 if op_name == "\\oiint" || op_name == "\\oiiint" {
1336 let w = base.width;
1337 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1338 let overlay_box = LayoutBox {
1339 width: w,
1340 height: base.height,
1341 depth: base.depth,
1342 content: BoxContent::SvgPath {
1343 commands: ellipse_commands,
1344 fill: false,
1345 },
1346 color: options.color,
1347 };
1348 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1349 return (with_overlay, italic);
1350 }
1351
1352 (base, italic)
1353 } else if let Some(body_nodes) = body {
1354 let base = layout_expression(body_nodes, options, true);
1355 (base, 0.0)
1356 } else {
1357 let base = layout_op_text(name.unwrap_or(""), options);
1358 (base, 0.0)
1359 }
1360}
1361
1362fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1364 let text = name.strip_prefix('\\').unwrap_or(name);
1365 let mut children = Vec::new();
1366 for ch in text.chars() {
1367 let char_code = ch as u32;
1368 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1369 let (width, height, depth) = match metrics {
1370 Some(m) => (m.width, m.height, m.depth),
1371 None => (0.5, 0.43, 0.0),
1372 };
1373 children.push(LayoutBox {
1374 width,
1375 height,
1376 depth,
1377 content: BoxContent::Glyph {
1378 font_id: FontId::MainRegular,
1379 char_code,
1380 },
1381 color: options.color,
1382 });
1383 }
1384 make_hbox(children)
1385}
1386
1387fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1389 let metrics = options.metrics();
1390 (base.height - base.depth) / 2.0 - metrics.axis_height
1391}
1392
1393fn resolve_op_char(name: &str) -> char {
1395 match name {
1398 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1401 }
1402 let font_mode = ratex_font::Mode::Math;
1403 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1404 if let Some(cp) = info.codepoint {
1405 return cp;
1406 }
1407 }
1408 name.chars().next().unwrap_or('?')
1409}
1410
1411fn layout_op_with_limits(
1413 base_node: &ParseNode,
1414 sup_node: Option<&ParseNode>,
1415 sub_node: Option<&ParseNode>,
1416 options: &LayoutOptions,
1417) -> LayoutBox {
1418 let (name, symbol, body, suppress_base_shift) = match base_node {
1419 ParseNode::Op {
1420 name,
1421 symbol,
1422 body,
1423 suppress_base_shift,
1424 ..
1425 } => (
1426 name.as_deref(),
1427 *symbol,
1428 body.as_deref(),
1429 suppress_base_shift.unwrap_or(false),
1430 ),
1431 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1432 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1433 };
1434
1435 let legacy_limit_kern_padding = !suppress_base_shift;
1437
1438 let (base_box, slant) = build_op_base(name, symbol, body, options);
1439 let base_shift = if symbol && !suppress_base_shift {
1441 compute_op_base_shift(&base_box, options)
1442 } else {
1443 0.0
1444 };
1445
1446 layout_op_limits_inner(
1447 &base_box,
1448 sup_node,
1449 sub_node,
1450 slant,
1451 base_shift,
1452 legacy_limit_kern_padding,
1453 options,
1454 )
1455}
1456
1457fn layout_op_limits_inner(
1462 base: &LayoutBox,
1463 sup_node: Option<&ParseNode>,
1464 sub_node: Option<&ParseNode>,
1465 slant: f64,
1466 base_shift: f64,
1467 legacy_limit_kern_padding: bool,
1468 options: &LayoutOptions,
1469) -> LayoutBox {
1470 let metrics = options.metrics();
1471 let sup_style = options.style.superscript();
1472 let sub_style = options.style.subscript();
1473
1474 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1475 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1476
1477 let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 };
1478
1479 let sup_data = sup_node.map(|s| {
1480 let sup_opts = options.with_style(sup_style);
1481 let elem = layout_node(s, &sup_opts);
1482 let d = if legacy_limit_kern_padding {
1486 elem.depth * sup_ratio
1487 } else {
1488 elem.depth
1489 };
1490 let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1491 (elem, kern)
1492 });
1493
1494 let sub_data = sub_node.map(|s| {
1495 let sub_opts = options.with_style(sub_style);
1496 let elem = layout_node(s, &sub_opts);
1497 let h = if legacy_limit_kern_padding {
1498 elem.height * sub_ratio
1499 } else {
1500 elem.height
1501 };
1502 let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1503 (elem, kern)
1504 });
1505
1506 let sp5 = metrics.big_op_spacing5;
1507
1508 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1509 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1510 let sup_h = sup_elem.height * sup_ratio;
1513 let sup_d = sup_elem.depth * sup_ratio;
1514 let sub_h = sub_elem.height * sub_ratio;
1515 let sub_d = sub_elem.depth * sub_ratio;
1516
1517 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1518
1519 let height = bottom
1520 + base.height - base_shift
1521 + sup_kern
1522 + sup_h + sup_d
1523 + sp5
1524 - (base.height + base.depth);
1525
1526 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1527 let total_d = bottom;
1528
1529 let w = base
1530 .width
1531 .max(sup_elem.width * sup_ratio)
1532 .max(sub_elem.width * sub_ratio);
1533 let _ = height; (total_h, total_d, w)
1535 }
1536 (None, Some((sub_elem, sub_kern))) => {
1537 let sub_h = sub_elem.height * sub_ratio;
1540 let sub_d = sub_elem.depth * sub_ratio;
1541
1542 let total_h = base.height - base_shift;
1543 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1544
1545 let w = base.width.max(sub_elem.width * sub_ratio);
1546 (total_h, total_d, w)
1547 }
1548 (Some((sup_elem, sup_kern)), None) => {
1549 let sup_h = sup_elem.height * sup_ratio;
1552 let sup_d = sup_elem.depth * sup_ratio;
1553
1554 let total_h =
1555 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1556 let total_d = base.depth + base_shift;
1557
1558 let w = base.width.max(sup_elem.width * sup_ratio);
1559 (total_h, total_d, w)
1560 }
1561 (None, None) => {
1562 return base.clone();
1563 }
1564 };
1565
1566 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1567 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1568
1569 LayoutBox {
1570 width: total_width,
1571 height: total_height,
1572 depth: total_depth,
1573 content: BoxContent::OpLimits {
1574 base: Box::new(base.clone()),
1575 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1576 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1577 base_shift,
1578 sup_kern: sup_kern_val,
1579 sub_kern: sub_kern_val,
1580 slant,
1581 sup_scale: sup_ratio,
1582 sub_scale: sub_ratio,
1583 },
1584 color: options.color,
1585 }
1586}
1587
1588fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1590 let mut children = Vec::new();
1591 for node in body {
1592 match node {
1593 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1594 let ch = text.chars().next().unwrap_or('?');
1595 let char_code = ch as u32;
1596 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1597 let (width, height, depth) = match metrics {
1598 Some(m) => (m.width, m.height, m.depth),
1599 None => (0.5, 0.43, 0.0),
1600 };
1601 children.push(LayoutBox {
1602 width,
1603 height,
1604 depth,
1605 content: BoxContent::Glyph {
1606 font_id: FontId::MainRegular,
1607 char_code,
1608 },
1609 color: options.color,
1610 });
1611 }
1612 _ => {
1613 children.push(layout_node(node, options));
1614 }
1615 }
1616 }
1617 make_hbox(children)
1618}
1619
1620const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1626
1627fn glyph_italic(lb: &LayoutBox) -> f64 {
1631 match &lb.content {
1632 BoxContent::Glyph { font_id, char_code } => {
1633 get_char_metrics(*font_id, *char_code)
1634 .map(|m| m.italic)
1635 .unwrap_or(0.0)
1636 }
1637 BoxContent::HBox(children) => {
1638 children.last().map(glyph_italic).unwrap_or(0.0)
1639 }
1640 _ => 0.0,
1641 }
1642}
1643
1644fn accent_ordgroup_len(base: &ParseNode) -> usize {
1649 match base {
1650 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1651 _ => 1,
1652 }
1653}
1654
1655fn glyph_skew(lb: &LayoutBox) -> f64 {
1656 match &lb.content {
1657 BoxContent::Glyph { font_id, char_code } => {
1658 get_char_metrics(*font_id, *char_code)
1659 .map(|m| m.skew)
1660 .unwrap_or(0.0)
1661 }
1662 BoxContent::HBox(children) => {
1663 children.last().map(glyph_skew).unwrap_or(0.0)
1664 }
1665 _ => 0.0,
1666 }
1667}
1668
1669fn layout_accent(
1670 label: &str,
1671 base: &ParseNode,
1672 is_stretchy: bool,
1673 is_shifty: bool,
1674 is_below: bool,
1675 options: &LayoutOptions,
1676) -> LayoutBox {
1677 let body_box = layout_node(base, options);
1678 let base_w = body_box.width.max(0.5);
1679
1680 if label == "\\textcircled" {
1682 return layout_textcircled(body_box, options);
1683 }
1684
1685 if let Some((commands, w, h, fill)) =
1687 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1688 {
1689 let accent_box = LayoutBox {
1691 width: w,
1692 height: 0.0,
1693 depth: h,
1694 content: BoxContent::SvgPath { commands, fill },
1695 color: options.color,
1696 };
1697 let gap = 0.065;
1702 let under_gap_em = if is_below && label == "\\utilde" {
1703 0.12
1704 } else {
1705 0.0
1706 };
1707 let clearance = if is_below {
1708 body_box.height + body_box.depth + gap
1709 } else if label == "\\vec" {
1710 (body_box.height - options.metrics().x_height).max(0.0)
1713 } else {
1714 body_box.height + gap
1715 };
1716 let (height, depth) = if is_below {
1717 (body_box.height, body_box.depth + h + gap + under_gap_em)
1718 } else if label == "\\vec" {
1719 (clearance + h, body_box.depth)
1721 } else {
1722 (body_box.height + gap + h, body_box.depth)
1723 };
1724 let vec_skew = if label == "\\vec" {
1725 (if is_shifty {
1726 glyph_skew(&body_box)
1727 } else {
1728 0.0
1729 }) + VEC_SKEW_EXTRA_RIGHT_EM
1730 } else {
1731 0.0
1732 };
1733 return LayoutBox {
1734 width: body_box.width,
1735 height,
1736 depth,
1737 content: BoxContent::Accent {
1738 base: Box::new(body_box),
1739 accent: Box::new(accent_box),
1740 clearance,
1741 skew: vec_skew,
1742 is_below,
1743 under_gap_em,
1744 },
1745 color: options.color,
1746 };
1747 }
1748
1749 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1751
1752 let accent_box = if use_arrow_path {
1753 let (commands, arrow_h, fill_arrow) =
1754 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1755 Some((c, h)) => (c, h, true),
1756 None => {
1757 let h = 0.3_f64;
1758 let c = stretchy_accent_path(label, base_w, h);
1759 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1760 (c, h, fill)
1761 }
1762 };
1763 LayoutBox {
1764 width: base_w,
1765 height: arrow_h / 2.0,
1766 depth: arrow_h / 2.0,
1767 content: BoxContent::SvgPath {
1768 commands,
1769 fill: fill_arrow,
1770 },
1771 color: options.color,
1772 }
1773 } else {
1774 let accent_char = {
1776 let ch = resolve_symbol_char(label, Mode::Text);
1777 if ch == label.chars().next().unwrap_or('?') {
1778 resolve_symbol_char(label, Mode::Math)
1781 } else {
1782 ch
1783 }
1784 };
1785 let accent_code = accent_char as u32;
1786 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1787 let (accent_w, accent_h, accent_d) = match accent_metrics {
1788 Some(m) => (m.width, m.height, m.depth),
1789 None => (body_box.width, 0.25, 0.0),
1790 };
1791 LayoutBox {
1792 width: accent_w,
1793 height: accent_h,
1794 depth: accent_d,
1795 content: BoxContent::Glyph {
1796 font_id: FontId::MainRegular,
1797 char_code: accent_code,
1798 },
1799 color: options.color,
1800 }
1801 };
1802
1803 let skew = if use_arrow_path {
1804 0.0
1805 } else if is_shifty {
1806 glyph_skew(&body_box)
1809 } else {
1810 0.0
1811 };
1812
1813 let gap = if use_arrow_path {
1822 if label == "\\Overrightarrow" {
1823 0.21
1824 } else {
1825 0.26
1826 }
1827 } else {
1828 0.0
1829 };
1830
1831 let clearance = if is_below {
1832 body_box.height + body_box.depth + accent_box.depth + gap
1833 } else if use_arrow_path {
1834 body_box.height + gap
1835 } else {
1836 let base_clearance = match &body_box.content {
1843 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1844 if !is_below =>
1845 {
1846 if inner_accent.height <= 0.001 {
1850 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1856 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1857 katex_pos + correction
1858 } else {
1859 if label == "\\bar" || label == "\\=" {
1865 body_box.height
1866 } else {
1867 let inner_visual_top = inner_cl + 0.35_f64.min(inner_accent.height);
1872 let h_for_kern = if body_box.height > inner_visual_top + 0.002 {
1873 inner_visual_top
1874 } else {
1875 body_box.height
1876 };
1877 let katex_pos = (h_for_kern - options.metrics().x_height).max(0.0);
1878 let correction =
1879 (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1880 katex_pos + correction
1881 }
1882 }
1883 }
1884 _ => {
1885 if label == "\\bar" || label == "\\=" {
1898 body_box.height
1899 } else {
1900 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1901 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1902 katex_pos + correction
1903 }
1904 }
1905 };
1906 let base_clearance = base_clearance + accent_box.depth;
1911 if label == "\\bar" || label == "\\=" {
1912 (base_clearance - 0.12).max(0.0)
1913 } else {
1914 base_clearance
1915 }
1916 };
1917
1918 let (height, depth) = if is_below {
1919 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1920 } else if use_arrow_path {
1921 (body_box.height + gap + accent_box.height, body_box.depth)
1922 } else {
1923 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1930 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1931 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1932 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1933 } else {
1934 body_box.height.max(accent_visual_top)
1935 };
1936 (h, body_box.depth)
1937 };
1938
1939 LayoutBox {
1940 width: body_box.width,
1941 height,
1942 depth,
1943 content: BoxContent::Accent {
1944 base: Box::new(body_box),
1945 accent: Box::new(accent_box),
1946 clearance,
1947 skew,
1948 is_below,
1949 under_gap_em: 0.0,
1950 },
1951 color: options.color,
1952 }
1953}
1954
1955fn node_contains_middle(node: &ParseNode) -> bool {
1961 match node {
1962 ParseNode::Middle { .. } => true,
1963 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1964 body.iter().any(node_contains_middle)
1965 }
1966 ParseNode::SupSub { base, sup, sub, .. } => {
1967 base.as_deref().is_some_and(node_contains_middle)
1968 || sup.as_deref().is_some_and(node_contains_middle)
1969 || sub.as_deref().is_some_and(node_contains_middle)
1970 }
1971 ParseNode::GenFrac { numer, denom, .. } => {
1972 node_contains_middle(numer) || node_contains_middle(denom)
1973 }
1974 ParseNode::Sqrt { body, index, .. } => {
1975 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1976 }
1977 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1978 node_contains_middle(base)
1979 }
1980 ParseNode::Op { body, .. } => body
1981 .as_ref()
1982 .is_some_and(|b| b.iter().any(node_contains_middle)),
1983 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1984 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1985 ParseNode::Font { body, .. } => node_contains_middle(body),
1986 ParseNode::Text { body, .. }
1987 | ParseNode::Color { body, .. }
1988 | ParseNode::Styling { body, .. }
1989 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1990 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1991 node_contains_middle(body)
1992 }
1993 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1994 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1995 node_contains_middle(body)
1996 }
1997 ParseNode::Array { body, .. } => body
1998 .iter()
1999 .any(|row| row.iter().any(node_contains_middle)),
2000 ParseNode::Enclose { body, .. }
2001 | ParseNode::Lap { body, .. }
2002 | ParseNode::RaiseBox { body, .. }
2003 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
2004 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
2005 ParseNode::XArrow { body, below, .. } => {
2006 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
2007 }
2008 ParseNode::CdArrow { label_above, label_below, .. } => {
2009 label_above.as_deref().is_some_and(node_contains_middle)
2010 || label_below.as_deref().is_some_and(node_contains_middle)
2011 }
2012 ParseNode::MathChoice {
2013 display,
2014 text,
2015 script,
2016 scriptscript,
2017 ..
2018 } => {
2019 display.iter().any(node_contains_middle)
2020 || text.iter().any(node_contains_middle)
2021 || script.iter().any(node_contains_middle)
2022 || scriptscript.iter().any(node_contains_middle)
2023 }
2024 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
2025 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
2026 ParseNode::Html { body, .. } => body.iter().any(node_contains_middle),
2027 _ => false,
2028 }
2029}
2030
2031fn body_contains_middle(nodes: &[ParseNode]) -> bool {
2033 nodes.iter().any(node_contains_middle)
2034}
2035
2036fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
2039 let m = options.metrics();
2040 if options.style.is_display() {
2041 m.delim1
2042 } else if matches!(
2043 options.style,
2044 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
2045 ) {
2046 options
2047 .with_style(MathStyle::Script)
2048 .metrics()
2049 .delim2
2050 } else {
2051 m.delim2
2052 }
2053}
2054
2055fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2057 let metrics = options.metrics();
2058 let inner_height = inner.height;
2059 let inner_depth = inner.depth;
2060 let axis = metrics.axis_height;
2061 let max_dist = (inner_height - axis).max(inner_depth + axis);
2062 let delim_factor = 901.0;
2063 let delim_extend = 5.0 / metrics.pt_per_em;
2064 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2065 from_formula.max(inner_height + inner_depth)
2067}
2068
2069fn layout_left_right(
2070 body: &[ParseNode],
2071 left_delim: &str,
2072 right_delim: &str,
2073 options: &LayoutOptions,
2074) -> LayoutBox {
2075 let (inner, total_height) = if body_contains_middle(body) {
2076 let opts_first = LayoutOptions {
2078 leftright_delim_height: None,
2079 ..options.clone()
2080 };
2081 let inner_first = layout_expression(body, &opts_first, true);
2082 let total_height = left_right_delim_total_height(&inner_first, options);
2083 let opts_second = LayoutOptions {
2085 leftright_delim_height: Some(total_height),
2086 ..options.clone()
2087 };
2088 let inner_second = layout_expression(body, &opts_second, true);
2089 (inner_second, total_height)
2090 } else {
2091 let inner = layout_expression(body, options, true);
2092 let total_height = left_right_delim_total_height(&inner, options);
2093 (inner, total_height)
2094 };
2095
2096 let inner_height = inner.height;
2097 let inner_depth = inner.depth;
2098
2099 let left_box = make_stretchy_delim(left_delim, total_height, options);
2100 let right_box = make_stretchy_delim(right_delim, total_height, options);
2101
2102 let width = left_box.width + inner.width + right_box.width;
2103 let height = left_box.height.max(right_box.height).max(inner_height);
2104 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2105
2106 LayoutBox {
2107 width,
2108 height,
2109 depth,
2110 content: BoxContent::LeftRight {
2111 left: Box::new(left_box),
2112 right: Box::new(right_box),
2113 inner: Box::new(inner),
2114 },
2115 color: options.color,
2116 }
2117}
2118
2119const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2120 FontId::MainRegular,
2121 FontId::Size1Regular,
2122 FontId::Size2Regular,
2123 FontId::Size3Regular,
2124 FontId::Size4Regular,
2125];
2126
2127fn normalize_delim(delim: &str) -> &str {
2129 match delim {
2130 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2131 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2132 _ => delim,
2133 }
2134}
2135
2136fn is_vert_delim(delim: &str) -> bool {
2138 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2139}
2140
2141fn is_double_vert_delim(delim: &str) -> bool {
2143 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2144}
2145
2146fn vert_repeat_piece_height(is_double: bool) -> f64 {
2148 let code = if is_double { 8741_u32 } else { 8739 };
2149 get_char_metrics(FontId::Size1Regular, code)
2150 .map(|m| m.height + m.depth)
2151 .unwrap_or(0.5)
2152}
2153
2154fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2156 let piece = vert_repeat_piece_height(is_double);
2157 let min_h = 2.0 * piece;
2158 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2159 let mut h = min_h + repeat_count * piece;
2160 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2164 h *= 1.135;
2165 }
2166 h
2167}
2168
2169fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2171 let neg = -mid_th;
2172 if !is_double {
2173 format!(
2174 "M145 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v{mid_th} v585 h43z"
2175 )
2176 } else {
2177 format!(
2178 "M145 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v{mid_th} v585 h43z M367 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M410 15 H367 v585 v{mid_th} v585 h43z"
2179 )
2180 }
2181}
2182
2183fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2184 let s = 0.001_f64;
2185 cmds.iter()
2186 .map(|c| match *c {
2187 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2188 x: x * s,
2189 y: y * s,
2190 },
2191 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2192 x: x * s,
2193 y: y * s,
2194 },
2195 PathCommand::CubicTo {
2196 x1,
2197 y1,
2198 x2,
2199 y2,
2200 x,
2201 y,
2202 } => PathCommand::CubicTo {
2203 x1: x1 * s,
2204 y1: y1 * s,
2205 x2: x2 * s,
2206 y2: y2 * s,
2207 x: x * s,
2208 y: y * s,
2209 },
2210 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2211 x1: x1 * s,
2212 y1: y1 * s,
2213 x: x * s,
2214 y: y * s,
2215 },
2216 PathCommand::Close => PathCommand::Close,
2217 })
2218 .collect()
2219}
2220
2221fn map_vert_path_y_to_baseline(
2223 cmds: Vec<PathCommand>,
2224 height: f64,
2225 depth: f64,
2226 view_box_height: i64,
2227) -> Vec<PathCommand> {
2228 let span_em = view_box_height as f64 / 1000.0;
2229 let total = height + depth;
2230 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2231 cmds.into_iter()
2232 .map(|c| match c {
2233 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2234 x,
2235 y: -height + y * scale_y,
2236 },
2237 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2238 x,
2239 y: -height + y * scale_y,
2240 },
2241 PathCommand::CubicTo {
2242 x1,
2243 y1,
2244 x2,
2245 y2,
2246 x,
2247 y,
2248 } => PathCommand::CubicTo {
2249 x1,
2250 y1: -height + y1 * scale_y,
2251 x2,
2252 y2: -height + y2 * scale_y,
2253 x,
2254 y: -height + y * scale_y,
2255 },
2256 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2257 x1,
2258 y1: -height + y1 * scale_y,
2259 x,
2260 y: -height + y * scale_y,
2261 },
2262 PathCommand::Close => PathCommand::Close,
2263 })
2264 .collect()
2265}
2266
2267fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2270 let real_h = katex_vert_real_height(total_height, is_double);
2271 let axis = options.metrics().axis_height;
2272 let depth = (real_h / 2.0 - axis).max(0.0);
2273 let height = real_h - depth;
2274 let width = if is_double { 0.556 } else { 0.333 };
2275
2276 let piece = vert_repeat_piece_height(is_double);
2277 let mid_em = (real_h - 2.0 * piece).max(0.0);
2278 let mid_th = (mid_em * 1000.0).round() as i64;
2279 let view_box_height = (real_h * 1000.0).round() as i64;
2280
2281 let d = tall_vert_svg_path_data(mid_th, is_double);
2282 let raw = parse_svg_path_data(&d);
2283 let scaled = scale_svg_path_to_em(&raw);
2284 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2285
2286 LayoutBox {
2287 width,
2288 height,
2289 depth,
2290 content: BoxContent::SvgPath { commands, fill: true },
2291 color: options.color,
2292 }
2293}
2294
2295fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2297 if delim == "." || delim.is_empty() {
2298 return LayoutBox::new_kern(0.0);
2299 }
2300
2301 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2306 return make_vert_delim_box(total_height, false, options);
2307 }
2308 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2309 return make_vert_delim_box(total_height, true, options);
2310 }
2311
2312 let delim = normalize_delim(delim);
2314
2315 let ch = resolve_symbol_char(delim, Mode::Math);
2316 let char_code = ch as u32;
2317
2318 let mut best_font = FontId::MainRegular;
2319 let mut best_w = 0.4;
2320 let mut best_h = 0.7;
2321 let mut best_d = 0.2;
2322
2323 for &font_id in &DELIM_FONT_SEQUENCE {
2324 if let Some(m) = get_char_metrics(font_id, char_code) {
2325 best_font = font_id;
2326 best_w = m.width;
2327 best_h = m.height;
2328 best_d = m.depth;
2329 if best_h + best_d >= total_height {
2330 break;
2331 }
2332 }
2333 }
2334
2335 let best_total = best_h + best_d;
2336 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2337 return stacked;
2338 }
2339
2340 LayoutBox {
2341 width: best_w,
2342 height: best_h,
2343 depth: best_d,
2344 content: BoxContent::Glyph {
2345 font_id: best_font,
2346 char_code,
2347 },
2348 color: options.color,
2349 }
2350}
2351
2352const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2354
2355fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2357 if delim == "." || delim.is_empty() {
2358 return LayoutBox::new_kern(0.0);
2359 }
2360
2361 if is_vert_delim(delim) {
2363 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2364 return make_vert_delim_box(total, false, options);
2365 }
2366 if is_double_vert_delim(delim) {
2367 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2368 return make_vert_delim_box(total, true, options);
2369 }
2370
2371 let delim = normalize_delim(delim);
2373
2374 let ch = resolve_symbol_char(delim, Mode::Math);
2375 let char_code = ch as u32;
2376
2377 let font_id = match size {
2378 1 => FontId::Size1Regular,
2379 2 => FontId::Size2Regular,
2380 3 => FontId::Size3Regular,
2381 4 => FontId::Size4Regular,
2382 _ => FontId::Size1Regular,
2383 };
2384
2385 let metrics = get_char_metrics(font_id, char_code);
2386 let (width, height, depth, actual_font) = match metrics {
2387 Some(m) => (m.width, m.height, m.depth, font_id),
2388 None => {
2389 let m = get_char_metrics(FontId::MainRegular, char_code);
2390 match m {
2391 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2392 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2393 }
2394 }
2395 };
2396
2397 LayoutBox {
2398 width,
2399 height,
2400 depth,
2401 content: BoxContent::Glyph {
2402 font_id: actual_font,
2403 char_code,
2404 },
2405 color: options.color,
2406 }
2407}
2408
2409#[allow(clippy::too_many_arguments)]
2414fn layout_array(
2415 body: &[Vec<ParseNode>],
2416 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2417 arraystretch: f64,
2418 add_jot: bool,
2419 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2420 hlines: &[Vec<bool>],
2421 col_sep_type: Option<&str>,
2422 hskip: bool,
2423 tags: Option<&[ArrayTag]>,
2424 _leqno: bool,
2425 options: &LayoutOptions,
2426) -> LayoutBox {
2427 let metrics = options.metrics();
2428 let pt = 1.0 / metrics.pt_per_em;
2429 let baselineskip = 12.0 * pt;
2430 let jot = 3.0 * pt;
2431 let arrayskip = arraystretch * baselineskip;
2432 let arstrut_h = 0.7 * arrayskip;
2433 let arstrut_d = 0.3 * arrayskip;
2434 const ALIGN_RELATION_MU: f64 = 3.0;
2437 let col_gap = match col_sep_type {
2438 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2439 Some("alignat") => 0.0,
2440 Some("small") => {
2441 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2444 / options.size_multiplier()
2445 }
2446 _ => 2.0 * 5.0 * pt, };
2448 let cell_options = match col_sep_type {
2449 Some("align") | Some("alignat") => LayoutOptions {
2450 align_relation_spacing: Some(ALIGN_RELATION_MU),
2451 ..options.clone()
2452 },
2453 _ => options.clone(),
2454 };
2455
2456 let num_rows = body.len();
2457 if num_rows == 0 {
2458 return LayoutBox::new_empty();
2459 }
2460
2461 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2462
2463 use ratex_parser::parse_node::AlignType;
2465 let col_aligns: Vec<u8> = {
2466 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2467 .map(|cs| {
2468 cs.iter()
2469 .filter(|s| matches!(s.align_type, AlignType::Align))
2470 .collect()
2471 })
2472 .unwrap_or_default();
2473 (0..num_cols)
2474 .map(|c| {
2475 align_specs
2476 .get(c)
2477 .and_then(|s| s.align.as_deref())
2478 .and_then(|a| a.bytes().next())
2479 .unwrap_or(b'c')
2480 })
2481 .collect()
2482 };
2483
2484 let col_separators: Vec<Option<bool>> = {
2487 let mut seps = vec![None; num_cols + 1];
2488 let mut align_count = 0usize;
2489 if let Some(cs) = cols {
2490 for spec in cs {
2491 match spec.align_type {
2492 AlignType::Align => align_count += 1,
2493 AlignType::Separator
2494 if spec.align.as_deref() == Some("|") && align_count <= num_cols =>
2495 {
2496 seps[align_count] = Some(false);
2497 }
2498 AlignType::Separator
2499 if spec.align.as_deref() == Some(":") && align_count <= num_cols =>
2500 {
2501 seps[align_count] = Some(true);
2502 }
2503 _ => {}
2504 }
2505 }
2506 }
2507 seps
2508 };
2509
2510 let rule_thickness = 0.4 * pt;
2511 let double_rule_sep = metrics.double_rule_sep;
2512
2513 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2515 let mut col_widths = vec![0.0_f64; num_cols];
2516 let mut row_heights = Vec::with_capacity(num_rows);
2517 let mut row_depths = Vec::with_capacity(num_rows);
2518
2519 for row in body {
2520 let mut row_boxes = Vec::with_capacity(num_cols);
2521 let mut rh = arstrut_h;
2522 let mut rd = arstrut_d;
2523
2524 for (c, cell) in row.iter().enumerate() {
2525 let cell_nodes = match cell {
2526 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2527 other => std::slice::from_ref(other),
2528 };
2529 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2530 rh = rh.max(cell_box.height);
2531 rd = rd.max(cell_box.depth);
2532 if c < num_cols {
2533 col_widths[c] = col_widths[c].max(cell_box.width);
2534 }
2535 row_boxes.push(cell_box);
2536 }
2537
2538 while row_boxes.len() < num_cols {
2540 row_boxes.push(LayoutBox::new_empty());
2541 }
2542
2543 if add_jot {
2544 rd += jot;
2545 }
2546
2547 row_heights.push(rh);
2548 row_depths.push(rd);
2549 cell_boxes.push(row_boxes);
2550 }
2551
2552 for (r, gap) in row_gaps.iter().enumerate() {
2554 if r < row_depths.len() {
2555 if let Some(m) = gap {
2556 let gap_em = measurement_to_em(m, options);
2557 if gap_em > 0.0 {
2558 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2559 }
2560 }
2561 }
2562 }
2563
2564 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2566 while hlines_before_row.len() < num_rows + 1 {
2567 hlines_before_row.push(vec![]);
2568 }
2569
2570 for r in 0..=num_rows {
2576 let n = hlines_before_row[r].len();
2577 if n > 1 {
2578 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2579 if r == 0 {
2580 if num_rows > 0 {
2581 row_heights[0] += extra;
2582 }
2583 } else {
2584 row_depths[r - 1] += extra;
2585 }
2586 }
2587 }
2588
2589 let mut total_height = 0.0;
2591 let mut row_positions = Vec::with_capacity(num_rows);
2592 for r in 0..num_rows {
2593 total_height += row_heights[r];
2594 row_positions.push(total_height);
2595 total_height += row_depths[r];
2596 }
2597
2598 let offset = total_height / 2.0 + metrics.axis_height;
2599
2600 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2602
2603 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2605 + col_gap * (num_cols.saturating_sub(1)) as f64
2606 + 2.0 * content_x_offset;
2607
2608 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2609 let mut tag_col_width = 0.0_f64;
2610 let text_opts = options.with_style(options.style.text());
2611 if let Some(tag_slice) = tags {
2612 if tag_slice.len() == num_rows {
2613 for (r, t) in tag_slice.iter().enumerate() {
2614 if let ArrayTag::Explicit(nodes) = t {
2615 if !nodes.is_empty() {
2616 let tb = layout_expression(nodes, &text_opts, true);
2617 tag_col_width = tag_col_width.max(tb.width);
2618 row_tag_boxes[r] = Some(tb);
2619 }
2620 }
2621 }
2622 }
2623 }
2624 let tag_gap_em = if tag_col_width > 0.0 {
2625 text_opts.metrics().quad
2626 } else {
2627 0.0
2628 };
2629 let tags_left = false;
2631
2632 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2633
2634 let height = offset;
2635 let depth = total_height - offset;
2636
2637 LayoutBox {
2638 width: total_width,
2639 height,
2640 depth,
2641 content: BoxContent::Array {
2642 cells: cell_boxes,
2643 col_widths: col_widths.clone(),
2644 col_aligns,
2645 row_heights: row_heights.clone(),
2646 row_depths: row_depths.clone(),
2647 col_gap,
2648 offset,
2649 content_x_offset,
2650 col_separators,
2651 hlines_before_row,
2652 rule_thickness,
2653 double_rule_sep,
2654 array_inner_width,
2655 tag_gap_em,
2656 tag_col_width,
2657 row_tags: row_tag_boxes,
2658 tags_left,
2659 },
2660 color: options.color,
2661 }
2662}
2663
2664fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2669 let multiplier = match size {
2671 1 => 0.5,
2672 2 => 0.6,
2673 3 => 0.7,
2674 4 => 0.8,
2675 5 => 0.9,
2676 6 => 1.0,
2677 7 => 1.2,
2678 8 => 1.44,
2679 9 => 1.728,
2680 10 => 2.074,
2681 11 => 2.488,
2682 _ => 1.0,
2683 };
2684
2685 let inner_opts = options.with_style(options.style.text());
2687 let inner = layout_expression(body, &inner_opts, true);
2688 let ratio = multiplier / options.size_multiplier();
2689 if (ratio - 1.0).abs() < 0.001 {
2690 inner
2691 } else {
2692 LayoutBox {
2693 width: inner.width * ratio,
2694 height: inner.height * ratio,
2695 depth: inner.depth * ratio,
2696 content: BoxContent::Scaled {
2697 body: Box::new(inner),
2698 child_scale: ratio,
2699 },
2700 color: options.color,
2701 }
2702 }
2703}
2704
2705#[derive(Default)]
2706struct HtmlStyle {
2707 color: Option<Color>,
2708 font_size_scale: Option<f64>,
2709 bold: bool,
2710 italic: bool,
2711 background_color: Option<Color>,
2712 underline: bool,
2713}
2714
2715fn layout_html(attributes: &HashMap<String, String>, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2716 let style = attributes
2717 .get("style")
2718 .map(|style| parse_html_style(style))
2719 .unwrap_or_default();
2720
2721 let body_options = match style.color {
2722 Some(color) => options.with_color(color),
2723 None => options.clone(),
2724 };
2725 let font_id = match (style.bold, style.italic) {
2726 (true, true) => Some(FontId::MainBoldItalic),
2727 (true, false) => Some(FontId::MainBold),
2728 (false, true) => Some(FontId::MainItalic),
2729 (false, false) => None,
2730 };
2731
2732 let body_node = ParseNode::OrdGroup {
2733 mode: body.first().map(ParseNode::mode).unwrap_or(Mode::Math),
2734 body: body.to_vec(),
2735 semisimple: None,
2736 loc: None,
2737 };
2738 let mut lbox = match font_id {
2739 Some(font_id) => layout_with_font(&body_node, font_id, &body_options),
2740 None => layout_expression(body, &body_options, true),
2741 };
2742
2743 if let Some(scale) = style.font_size_scale {
2744 if (scale - 1.0).abs() >= 0.001 {
2745 lbox = LayoutBox {
2746 width: lbox.width * scale,
2747 height: lbox.height * scale,
2748 depth: lbox.depth * scale,
2749 content: BoxContent::Scaled {
2750 body: Box::new(lbox),
2751 child_scale: scale,
2752 },
2753 color: body_options.color,
2754 };
2755 }
2756 }
2757
2758 if let Some(background_color) = style.background_color {
2759 lbox = LayoutBox {
2760 width: lbox.width,
2761 height: lbox.height,
2762 depth: lbox.depth,
2763 content: BoxContent::Framed {
2764 body: Box::new(lbox),
2765 padding: 0.0,
2766 border_thickness: 0.0,
2767 has_border: false,
2768 bg_color: Some(background_color),
2769 border_color: Color::BLACK,
2770 },
2771 color: body_options.color,
2772 };
2773 }
2774
2775 if style.underline {
2776 lbox = layout_underline_laid_out(lbox, options, body_options.color);
2777 }
2778
2779 lbox
2780}
2781
2782fn parse_html_style(style: &str) -> HtmlStyle {
2783 let mut parsed = HtmlStyle::default();
2784 for declaration in style.split(';') {
2785 let Some((property, value)) = declaration.split_once(':') else {
2786 continue;
2787 };
2788 let property = property.trim().to_ascii_lowercase();
2789 let value = value.trim();
2790 match property.as_str() {
2791 "color" => parsed.color = Color::parse(value),
2792 "font-size" => parsed.font_size_scale = parse_css_font_size(value),
2793 "font-weight" => parsed.bold = is_css_bold(value),
2794 "font-style" => parsed.italic = is_css_italic(value),
2795 "background" | "background-color" => parsed.background_color = Color::parse(value),
2796 "text-decoration" | "text-decoration-line" => {
2797 parsed.underline = value
2798 .split_whitespace()
2799 .any(|part| part.eq_ignore_ascii_case("underline"));
2800 }
2801 _ => {}
2802 }
2803 }
2804 parsed
2805}
2806
2807fn parse_css_font_size(value: &str) -> Option<f64> {
2808 let value = value.trim().to_ascii_lowercase().replace(' ', "");
2809 let parse_number = |s: &str| s.parse::<f64>().ok().filter(|n| n.is_finite() && *n > 0.0);
2810 if let Some(px) = value.strip_suffix("px") {
2811 parse_number(px).map(|n| n / 16.0)
2812 } else if let Some(em) = value.strip_suffix("em").or_else(|| value.strip_suffix("rem")) {
2813 parse_number(em)
2814 } else if let Some(percent) = value.strip_suffix('%') {
2815 parse_number(percent).map(|n| n / 100.0)
2816 } else {
2817 None
2818 }
2819}
2820
2821fn is_css_bold(value: &str) -> bool {
2822 let value = value.trim();
2823 value.eq_ignore_ascii_case("bold")
2824 || value.eq_ignore_ascii_case("bolder")
2825 || value.parse::<u16>().is_ok_and(|weight| weight >= 600)
2826}
2827
2828fn is_css_italic(value: &str) -> bool {
2829 let value = value.trim();
2830 value.eq_ignore_ascii_case("italic") || value.eq_ignore_ascii_case("oblique")
2831}
2832
2833fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2836 let metrics = options.metrics();
2837 let mut children = Vec::new();
2838 for c in body.chars() {
2839 let ch = if star && c == ' ' {
2840 '\u{2423}' } else {
2842 c
2843 };
2844 let code = ch as u32;
2845 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2846 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2847 None => match get_char_metrics(FontId::MainRegular, code) {
2848 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2849 None => (
2850 FontId::TypewriterRegular,
2851 0.5,
2852 metrics.x_height,
2853 0.0,
2854 ),
2855 },
2856 };
2857 children.push(LayoutBox {
2858 width: w,
2859 height: h,
2860 depth: d,
2861 content: BoxContent::Glyph {
2862 font_id,
2863 char_code: code,
2864 },
2865 color: options.color,
2866 });
2867 }
2868 let mut hbox = make_hbox(children);
2869 hbox.color = options.color;
2870 hbox
2871}
2872
2873fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2881 let mut children = Vec::new();
2882 for node in body {
2883 match node {
2884 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2885 children.push(layout_symbol(text, *mode, options));
2886 }
2887 ParseNode::SpacingNode { text, .. } => {
2888 children.push(layout_spacing_command(text, options));
2889 }
2890 _ => {
2891 children.push(layout_node(node, options));
2892 }
2893 }
2894 }
2895 make_hbox(children)
2896}
2897
2898fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2901 let base = layout_expression(body, options, true);
2902 let w = base.width;
2903 let h = base.height;
2904 let d = base.depth;
2905
2906 let shadow = layout_expression(body, options, true);
2908 let shadow_shift_x = 0.02_f64;
2909 let _shadow_shift_y = 0.01_f64;
2910
2911 let kern_back = LayoutBox::new_kern(-w);
2915 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2916
2917 let children = vec![
2924 kern_x,
2925 shadow,
2926 kern_back,
2927 base,
2928 ];
2929 let hbox = make_hbox(children);
2931 LayoutBox {
2933 width: w,
2934 height: h,
2935 depth: d,
2936 content: hbox.content,
2937 color: options.color,
2938 }
2939}
2940
2941fn layout_enclose(
2944 label: &str,
2945 background_color: Option<&str>,
2946 border_color: Option<&str>,
2947 body: &ParseNode,
2948 options: &LayoutOptions,
2949) -> LayoutBox {
2950 use crate::layout_box::BoxContent;
2951 use ratex_types::color::Color;
2952
2953 if label == "\\phase" {
2955 return layout_phase(body, options);
2956 }
2957
2958 if label == "\\angl" {
2960 return layout_angl(body, options);
2961 }
2962
2963 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2965 return layout_cancel(label, body, options);
2966 }
2967
2968 let metrics = options.metrics();
2970 let padding = 3.0 / metrics.pt_per_em;
2971 let border_thickness = 0.4 / metrics.pt_per_em;
2972
2973 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2974
2975 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2976 let border = border_color
2977 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2978 .unwrap_or(Color::BLACK);
2979
2980 let inner = layout_node(body, options);
2981 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2982
2983 let width = inner.width + 2.0 * outer_pad;
2984 let height = inner.height + outer_pad;
2985 let depth = inner.depth + outer_pad;
2986
2987 LayoutBox {
2988 width,
2989 height,
2990 depth,
2991 content: BoxContent::Framed {
2992 body: Box::new(inner),
2993 padding,
2994 border_thickness,
2995 has_border,
2996 bg_color: bg,
2997 border_color: border,
2998 },
2999 color: options.color,
3000 }
3001}
3002
3003fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3005 use crate::layout_box::BoxContent;
3006 let inner = layout_node(body, options);
3007 let height = inner.height + shift;
3009 let depth = (inner.depth - shift).max(0.0);
3010 let width = inner.width;
3011 LayoutBox {
3012 width,
3013 height,
3014 depth,
3015 content: BoxContent::RaiseBox {
3016 body: Box::new(inner),
3017 shift,
3018 },
3019 color: options.color,
3020 }
3021}
3022
3023fn is_single_char_body(node: &ParseNode) -> bool {
3026 use ratex_parser::parse_node::ParseNode as PN;
3027 match node {
3028 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3030 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3031 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
3033 _ => false,
3034 }
3035}
3036
3037fn layout_cancel(
3043 label: &str,
3044 body: &ParseNode,
3045 options: &LayoutOptions,
3046) -> LayoutBox {
3047 use crate::layout_box::BoxContent;
3048 let inner = layout_node(body, options);
3049 let w = inner.width.max(0.01);
3050 let h = inner.height;
3051 let d = inner.depth;
3052
3053 let single = is_single_char_body(body);
3056 let (v_pad, h_pad) = if label == "\\sout" {
3057 (0.0, 0.0)
3058 } else if single {
3059 (0.2, 0.0)
3060 } else {
3061 (0.0, 0.2)
3062 };
3063
3064 let commands: Vec<PathCommand> = match label {
3068 "\\cancel" => vec![
3069 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
3072 "\\bcancel" => vec![
3073 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
3076 "\\xcancel" => vec![
3077 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
3078 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
3079 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
3080 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
3081 ],
3082 "\\sout" => {
3083 let mid_y = -0.5 * options.metrics().x_height;
3085 vec![
3086 PathCommand::MoveTo { x: 0.0, y: mid_y },
3087 PathCommand::LineTo { x: w, y: mid_y },
3088 ]
3089 }
3090 _ => vec![],
3091 };
3092
3093 let line_w = w + 2.0 * h_pad;
3094 let line_h = h + v_pad;
3095 let line_d = d + v_pad;
3096 let line_box = LayoutBox {
3097 width: line_w,
3098 height: line_h,
3099 depth: line_d,
3100 content: BoxContent::SvgPath { commands, fill: false },
3101 color: options.color,
3102 };
3103
3104 let body_kern = -(line_w - h_pad);
3106 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
3107 LayoutBox {
3108 width: w,
3109 height: h,
3110 depth: d,
3111 content: BoxContent::HBox(vec![line_box, body_shifted]),
3112 color: options.color,
3113 }
3114}
3115
3116fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3119 use crate::layout_box::BoxContent;
3120 let metrics = options.metrics();
3121 let inner = layout_node(body, options);
3122 let line_weight = 0.6_f64 / metrics.pt_per_em;
3124 let clearance = 0.35_f64 * metrics.x_height;
3125 let angle_height = inner.height + inner.depth + line_weight + clearance;
3126 let left_pad = angle_height / 2.0 + line_weight;
3127 let width = inner.width + left_pad;
3128
3129 let y_svg = (1000.0 * angle_height).floor().max(80.0);
3131
3132 let sy = angle_height / y_svg;
3134 let sx = sy;
3137 let right_x = (400_000.0_f64 * sx).min(width);
3138
3139 let bottom_y = inner.depth + line_weight + clearance;
3141 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
3142
3143 let x_peak = y_svg / 2.0;
3145 let commands = vec![
3146 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
3147 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
3148 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
3149 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
3150 PathCommand::LineTo {
3151 x: 145.0 * sx,
3152 y: vy(y_svg - 80.0),
3153 },
3154 PathCommand::LineTo {
3155 x: right_x,
3156 y: vy(y_svg - 80.0),
3157 },
3158 PathCommand::Close,
3159 ];
3160
3161 let body_shifted = make_hbox(vec![
3162 LayoutBox::new_kern(left_pad),
3163 inner.clone(),
3164 ]);
3165
3166 let path_height = inner.height;
3167 let path_depth = bottom_y;
3168
3169 LayoutBox {
3170 width,
3171 height: path_height,
3172 depth: path_depth,
3173 content: BoxContent::HBox(vec![
3174 LayoutBox {
3175 width,
3176 height: path_height,
3177 depth: path_depth,
3178 content: BoxContent::SvgPath { commands, fill: true },
3179 color: options.color,
3180 },
3181 LayoutBox::new_kern(-width),
3182 body_shifted,
3183 ]),
3184 color: options.color,
3185 }
3186}
3187
3188fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3191 use crate::layout_box::BoxContent;
3192 let inner = layout_node(body, options);
3193 let w = inner.width.max(0.3);
3194 let clearance = 0.1_f64;
3196 let arc_h = inner.height + clearance;
3197
3198 let path_commands = vec![
3200 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3201 PathCommand::LineTo { x: w, y: -arc_h },
3202 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
3203 ];
3204
3205 let height = arc_h;
3206 LayoutBox {
3207 width: w,
3208 height,
3209 depth: inner.depth,
3210 content: BoxContent::Angl {
3211 path_commands,
3212 body: Box::new(inner),
3213 },
3214 color: options.color,
3215 }
3216}
3217
3218fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3219 let font_id = match font {
3220 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3221 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3222 "mathit" | "\\mathit" | "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3223 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3224 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3225 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3226 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3227 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3228 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3229 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3230 _ => None,
3231 };
3232
3233 if let Some(fid) = font_id {
3234 layout_with_font(body, fid, options)
3235 } else {
3236 layout_node(body, options)
3237 }
3238}
3239
3240fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3241 match node {
3242 ParseNode::OrdGroup { body, .. } => {
3243 let kern = options.inter_glyph_kern_em;
3244 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3245 for (i, n) in body.iter().enumerate() {
3246 if i > 0 && kern > 0.0 {
3247 children.push(LayoutBox::new_kern(kern));
3248 }
3249 children.push(layout_with_font(n, font_id, options));
3250 }
3251 make_hbox(children)
3252 }
3253 ParseNode::SupSub {
3254 base, sup, sub, ..
3255 } => {
3256 if let Some(base_node) = base.as_deref() {
3257 if should_use_op_limits(base_node, options) {
3258 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3259 }
3260 }
3261 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3262 }
3263 ParseNode::MathOrd { text, mode, .. }
3264 | ParseNode::TextOrd { text, mode, .. }
3265 | ParseNode::Atom { text, mode, .. } => {
3266 let ch = resolve_symbol_char(text, *mode);
3267 let char_code = ch as u32;
3268 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3269 .map(|(_, m)| m)
3270 .unwrap_or(char_code);
3271 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3272 LayoutBox {
3273 width: math_glyph_advance_em(&m, *mode),
3275 height: m.height,
3276 depth: m.depth,
3277 content: BoxContent::Glyph { font_id, char_code },
3278 color: options.color,
3279 }
3280 } else {
3281 layout_node(node, options)
3283 }
3284 }
3285 _ => layout_node(node, options),
3286 }
3287}
3288
3289fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3294 let cramped = options.with_style(options.style.cramped());
3295 let body_box = layout_node(body, &cramped);
3296 let metrics = options.metrics();
3297 let rule = metrics.default_rule_thickness;
3298
3299 let height = body_box.height + 3.0 * rule;
3301 LayoutBox {
3302 width: body_box.width,
3303 height,
3304 depth: body_box.depth,
3305 content: BoxContent::Overline {
3306 body: Box::new(body_box),
3307 rule_thickness: rule,
3308 },
3309 color: options.color,
3310 }
3311}
3312
3313fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3314 let body_box = layout_node(body, options);
3315 let metrics = options.metrics();
3316 let rule = metrics.default_rule_thickness;
3317
3318 let depth = body_box.depth + 3.0 * rule;
3320 LayoutBox {
3321 width: body_box.width,
3322 height: body_box.height,
3323 depth,
3324 content: BoxContent::Underline {
3325 body: Box::new(body_box),
3326 rule_thickness: rule,
3327 },
3328 color: options.color,
3329 }
3330}
3331
3332fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3334 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3335 let body_opts = options
3337 .with_color(link_color)
3338 .with_inter_glyph_kern(0.024);
3339 let body_box = layout_expression(body, &body_opts, true);
3340 layout_underline_laid_out(body_box, options, link_color)
3341}
3342
3343fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3345 let metrics = options.metrics();
3346 let rule = metrics.default_rule_thickness;
3347 let depth = body_box.depth + 3.0 * rule;
3348 LayoutBox {
3349 width: body_box.width,
3350 height: body_box.height,
3351 depth,
3352 content: BoxContent::Underline {
3353 body: Box::new(body_box),
3354 rule_thickness: rule,
3355 },
3356 color,
3357 }
3358}
3359
3360fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3365 let metrics = options.metrics();
3366 let mu = metrics.css_em_per_mu();
3367
3368 let width = match text {
3369 "\\," | "\\thinspace" => 3.0 * mu,
3370 "\\:" | "\\medspace" => 4.0 * mu,
3371 "\\;" | "\\thickspace" => 5.0 * mu,
3372 "\\!" | "\\negthinspace" => -3.0 * mu,
3373 "\\negmedspace" => -4.0 * mu,
3374 "\\negthickspace" => -5.0 * mu,
3375 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3376 get_char_metrics(FontId::MainRegular, 160)
3380 .map(|m| m.width)
3381 .unwrap_or(0.25)
3382 }
3383 "\\quad" => metrics.quad,
3384 "\\qquad" => 2.0 * metrics.quad,
3385 "\\enspace" => metrics.quad / 2.0,
3386 _ => 0.0,
3387 };
3388
3389 LayoutBox::new_kern(width)
3390}
3391
3392fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3397 let metrics = options.metrics();
3398 match m.unit.as_str() {
3399 "em" => m.number,
3400 "ex" => m.number * metrics.x_height,
3401 "mu" => m.number * metrics.css_em_per_mu(),
3402 "pt" => m.number / metrics.pt_per_em,
3403 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3404 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3405 "in" => m.number * 72.27 / metrics.pt_per_em,
3406 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3407 "pc" => m.number * 12.0 / metrics.pt_per_em,
3408 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3409 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3410 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3411 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3412 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3413 _ => m.number,
3414 }
3415}
3416
3417fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3423 match node {
3424 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3425 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3426 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3427 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3428 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3430 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3431 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3432 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3433 }
3434 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3435 ParseNode::SupSub { base, .. } => {
3436 base.as_ref().and_then(|b| node_math_class(b))
3437 }
3438 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3439 ParseNode::SpacingNode { .. } => None,
3440 ParseNode::Kern { .. } => None,
3441 ParseNode::HtmlMathMl { html, .. } => {
3442 for child in html {
3444 if let Some(cls) = node_math_class(child) {
3445 return Some(cls);
3446 }
3447 }
3448 None
3449 }
3450 ParseNode::Html { body, .. } => {
3451 for child in body {
3452 if let Some(cls) = node_math_class(child) {
3453 return Some(cls);
3454 }
3455 }
3456 None
3457 }
3458 ParseNode::Lap { .. } => None,
3459 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3460 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3461 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3463 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3465 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3466 ParseNode::Middle { .. } => Some(MathClass::Ord),
3467 _ => Some(MathClass::Ord),
3468 }
3469}
3470
3471fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3472 match mclass {
3473 "mord" => MathClass::Ord,
3474 "mop" => MathClass::Op,
3475 "mbin" => MathClass::Bin,
3476 "mrel" => MathClass::Rel,
3477 "mopen" => MathClass::Open,
3478 "mclose" => MathClass::Close,
3479 "mpunct" => MathClass::Punct,
3480 "minner" => MathClass::Inner,
3481 _ => MathClass::Ord,
3482 }
3483}
3484
3485fn get_base_elem(node: &ParseNode) -> &ParseNode {
3489 match node {
3490 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3491 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3492 ParseNode::Html { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3493 ParseNode::Font { body, .. } => get_base_elem(body),
3494 _ => node,
3495 }
3496}
3497
3498fn is_character_box(node: &ParseNode) -> bool {
3499 matches!(
3500 get_base_elem(node),
3501 ParseNode::MathOrd { .. }
3502 | ParseNode::TextOrd { .. }
3503 | ParseNode::Atom { .. }
3504 | ParseNode::AccentToken { .. }
3505 )
3506}
3507
3508fn family_to_math_class(family: AtomFamily) -> MathClass {
3509 match family {
3510 AtomFamily::Bin => MathClass::Bin,
3511 AtomFamily::Rel => MathClass::Rel,
3512 AtomFamily::Open => MathClass::Open,
3513 AtomFamily::Close => MathClass::Close,
3514 AtomFamily::Punct => MathClass::Punct,
3515 AtomFamily::Inner => MathClass::Inner,
3516 }
3517}
3518
3519fn layout_horiz_brace(
3524 base: &ParseNode,
3525 is_over: bool,
3526 func_label: &str,
3527 options: &LayoutOptions,
3528) -> LayoutBox {
3529 let body_box = layout_node(base, options);
3530 let w = body_box.width.max(0.5);
3531
3532 let is_bracket = func_label
3533 .trim_start_matches('\\')
3534 .ends_with("bracket");
3535
3536 let stretch_key = if is_bracket {
3538 if is_over {
3539 "overbracket"
3540 } else {
3541 "underbracket"
3542 }
3543 } else if is_over {
3544 "overbrace"
3545 } else {
3546 "underbrace"
3547 };
3548
3549 let (raw_commands, brace_h, brace_fill) =
3550 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3551 Some((c, h)) => (c, h, true),
3552 None => {
3553 let h = 0.35_f64;
3554 (horiz_brace_path(w, h, is_over), h, false)
3555 }
3556 };
3557
3558 let y_shift = brace_h / 2.0;
3564 let commands = shift_path_y(raw_commands, y_shift);
3565
3566 let brace_box = LayoutBox {
3567 width: w,
3568 height: 0.0,
3569 depth: brace_h,
3570 content: BoxContent::SvgPath {
3571 commands,
3572 fill: brace_fill,
3573 },
3574 color: options.color,
3575 };
3576
3577 let gap = 0.1;
3578 let (height, depth) = if is_over {
3579 (body_box.height + brace_h + gap, body_box.depth)
3580 } else {
3581 (body_box.height, body_box.depth + brace_h + gap)
3582 };
3583
3584 let clearance = if is_over {
3585 height - brace_h
3586 } else {
3587 body_box.height + body_box.depth + gap
3588 };
3589 let total_w = body_box.width;
3590
3591 LayoutBox {
3592 width: total_w,
3593 height,
3594 depth,
3595 content: BoxContent::Accent {
3596 base: Box::new(body_box),
3597 accent: Box::new(brace_box),
3598 clearance,
3599 skew: 0.0,
3600 is_below: !is_over,
3601 under_gap_em: 0.0,
3602 },
3603 color: options.color,
3604 }
3605}
3606
3607fn layout_xarrow(
3612 label: &str,
3613 body: &ParseNode,
3614 below: Option<&ParseNode>,
3615 options: &LayoutOptions,
3616) -> LayoutBox {
3617 let sup_style = options.style.superscript();
3618 let sub_style = options.style.subscript();
3619 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3620 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3621
3622 let sup_opts = options.with_style(sup_style);
3623 let body_box = layout_node(body, &sup_opts);
3624 let body_w = body_box.width * sup_ratio;
3625
3626 let below_box = below.map(|b| {
3627 let sub_opts = options.with_style(sub_style);
3628 layout_node(b, &sub_opts)
3629 });
3630 let below_w = below_box
3631 .as_ref()
3632 .map(|b| b.width * sub_ratio)
3633 .unwrap_or(0.0);
3634
3635 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3638 let upper_w = body_w + sup_ratio;
3639 let lower_w = if below_box.is_some() {
3640 below_w + sub_ratio
3641 } else {
3642 0.0
3643 };
3644 let arrow_w = upper_w.max(lower_w).max(min_w);
3645 let arrow_h = 0.3;
3646
3647 let (commands, actual_arrow_h, fill_arrow) =
3648 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3649 Some((c, h)) => (c, h, true),
3650 None => (
3651 stretchy_accent_path(label, arrow_w, arrow_h),
3652 arrow_h,
3653 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3654 ),
3655 };
3656 let arrow_box = LayoutBox {
3657 width: arrow_w,
3658 height: actual_arrow_h / 2.0,
3659 depth: actual_arrow_h / 2.0,
3660 content: BoxContent::SvgPath {
3661 commands,
3662 fill: fill_arrow,
3663 },
3664 color: options.color,
3665 };
3666
3667 let metrics = options.metrics();
3670 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3672 let gap = 0.111; let base_shift = -axis;
3676
3677 let sup_kern = gap;
3685 let sub_kern = gap;
3686
3687 let sup_h = body_box.height * sup_ratio;
3688 let sup_d = body_box.depth * sup_ratio;
3689
3690 let height = axis + arrow_half + gap + sup_h + sup_d;
3692 let mut depth = (arrow_half - axis).max(0.0);
3694
3695 if let Some(ref bel) = below_box {
3696 let sub_h = bel.height * sub_ratio;
3697 let sub_d = bel.depth * sub_ratio;
3698 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3700 }
3701
3702 LayoutBox {
3703 width: arrow_w,
3704 height,
3705 depth,
3706 content: BoxContent::OpLimits {
3707 base: Box::new(arrow_box),
3708 sup: Some(Box::new(body_box)),
3709 sub: below_box.map(Box::new),
3710 base_shift,
3711 sup_kern,
3712 sub_kern,
3713 slant: 0.0,
3714 sup_scale: sup_ratio,
3715 sub_scale: sub_ratio,
3716 },
3717 color: options.color,
3718 }
3719}
3720
3721fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3726 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3729 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3730 let diameter = radius * 2.0;
3731
3732 let cx = radius;
3734 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3737
3738 let circle_commands = vec![
3739 PathCommand::MoveTo { x: cx + r, y: cy },
3740 PathCommand::CubicTo {
3741 x1: cx + r, y1: cy - k * r,
3742 x2: cx + k * r, y2: cy - r,
3743 x: cx, y: cy - r,
3744 },
3745 PathCommand::CubicTo {
3746 x1: cx - k * r, y1: cy - r,
3747 x2: cx - r, y2: cy - k * r,
3748 x: cx - r, y: cy,
3749 },
3750 PathCommand::CubicTo {
3751 x1: cx - r, y1: cy + k * r,
3752 x2: cx - k * r, y2: cy + r,
3753 x: cx, y: cy + r,
3754 },
3755 PathCommand::CubicTo {
3756 x1: cx + k * r, y1: cy + r,
3757 x2: cx + r, y2: cy + k * r,
3758 x: cx + r, y: cy,
3759 },
3760 PathCommand::Close,
3761 ];
3762
3763 let circle_box = LayoutBox {
3764 width: diameter,
3765 height: r - cy.min(0.0),
3766 depth: (r + cy).max(0.0),
3767 content: BoxContent::SvgPath {
3768 commands: circle_commands,
3769 fill: false,
3770 },
3771 color: options.color,
3772 };
3773
3774 let content_shift = (diameter - body_box.width) / 2.0;
3776 let children = vec![
3778 circle_box,
3779 LayoutBox::new_kern(-(diameter) + content_shift),
3780 body_box.clone(),
3781 ];
3782
3783 let height = r - cy.min(0.0);
3784 let depth = (r + cy).max(0.0);
3785
3786 LayoutBox {
3787 width: diameter,
3788 height,
3789 depth,
3790 content: BoxContent::HBox(children),
3791 color: options.color,
3792 }
3793}
3794
3795fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3819 let r: f64 = 0.1125;
3821 let cy: f64 = -0.2625;
3825 let k: f64 = 0.5523;
3827 let cx: f64 = r;
3829
3830 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3833
3834 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3843 vec![
3844 PathCommand::MoveTo { x: ox + rad, y: cy },
3845 PathCommand::CubicTo {
3846 x1: ox + rad, y1: cy - k * rad,
3847 x2: ox + k * rad, y2: cy - rad,
3848 x: ox, y: cy - rad,
3849 },
3850 PathCommand::CubicTo {
3851 x1: ox - k * rad, y1: cy - rad,
3852 x2: ox - rad, y2: cy - k * rad,
3853 x: ox - rad, y: cy,
3854 },
3855 PathCommand::CubicTo {
3856 x1: ox - rad, y1: cy + k * rad,
3857 x2: ox - k * rad, y2: cy + rad,
3858 x: ox, y: cy + rad,
3859 },
3860 PathCommand::CubicTo {
3861 x1: ox + k * rad, y1: cy + rad,
3862 x2: ox + rad, y2: cy + k * rad,
3863 x: ox + rad, y: cy,
3864 },
3865 PathCommand::Close,
3866 ]
3867 };
3868
3869 let disk = LayoutBox {
3870 width: 2.0 * r,
3871 height: h,
3872 depth: d,
3873 content: BoxContent::SvgPath {
3874 commands: circle_commands(cx, r),
3875 fill: true,
3876 },
3877 color: options.color,
3878 };
3879
3880 let ring = LayoutBox {
3881 width: 2.0 * r,
3882 height: h,
3883 depth: d,
3884 content: BoxContent::SvgPath {
3885 commands: circle_commands(cx, r_ring),
3886 fill: false,
3887 },
3888 color: options.color,
3889 };
3890
3891 let bar_len: f64 = 0.25;
3895 let bar_th: f64 = 0.04;
3896 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3899
3900 let children = if imageof {
3901 vec![disk, bar, ring]
3902 } else {
3903 vec![ring, bar, disk]
3904 };
3905
3906 let total_width = 4.0 * r + bar_len;
3908 LayoutBox {
3909 width: total_width,
3910 height: h,
3911 depth: d,
3912 content: BoxContent::HBox(children),
3913 color: options.color,
3914 }
3915}
3916
3917fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3921 let cx = width / 2.0;
3922 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3927 PathCommand::MoveTo { x: cx + a, y: cy },
3928 PathCommand::CubicTo {
3929 x1: cx + a,
3930 y1: cy - k * b,
3931 x2: cx + k * a,
3932 y2: cy - b,
3933 x: cx,
3934 y: cy - b,
3935 },
3936 PathCommand::CubicTo {
3937 x1: cx - k * a,
3938 y1: cy - b,
3939 x2: cx - a,
3940 y2: cy - k * b,
3941 x: cx - a,
3942 y: cy,
3943 },
3944 PathCommand::CubicTo {
3945 x1: cx - a,
3946 y1: cy + k * b,
3947 x2: cx - k * a,
3948 y2: cy + b,
3949 x: cx,
3950 y: cy + b,
3951 },
3952 PathCommand::CubicTo {
3953 x1: cx + k * a,
3954 y1: cy + b,
3955 x2: cx + a,
3956 y2: cy + k * b,
3957 x: cx + a,
3958 y: cy,
3959 },
3960 PathCommand::Close,
3961 ]
3962}
3963
3964fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3965 cmds.into_iter().map(|c| match c {
3966 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3967 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3968 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3969 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3970 },
3971 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3972 x1, y1: y1 + dy, x, y: y + dy,
3973 },
3974 PathCommand::Close => PathCommand::Close,
3975 }).collect()
3976}
3977
3978fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3979 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3980 return commands;
3981 }
3982 let ah = height * 0.35; let mid_y = -height / 2.0;
3984
3985 match label {
3986 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3987 vec![
3988 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3989 PathCommand::LineTo { x: 0.0, y: mid_y },
3990 PathCommand::LineTo { x: ah, y: mid_y + ah },
3991 PathCommand::MoveTo { x: 0.0, y: mid_y },
3992 PathCommand::LineTo { x: width, y: mid_y },
3993 ]
3994 }
3995 "\\overleftrightarrow" | "\\underleftrightarrow"
3996 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3997 vec![
3998 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3999 PathCommand::LineTo { x: 0.0, y: mid_y },
4000 PathCommand::LineTo { x: ah, y: mid_y + ah },
4001 PathCommand::MoveTo { x: 0.0, y: mid_y },
4002 PathCommand::LineTo { x: width, y: mid_y },
4003 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4004 PathCommand::LineTo { x: width, y: mid_y },
4005 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4006 ]
4007 }
4008 "\\xlongequal" => {
4009 let gap = 0.04;
4010 vec![
4011 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4012 PathCommand::LineTo { x: width, y: mid_y - gap },
4013 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
4014 PathCommand::LineTo { x: width, y: mid_y + gap },
4015 ]
4016 }
4017 "\\xhookleftarrow" => {
4018 vec![
4019 PathCommand::MoveTo { x: ah, y: mid_y - ah },
4020 PathCommand::LineTo { x: 0.0, y: mid_y },
4021 PathCommand::LineTo { x: ah, y: mid_y + ah },
4022 PathCommand::MoveTo { x: 0.0, y: mid_y },
4023 PathCommand::LineTo { x: width, y: mid_y },
4024 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
4025 ]
4026 }
4027 "\\xhookrightarrow" => {
4028 vec![
4029 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
4030 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
4031 PathCommand::LineTo { x: width, y: mid_y },
4032 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4033 PathCommand::LineTo { x: width, y: mid_y },
4034 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4035 ]
4036 }
4037 "\\xrightharpoonup" | "\\xleftharpoonup" => {
4038 let right = label.contains("right");
4039 if right {
4040 vec![
4041 PathCommand::MoveTo { x: 0.0, y: mid_y },
4042 PathCommand::LineTo { x: width, y: mid_y },
4043 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4044 PathCommand::LineTo { x: width, y: mid_y },
4045 ]
4046 } else {
4047 vec![
4048 PathCommand::MoveTo { x: ah, y: mid_y - ah },
4049 PathCommand::LineTo { x: 0.0, y: mid_y },
4050 PathCommand::LineTo { x: width, y: mid_y },
4051 ]
4052 }
4053 }
4054 "\\xrightharpoondown" | "\\xleftharpoondown" => {
4055 let right = label.contains("right");
4056 if right {
4057 vec![
4058 PathCommand::MoveTo { x: 0.0, y: mid_y },
4059 PathCommand::LineTo { x: width, y: mid_y },
4060 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
4061 PathCommand::LineTo { x: width, y: mid_y },
4062 ]
4063 } else {
4064 vec![
4065 PathCommand::MoveTo { x: ah, y: mid_y + ah },
4066 PathCommand::LineTo { x: 0.0, y: mid_y },
4067 PathCommand::LineTo { x: width, y: mid_y },
4068 ]
4069 }
4070 }
4071 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
4072 let gap = 0.06;
4073 vec![
4074 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4075 PathCommand::LineTo { x: width, y: mid_y - gap },
4076 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
4077 PathCommand::LineTo { x: width, y: mid_y - gap },
4078 PathCommand::MoveTo { x: width, y: mid_y + gap },
4079 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4080 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
4081 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4082 ]
4083 }
4084 "\\xtofrom" | "\\xrightleftarrows" => {
4085 let gap = 0.06;
4086 vec![
4087 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4088 PathCommand::LineTo { x: width, y: mid_y - gap },
4089 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
4090 PathCommand::LineTo { x: width, y: mid_y - gap },
4091 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
4092 PathCommand::MoveTo { x: width, y: mid_y + gap },
4093 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4094 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
4095 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4096 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
4097 ]
4098 }
4099 "\\overlinesegment" | "\\underlinesegment" => {
4100 vec![
4101 PathCommand::MoveTo { x: 0.0, y: mid_y },
4102 PathCommand::LineTo { x: width, y: mid_y },
4103 ]
4104 }
4105 _ => {
4106 vec![
4107 PathCommand::MoveTo { x: 0.0, y: mid_y },
4108 PathCommand::LineTo { x: width, y: mid_y },
4109 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4110 PathCommand::LineTo { x: width, y: mid_y },
4111 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4112 ]
4113 }
4114 }
4115}
4116
4117fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
4123 let h = inner.height;
4124 let d = inner.depth;
4125 let w = inner.width + pad_l + pad_r;
4126 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
4127 if pad_l > 0.0 {
4128 children.push(LayoutBox::new_kern(pad_l));
4129 }
4130 children.push(inner);
4131 if pad_r > 0.0 {
4132 children.push(LayoutBox::new_kern(pad_r));
4133 }
4134 LayoutBox {
4135 width: w,
4136 height: h,
4137 depth: d,
4138 content: BoxContent::HBox(children),
4139 color,
4140 }
4141}
4142
4143fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
4154 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
4155 LayoutBox {
4156 width: label.width,
4157 height: box_h,
4158 depth: box_d,
4159 content: BoxContent::RaiseBox {
4160 body: Box::new(label),
4161 shift,
4162 },
4163 color,
4164 }
4165}
4166
4167fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
4171 let sup_style = options.style.superscript();
4172 let sup_opts = options.with_style(sup_style);
4173 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4174 let inner = layout_node(body, &sup_opts);
4175 if (sup_ratio - 1.0).abs() < 1e-6 {
4176 inner
4177 } else {
4178 LayoutBox {
4179 width: inner.width * sup_ratio,
4180 height: inner.height * sup_ratio,
4181 depth: inner.depth * sup_ratio,
4182 content: BoxContent::Scaled {
4183 body: Box::new(inner),
4184 child_scale: sup_ratio,
4185 },
4186 color: options.color,
4187 }
4188 }
4189}
4190
4191fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4197 let axis = options.metrics().axis_height;
4198 let depth = (total_height / 2.0 - axis).max(0.0);
4199 let height = total_height - depth;
4200 if let Some((commands, w)) =
4201 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4202 {
4203 return LayoutBox {
4204 width: w,
4205 height,
4206 depth,
4207 content: BoxContent::SvgPath {
4208 commands,
4209 fill: true,
4210 },
4211 color: options.color,
4212 };
4213 }
4214 if down {
4216 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4217 } else {
4218 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4219 }
4220}
4221
4222fn layout_cd_arrow(
4238 direction: &str,
4239 label_above: Option<&ParseNode>,
4240 label_below: Option<&ParseNode>,
4241 target_size: f64,
4242 target_col_width: f64,
4243 _target_depth: f64,
4244 options: &LayoutOptions,
4245) -> LayoutBox {
4246 let metrics = options.metrics();
4247 let axis = metrics.axis_height;
4248
4249 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4252
4253 match direction {
4254 "right" | "left" | "horiz_eq" => {
4255 let sup_style = options.style.superscript();
4257 let sub_style = options.style.subscript();
4258 let sup_opts = options.with_style(sup_style);
4259 let sub_opts = options.with_style(sub_style);
4260 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4261 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4262
4263 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4264 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4265
4266 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4267 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4268
4269 let path_label = if direction == "right" {
4271 "\\cdrightarrow"
4272 } else if direction == "left" {
4273 "\\cdleftarrow"
4274 } else {
4275 "\\cdlongequal"
4276 };
4277 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4278 const CD_LABEL_PAD_L: f64 = 0.22;
4281 const CD_LABEL_PAD_R: f64 = 0.48;
4282 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4283 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4284 let upper_need = above_box
4285 .as_ref()
4286 .map(|_| above_w + cd_pad_sup)
4287 .unwrap_or(0.0);
4288 let lower_need = below_box
4289 .as_ref()
4290 .map(|_| below_w + cd_pad_sub)
4291 .unwrap_or(0.0);
4292 let natural_w = upper_need.max(lower_need).max(0.0);
4293 let shaft_w = if target_size > 0.0 {
4294 target_size
4295 } else {
4296 natural_w.max(min_shaft_w)
4297 };
4298
4299 let (commands, actual_arrow_h, fill_arrow) =
4300 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4301 Some((c, h)) => (c, h, true),
4302 None => {
4303 let arrow_h = 0.3_f64;
4305 let ah = 0.12_f64;
4306 let cmds = if direction == "horiz_eq" {
4307 let gap = 0.06;
4308 vec![
4309 PathCommand::MoveTo { x: 0.0, y: -gap },
4310 PathCommand::LineTo { x: shaft_w, y: -gap },
4311 PathCommand::MoveTo { x: 0.0, y: gap },
4312 PathCommand::LineTo { x: shaft_w, y: gap },
4313 ]
4314 } else if direction == "right" {
4315 vec![
4316 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4317 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4318 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4319 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4320 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4321 ]
4322 } else {
4323 vec![
4324 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4325 PathCommand::LineTo { x: 0.0, y: 0.0 },
4326 PathCommand::MoveTo { x: ah, y: -ah },
4327 PathCommand::LineTo { x: 0.0, y: 0.0 },
4328 PathCommand::LineTo { x: ah, y: ah },
4329 ]
4330 };
4331 (cmds, arrow_h, false)
4332 }
4333 };
4334
4335 let arrow_half = actual_arrow_h / 2.0;
4337 let arrow_box = LayoutBox {
4338 width: shaft_w,
4339 height: arrow_half,
4340 depth: arrow_half,
4341 content: BoxContent::SvgPath {
4342 commands,
4343 fill: fill_arrow,
4344 },
4345 color: options.color,
4346 };
4347
4348 let gap = 0.111;
4350 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4351 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4352 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4356 sup_d
4357 } else {
4358 0.0
4359 };
4360 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4361 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4362 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4363 let depth = if below_box.is_some() {
4364 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4365 } else {
4366 (arrow_half - axis).max(0.0)
4367 };
4368
4369 let inner = LayoutBox {
4370 width: shaft_w,
4371 height,
4372 depth,
4373 content: BoxContent::OpLimits {
4374 base: Box::new(arrow_box),
4375 sup: above_box.map(Box::new),
4376 sub: below_box.map(Box::new),
4377 base_shift: -axis,
4378 sup_kern: gap,
4379 sub_kern: gap,
4380 slant: 0.0,
4381 sup_scale: sup_ratio,
4382 sub_scale: sub_ratio,
4383 },
4384 color: options.color,
4385 };
4386
4387 if target_col_width > inner.width + 1e-6 {
4391 let extra = target_col_width - inner.width;
4392 let kl = extra / 2.0;
4393 let kr = extra - kl;
4394 cd_wrap_hpad(inner, kl, kr, options.color)
4395 } else {
4396 inner
4397 }
4398 }
4399
4400 "down" | "up" | "vert_eq" => {
4401 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4405 "vert_eq" if target_size > 0.0 => {
4406 make_vert_delim_box(target_size.max(big_total), true, options)
4407 }
4408 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4409 "down" if target_size > 0.0 => {
4410 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4411 }
4412 "up" if target_size > 0.0 => {
4413 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4414 }
4415 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4416 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4417 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4418 };
4419 let box_h = shaft_box.height;
4420 let box_d = shaft_box.depth;
4421 let shaft_w = shaft_box.width;
4422
4423 let left_box = label_above.map(|n| {
4426 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4427 });
4428 let right_box = label_below.map(|n| {
4429 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4430 });
4431
4432 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4433 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4434 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4435 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4436 let inner_w = left_part + shaft_w + right_part;
4437
4438 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4440 let extra = target_col_width - inner_w;
4441 let kl = extra / 2.0;
4442 let kr = extra - kl;
4443 (kl, kr, target_col_width)
4444 } else {
4445 (0.0, 0.0, inner_w)
4446 };
4447
4448 let mut children: Vec<LayoutBox> = Vec::new();
4449 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4450 if let Some(lb) = left_box {
4451 children.push(lb);
4452 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4453 }
4454 children.push(shaft_box);
4455 if let Some(rb) = right_box {
4456 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4457 children.push(rb);
4458 }
4459 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4460
4461 LayoutBox {
4462 width: total_w,
4463 height: box_h,
4464 depth: box_d,
4465 content: BoxContent::HBox(children),
4466 color: options.color,
4467 }
4468 }
4469
4470 _ => LayoutBox::new_empty(),
4472 }
4473}
4474
4475fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4477 let metrics = options.metrics();
4478 let pt = 1.0 / metrics.pt_per_em;
4479 let baselineskip = 3.0 * metrics.x_height;
4481 let arstrut_h = 0.7 * baselineskip;
4482 let arstrut_d = 0.3 * baselineskip;
4483
4484 let num_rows = body.len();
4485 if num_rows == 0 {
4486 return LayoutBox::new_empty();
4487 }
4488 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4489 if num_cols == 0 {
4490 return LayoutBox::new_empty();
4491 }
4492
4493 let jot = 3.0 * pt;
4495
4496 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4498 let mut col_widths = vec![0.0_f64; num_cols];
4499 let mut row_heights = vec![arstrut_h; num_rows];
4500 let mut row_depths = vec![arstrut_d; num_rows];
4501
4502 for (r, row) in body.iter().enumerate() {
4503 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4504
4505 for (c, cell) in row.iter().enumerate() {
4506 let cbox = match cell {
4507 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4508 layout_cd_arrow(
4509 direction,
4510 label_above.as_deref(),
4511 label_below.as_deref(),
4512 0.0, 0.0, 0.0, options,
4516 )
4517 }
4518 ParseNode::OrdGroup { body: cell_body, .. } => {
4522 layout_expression(cell_body, options, false)
4523 }
4524 other => layout_node(other, options),
4525 };
4526
4527 row_heights[r] = row_heights[r].max(cbox.height);
4528 row_depths[r] = row_depths[r].max(cbox.depth);
4529 col_widths[c] = col_widths[c].max(cbox.width);
4530 row_boxes.push(cbox);
4531 }
4532
4533 while row_boxes.len() < num_cols {
4535 row_boxes.push(LayoutBox::new_empty());
4536 }
4537 cell_boxes.push(row_boxes);
4538 }
4539
4540 let col_target_w: Vec<f64> = col_widths.clone();
4544
4545 #[cfg(debug_assertions)]
4546 {
4547 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4548 for (r, row) in cell_boxes.iter().enumerate() {
4549 for (c, b) in row.iter().enumerate() {
4550 if b.width > 0.0 {
4551 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4552 }
4553 }
4554 }
4555 }
4556
4557 for (r, row) in body.iter().enumerate() {
4559 let is_arrow_row = r % 2 == 1;
4560 for (c, cell) in row.iter().enumerate() {
4561 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4562 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4563 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4564 let b = layout_cd_arrow(
4565 direction,
4566 label_above.as_deref(),
4567 label_below.as_deref(),
4568 cell_boxes[r][c].width,
4569 col_target_w[c],
4570 0.0,
4571 options,
4572 );
4573 let w = b.width;
4574 (b, w)
4575 } else if is_arrow_row && c % 2 == 0 {
4576 let v_span = row_heights[r] + row_depths[r];
4580 let b = layout_cd_arrow(
4581 direction,
4582 label_above.as_deref(),
4583 label_below.as_deref(),
4584 v_span,
4585 col_widths[c],
4586 0.0,
4587 options,
4588 );
4589 let w = b.width;
4590 (b, w)
4591 } else {
4592 continue;
4593 };
4594 col_widths[c] = col_widths[c].max(col_w);
4595 cell_boxes[r][c] = new_box;
4596 }
4597 }
4598 }
4599
4600 #[cfg(debug_assertions)]
4601 {
4602 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4603 }
4604
4605 for rd in &mut row_depths {
4608 *rd += jot;
4609 }
4610
4611 let col_gap = 0.5;
4616
4617 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4619
4620 let col_separators = vec![None; num_cols + 1];
4622
4623 let mut total_height = 0.0_f64;
4624 let mut row_positions = Vec::with_capacity(num_rows);
4625 for r in 0..num_rows {
4626 total_height += row_heights[r];
4627 row_positions.push(total_height);
4628 total_height += row_depths[r];
4629 }
4630
4631 let offset = total_height / 2.0 + metrics.axis_height;
4632 let height = offset;
4633 let depth = total_height - offset;
4634
4635 let total_width = col_widths.iter().sum::<f64>()
4637 + col_gap * (num_cols.saturating_sub(1)) as f64;
4638
4639 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4641
4642 LayoutBox {
4643 width: total_width,
4644 height,
4645 depth,
4646 content: BoxContent::Array {
4647 cells: cell_boxes,
4648 col_widths,
4649 col_aligns,
4650 row_heights,
4651 row_depths,
4652 col_gap,
4653 offset,
4654 content_x_offset: 0.0,
4655 col_separators,
4656 hlines_before_row,
4657 rule_thickness: 0.04 * pt,
4658 double_rule_sep: metrics.double_rule_sep,
4659 array_inner_width: total_width,
4660 tag_gap_em: 0.0,
4661 tag_col_width: 0.0,
4662 row_tags: (0..num_rows).map(|_| None).collect(),
4663 tags_left: false,
4664 },
4665 color: options.color,
4666 }
4667}
4668
4669fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4670 let mid = width / 2.0;
4671 let q = height * 0.6;
4672 if is_over {
4673 vec![
4674 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4675 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4676 PathCommand::LineTo { x: mid - 0.05, y: -q },
4677 PathCommand::LineTo { x: mid, y: -height },
4678 PathCommand::LineTo { x: mid + 0.05, y: -q },
4679 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4680 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4681 ]
4682 } else {
4683 vec![
4684 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4685 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4686 PathCommand::LineTo { x: mid - 0.05, y: q },
4687 PathCommand::LineTo { x: mid, y: height },
4688 PathCommand::LineTo { x: mid + 0.05, y: q },
4689 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4690 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4691 ]
4692 }
4693}
4694
4695#[cfg(test)]
4696mod missing_glyph_width_em_tests {
4697 use super::{missing_glyph_height_em, missing_glyph_width_em};
4698 use ratex_font::get_global_metrics;
4699
4700 #[test]
4701 fn supplementary_plane_emoji_is_one_em() {
4702 assert_eq!(missing_glyph_width_em('😊'), 1.0);
4703 assert_eq!(missing_glyph_width_em('🚀'), 1.0);
4704 }
4705
4706 #[test]
4707 fn supplementary_plane_emoji_uses_shorter_box_height() {
4708 let m = get_global_metrics(0);
4709 let emoji_h = missing_glyph_height_em('😊', m);
4710 let default_h = (m.quad * 0.92).max(m.x_height);
4711 assert!(
4712 emoji_h < default_h,
4713 "tall placeholder box must not push \\sqrt past KaTeX's small-surd threshold"
4714 );
4715 assert!((emoji_h - 0.74).abs() < 1e-9);
4716 }
4717
4718 #[test]
4719 fn dingbats_block_is_one_em() {
4720 assert_eq!(missing_glyph_width_em('\u{2708}'), 1.0); }
4722
4723 #[test]
4724 fn miscellaneous_symbols_is_one_em() {
4725 assert_eq!(missing_glyph_width_em('\u{2605}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2615}'), 1.0); }
4728
4729 #[test]
4730 fn misc_symbols_and_arrows_is_one_em() {
4731 assert_eq!(missing_glyph_width_em('\u{2B50}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2B1B}'), 1.0); }
4734
4735 #[test]
4736 fn latin_without_metrics_stays_half_em() {
4737 assert_eq!(missing_glyph_width_em('z'), 0.5);
4738 }
4739}
4740
4741#[cfg(test)]
4742mod cjk_font_switching_tests {
4743 use super::super::to_display::to_display_list;
4744 use super::*;
4745 use ratex_parser::parser::parse;
4746 use ratex_types::display_item::DisplayItem;
4747
4748 fn first_glyph_font_name(latex: &str) -> Option<String> {
4749 let ast = parse(latex).ok()?;
4750 let lbox = layout(&ast, &LayoutOptions::default());
4751 let dl = to_display_list(&lbox);
4752 for item in &dl.items {
4753 if let DisplayItem::GlyphPath { font, .. } = item {
4754 return Some(font.clone());
4755 }
4756 }
4757 None
4758 }
4759
4760 #[test]
4761 fn cjk_in_text_uses_cjk_regular() {
4762 assert_eq!(
4763 first_glyph_font_name(r"\text{中}").as_deref(),
4764 Some("CJK-Regular")
4765 );
4766 }
4767
4768 #[test]
4769 fn emoji_in_text_uses_cjk_regular() {
4770 assert_eq!(
4771 first_glyph_font_name(r"\text{😊}").as_deref(),
4772 Some("CJK-Regular")
4773 );
4774 }
4775
4776 #[test]
4777 fn latin_in_text_is_not_cjk() {
4778 assert_ne!(
4779 first_glyph_font_name(r"\text{a}").as_deref(),
4780 Some("CJK-Regular")
4781 );
4782 }
4783
4784 #[test]
4785 fn hiragana_in_text_uses_cjk_regular() {
4786 assert_eq!(
4787 first_glyph_font_name(r"\text{あ}").as_deref(),
4788 Some("CJK-Regular")
4789 );
4790 }
4791}