1use ratex_font::{get_char_metrics, get_global_metrics, FontId};
2use ratex_parser::parse_node::{ArrayTag, AtomFamily, Mode, ParseNode};
3use ratex_types::color::Color;
4use ratex_types::math_style::MathStyle;
5use ratex_types::path_command::PathCommand;
6
7use crate::hbox::make_hbox;
8use crate::layout_box::{BoxContent, LayoutBox};
9use crate::layout_options::LayoutOptions;
10
11use crate::katex_svg::parse_svg_path_data;
12use crate::spacing::{atom_spacing, mu_to_em, MathClass};
13use crate::stacked_delim::make_stacked_delim_if_needed;
14
15const NULL_DELIMITER_SPACE: f64 = 0.12;
18
19pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
21 layout_expression(nodes, options, true)
22}
23
24fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
27 let n = raw.len();
28 let mut eff = raw.to_vec();
29 for i in 0..n {
30 if raw[i] != Some(MathClass::Bin) {
31 continue;
32 }
33 let prev = if i == 0 { None } else { raw[i - 1] };
34 let left_cancel = matches!(
35 prev,
36 None
37 | Some(MathClass::Bin)
38 | Some(MathClass::Open)
39 | Some(MathClass::Rel)
40 | Some(MathClass::Op)
41 | Some(MathClass::Punct)
42 );
43 if left_cancel {
44 eff[i] = Some(MathClass::Ord);
45 }
46 }
47 for i in 0..n {
48 if raw[i] != Some(MathClass::Bin) {
49 continue;
50 }
51 let next = if i + 1 < n { raw[i + 1] } else { None };
52 let right_cancel = matches!(
53 next,
54 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
55 );
56 if right_cancel {
57 eff[i] = Some(MathClass::Ord);
58 }
59 }
60 eff
61}
62
63fn node_is_middle_fence(node: &ParseNode) -> bool {
68 matches!(node, ParseNode::Middle { .. })
69}
70
71fn layout_expression(
73 nodes: &[ParseNode],
74 options: &LayoutOptions,
75 is_real_group: bool,
76) -> LayoutBox {
77 if nodes.is_empty() {
78 return LayoutBox::new_empty();
79 }
80
81 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
83 if has_cr {
84 return layout_multiline(nodes, options, is_real_group);
85 }
86
87 let raw_classes: Vec<Option<MathClass>> =
88 nodes.iter().map(node_math_class).collect();
89 let eff_classes = apply_bin_cancellation(&raw_classes);
90
91 let mut children = Vec::new();
92 let mut prev_class: Option<MathClass> = None;
93 let mut prev_class_node_idx: Option<usize> = None;
95
96 for (i, node) in nodes.iter().enumerate() {
97 let lbox = layout_node(node, options);
98 let cur_class = eff_classes.get(i).copied().flatten();
99
100 if is_real_group {
101 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
102 let prev_middle = prev_class_node_idx
103 .is_some_and(|j| node_is_middle_fence(&nodes[j]));
104 let cur_middle = node_is_middle_fence(node);
105 let mu = if prev_middle || cur_middle {
106 0.0
107 } else {
108 atom_spacing(prev, cur, options.style.is_tight())
109 };
110 let mu = if let Some(cap) = options.align_relation_spacing {
111 if prev == MathClass::Rel || cur == MathClass::Rel {
112 mu.min(cap)
113 } else {
114 mu
115 }
116 } else {
117 mu
118 };
119 if mu > 0.0 {
120 let em = mu_to_em(mu, options.metrics().quad);
121 children.push(LayoutBox::new_kern(em));
122 }
123 }
124 }
125
126 if cur_class.is_some() {
127 prev_class = cur_class;
128 prev_class_node_idx = Some(i);
129 }
130
131 children.push(lbox);
132 }
133
134 make_hbox(children)
135}
136
137fn layout_multiline(
139 nodes: &[ParseNode],
140 options: &LayoutOptions,
141 is_real_group: bool,
142) -> LayoutBox {
143 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
144 let metrics = options.metrics();
145 let pt = 1.0 / metrics.pt_per_em;
146 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
151 let mut start = 0;
152 for (i, node) in nodes.iter().enumerate() {
153 if matches!(node, ParseNode::Cr { .. }) {
154 rows.push(&nodes[start..i]);
155 start = i + 1;
156 }
157 }
158 rows.push(&nodes[start..]);
159
160 let row_boxes: Vec<LayoutBox> = rows
161 .iter()
162 .map(|row| layout_expression(row, options, is_real_group))
163 .collect();
164
165 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
166
167 let mut vchildren: Vec<VBoxChild> = Vec::new();
168 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
169 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
170 for (i, row) in row_boxes.iter().enumerate() {
171 if i > 0 {
172 let prev_depth = row_boxes[i - 1].depth;
174 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
175 vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
176 h += gap + row.height + prev_depth;
177 }
178 vchildren.push(VBoxChild {
179 kind: VBoxChildKind::Box(Box::new(row.clone())),
180 shift: 0.0,
181 });
182 }
183
184 LayoutBox {
185 width: total_width,
186 height: h,
187 depth: d,
188 content: BoxContent::VBox(vchildren),
189 color: options.color,
190 }
191}
192
193
194fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
196 match node {
197 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
198 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
199 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
200 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
201
202 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
203
204 ParseNode::SupSub {
205 base, sup, sub, ..
206 } => {
207 if let Some(base_node) = base.as_deref() {
208 if should_use_op_limits(base_node, options) {
209 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
210 }
211 }
212 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
213 }
214
215 ParseNode::GenFrac {
216 numer,
217 denom,
218 has_bar_line,
219 bar_size,
220 left_delim,
221 right_delim,
222 continued,
223 ..
224 } => {
225 let bar_thickness = if *has_bar_line {
226 bar_size
227 .as_ref()
228 .map(|m| measurement_to_em(m, options))
229 .unwrap_or(options.metrics().default_rule_thickness)
230 } else {
231 0.0
232 };
233 let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
234
235 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
236 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
237
238 if has_left || has_right {
239 let total_h = genfrac_delim_target_height(options);
240 let left_d = left_delim.as_deref().unwrap_or(".");
241 let right_d = right_delim.as_deref().unwrap_or(".");
242 let left_box = make_stretchy_delim(left_d, total_h, options);
243 let right_box = make_stretchy_delim(right_d, total_h, options);
244
245 let width = left_box.width + frac.width + right_box.width;
246 let height = frac.height.max(left_box.height).max(right_box.height);
247 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
248
249 LayoutBox {
250 width,
251 height,
252 depth,
253 content: BoxContent::LeftRight {
254 left: Box::new(left_box),
255 right: Box::new(right_box),
256 inner: Box::new(frac),
257 },
258 color: options.color,
259 }
260 } else {
261 let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE };
262 make_hbox(vec![
263 LayoutBox::new_kern(NULL_DELIMITER_SPACE),
264 frac,
265 LayoutBox::new_kern(right_nds),
266 ])
267 }
268 }
269
270 ParseNode::Sqrt { body, index, .. } => {
271 layout_radical(body, index.as_deref(), options)
272 }
273
274 ParseNode::Op {
275 name,
276 symbol,
277 body,
278 limits,
279 suppress_base_shift,
280 ..
281 } => layout_op(
282 name.as_deref(),
283 *symbol,
284 body.as_deref(),
285 *limits,
286 suppress_base_shift.unwrap_or(false),
287 options,
288 ),
289
290 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
291
292 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
293
294 ParseNode::Kern { dimension, .. } => {
295 let em = measurement_to_em(dimension, options);
296 LayoutBox::new_kern(em)
297 }
298
299 ParseNode::Color { color, body, .. } => {
300 let new_color = Color::parse(color).unwrap_or(options.color);
301 let new_opts = options.with_color(new_color);
302 let mut lbox = layout_expression(body, &new_opts, true);
303 lbox.color = new_color;
304 lbox
305 }
306
307 ParseNode::Styling { style, body, .. } => {
308 let new_style = match style {
309 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
310 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
311 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
312 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
313 };
314 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
315 let new_opts = options.with_style(new_style);
316 let inner = layout_expression(body, &new_opts, true);
317 if (ratio - 1.0).abs() < 0.001 {
318 inner
319 } else {
320 LayoutBox {
321 width: inner.width * ratio,
322 height: inner.height * ratio,
323 depth: inner.depth * ratio,
324 content: BoxContent::Scaled {
325 body: Box::new(inner),
326 child_scale: ratio,
327 },
328 color: options.color,
329 }
330 }
331 }
332
333 ParseNode::Accent {
334 label, base, is_stretchy, is_shifty, ..
335 } => {
336 let is_below = matches!(label.as_str(), "\\c");
338 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
339 }
340
341 ParseNode::AccentUnder {
342 label, base, is_stretchy, ..
343 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
344
345 ParseNode::LeftRight {
346 body, left, right, ..
347 } => layout_left_right(body, left, right, options),
348
349 ParseNode::DelimSizing {
350 size, delim, ..
351 } => layout_delim_sizing(*size, delim, options),
352
353 ParseNode::Array {
354 body,
355 cols,
356 arraystretch,
357 add_jot,
358 row_gaps,
359 hlines_before_row,
360 col_separation_type,
361 hskip_before_and_after,
362 is_cd,
363 tags,
364 leqno,
365 ..
366 } => {
367 if is_cd.unwrap_or(false) {
368 layout_cd(body, options)
369 } else {
370 layout_array(
371 body,
372 cols.as_deref(),
373 *arraystretch,
374 add_jot.unwrap_or(false),
375 row_gaps,
376 hlines_before_row,
377 col_separation_type.as_deref(),
378 hskip_before_and_after.unwrap_or(false),
379 tags.as_deref(),
380 leqno.unwrap_or(false),
381 options,
382 )
383 }
384 }
385
386 ParseNode::CdArrow {
387 direction,
388 label_above,
389 label_below,
390 ..
391 } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
392
393 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
394
395 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
396 Some(f) => {
397 let group = ParseNode::OrdGroup {
398 mode: *mode,
399 body: body.clone(),
400 semisimple: None,
401 loc: None,
402 };
403 layout_font(f, &group, options)
404 }
405 None => layout_text(body, options),
406 },
407
408 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
409
410 ParseNode::Href { body, .. } => layout_href(body, options),
411
412 ParseNode::Overline { body, .. } => layout_overline(body, options),
413 ParseNode::Underline { body, .. } => layout_underline(body, options),
414
415 ParseNode::Rule {
416 width: w,
417 height: h,
418 shift,
419 ..
420 } => {
421 let width = measurement_to_em(w, options);
422 let ink_h = measurement_to_em(h, options);
423 let raise = shift
424 .as_ref()
425 .map(|s| measurement_to_em(s, options))
426 .unwrap_or(0.0);
427 let box_height = (raise + ink_h).max(0.0);
428 let box_depth = (-raise).max(0.0);
429 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
430 }
431
432 ParseNode::Phantom { body, .. } => {
433 let inner = layout_expression(body, options, true);
434 LayoutBox {
435 width: inner.width,
436 height: inner.height,
437 depth: inner.depth,
438 content: BoxContent::Empty,
439 color: Color::BLACK,
440 }
441 }
442
443 ParseNode::VPhantom { body, .. } => {
444 let inner = layout_node(body, options);
445 LayoutBox {
446 width: 0.0,
447 height: inner.height,
448 depth: inner.depth,
449 content: BoxContent::Empty,
450 color: Color::BLACK,
451 }
452 }
453
454 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
455 let mut inner = layout_node(body, options);
456 if *smash_height { inner.height = 0.0; }
457 if *smash_depth { inner.depth = 0.0; }
458 inner
459 }
460
461 ParseNode::Middle { delim, .. } => {
462 match options.leftright_delim_height {
463 Some(h) => make_stretchy_delim(delim, h, options),
464 None => {
465 let placeholder = make_stretchy_delim(delim, 1.0, options);
467 LayoutBox {
468 width: placeholder.width,
469 height: 0.0,
470 depth: 0.0,
471 content: BoxContent::Empty,
472 color: options.color,
473 }
474 }
475 }
476 }
477
478 ParseNode::HtmlMathMl { html, .. } => {
479 layout_expression(html, options, true)
480 }
481
482 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
483
484 ParseNode::MathChoice {
485 display, text, script, scriptscript, ..
486 } => {
487 let branch = match options.style {
488 MathStyle::Display | MathStyle::DisplayCramped => display,
489 MathStyle::Text | MathStyle::TextCramped => text,
490 MathStyle::Script | MathStyle::ScriptCramped => script,
491 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
492 };
493 layout_expression(branch, options, true)
494 }
495
496 ParseNode::Lap { alignment, body, .. } => {
497 let inner = layout_node(body, options);
498 let shift = match alignment.as_str() {
499 "llap" => -inner.width,
500 "clap" => -inner.width / 2.0,
501 _ => 0.0, };
503 let mut children = Vec::new();
504 if shift != 0.0 {
505 children.push(LayoutBox::new_kern(shift));
506 }
507 let h = inner.height;
508 let d = inner.depth;
509 children.push(inner);
510 LayoutBox {
511 width: 0.0,
512 height: h,
513 depth: d,
514 content: BoxContent::HBox(children),
515 color: options.color,
516 }
517 }
518
519 ParseNode::HorizBrace {
520 base,
521 is_over,
522 label,
523 ..
524 } => layout_horiz_brace(base, *is_over, label, options),
525
526 ParseNode::XArrow {
527 label, body, below, ..
528 } => layout_xarrow(label, body, below.as_deref(), options),
529
530 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
531
532 ParseNode::HBox { body, .. } => layout_text(body, options),
533
534 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
535 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
536 }
537
538 ParseNode::RaiseBox { dy, body, .. } => {
539 let shift = measurement_to_em(dy, options);
540 layout_raisebox(shift, body, options)
541 }
542
543 ParseNode::VCenter { body, .. } => {
544 let inner = layout_node(body, options);
546 let axis = options.metrics().axis_height;
547 let total = inner.height + inner.depth;
548 let height = total / 2.0 + axis;
549 let depth = total - height;
550 LayoutBox {
551 width: inner.width,
552 height,
553 depth,
554 content: inner.content,
555 color: inner.color,
556 }
557 }
558
559 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
560
561 ParseNode::Tag { tag, .. } => {
562 let text_opts = options.with_style(options.style.text());
563 layout_expression(tag, &text_opts, true)
564 },
565
566 _ => LayoutBox::new_empty(),
568 }
569}
570
571fn missing_glyph_width_em(ch: char) -> f64 {
581 match ch as u32 {
582 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
584 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
586 0xAC00..=0xD7AF => 1.0,
588 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
590 _ => 0.5,
591 }
592}
593
594fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
595 let m = get_global_metrics(options.style.size_index());
596 let w = missing_glyph_width_em(ch);
597 if w >= 0.99 {
598 let h = (m.quad * 0.92).max(m.x_height);
599 (w, h, 0.0)
600 } else {
601 (w, m.x_height, 0.0)
602 }
603}
604
605#[inline]
607fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
608 if mode == Mode::Math {
609 m.width + m.italic
610 } else {
611 m.width
612 }
613}
614
615fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
616 let ch = resolve_symbol_char(text, mode);
617
618 match ch as u32 {
620 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
623 }
624
625 let char_code = ch as u32;
626
627 if let Some((font_id, metric_cp)) =
628 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
629 {
630 let m = get_char_metrics(font_id, metric_cp);
631 let (width, height, depth) = match m {
632 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
633 None => missing_glyph_metrics_fallback(ch, options),
634 };
635 return LayoutBox {
636 width,
637 height,
638 depth,
639 content: BoxContent::Glyph {
640 font_id,
641 char_code,
642 },
643 color: options.color,
644 };
645 }
646
647 let mut font_id = select_font(text, ch, mode, options);
648 let mut metrics = get_char_metrics(font_id, char_code);
649
650 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
651 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
652 font_id = FontId::MathItalic;
653 metrics = Some(m);
654 }
655 }
656
657 let (width, height, depth) = if let Some(m) = metrics {
663 (math_glyph_advance_em(&m, mode), m.height, m.depth)
664 } else if mode == Mode::Math {
665 let size_font = if options.style.is_display() {
666 FontId::Size2Regular
667 } else {
668 FontId::Size1Regular
669 };
670 match get_char_metrics(size_font, char_code)
671 .or_else(|| get_char_metrics(FontId::Size1Regular, char_code))
672 {
673 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
674 None => missing_glyph_metrics_fallback(ch, options),
675 }
676 } else {
677 missing_glyph_metrics_fallback(ch, options)
678 };
679
680 LayoutBox {
681 width,
682 height,
683 depth,
684 content: BoxContent::Glyph {
685 font_id,
686 char_code,
687 },
688 color: options.color,
689 }
690}
691
692fn resolve_symbol_char(text: &str, mode: Mode) -> char {
694 let font_mode = match mode {
695 Mode::Math => ratex_font::Mode::Math,
696 Mode::Text => ratex_font::Mode::Text,
697 };
698
699 if let Some(raw) = text.chars().next() {
700 let ru = raw as u32;
701 if (0x1D400..=0x1D7FF).contains(&ru) {
702 return raw;
703 }
704 }
705
706 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
707 if let Some(cp) = info.codepoint {
708 return cp;
709 }
710 }
711
712 text.chars().next().unwrap_or('?')
713}
714
715fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
719 let font_mode = match mode {
720 Mode::Math => ratex_font::Mode::Math,
721 Mode::Text => ratex_font::Mode::Text,
722 };
723
724 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
725 if info.font == ratex_font::SymbolFont::Ams {
726 return FontId::AmsRegular;
727 }
728 }
729
730 match mode {
731 Mode::Math => {
732 if resolved_char.is_ascii_lowercase()
733 || resolved_char.is_ascii_uppercase()
734 || is_math_italic_greek(resolved_char)
735 {
736 FontId::MathItalic
737 } else {
738 FontId::MainRegular
739 }
740 }
741 Mode::Text => FontId::MainRegular,
742 }
743}
744
745fn is_math_italic_greek(ch: char) -> bool {
748 matches!(ch,
749 '\u{03B1}'..='\u{03C9}' |
750 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
751 '\u{03F1}' | '\u{03F5}'
752 )
753}
754
755fn is_arrow_accent(label: &str) -> bool {
756 matches!(
757 label,
758 "\\overrightarrow"
759 | "\\overleftarrow"
760 | "\\Overrightarrow"
761 | "\\overleftrightarrow"
762 | "\\underrightarrow"
763 | "\\underleftarrow"
764 | "\\underleftrightarrow"
765 | "\\overleftharpoon"
766 | "\\overrightharpoon"
767 | "\\overlinesegment"
768 | "\\underlinesegment"
769 )
770}
771
772fn layout_fraction(
777 numer: &ParseNode,
778 denom: &ParseNode,
779 bar_thickness: f64,
780 continued: bool,
781 options: &LayoutOptions,
782) -> LayoutBox {
783 let numer_s = options.style.numerator();
784 let denom_s = options.style.denominator();
785 let numer_style = options.with_style(numer_s);
786 let denom_style = options.with_style(denom_s);
787
788 let mut numer_box = layout_node(numer, &numer_style);
789 if continued {
791 let pt = options.metrics().pt_per_em;
792 let h_min = 8.5 / pt;
793 let d_min = 3.5 / pt;
794 if numer_box.height < h_min {
795 numer_box.height = h_min;
796 }
797 if numer_box.depth < d_min {
798 numer_box.depth = d_min;
799 }
800 }
801 let denom_box = layout_node(denom, &denom_style);
802
803 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
805 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
806
807 let numer_height = numer_box.height * numer_ratio;
808 let numer_depth = numer_box.depth * numer_ratio;
809 let denom_height = denom_box.height * denom_ratio;
810 let denom_depth = denom_box.depth * denom_ratio;
811 let numer_width = numer_box.width * numer_ratio;
812 let denom_width = denom_box.width * denom_ratio;
813
814 let metrics = options.metrics();
815 let axis = metrics.axis_height;
816 let rule = bar_thickness;
817
818 let (mut num_shift, mut den_shift) = if options.style.is_display() {
820 (metrics.num1, metrics.denom1)
821 } else if bar_thickness > 0.0 {
822 (metrics.num2, metrics.denom2)
823 } else {
824 (metrics.num3, metrics.denom2)
825 };
826
827 if bar_thickness > 0.0 {
828 let min_clearance = if options.style.is_display() {
829 3.0 * rule
830 } else {
831 rule
832 };
833
834 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
835 if num_clearance < min_clearance {
836 num_shift += min_clearance - num_clearance;
837 }
838
839 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
840 if den_clearance < min_clearance {
841 den_shift += min_clearance - den_clearance;
842 }
843 } else {
844 let min_gap = if options.style.is_display() {
845 7.0 * metrics.default_rule_thickness
846 } else {
847 3.0 * metrics.default_rule_thickness
848 };
849
850 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
851 if gap < min_gap {
852 let adjust = (min_gap - gap) / 2.0;
853 num_shift += adjust;
854 den_shift += adjust;
855 }
856 }
857
858 let total_width = numer_width.max(denom_width);
859 let height = numer_height + num_shift;
860 let depth = denom_depth + den_shift;
861
862 LayoutBox {
863 width: total_width,
864 height,
865 depth,
866 content: BoxContent::Fraction {
867 numer: Box::new(numer_box),
868 denom: Box::new(denom_box),
869 numer_shift: num_shift,
870 denom_shift: den_shift,
871 bar_thickness: rule,
872 numer_scale: numer_ratio,
873 denom_scale: denom_ratio,
874 },
875 color: options.color,
876 }
877}
878
879fn layout_supsub(
884 base: Option<&ParseNode>,
885 sup: Option<&ParseNode>,
886 sub: Option<&ParseNode>,
887 options: &LayoutOptions,
888 inherited_font: Option<FontId>,
889) -> LayoutBox {
890 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
891 Some(fid) => layout_with_font(n, fid, opts),
892 None => layout_node(n, opts),
893 };
894
895 let horiz_brace_over = matches!(
896 base,
897 Some(ParseNode::HorizBrace {
898 is_over: true,
899 ..
900 })
901 );
902 let horiz_brace_under = matches!(
903 base,
904 Some(ParseNode::HorizBrace {
905 is_over: false,
906 ..
907 })
908 );
909 let center_scripts = horiz_brace_over || horiz_brace_under;
910
911 let base_box = base
912 .map(|b| layout_child(b, options))
913 .unwrap_or_else(LayoutBox::new_empty);
914
915 let is_char_box = base.is_some_and(is_character_box);
916 let metrics = options.metrics();
917 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
921
922 let sup_style = options.style.superscript();
923 let sub_style = options.style.subscript();
924
925 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
926 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
927
928 let sup_box = sup.map(|s| {
929 let sup_opts = options.with_style(sup_style);
930 layout_child(s, &sup_opts)
931 });
932
933 let sub_box = sub.map(|s| {
934 let sub_opts = options.with_style(sub_style);
935 layout_child(s, &sub_opts)
936 });
937
938 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
939 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
940 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
941 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
942
943 let sup_style_metrics = get_global_metrics(sup_style.size_index());
945 let sub_style_metrics = get_global_metrics(sub_style.size_index());
946
947 let mut sup_shift = if !is_char_box && sup_box.is_some() {
950 base_box.height - sup_style_metrics.sup_drop * sup_ratio
951 } else {
952 0.0
953 };
954
955 let mut sub_shift = if !is_char_box && sub_box.is_some() {
956 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
957 } else {
958 0.0
959 };
960
961 let min_sup_shift = if options.style.is_cramped() {
962 metrics.sup3
963 } else if options.style.is_display() {
964 metrics.sup1
965 } else {
966 metrics.sup2
967 };
968
969 if sup_box.is_some() && sub_box.is_some() {
970 sup_shift = sup_shift
972 .max(min_sup_shift)
973 .max(sup_depth_scaled + 0.25 * metrics.x_height);
974 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
977 let max_width = 4.0 * rule_width;
978 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
979 if gap < max_width {
980 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
981 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
982 if psi > 0.0 {
983 sup_shift += psi;
984 sub_shift -= psi;
985 }
986 }
987 } else if sub_box.is_some() {
988 sub_shift = sub_shift
990 .max(metrics.sub1)
991 .max(sub_height_scaled - 0.8 * metrics.x_height);
992 } else if sup_box.is_some() {
993 sup_shift = sup_shift
995 .max(min_sup_shift)
996 .max(sup_depth_scaled + 0.25 * metrics.x_height);
997 }
998
999 if horiz_brace_over && sup_box.is_some() {
1003 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
1004 }
1005 if horiz_brace_under && sub_box.is_some() {
1006 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
1007 }
1008
1009 let italic_correction = 0.0;
1012
1013 let sub_h_kern = if sub_box.is_some() && !center_scripts {
1016 -glyph_italic(&base_box)
1017 } else {
1018 0.0
1019 };
1020
1021 let mut height = base_box.height;
1023 let mut depth = base_box.depth;
1024 let mut total_width = base_box.width;
1025
1026 if let Some(ref sup_b) = sup_box {
1027 height = height.max(sup_shift + sup_height_scaled);
1028 if center_scripts {
1029 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1030 } else {
1031 total_width = total_width.max(
1032 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1033 );
1034 }
1035 }
1036 if let Some(ref sub_b) = sub_box {
1037 depth = depth.max(sub_shift + sub_depth_scaled);
1038 if center_scripts {
1039 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1040 } else {
1041 total_width = total_width.max(
1042 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1043 );
1044 }
1045 }
1046
1047 LayoutBox {
1048 width: total_width,
1049 height,
1050 depth,
1051 content: BoxContent::SupSub {
1052 base: Box::new(base_box),
1053 sup: sup_box.map(Box::new),
1054 sub: sub_box.map(Box::new),
1055 sup_shift,
1056 sub_shift,
1057 sup_scale: sup_ratio,
1058 sub_scale: sub_ratio,
1059 center_scripts,
1060 italic_correction,
1061 sub_h_kern,
1062 },
1063 color: options.color,
1064 }
1065}
1066
1067fn layout_radical(
1072 body: &ParseNode,
1073 index: Option<&ParseNode>,
1074 options: &LayoutOptions,
1075) -> LayoutBox {
1076 let cramped = options.style.cramped();
1077 let cramped_opts = options.with_style(cramped);
1078 let mut body_box = layout_node(body, &cramped_opts);
1079
1080 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1082 body_box.height *= body_ratio;
1083 body_box.depth *= body_ratio;
1084 body_box.width *= body_ratio;
1085
1086 if body_box.height == 0.0 {
1088 body_box.height = options.metrics().x_height;
1089 }
1090
1091 let metrics = options.metrics();
1092 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1097 metrics.x_height
1098 } else {
1099 theta
1100 };
1101
1102 let mut line_clearance = theta + phi / 4.0;
1103
1104 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1106
1107 let tex_height = select_surd_height(min_delim_height);
1110 let rule_width = theta;
1111 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1112 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1113 .map(|m| m.width)
1114 .unwrap_or(0.833);
1115
1116 let delim_depth = tex_height - rule_width;
1118 if delim_depth > body_box.height + body_box.depth + line_clearance {
1119 line_clearance =
1120 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1121 }
1122
1123 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1124
1125 let height = tex_height + rule_width - img_shift;
1128 let depth = if img_shift > body_box.depth {
1129 img_shift
1130 } else {
1131 body_box.depth
1132 };
1133
1134 const INDEX_KERN: f64 = 0.05;
1136 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1137 let root_style = options.style.superscript().superscript();
1138 let root_opts = options.with_style(root_style);
1139 let idx = layout_node(index_node, &root_opts);
1140 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1141 let offset = idx.width * index_ratio + INDEX_KERN;
1142 (Some(Box::new(idx)), offset, index_ratio)
1143 } else {
1144 (None, 0.0, 1.0)
1145 };
1146
1147 let width = index_offset + advance_width + body_box.width;
1148
1149 LayoutBox {
1150 width,
1151 height,
1152 depth,
1153 content: BoxContent::Radical {
1154 body: Box::new(body_box),
1155 index: index_box,
1156 index_offset,
1157 index_scale,
1158 rule_thickness: rule_width,
1159 inner_height: tex_height,
1160 },
1161 color: options.color,
1162 }
1163}
1164
1165fn select_surd_height(min_height: f64) -> f64 {
1168 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1169 for &h in &SURD_HEIGHTS {
1170 if h >= min_height {
1171 return h;
1172 }
1173 }
1174 SURD_HEIGHTS[4].max(min_height)
1176}
1177
1178const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1183
1184fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1186 match base {
1187 ParseNode::Op {
1188 limits,
1189 always_handle_sup_sub,
1190 ..
1191 } => {
1192 *limits
1193 && (options.style.is_display()
1194 || always_handle_sup_sub.unwrap_or(false))
1195 }
1196 ParseNode::OperatorName {
1197 always_handle_sup_sub,
1198 limits,
1199 ..
1200 } => {
1201 *always_handle_sup_sub
1202 && (options.style.is_display() || *limits)
1203 }
1204 _ => false,
1205 }
1206}
1207
1208fn layout_op(
1214 name: Option<&str>,
1215 symbol: bool,
1216 body: Option<&[ParseNode]>,
1217 _limits: bool,
1218 suppress_base_shift: bool,
1219 options: &LayoutOptions,
1220) -> LayoutBox {
1221 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1222
1223 if symbol && !suppress_base_shift {
1225 let axis = options.metrics().axis_height;
1226 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1227 if shift.abs() > 0.001 {
1228 base_box.height -= shift;
1229 base_box.depth += shift;
1230 }
1231 }
1232
1233 if !suppress_base_shift && !symbol && body.is_some() {
1238 let axis = options.metrics().axis_height;
1239 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1240 if delta.abs() > 0.001 {
1241 let w = base_box.width;
1242 let raise = -delta;
1244 base_box = LayoutBox {
1245 width: w,
1246 height: (base_box.height + raise).max(0.0),
1247 depth: (base_box.depth - raise).max(0.0),
1248 content: BoxContent::RaiseBox {
1249 body: Box::new(base_box),
1250 shift: raise,
1251 },
1252 color: options.color,
1253 };
1254 }
1255 }
1256
1257 base_box
1258}
1259
1260fn build_op_base(
1263 name: Option<&str>,
1264 symbol: bool,
1265 body: Option<&[ParseNode]>,
1266 options: &LayoutOptions,
1267) -> (LayoutBox, f64) {
1268 if symbol {
1269 let large = options.style.is_display()
1270 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1271 let font_id = if large {
1272 FontId::Size2Regular
1273 } else {
1274 FontId::Size1Regular
1275 };
1276
1277 let op_name = name.unwrap_or("");
1278 let ch = resolve_op_char(op_name);
1279 let char_code = ch as u32;
1280
1281 let metrics = get_char_metrics(font_id, char_code);
1282 let (width, height, depth, italic) = match metrics {
1283 Some(m) => (m.width, m.height, m.depth, m.italic),
1284 None => (1.0, 0.75, 0.25, 0.0),
1285 };
1286 let width_with_italic = width + italic;
1289
1290 let base = LayoutBox {
1291 width: width_with_italic,
1292 height,
1293 depth,
1294 content: BoxContent::Glyph {
1295 font_id,
1296 char_code,
1297 },
1298 color: options.color,
1299 };
1300
1301 if op_name == "\\oiint" || op_name == "\\oiiint" {
1304 let w = base.width;
1305 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1306 let overlay_box = LayoutBox {
1307 width: w,
1308 height: base.height,
1309 depth: base.depth,
1310 content: BoxContent::SvgPath {
1311 commands: ellipse_commands,
1312 fill: false,
1313 },
1314 color: options.color,
1315 };
1316 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1317 return (with_overlay, italic);
1318 }
1319
1320 (base, italic)
1321 } else if let Some(body_nodes) = body {
1322 let base = layout_expression(body_nodes, options, true);
1323 (base, 0.0)
1324 } else {
1325 let base = layout_op_text(name.unwrap_or(""), options);
1326 (base, 0.0)
1327 }
1328}
1329
1330fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1332 let text = name.strip_prefix('\\').unwrap_or(name);
1333 let mut children = Vec::new();
1334 for ch in text.chars() {
1335 let char_code = ch as u32;
1336 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1337 let (width, height, depth) = match metrics {
1338 Some(m) => (m.width, m.height, m.depth),
1339 None => (0.5, 0.43, 0.0),
1340 };
1341 children.push(LayoutBox {
1342 width,
1343 height,
1344 depth,
1345 content: BoxContent::Glyph {
1346 font_id: FontId::MainRegular,
1347 char_code,
1348 },
1349 color: options.color,
1350 });
1351 }
1352 make_hbox(children)
1353}
1354
1355fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1357 let metrics = options.metrics();
1358 (base.height - base.depth) / 2.0 - metrics.axis_height
1359}
1360
1361fn resolve_op_char(name: &str) -> char {
1363 match name {
1366 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1369 }
1370 let font_mode = ratex_font::Mode::Math;
1371 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1372 if let Some(cp) = info.codepoint {
1373 return cp;
1374 }
1375 }
1376 name.chars().next().unwrap_or('?')
1377}
1378
1379fn layout_op_with_limits(
1381 base_node: &ParseNode,
1382 sup_node: Option<&ParseNode>,
1383 sub_node: Option<&ParseNode>,
1384 options: &LayoutOptions,
1385) -> LayoutBox {
1386 let (name, symbol, body, suppress_base_shift) = match base_node {
1387 ParseNode::Op {
1388 name,
1389 symbol,
1390 body,
1391 suppress_base_shift,
1392 ..
1393 } => (
1394 name.as_deref(),
1395 *symbol,
1396 body.as_deref(),
1397 suppress_base_shift.unwrap_or(false),
1398 ),
1399 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1400 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1401 };
1402
1403 let legacy_limit_kern_padding = !suppress_base_shift;
1405
1406 let (base_box, slant) = build_op_base(name, symbol, body, options);
1407 let base_shift = if symbol && !suppress_base_shift {
1409 compute_op_base_shift(&base_box, options)
1410 } else {
1411 0.0
1412 };
1413
1414 layout_op_limits_inner(
1415 &base_box,
1416 sup_node,
1417 sub_node,
1418 slant,
1419 base_shift,
1420 legacy_limit_kern_padding,
1421 options,
1422 )
1423}
1424
1425fn layout_op_limits_inner(
1430 base: &LayoutBox,
1431 sup_node: Option<&ParseNode>,
1432 sub_node: Option<&ParseNode>,
1433 slant: f64,
1434 base_shift: f64,
1435 legacy_limit_kern_padding: bool,
1436 options: &LayoutOptions,
1437) -> LayoutBox {
1438 let metrics = options.metrics();
1439 let sup_style = options.style.superscript();
1440 let sub_style = options.style.subscript();
1441
1442 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1443 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1444
1445 let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 };
1446
1447 let sup_data = sup_node.map(|s| {
1448 let sup_opts = options.with_style(sup_style);
1449 let elem = layout_node(s, &sup_opts);
1450 let d = if legacy_limit_kern_padding {
1454 elem.depth * sup_ratio
1455 } else {
1456 elem.depth
1457 };
1458 let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1459 (elem, kern)
1460 });
1461
1462 let sub_data = sub_node.map(|s| {
1463 let sub_opts = options.with_style(sub_style);
1464 let elem = layout_node(s, &sub_opts);
1465 let h = if legacy_limit_kern_padding {
1466 elem.height * sub_ratio
1467 } else {
1468 elem.height
1469 };
1470 let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1471 (elem, kern)
1472 });
1473
1474 let sp5 = metrics.big_op_spacing5;
1475
1476 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1477 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1478 let sup_h = sup_elem.height * sup_ratio;
1481 let sup_d = sup_elem.depth * sup_ratio;
1482 let sub_h = sub_elem.height * sub_ratio;
1483 let sub_d = sub_elem.depth * sub_ratio;
1484
1485 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1486
1487 let height = bottom
1488 + base.height - base_shift
1489 + sup_kern
1490 + sup_h + sup_d
1491 + sp5
1492 - (base.height + base.depth);
1493
1494 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1495 let total_d = bottom;
1496
1497 let w = base
1498 .width
1499 .max(sup_elem.width * sup_ratio)
1500 .max(sub_elem.width * sub_ratio);
1501 let _ = height; (total_h, total_d, w)
1503 }
1504 (None, Some((sub_elem, sub_kern))) => {
1505 let sub_h = sub_elem.height * sub_ratio;
1508 let sub_d = sub_elem.depth * sub_ratio;
1509
1510 let total_h = base.height - base_shift;
1511 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1512
1513 let w = base.width.max(sub_elem.width * sub_ratio);
1514 (total_h, total_d, w)
1515 }
1516 (Some((sup_elem, sup_kern)), None) => {
1517 let sup_h = sup_elem.height * sup_ratio;
1520 let sup_d = sup_elem.depth * sup_ratio;
1521
1522 let total_h =
1523 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1524 let total_d = base.depth + base_shift;
1525
1526 let w = base.width.max(sup_elem.width * sup_ratio);
1527 (total_h, total_d, w)
1528 }
1529 (None, None) => {
1530 return base.clone();
1531 }
1532 };
1533
1534 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1535 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1536
1537 LayoutBox {
1538 width: total_width,
1539 height: total_height,
1540 depth: total_depth,
1541 content: BoxContent::OpLimits {
1542 base: Box::new(base.clone()),
1543 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1544 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1545 base_shift,
1546 sup_kern: sup_kern_val,
1547 sub_kern: sub_kern_val,
1548 slant,
1549 sup_scale: sup_ratio,
1550 sub_scale: sub_ratio,
1551 },
1552 color: options.color,
1553 }
1554}
1555
1556fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1558 let mut children = Vec::new();
1559 for node in body {
1560 match node {
1561 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1562 let ch = text.chars().next().unwrap_or('?');
1563 let char_code = ch as u32;
1564 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1565 let (width, height, depth) = match metrics {
1566 Some(m) => (m.width, m.height, m.depth),
1567 None => (0.5, 0.43, 0.0),
1568 };
1569 children.push(LayoutBox {
1570 width,
1571 height,
1572 depth,
1573 content: BoxContent::Glyph {
1574 font_id: FontId::MainRegular,
1575 char_code,
1576 },
1577 color: options.color,
1578 });
1579 }
1580 _ => {
1581 children.push(layout_node(node, options));
1582 }
1583 }
1584 }
1585 make_hbox(children)
1586}
1587
1588const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1594
1595fn glyph_italic(lb: &LayoutBox) -> f64 {
1599 match &lb.content {
1600 BoxContent::Glyph { font_id, char_code } => {
1601 get_char_metrics(*font_id, *char_code)
1602 .map(|m| m.italic)
1603 .unwrap_or(0.0)
1604 }
1605 BoxContent::HBox(children) => {
1606 children.last().map(glyph_italic).unwrap_or(0.0)
1607 }
1608 _ => 0.0,
1609 }
1610}
1611
1612fn accent_ordgroup_len(base: &ParseNode) -> usize {
1617 match base {
1618 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1619 _ => 1,
1620 }
1621}
1622
1623fn glyph_skew(lb: &LayoutBox) -> f64 {
1624 match &lb.content {
1625 BoxContent::Glyph { font_id, char_code } => {
1626 get_char_metrics(*font_id, *char_code)
1627 .map(|m| m.skew)
1628 .unwrap_or(0.0)
1629 }
1630 BoxContent::HBox(children) => {
1631 children.last().map(glyph_skew).unwrap_or(0.0)
1632 }
1633 _ => 0.0,
1634 }
1635}
1636
1637fn layout_accent(
1638 label: &str,
1639 base: &ParseNode,
1640 is_stretchy: bool,
1641 is_shifty: bool,
1642 is_below: bool,
1643 options: &LayoutOptions,
1644) -> LayoutBox {
1645 let body_box = layout_node(base, options);
1646 let base_w = body_box.width.max(0.5);
1647
1648 if label == "\\textcircled" {
1650 return layout_textcircled(body_box, options);
1651 }
1652
1653 if let Some((commands, w, h, fill)) =
1655 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1656 {
1657 let accent_box = LayoutBox {
1659 width: w,
1660 height: 0.0,
1661 depth: h,
1662 content: BoxContent::SvgPath { commands, fill },
1663 color: options.color,
1664 };
1665 let gap = 0.065;
1670 let under_gap_em = if is_below && label == "\\utilde" {
1671 0.12
1672 } else {
1673 0.0
1674 };
1675 let clearance = if is_below {
1676 body_box.height + body_box.depth + gap
1677 } else if label == "\\vec" {
1678 (body_box.height - options.metrics().x_height).max(0.0)
1681 } else {
1682 body_box.height + gap
1683 };
1684 let (height, depth) = if is_below {
1685 (body_box.height, body_box.depth + h + gap + under_gap_em)
1686 } else if label == "\\vec" {
1687 (clearance + h, body_box.depth)
1689 } else {
1690 (body_box.height + gap + h, body_box.depth)
1691 };
1692 let vec_skew = if label == "\\vec" {
1693 (if is_shifty {
1694 glyph_skew(&body_box)
1695 } else {
1696 0.0
1697 }) + VEC_SKEW_EXTRA_RIGHT_EM
1698 } else {
1699 0.0
1700 };
1701 return LayoutBox {
1702 width: body_box.width,
1703 height,
1704 depth,
1705 content: BoxContent::Accent {
1706 base: Box::new(body_box),
1707 accent: Box::new(accent_box),
1708 clearance,
1709 skew: vec_skew,
1710 is_below,
1711 under_gap_em,
1712 },
1713 color: options.color,
1714 };
1715 }
1716
1717 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1719
1720 let accent_box = if use_arrow_path {
1721 let (commands, arrow_h, fill_arrow) =
1722 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1723 Some((c, h)) => (c, h, true),
1724 None => {
1725 let h = 0.3_f64;
1726 let c = stretchy_accent_path(label, base_w, h);
1727 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1728 (c, h, fill)
1729 }
1730 };
1731 LayoutBox {
1732 width: base_w,
1733 height: arrow_h / 2.0,
1734 depth: arrow_h / 2.0,
1735 content: BoxContent::SvgPath {
1736 commands,
1737 fill: fill_arrow,
1738 },
1739 color: options.color,
1740 }
1741 } else {
1742 let accent_char = {
1744 let ch = resolve_symbol_char(label, Mode::Text);
1745 if ch == label.chars().next().unwrap_or('?') {
1746 resolve_symbol_char(label, Mode::Math)
1749 } else {
1750 ch
1751 }
1752 };
1753 let accent_code = accent_char as u32;
1754 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1755 let (accent_w, accent_h, accent_d) = match accent_metrics {
1756 Some(m) => (m.width, m.height, m.depth),
1757 None => (body_box.width, 0.25, 0.0),
1758 };
1759 LayoutBox {
1760 width: accent_w,
1761 height: accent_h,
1762 depth: accent_d,
1763 content: BoxContent::Glyph {
1764 font_id: FontId::MainRegular,
1765 char_code: accent_code,
1766 },
1767 color: options.color,
1768 }
1769 };
1770
1771 let skew = if use_arrow_path {
1772 0.0
1773 } else if is_shifty {
1774 glyph_skew(&body_box)
1777 } else {
1778 0.0
1779 };
1780
1781 let gap = if use_arrow_path {
1790 if label == "\\Overrightarrow" {
1791 0.21
1792 } else {
1793 0.26
1794 }
1795 } else {
1796 0.0
1797 };
1798
1799 let clearance = if is_below {
1800 body_box.height + body_box.depth + accent_box.depth + gap
1801 } else if use_arrow_path {
1802 body_box.height + gap
1803 } else {
1804 let base_clearance = match &body_box.content {
1814 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1815 if !is_below =>
1816 {
1817 if inner_accent.height <= 0.001 {
1823 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1829 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1830 katex_pos + correction
1831 } else {
1832 inner_cl + 0.3
1833 }
1834 }
1835 _ => {
1836 if label == "\\bar" || label == "\\=" {
1849 body_box.height
1850 } else {
1851 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1852 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1853 katex_pos + correction
1854 }
1855 }
1856 };
1857 let base_clearance = base_clearance + accent_box.depth;
1862 if label == "\\bar" || label == "\\=" {
1863 (base_clearance - 0.12).max(0.0)
1864 } else {
1865 base_clearance
1866 }
1867 };
1868
1869 let (height, depth) = if is_below {
1870 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1871 } else if use_arrow_path {
1872 (body_box.height + gap + accent_box.height, body_box.depth)
1873 } else {
1874 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1881 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1882 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1883 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1884 } else {
1885 body_box.height.max(accent_visual_top)
1886 };
1887 (h, body_box.depth)
1888 };
1889
1890 LayoutBox {
1891 width: body_box.width,
1892 height,
1893 depth,
1894 content: BoxContent::Accent {
1895 base: Box::new(body_box),
1896 accent: Box::new(accent_box),
1897 clearance,
1898 skew,
1899 is_below,
1900 under_gap_em: 0.0,
1901 },
1902 color: options.color,
1903 }
1904}
1905
1906fn node_contains_middle(node: &ParseNode) -> bool {
1912 match node {
1913 ParseNode::Middle { .. } => true,
1914 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1915 body.iter().any(node_contains_middle)
1916 }
1917 ParseNode::SupSub { base, sup, sub, .. } => {
1918 base.as_deref().is_some_and(node_contains_middle)
1919 || sup.as_deref().is_some_and(node_contains_middle)
1920 || sub.as_deref().is_some_and(node_contains_middle)
1921 }
1922 ParseNode::GenFrac { numer, denom, .. } => {
1923 node_contains_middle(numer) || node_contains_middle(denom)
1924 }
1925 ParseNode::Sqrt { body, index, .. } => {
1926 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1927 }
1928 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1929 node_contains_middle(base)
1930 }
1931 ParseNode::Op { body, .. } => body
1932 .as_ref()
1933 .is_some_and(|b| b.iter().any(node_contains_middle)),
1934 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1935 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1936 ParseNode::Font { body, .. } => node_contains_middle(body),
1937 ParseNode::Text { body, .. }
1938 | ParseNode::Color { body, .. }
1939 | ParseNode::Styling { body, .. }
1940 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1941 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1942 node_contains_middle(body)
1943 }
1944 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1945 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1946 node_contains_middle(body)
1947 }
1948 ParseNode::Array { body, .. } => body
1949 .iter()
1950 .any(|row| row.iter().any(node_contains_middle)),
1951 ParseNode::Enclose { body, .. }
1952 | ParseNode::Lap { body, .. }
1953 | ParseNode::RaiseBox { body, .. }
1954 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1955 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1956 ParseNode::XArrow { body, below, .. } => {
1957 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1958 }
1959 ParseNode::CdArrow { label_above, label_below, .. } => {
1960 label_above.as_deref().is_some_and(node_contains_middle)
1961 || label_below.as_deref().is_some_and(node_contains_middle)
1962 }
1963 ParseNode::MathChoice {
1964 display,
1965 text,
1966 script,
1967 scriptscript,
1968 ..
1969 } => {
1970 display.iter().any(node_contains_middle)
1971 || text.iter().any(node_contains_middle)
1972 || script.iter().any(node_contains_middle)
1973 || scriptscript.iter().any(node_contains_middle)
1974 }
1975 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1976 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1977 _ => false,
1978 }
1979}
1980
1981fn body_contains_middle(nodes: &[ParseNode]) -> bool {
1983 nodes.iter().any(node_contains_middle)
1984}
1985
1986fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
1989 let m = options.metrics();
1990 if options.style.is_display() {
1991 m.delim1
1992 } else if matches!(
1993 options.style,
1994 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
1995 ) {
1996 options
1997 .with_style(MathStyle::Script)
1998 .metrics()
1999 .delim2
2000 } else {
2001 m.delim2
2002 }
2003}
2004
2005fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2007 let metrics = options.metrics();
2008 let inner_height = inner.height;
2009 let inner_depth = inner.depth;
2010 let axis = metrics.axis_height;
2011 let max_dist = (inner_height - axis).max(inner_depth + axis);
2012 let delim_factor = 901.0;
2013 let delim_extend = 5.0 / metrics.pt_per_em;
2014 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2015 from_formula.max(inner_height + inner_depth)
2017}
2018
2019fn layout_left_right(
2020 body: &[ParseNode],
2021 left_delim: &str,
2022 right_delim: &str,
2023 options: &LayoutOptions,
2024) -> LayoutBox {
2025 let (inner, total_height) = if body_contains_middle(body) {
2026 let opts_first = LayoutOptions {
2028 leftright_delim_height: None,
2029 ..options.clone()
2030 };
2031 let inner_first = layout_expression(body, &opts_first, true);
2032 let total_height = left_right_delim_total_height(&inner_first, options);
2033 let opts_second = LayoutOptions {
2035 leftright_delim_height: Some(total_height),
2036 ..options.clone()
2037 };
2038 let inner_second = layout_expression(body, &opts_second, true);
2039 (inner_second, total_height)
2040 } else {
2041 let inner = layout_expression(body, options, true);
2042 let total_height = left_right_delim_total_height(&inner, options);
2043 (inner, total_height)
2044 };
2045
2046 let inner_height = inner.height;
2047 let inner_depth = inner.depth;
2048
2049 let left_box = make_stretchy_delim(left_delim, total_height, options);
2050 let right_box = make_stretchy_delim(right_delim, total_height, options);
2051
2052 let width = left_box.width + inner.width + right_box.width;
2053 let height = left_box.height.max(right_box.height).max(inner_height);
2054 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2055
2056 LayoutBox {
2057 width,
2058 height,
2059 depth,
2060 content: BoxContent::LeftRight {
2061 left: Box::new(left_box),
2062 right: Box::new(right_box),
2063 inner: Box::new(inner),
2064 },
2065 color: options.color,
2066 }
2067}
2068
2069const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2070 FontId::MainRegular,
2071 FontId::Size1Regular,
2072 FontId::Size2Regular,
2073 FontId::Size3Regular,
2074 FontId::Size4Regular,
2075];
2076
2077fn normalize_delim(delim: &str) -> &str {
2079 match delim {
2080 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2081 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2082 _ => delim,
2083 }
2084}
2085
2086fn is_vert_delim(delim: &str) -> bool {
2088 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2089}
2090
2091fn is_double_vert_delim(delim: &str) -> bool {
2093 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2094}
2095
2096fn vert_repeat_piece_height(is_double: bool) -> f64 {
2098 let code = if is_double { 8741_u32 } else { 8739 };
2099 get_char_metrics(FontId::Size1Regular, code)
2100 .map(|m| m.height + m.depth)
2101 .unwrap_or(0.5)
2102}
2103
2104fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2106 let piece = vert_repeat_piece_height(is_double);
2107 let min_h = 2.0 * piece;
2108 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2109 let mut h = min_h + repeat_count * piece;
2110 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2114 h *= 1.135;
2115 }
2116 h
2117}
2118
2119fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2121 let neg = -mid_th;
2122 if !is_double {
2123 format!(
2124 "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"
2125 )
2126 } else {
2127 format!(
2128 "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"
2129 )
2130 }
2131}
2132
2133fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2134 let s = 0.001_f64;
2135 cmds.iter()
2136 .map(|c| match *c {
2137 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2138 x: x * s,
2139 y: y * s,
2140 },
2141 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2142 x: x * s,
2143 y: y * s,
2144 },
2145 PathCommand::CubicTo {
2146 x1,
2147 y1,
2148 x2,
2149 y2,
2150 x,
2151 y,
2152 } => PathCommand::CubicTo {
2153 x1: x1 * s,
2154 y1: y1 * s,
2155 x2: x2 * s,
2156 y2: y2 * s,
2157 x: x * s,
2158 y: y * s,
2159 },
2160 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2161 x1: x1 * s,
2162 y1: y1 * s,
2163 x: x * s,
2164 y: y * s,
2165 },
2166 PathCommand::Close => PathCommand::Close,
2167 })
2168 .collect()
2169}
2170
2171fn map_vert_path_y_to_baseline(
2173 cmds: Vec<PathCommand>,
2174 height: f64,
2175 depth: f64,
2176 view_box_height: i64,
2177) -> Vec<PathCommand> {
2178 let span_em = view_box_height as f64 / 1000.0;
2179 let total = height + depth;
2180 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2181 cmds.into_iter()
2182 .map(|c| match c {
2183 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2184 x,
2185 y: -height + y * scale_y,
2186 },
2187 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2188 x,
2189 y: -height + y * scale_y,
2190 },
2191 PathCommand::CubicTo {
2192 x1,
2193 y1,
2194 x2,
2195 y2,
2196 x,
2197 y,
2198 } => PathCommand::CubicTo {
2199 x1,
2200 y1: -height + y1 * scale_y,
2201 x2,
2202 y2: -height + y2 * scale_y,
2203 x,
2204 y: -height + y * scale_y,
2205 },
2206 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2207 x1,
2208 y1: -height + y1 * scale_y,
2209 x,
2210 y: -height + y * scale_y,
2211 },
2212 PathCommand::Close => PathCommand::Close,
2213 })
2214 .collect()
2215}
2216
2217fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2220 let real_h = katex_vert_real_height(total_height, is_double);
2221 let axis = options.metrics().axis_height;
2222 let depth = (real_h / 2.0 - axis).max(0.0);
2223 let height = real_h - depth;
2224 let width = if is_double { 0.556 } else { 0.333 };
2225
2226 let piece = vert_repeat_piece_height(is_double);
2227 let mid_em = (real_h - 2.0 * piece).max(0.0);
2228 let mid_th = (mid_em * 1000.0).round() as i64;
2229 let view_box_height = (real_h * 1000.0).round() as i64;
2230
2231 let d = tall_vert_svg_path_data(mid_th, is_double);
2232 let raw = parse_svg_path_data(&d);
2233 let scaled = scale_svg_path_to_em(&raw);
2234 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2235
2236 LayoutBox {
2237 width,
2238 height,
2239 depth,
2240 content: BoxContent::SvgPath { commands, fill: true },
2241 color: options.color,
2242 }
2243}
2244
2245fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2247 if delim == "." || delim.is_empty() {
2248 return LayoutBox::new_kern(0.0);
2249 }
2250
2251 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2256 return make_vert_delim_box(total_height, false, options);
2257 }
2258 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2259 return make_vert_delim_box(total_height, true, options);
2260 }
2261
2262 let delim = normalize_delim(delim);
2264
2265 let ch = resolve_symbol_char(delim, Mode::Math);
2266 let char_code = ch as u32;
2267
2268 let mut best_font = FontId::MainRegular;
2269 let mut best_w = 0.4;
2270 let mut best_h = 0.7;
2271 let mut best_d = 0.2;
2272
2273 for &font_id in &DELIM_FONT_SEQUENCE {
2274 if let Some(m) = get_char_metrics(font_id, char_code) {
2275 best_font = font_id;
2276 best_w = m.width;
2277 best_h = m.height;
2278 best_d = m.depth;
2279 if best_h + best_d >= total_height {
2280 break;
2281 }
2282 }
2283 }
2284
2285 let best_total = best_h + best_d;
2286 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2287 return stacked;
2288 }
2289
2290 LayoutBox {
2291 width: best_w,
2292 height: best_h,
2293 depth: best_d,
2294 content: BoxContent::Glyph {
2295 font_id: best_font,
2296 char_code,
2297 },
2298 color: options.color,
2299 }
2300}
2301
2302const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2304
2305fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2307 if delim == "." || delim.is_empty() {
2308 return LayoutBox::new_kern(0.0);
2309 }
2310
2311 if is_vert_delim(delim) {
2313 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2314 return make_vert_delim_box(total, false, options);
2315 }
2316 if is_double_vert_delim(delim) {
2317 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2318 return make_vert_delim_box(total, true, options);
2319 }
2320
2321 let delim = normalize_delim(delim);
2323
2324 let ch = resolve_symbol_char(delim, Mode::Math);
2325 let char_code = ch as u32;
2326
2327 let font_id = match size {
2328 1 => FontId::Size1Regular,
2329 2 => FontId::Size2Regular,
2330 3 => FontId::Size3Regular,
2331 4 => FontId::Size4Regular,
2332 _ => FontId::Size1Regular,
2333 };
2334
2335 let metrics = get_char_metrics(font_id, char_code);
2336 let (width, height, depth, actual_font) = match metrics {
2337 Some(m) => (m.width, m.height, m.depth, font_id),
2338 None => {
2339 let m = get_char_metrics(FontId::MainRegular, char_code);
2340 match m {
2341 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2342 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2343 }
2344 }
2345 };
2346
2347 LayoutBox {
2348 width,
2349 height,
2350 depth,
2351 content: BoxContent::Glyph {
2352 font_id: actual_font,
2353 char_code,
2354 },
2355 color: options.color,
2356 }
2357}
2358
2359#[allow(clippy::too_many_arguments)]
2364fn layout_array(
2365 body: &[Vec<ParseNode>],
2366 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2367 arraystretch: f64,
2368 add_jot: bool,
2369 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2370 hlines: &[Vec<bool>],
2371 col_sep_type: Option<&str>,
2372 hskip: bool,
2373 tags: Option<&[ArrayTag]>,
2374 _leqno: bool,
2375 options: &LayoutOptions,
2376) -> LayoutBox {
2377 let metrics = options.metrics();
2378 let pt = 1.0 / metrics.pt_per_em;
2379 let baselineskip = 12.0 * pt;
2380 let jot = 3.0 * pt;
2381 let arrayskip = arraystretch * baselineskip;
2382 let arstrut_h = 0.7 * arrayskip;
2383 let arstrut_d = 0.3 * arrayskip;
2384 const ALIGN_RELATION_MU: f64 = 3.0;
2387 let col_gap = match col_sep_type {
2388 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2389 Some("alignat") => 0.0,
2390 Some("small") => {
2391 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2394 / options.size_multiplier()
2395 }
2396 _ => 2.0 * 5.0 * pt, };
2398 let cell_options = match col_sep_type {
2399 Some("align") | Some("alignat") => LayoutOptions {
2400 align_relation_spacing: Some(ALIGN_RELATION_MU),
2401 ..options.clone()
2402 },
2403 _ => options.clone(),
2404 };
2405
2406 let num_rows = body.len();
2407 if num_rows == 0 {
2408 return LayoutBox::new_empty();
2409 }
2410
2411 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2412
2413 use ratex_parser::parse_node::AlignType;
2415 let col_aligns: Vec<u8> = {
2416 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2417 .map(|cs| {
2418 cs.iter()
2419 .filter(|s| matches!(s.align_type, AlignType::Align))
2420 .collect()
2421 })
2422 .unwrap_or_default();
2423 (0..num_cols)
2424 .map(|c| {
2425 align_specs
2426 .get(c)
2427 .and_then(|s| s.align.as_deref())
2428 .and_then(|a| a.bytes().next())
2429 .unwrap_or(b'c')
2430 })
2431 .collect()
2432 };
2433
2434 let col_separators: Vec<Option<bool>> = {
2437 let mut seps = vec![None; num_cols + 1];
2438 let mut align_count = 0usize;
2439 if let Some(cs) = cols {
2440 for spec in cs {
2441 match spec.align_type {
2442 AlignType::Align => align_count += 1,
2443 AlignType::Separator if spec.align.as_deref() == Some("|") => {
2444 if align_count <= num_cols {
2445 seps[align_count] = Some(false);
2446 }
2447 }
2448 AlignType::Separator if spec.align.as_deref() == Some(":") => {
2449 if align_count <= num_cols {
2450 seps[align_count] = Some(true);
2451 }
2452 }
2453 _ => {}
2454 }
2455 }
2456 }
2457 seps
2458 };
2459
2460 let rule_thickness = 0.4 * pt;
2461 let double_rule_sep = metrics.double_rule_sep;
2462
2463 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2465 let mut col_widths = vec![0.0_f64; num_cols];
2466 let mut row_heights = Vec::with_capacity(num_rows);
2467 let mut row_depths = Vec::with_capacity(num_rows);
2468
2469 for row in body {
2470 let mut row_boxes = Vec::with_capacity(num_cols);
2471 let mut rh = arstrut_h;
2472 let mut rd = arstrut_d;
2473
2474 for (c, cell) in row.iter().enumerate() {
2475 let cell_nodes = match cell {
2476 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2477 other => std::slice::from_ref(other),
2478 };
2479 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2480 rh = rh.max(cell_box.height);
2481 rd = rd.max(cell_box.depth);
2482 if c < num_cols {
2483 col_widths[c] = col_widths[c].max(cell_box.width);
2484 }
2485 row_boxes.push(cell_box);
2486 }
2487
2488 while row_boxes.len() < num_cols {
2490 row_boxes.push(LayoutBox::new_empty());
2491 }
2492
2493 if add_jot {
2494 rd += jot;
2495 }
2496
2497 row_heights.push(rh);
2498 row_depths.push(rd);
2499 cell_boxes.push(row_boxes);
2500 }
2501
2502 for (r, gap) in row_gaps.iter().enumerate() {
2504 if r < row_depths.len() {
2505 if let Some(m) = gap {
2506 let gap_em = measurement_to_em(m, options);
2507 if gap_em > 0.0 {
2508 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2509 }
2510 }
2511 }
2512 }
2513
2514 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2516 while hlines_before_row.len() < num_rows + 1 {
2517 hlines_before_row.push(vec![]);
2518 }
2519
2520 for r in 0..=num_rows {
2526 let n = hlines_before_row[r].len();
2527 if n > 1 {
2528 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2529 if r == 0 {
2530 if num_rows > 0 {
2531 row_heights[0] += extra;
2532 }
2533 } else {
2534 row_depths[r - 1] += extra;
2535 }
2536 }
2537 }
2538
2539 let mut total_height = 0.0;
2541 let mut row_positions = Vec::with_capacity(num_rows);
2542 for r in 0..num_rows {
2543 total_height += row_heights[r];
2544 row_positions.push(total_height);
2545 total_height += row_depths[r];
2546 }
2547
2548 let offset = total_height / 2.0 + metrics.axis_height;
2549
2550 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2552
2553 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2555 + col_gap * (num_cols.saturating_sub(1)) as f64
2556 + 2.0 * content_x_offset;
2557
2558 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2559 let mut tag_col_width = 0.0_f64;
2560 let text_opts = options.with_style(options.style.text());
2561 if let Some(tag_slice) = tags {
2562 if tag_slice.len() == num_rows {
2563 for (r, t) in tag_slice.iter().enumerate() {
2564 if let ArrayTag::Explicit(nodes) = t {
2565 if !nodes.is_empty() {
2566 let tb = layout_expression(nodes, &text_opts, true);
2567 tag_col_width = tag_col_width.max(tb.width);
2568 row_tag_boxes[r] = Some(tb);
2569 }
2570 }
2571 }
2572 }
2573 }
2574 let tag_gap_em = if tag_col_width > 0.0 {
2575 text_opts.metrics().quad
2576 } else {
2577 0.0
2578 };
2579 let tags_left = false;
2581
2582 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2583
2584 let height = offset;
2585 let depth = total_height - offset;
2586
2587 LayoutBox {
2588 width: total_width,
2589 height,
2590 depth,
2591 content: BoxContent::Array {
2592 cells: cell_boxes,
2593 col_widths: col_widths.clone(),
2594 col_aligns,
2595 row_heights: row_heights.clone(),
2596 row_depths: row_depths.clone(),
2597 col_gap,
2598 offset,
2599 content_x_offset,
2600 col_separators,
2601 hlines_before_row,
2602 rule_thickness,
2603 double_rule_sep,
2604 array_inner_width,
2605 tag_gap_em,
2606 tag_col_width,
2607 row_tags: row_tag_boxes,
2608 tags_left,
2609 },
2610 color: options.color,
2611 }
2612}
2613
2614fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2619 let multiplier = match size {
2621 1 => 0.5,
2622 2 => 0.6,
2623 3 => 0.7,
2624 4 => 0.8,
2625 5 => 0.9,
2626 6 => 1.0,
2627 7 => 1.2,
2628 8 => 1.44,
2629 9 => 1.728,
2630 10 => 2.074,
2631 11 => 2.488,
2632 _ => 1.0,
2633 };
2634
2635 let inner_opts = options.with_style(options.style.text());
2637 let inner = layout_expression(body, &inner_opts, true);
2638 let ratio = multiplier / options.size_multiplier();
2639 if (ratio - 1.0).abs() < 0.001 {
2640 inner
2641 } else {
2642 LayoutBox {
2643 width: inner.width * ratio,
2644 height: inner.height * ratio,
2645 depth: inner.depth * ratio,
2646 content: BoxContent::Scaled {
2647 body: Box::new(inner),
2648 child_scale: ratio,
2649 },
2650 color: options.color,
2651 }
2652 }
2653}
2654
2655fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2658 let metrics = options.metrics();
2659 let mut children = Vec::new();
2660 for c in body.chars() {
2661 let ch = if star && c == ' ' {
2662 '\u{2423}' } else {
2664 c
2665 };
2666 let code = ch as u32;
2667 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2668 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2669 None => match get_char_metrics(FontId::MainRegular, code) {
2670 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2671 None => (
2672 FontId::TypewriterRegular,
2673 0.5,
2674 metrics.x_height,
2675 0.0,
2676 ),
2677 },
2678 };
2679 children.push(LayoutBox {
2680 width: w,
2681 height: h,
2682 depth: d,
2683 content: BoxContent::Glyph {
2684 font_id,
2685 char_code: code,
2686 },
2687 color: options.color,
2688 });
2689 }
2690 let mut hbox = make_hbox(children);
2691 hbox.color = options.color;
2692 hbox
2693}
2694
2695fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2703 let mut children = Vec::new();
2704 for node in body {
2705 match node {
2706 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2707 children.push(layout_symbol(text, *mode, options));
2708 }
2709 ParseNode::SpacingNode { text, .. } => {
2710 children.push(layout_spacing_command(text, options));
2711 }
2712 _ => {
2713 children.push(layout_node(node, options));
2714 }
2715 }
2716 }
2717 make_hbox(children)
2718}
2719
2720fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2723 let base = layout_expression(body, options, true);
2724 let w = base.width;
2725 let h = base.height;
2726 let d = base.depth;
2727
2728 let shadow = layout_expression(body, options, true);
2730 let shadow_shift_x = 0.02_f64;
2731 let _shadow_shift_y = 0.01_f64;
2732
2733 let kern_back = LayoutBox::new_kern(-w);
2737 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2738
2739 let children = vec![
2746 kern_x,
2747 shadow,
2748 kern_back,
2749 base,
2750 ];
2751 let hbox = make_hbox(children);
2753 LayoutBox {
2755 width: w,
2756 height: h,
2757 depth: d,
2758 content: hbox.content,
2759 color: options.color,
2760 }
2761}
2762
2763fn layout_enclose(
2766 label: &str,
2767 background_color: Option<&str>,
2768 border_color: Option<&str>,
2769 body: &ParseNode,
2770 options: &LayoutOptions,
2771) -> LayoutBox {
2772 use crate::layout_box::BoxContent;
2773 use ratex_types::color::Color;
2774
2775 if label == "\\phase" {
2777 return layout_phase(body, options);
2778 }
2779
2780 if label == "\\angl" {
2782 return layout_angl(body, options);
2783 }
2784
2785 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2787 return layout_cancel(label, body, options);
2788 }
2789
2790 let metrics = options.metrics();
2792 let padding = 3.0 / metrics.pt_per_em;
2793 let border_thickness = 0.4 / metrics.pt_per_em;
2794
2795 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2796
2797 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2798 let border = border_color
2799 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2800 .unwrap_or(Color::BLACK);
2801
2802 let inner = layout_node(body, options);
2803 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2804
2805 let width = inner.width + 2.0 * outer_pad;
2806 let height = inner.height + outer_pad;
2807 let depth = inner.depth + outer_pad;
2808
2809 LayoutBox {
2810 width,
2811 height,
2812 depth,
2813 content: BoxContent::Framed {
2814 body: Box::new(inner),
2815 padding,
2816 border_thickness,
2817 has_border,
2818 bg_color: bg,
2819 border_color: border,
2820 },
2821 color: options.color,
2822 }
2823}
2824
2825fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2827 use crate::layout_box::BoxContent;
2828 let inner = layout_node(body, options);
2829 let height = inner.height + shift;
2831 let depth = (inner.depth - shift).max(0.0);
2832 let width = inner.width;
2833 LayoutBox {
2834 width,
2835 height,
2836 depth,
2837 content: BoxContent::RaiseBox {
2838 body: Box::new(inner),
2839 shift,
2840 },
2841 color: options.color,
2842 }
2843}
2844
2845fn is_single_char_body(node: &ParseNode) -> bool {
2848 use ratex_parser::parse_node::ParseNode as PN;
2849 match node {
2850 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2852 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2853 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2855 _ => false,
2856 }
2857}
2858
2859fn layout_cancel(
2865 label: &str,
2866 body: &ParseNode,
2867 options: &LayoutOptions,
2868) -> LayoutBox {
2869 use crate::layout_box::BoxContent;
2870 let inner = layout_node(body, options);
2871 let w = inner.width.max(0.01);
2872 let h = inner.height;
2873 let d = inner.depth;
2874
2875 let single = is_single_char_body(body);
2878 let (v_pad, h_pad) = if label == "\\sout" {
2879 (0.0, 0.0)
2880 } else if single {
2881 (0.2, 0.0)
2882 } else {
2883 (0.0, 0.2)
2884 };
2885
2886 let commands: Vec<PathCommand> = match label {
2890 "\\cancel" => vec![
2891 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2894 "\\bcancel" => vec![
2895 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2898 "\\xcancel" => vec![
2899 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2900 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2901 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2902 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2903 ],
2904 "\\sout" => {
2905 let mid_y = -0.5 * options.metrics().x_height;
2907 vec![
2908 PathCommand::MoveTo { x: 0.0, y: mid_y },
2909 PathCommand::LineTo { x: w, y: mid_y },
2910 ]
2911 }
2912 _ => vec![],
2913 };
2914
2915 let line_w = w + 2.0 * h_pad;
2916 let line_h = h + v_pad;
2917 let line_d = d + v_pad;
2918 let line_box = LayoutBox {
2919 width: line_w,
2920 height: line_h,
2921 depth: line_d,
2922 content: BoxContent::SvgPath { commands, fill: false },
2923 color: options.color,
2924 };
2925
2926 let body_kern = -(line_w - h_pad);
2928 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2929 LayoutBox {
2930 width: w,
2931 height: h,
2932 depth: d,
2933 content: BoxContent::HBox(vec![line_box, body_shifted]),
2934 color: options.color,
2935 }
2936}
2937
2938fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2941 use crate::layout_box::BoxContent;
2942 let metrics = options.metrics();
2943 let inner = layout_node(body, options);
2944 let line_weight = 0.6_f64 / metrics.pt_per_em;
2946 let clearance = 0.35_f64 * metrics.x_height;
2947 let angle_height = inner.height + inner.depth + line_weight + clearance;
2948 let left_pad = angle_height / 2.0 + line_weight;
2949 let width = inner.width + left_pad;
2950
2951 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2953
2954 let sy = angle_height / y_svg;
2956 let sx = sy;
2959 let right_x = (400_000.0_f64 * sx).min(width);
2960
2961 let bottom_y = inner.depth + line_weight + clearance;
2963 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2964
2965 let x_peak = y_svg / 2.0;
2967 let commands = vec![
2968 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2969 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2970 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2971 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2972 PathCommand::LineTo {
2973 x: 145.0 * sx,
2974 y: vy(y_svg - 80.0),
2975 },
2976 PathCommand::LineTo {
2977 x: right_x,
2978 y: vy(y_svg - 80.0),
2979 },
2980 PathCommand::Close,
2981 ];
2982
2983 let body_shifted = make_hbox(vec![
2984 LayoutBox::new_kern(left_pad),
2985 inner.clone(),
2986 ]);
2987
2988 let path_height = inner.height;
2989 let path_depth = bottom_y;
2990
2991 LayoutBox {
2992 width,
2993 height: path_height,
2994 depth: path_depth,
2995 content: BoxContent::HBox(vec![
2996 LayoutBox {
2997 width,
2998 height: path_height,
2999 depth: path_depth,
3000 content: BoxContent::SvgPath { commands, fill: true },
3001 color: options.color,
3002 },
3003 LayoutBox::new_kern(-width),
3004 body_shifted,
3005 ]),
3006 color: options.color,
3007 }
3008}
3009
3010fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3013 use crate::layout_box::BoxContent;
3014 let inner = layout_node(body, options);
3015 let w = inner.width.max(0.3);
3016 let clearance = 0.1_f64;
3018 let arc_h = inner.height + clearance;
3019
3020 let path_commands = vec![
3022 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3023 PathCommand::LineTo { x: w, y: -arc_h },
3024 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
3025 ];
3026
3027 let height = arc_h;
3028 LayoutBox {
3029 width: w,
3030 height,
3031 depth: inner.depth,
3032 content: BoxContent::Angl {
3033 path_commands,
3034 body: Box::new(inner),
3035 },
3036 color: options.color,
3037 }
3038}
3039
3040fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3041 let font_id = match font {
3042 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3043 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3044 "mathit" | "\\mathit" | "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3045 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3046 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3047 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3048 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3049 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3050 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3051 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3052 _ => None,
3053 };
3054
3055 if let Some(fid) = font_id {
3056 layout_with_font(body, fid, options)
3057 } else {
3058 layout_node(body, options)
3059 }
3060}
3061
3062fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3063 match node {
3064 ParseNode::OrdGroup { body, .. } => {
3065 let kern = options.inter_glyph_kern_em;
3066 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3067 for (i, n) in body.iter().enumerate() {
3068 if i > 0 && kern > 0.0 {
3069 children.push(LayoutBox::new_kern(kern));
3070 }
3071 children.push(layout_with_font(n, font_id, options));
3072 }
3073 make_hbox(children)
3074 }
3075 ParseNode::SupSub {
3076 base, sup, sub, ..
3077 } => {
3078 if let Some(base_node) = base.as_deref() {
3079 if should_use_op_limits(base_node, options) {
3080 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3081 }
3082 }
3083 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3084 }
3085 ParseNode::MathOrd { text, mode, .. }
3086 | ParseNode::TextOrd { text, mode, .. }
3087 | ParseNode::Atom { text, mode, .. } => {
3088 let ch = resolve_symbol_char(text, *mode);
3089 let char_code = ch as u32;
3090 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3091 .map(|(_, m)| m)
3092 .unwrap_or(char_code);
3093 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3094 LayoutBox {
3095 width: math_glyph_advance_em(&m, *mode),
3097 height: m.height,
3098 depth: m.depth,
3099 content: BoxContent::Glyph { font_id, char_code },
3100 color: options.color,
3101 }
3102 } else {
3103 layout_node(node, options)
3105 }
3106 }
3107 _ => layout_node(node, options),
3108 }
3109}
3110
3111fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3116 let cramped = options.with_style(options.style.cramped());
3117 let body_box = layout_node(body, &cramped);
3118 let metrics = options.metrics();
3119 let rule = metrics.default_rule_thickness;
3120
3121 let height = body_box.height + 3.0 * rule;
3123 LayoutBox {
3124 width: body_box.width,
3125 height,
3126 depth: body_box.depth,
3127 content: BoxContent::Overline {
3128 body: Box::new(body_box),
3129 rule_thickness: rule,
3130 },
3131 color: options.color,
3132 }
3133}
3134
3135fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3136 let body_box = layout_node(body, options);
3137 let metrics = options.metrics();
3138 let rule = metrics.default_rule_thickness;
3139
3140 let depth = body_box.depth + 3.0 * rule;
3142 LayoutBox {
3143 width: body_box.width,
3144 height: body_box.height,
3145 depth,
3146 content: BoxContent::Underline {
3147 body: Box::new(body_box),
3148 rule_thickness: rule,
3149 },
3150 color: options.color,
3151 }
3152}
3153
3154fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3156 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3157 let body_opts = options
3159 .with_color(link_color)
3160 .with_inter_glyph_kern(0.024);
3161 let body_box = layout_expression(body, &body_opts, true);
3162 layout_underline_laid_out(body_box, options, link_color)
3163}
3164
3165fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3167 let metrics = options.metrics();
3168 let rule = metrics.default_rule_thickness;
3169 let depth = body_box.depth + 3.0 * rule;
3170 LayoutBox {
3171 width: body_box.width,
3172 height: body_box.height,
3173 depth,
3174 content: BoxContent::Underline {
3175 body: Box::new(body_box),
3176 rule_thickness: rule,
3177 },
3178 color,
3179 }
3180}
3181
3182fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3187 let metrics = options.metrics();
3188 let mu = metrics.css_em_per_mu();
3189
3190 let width = match text {
3191 "\\," | "\\thinspace" => 3.0 * mu,
3192 "\\:" | "\\medspace" => 4.0 * mu,
3193 "\\;" | "\\thickspace" => 5.0 * mu,
3194 "\\!" | "\\negthinspace" => -3.0 * mu,
3195 "\\negmedspace" => -4.0 * mu,
3196 "\\negthickspace" => -5.0 * mu,
3197 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3198 get_char_metrics(FontId::MainRegular, 160)
3202 .map(|m| m.width)
3203 .unwrap_or(0.25)
3204 }
3205 "\\quad" => metrics.quad,
3206 "\\qquad" => 2.0 * metrics.quad,
3207 "\\enspace" => metrics.quad / 2.0,
3208 _ => 0.0,
3209 };
3210
3211 LayoutBox::new_kern(width)
3212}
3213
3214fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3219 let metrics = options.metrics();
3220 match m.unit.as_str() {
3221 "em" => m.number,
3222 "ex" => m.number * metrics.x_height,
3223 "mu" => m.number * metrics.css_em_per_mu(),
3224 "pt" => m.number / metrics.pt_per_em,
3225 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3226 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3227 "in" => m.number * 72.27 / metrics.pt_per_em,
3228 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3229 "pc" => m.number * 12.0 / metrics.pt_per_em,
3230 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3231 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3232 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3233 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3234 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3235 _ => m.number,
3236 }
3237}
3238
3239fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3245 match node {
3246 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3247 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3248 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3249 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3250 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3252 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3253 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3254 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3255 }
3256 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3257 ParseNode::SupSub { base, .. } => {
3258 base.as_ref().and_then(|b| node_math_class(b))
3259 }
3260 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3261 ParseNode::SpacingNode { .. } => None,
3262 ParseNode::Kern { .. } => None,
3263 ParseNode::HtmlMathMl { html, .. } => {
3264 for child in html {
3266 if let Some(cls) = node_math_class(child) {
3267 return Some(cls);
3268 }
3269 }
3270 None
3271 }
3272 ParseNode::Lap { .. } => None,
3273 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3274 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3275 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3277 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3279 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3280 ParseNode::Middle { .. } => Some(MathClass::Ord),
3281 _ => Some(MathClass::Ord),
3282 }
3283}
3284
3285fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3286 match mclass {
3287 "mord" => MathClass::Ord,
3288 "mop" => MathClass::Op,
3289 "mbin" => MathClass::Bin,
3290 "mrel" => MathClass::Rel,
3291 "mopen" => MathClass::Open,
3292 "mclose" => MathClass::Close,
3293 "mpunct" => MathClass::Punct,
3294 "minner" => MathClass::Inner,
3295 _ => MathClass::Ord,
3296 }
3297}
3298
3299fn get_base_elem(node: &ParseNode) -> &ParseNode {
3303 match node {
3304 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3305 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3306 ParseNode::Font { body, .. } => get_base_elem(body),
3307 _ => node,
3308 }
3309}
3310
3311fn is_character_box(node: &ParseNode) -> bool {
3312 matches!(
3313 get_base_elem(node),
3314 ParseNode::MathOrd { .. }
3315 | ParseNode::TextOrd { .. }
3316 | ParseNode::Atom { .. }
3317 | ParseNode::AccentToken { .. }
3318 )
3319}
3320
3321fn family_to_math_class(family: AtomFamily) -> MathClass {
3322 match family {
3323 AtomFamily::Bin => MathClass::Bin,
3324 AtomFamily::Rel => MathClass::Rel,
3325 AtomFamily::Open => MathClass::Open,
3326 AtomFamily::Close => MathClass::Close,
3327 AtomFamily::Punct => MathClass::Punct,
3328 AtomFamily::Inner => MathClass::Inner,
3329 }
3330}
3331
3332fn layout_horiz_brace(
3337 base: &ParseNode,
3338 is_over: bool,
3339 func_label: &str,
3340 options: &LayoutOptions,
3341) -> LayoutBox {
3342 let body_box = layout_node(base, options);
3343 let w = body_box.width.max(0.5);
3344
3345 let is_bracket = func_label
3346 .trim_start_matches('\\')
3347 .ends_with("bracket");
3348
3349 let stretch_key = if is_bracket {
3351 if is_over {
3352 "overbracket"
3353 } else {
3354 "underbracket"
3355 }
3356 } else if is_over {
3357 "overbrace"
3358 } else {
3359 "underbrace"
3360 };
3361
3362 let (raw_commands, brace_h, brace_fill) =
3363 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3364 Some((c, h)) => (c, h, true),
3365 None => {
3366 let h = 0.35_f64;
3367 (horiz_brace_path(w, h, is_over), h, false)
3368 }
3369 };
3370
3371 let y_shift = brace_h / 2.0;
3377 let commands = shift_path_y(raw_commands, y_shift);
3378
3379 let brace_box = LayoutBox {
3380 width: w,
3381 height: 0.0,
3382 depth: brace_h,
3383 content: BoxContent::SvgPath {
3384 commands,
3385 fill: brace_fill,
3386 },
3387 color: options.color,
3388 };
3389
3390 let gap = 0.1;
3391 let (height, depth) = if is_over {
3392 (body_box.height + brace_h + gap, body_box.depth)
3393 } else {
3394 (body_box.height, body_box.depth + brace_h + gap)
3395 };
3396
3397 let clearance = if is_over {
3398 height - brace_h
3399 } else {
3400 body_box.height + body_box.depth + gap
3401 };
3402 let total_w = body_box.width;
3403
3404 LayoutBox {
3405 width: total_w,
3406 height,
3407 depth,
3408 content: BoxContent::Accent {
3409 base: Box::new(body_box),
3410 accent: Box::new(brace_box),
3411 clearance,
3412 skew: 0.0,
3413 is_below: !is_over,
3414 under_gap_em: 0.0,
3415 },
3416 color: options.color,
3417 }
3418}
3419
3420fn layout_xarrow(
3425 label: &str,
3426 body: &ParseNode,
3427 below: Option<&ParseNode>,
3428 options: &LayoutOptions,
3429) -> LayoutBox {
3430 let sup_style = options.style.superscript();
3431 let sub_style = options.style.subscript();
3432 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3433 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3434
3435 let sup_opts = options.with_style(sup_style);
3436 let body_box = layout_node(body, &sup_opts);
3437 let body_w = body_box.width * sup_ratio;
3438
3439 let below_box = below.map(|b| {
3440 let sub_opts = options.with_style(sub_style);
3441 layout_node(b, &sub_opts)
3442 });
3443 let below_w = below_box
3444 .as_ref()
3445 .map(|b| b.width * sub_ratio)
3446 .unwrap_or(0.0);
3447
3448 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3451 let upper_w = body_w + sup_ratio;
3452 let lower_w = if below_box.is_some() {
3453 below_w + sub_ratio
3454 } else {
3455 0.0
3456 };
3457 let arrow_w = upper_w.max(lower_w).max(min_w);
3458 let arrow_h = 0.3;
3459
3460 let (commands, actual_arrow_h, fill_arrow) =
3461 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3462 Some((c, h)) => (c, h, true),
3463 None => (
3464 stretchy_accent_path(label, arrow_w, arrow_h),
3465 arrow_h,
3466 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3467 ),
3468 };
3469 let arrow_box = LayoutBox {
3470 width: arrow_w,
3471 height: actual_arrow_h / 2.0,
3472 depth: actual_arrow_h / 2.0,
3473 content: BoxContent::SvgPath {
3474 commands,
3475 fill: fill_arrow,
3476 },
3477 color: options.color,
3478 };
3479
3480 let metrics = options.metrics();
3483 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3485 let gap = 0.111; let base_shift = -axis;
3489
3490 let sup_kern = gap;
3498 let sub_kern = gap;
3499
3500 let sup_h = body_box.height * sup_ratio;
3501 let sup_d = body_box.depth * sup_ratio;
3502
3503 let height = axis + arrow_half + gap + sup_h + sup_d;
3505 let mut depth = (arrow_half - axis).max(0.0);
3507
3508 if let Some(ref bel) = below_box {
3509 let sub_h = bel.height * sub_ratio;
3510 let sub_d = bel.depth * sub_ratio;
3511 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3513 }
3514
3515 LayoutBox {
3516 width: arrow_w,
3517 height,
3518 depth,
3519 content: BoxContent::OpLimits {
3520 base: Box::new(arrow_box),
3521 sup: Some(Box::new(body_box)),
3522 sub: below_box.map(Box::new),
3523 base_shift,
3524 sup_kern,
3525 sub_kern,
3526 slant: 0.0,
3527 sup_scale: sup_ratio,
3528 sub_scale: sub_ratio,
3529 },
3530 color: options.color,
3531 }
3532}
3533
3534fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3539 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3542 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3543 let diameter = radius * 2.0;
3544
3545 let cx = radius;
3547 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3550
3551 let circle_commands = vec![
3552 PathCommand::MoveTo { x: cx + r, y: cy },
3553 PathCommand::CubicTo {
3554 x1: cx + r, y1: cy - k * r,
3555 x2: cx + k * r, y2: cy - r,
3556 x: cx, y: cy - r,
3557 },
3558 PathCommand::CubicTo {
3559 x1: cx - k * r, y1: cy - r,
3560 x2: cx - r, y2: cy - k * r,
3561 x: cx - r, y: cy,
3562 },
3563 PathCommand::CubicTo {
3564 x1: cx - r, y1: cy + k * r,
3565 x2: cx - k * r, y2: cy + r,
3566 x: cx, y: cy + r,
3567 },
3568 PathCommand::CubicTo {
3569 x1: cx + k * r, y1: cy + r,
3570 x2: cx + r, y2: cy + k * r,
3571 x: cx + r, y: cy,
3572 },
3573 PathCommand::Close,
3574 ];
3575
3576 let circle_box = LayoutBox {
3577 width: diameter,
3578 height: r - cy.min(0.0),
3579 depth: (r + cy).max(0.0),
3580 content: BoxContent::SvgPath {
3581 commands: circle_commands,
3582 fill: false,
3583 },
3584 color: options.color,
3585 };
3586
3587 let content_shift = (diameter - body_box.width) / 2.0;
3589 let children = vec![
3591 circle_box,
3592 LayoutBox::new_kern(-(diameter) + content_shift),
3593 body_box.clone(),
3594 ];
3595
3596 let height = r - cy.min(0.0);
3597 let depth = (r + cy).max(0.0);
3598
3599 LayoutBox {
3600 width: diameter,
3601 height,
3602 depth,
3603 content: BoxContent::HBox(children),
3604 color: options.color,
3605 }
3606}
3607
3608fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3632 let r: f64 = 0.1125;
3634 let cy: f64 = -0.2625;
3638 let k: f64 = 0.5523;
3640 let cx: f64 = r;
3642
3643 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3646
3647 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3656 vec![
3657 PathCommand::MoveTo { x: ox + rad, y: cy },
3658 PathCommand::CubicTo {
3659 x1: ox + rad, y1: cy - k * rad,
3660 x2: ox + k * rad, y2: cy - rad,
3661 x: ox, y: cy - rad,
3662 },
3663 PathCommand::CubicTo {
3664 x1: ox - k * rad, y1: cy - rad,
3665 x2: ox - rad, y2: cy - k * rad,
3666 x: ox - rad, y: cy,
3667 },
3668 PathCommand::CubicTo {
3669 x1: ox - rad, y1: cy + k * rad,
3670 x2: ox - k * rad, y2: cy + rad,
3671 x: ox, y: cy + rad,
3672 },
3673 PathCommand::CubicTo {
3674 x1: ox + k * rad, y1: cy + rad,
3675 x2: ox + rad, y2: cy + k * rad,
3676 x: ox + rad, y: cy,
3677 },
3678 PathCommand::Close,
3679 ]
3680 };
3681
3682 let disk = LayoutBox {
3683 width: 2.0 * r,
3684 height: h,
3685 depth: d,
3686 content: BoxContent::SvgPath {
3687 commands: circle_commands(cx, r),
3688 fill: true,
3689 },
3690 color: options.color,
3691 };
3692
3693 let ring = LayoutBox {
3694 width: 2.0 * r,
3695 height: h,
3696 depth: d,
3697 content: BoxContent::SvgPath {
3698 commands: circle_commands(cx, r_ring),
3699 fill: false,
3700 },
3701 color: options.color,
3702 };
3703
3704 let bar_len: f64 = 0.25;
3708 let bar_th: f64 = 0.04;
3709 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3712
3713 let children = if imageof {
3714 vec![disk, bar, ring]
3715 } else {
3716 vec![ring, bar, disk]
3717 };
3718
3719 let total_width = 4.0 * r + bar_len;
3721 LayoutBox {
3722 width: total_width,
3723 height: h,
3724 depth: d,
3725 content: BoxContent::HBox(children),
3726 color: options.color,
3727 }
3728}
3729
3730fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3734 let cx = width / 2.0;
3735 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3740 PathCommand::MoveTo { x: cx + a, y: cy },
3741 PathCommand::CubicTo {
3742 x1: cx + a,
3743 y1: cy - k * b,
3744 x2: cx + k * a,
3745 y2: cy - b,
3746 x: cx,
3747 y: cy - b,
3748 },
3749 PathCommand::CubicTo {
3750 x1: cx - k * a,
3751 y1: cy - b,
3752 x2: cx - a,
3753 y2: cy - k * b,
3754 x: cx - a,
3755 y: cy,
3756 },
3757 PathCommand::CubicTo {
3758 x1: cx - a,
3759 y1: cy + k * b,
3760 x2: cx - k * a,
3761 y2: cy + b,
3762 x: cx,
3763 y: cy + b,
3764 },
3765 PathCommand::CubicTo {
3766 x1: cx + k * a,
3767 y1: cy + b,
3768 x2: cx + a,
3769 y2: cy + k * b,
3770 x: cx + a,
3771 y: cy,
3772 },
3773 PathCommand::Close,
3774 ]
3775}
3776
3777fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3778 cmds.into_iter().map(|c| match c {
3779 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3780 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3781 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3782 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3783 },
3784 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3785 x1, y1: y1 + dy, x, y: y + dy,
3786 },
3787 PathCommand::Close => PathCommand::Close,
3788 }).collect()
3789}
3790
3791fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3792 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3793 return commands;
3794 }
3795 let ah = height * 0.35; let mid_y = -height / 2.0;
3797
3798 match label {
3799 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3800 vec![
3801 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3802 PathCommand::LineTo { x: 0.0, y: mid_y },
3803 PathCommand::LineTo { x: ah, y: mid_y + ah },
3804 PathCommand::MoveTo { x: 0.0, y: mid_y },
3805 PathCommand::LineTo { x: width, y: mid_y },
3806 ]
3807 }
3808 "\\overleftrightarrow" | "\\underleftrightarrow"
3809 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3810 vec![
3811 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3812 PathCommand::LineTo { x: 0.0, y: mid_y },
3813 PathCommand::LineTo { x: ah, y: mid_y + ah },
3814 PathCommand::MoveTo { x: 0.0, y: mid_y },
3815 PathCommand::LineTo { x: width, y: mid_y },
3816 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3817 PathCommand::LineTo { x: width, y: mid_y },
3818 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3819 ]
3820 }
3821 "\\xlongequal" => {
3822 let gap = 0.04;
3823 vec![
3824 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3825 PathCommand::LineTo { x: width, y: mid_y - gap },
3826 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3827 PathCommand::LineTo { x: width, y: mid_y + gap },
3828 ]
3829 }
3830 "\\xhookleftarrow" => {
3831 vec![
3832 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3833 PathCommand::LineTo { x: 0.0, y: mid_y },
3834 PathCommand::LineTo { x: ah, y: mid_y + ah },
3835 PathCommand::MoveTo { x: 0.0, y: mid_y },
3836 PathCommand::LineTo { x: width, y: mid_y },
3837 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3838 ]
3839 }
3840 "\\xhookrightarrow" => {
3841 vec![
3842 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3843 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3844 PathCommand::LineTo { x: width, y: mid_y },
3845 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3846 PathCommand::LineTo { x: width, y: mid_y },
3847 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3848 ]
3849 }
3850 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3851 let right = label.contains("right");
3852 if right {
3853 vec![
3854 PathCommand::MoveTo { x: 0.0, y: mid_y },
3855 PathCommand::LineTo { x: width, y: mid_y },
3856 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3857 PathCommand::LineTo { x: width, y: mid_y },
3858 ]
3859 } else {
3860 vec![
3861 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3862 PathCommand::LineTo { x: 0.0, y: mid_y },
3863 PathCommand::LineTo { x: width, y: mid_y },
3864 ]
3865 }
3866 }
3867 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3868 let right = label.contains("right");
3869 if right {
3870 vec![
3871 PathCommand::MoveTo { x: 0.0, y: mid_y },
3872 PathCommand::LineTo { x: width, y: mid_y },
3873 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3874 PathCommand::LineTo { x: width, y: mid_y },
3875 ]
3876 } else {
3877 vec![
3878 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3879 PathCommand::LineTo { x: 0.0, y: mid_y },
3880 PathCommand::LineTo { x: width, y: mid_y },
3881 ]
3882 }
3883 }
3884 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3885 let gap = 0.06;
3886 vec![
3887 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3888 PathCommand::LineTo { x: width, y: mid_y - gap },
3889 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3890 PathCommand::LineTo { x: width, y: mid_y - gap },
3891 PathCommand::MoveTo { x: width, y: mid_y + gap },
3892 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3893 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3894 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3895 ]
3896 }
3897 "\\xtofrom" | "\\xrightleftarrows" => {
3898 let gap = 0.06;
3899 vec![
3900 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3901 PathCommand::LineTo { x: width, y: mid_y - gap },
3902 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3903 PathCommand::LineTo { x: width, y: mid_y - gap },
3904 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3905 PathCommand::MoveTo { x: width, y: mid_y + gap },
3906 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3907 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3908 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3909 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3910 ]
3911 }
3912 "\\overlinesegment" | "\\underlinesegment" => {
3913 vec![
3914 PathCommand::MoveTo { x: 0.0, y: mid_y },
3915 PathCommand::LineTo { x: width, y: mid_y },
3916 ]
3917 }
3918 _ => {
3919 vec![
3920 PathCommand::MoveTo { x: 0.0, y: mid_y },
3921 PathCommand::LineTo { x: width, y: mid_y },
3922 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3923 PathCommand::LineTo { x: width, y: mid_y },
3924 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3925 ]
3926 }
3927 }
3928}
3929
3930fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
3936 let h = inner.height;
3937 let d = inner.depth;
3938 let w = inner.width + pad_l + pad_r;
3939 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
3940 if pad_l > 0.0 {
3941 children.push(LayoutBox::new_kern(pad_l));
3942 }
3943 children.push(inner);
3944 if pad_r > 0.0 {
3945 children.push(LayoutBox::new_kern(pad_r));
3946 }
3947 LayoutBox {
3948 width: w,
3949 height: h,
3950 depth: d,
3951 content: BoxContent::HBox(children),
3952 color,
3953 }
3954}
3955
3956fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
3967 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
3968 LayoutBox {
3969 width: label.width,
3970 height: box_h,
3971 depth: box_d,
3972 content: BoxContent::RaiseBox {
3973 body: Box::new(label),
3974 shift,
3975 },
3976 color,
3977 }
3978}
3979
3980fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3984 let sup_style = options.style.superscript();
3985 let sup_opts = options.with_style(sup_style);
3986 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3987 let inner = layout_node(body, &sup_opts);
3988 if (sup_ratio - 1.0).abs() < 1e-6 {
3989 inner
3990 } else {
3991 LayoutBox {
3992 width: inner.width * sup_ratio,
3993 height: inner.height * sup_ratio,
3994 depth: inner.depth * sup_ratio,
3995 content: BoxContent::Scaled {
3996 body: Box::new(inner),
3997 child_scale: sup_ratio,
3998 },
3999 color: options.color,
4000 }
4001 }
4002}
4003
4004fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4010 let axis = options.metrics().axis_height;
4011 let depth = (total_height / 2.0 - axis).max(0.0);
4012 let height = total_height - depth;
4013 if let Some((commands, w)) =
4014 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4015 {
4016 return LayoutBox {
4017 width: w,
4018 height,
4019 depth,
4020 content: BoxContent::SvgPath {
4021 commands,
4022 fill: true,
4023 },
4024 color: options.color,
4025 };
4026 }
4027 if down {
4029 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4030 } else {
4031 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4032 }
4033}
4034
4035fn layout_cd_arrow(
4051 direction: &str,
4052 label_above: Option<&ParseNode>,
4053 label_below: Option<&ParseNode>,
4054 target_size: f64,
4055 target_col_width: f64,
4056 _target_depth: f64,
4057 options: &LayoutOptions,
4058) -> LayoutBox {
4059 let metrics = options.metrics();
4060 let axis = metrics.axis_height;
4061
4062 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4065
4066 match direction {
4067 "right" | "left" | "horiz_eq" => {
4068 let sup_style = options.style.superscript();
4070 let sub_style = options.style.subscript();
4071 let sup_opts = options.with_style(sup_style);
4072 let sub_opts = options.with_style(sub_style);
4073 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4074 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4075
4076 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4077 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4078
4079 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4080 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4081
4082 let path_label = if direction == "right" {
4084 "\\cdrightarrow"
4085 } else if direction == "left" {
4086 "\\cdleftarrow"
4087 } else {
4088 "\\cdlongequal"
4089 };
4090 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4091 const CD_LABEL_PAD_L: f64 = 0.22;
4094 const CD_LABEL_PAD_R: f64 = 0.48;
4095 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4096 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4097 let upper_need = above_box
4098 .as_ref()
4099 .map(|_| above_w + cd_pad_sup)
4100 .unwrap_or(0.0);
4101 let lower_need = below_box
4102 .as_ref()
4103 .map(|_| below_w + cd_pad_sub)
4104 .unwrap_or(0.0);
4105 let natural_w = upper_need.max(lower_need).max(0.0);
4106 let shaft_w = if target_size > 0.0 {
4107 target_size
4108 } else {
4109 natural_w.max(min_shaft_w)
4110 };
4111
4112 let (commands, actual_arrow_h, fill_arrow) =
4113 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4114 Some((c, h)) => (c, h, true),
4115 None => {
4116 let arrow_h = 0.3_f64;
4118 let ah = 0.12_f64;
4119 let cmds = if direction == "horiz_eq" {
4120 let gap = 0.06;
4121 vec![
4122 PathCommand::MoveTo { x: 0.0, y: -gap },
4123 PathCommand::LineTo { x: shaft_w, y: -gap },
4124 PathCommand::MoveTo { x: 0.0, y: gap },
4125 PathCommand::LineTo { x: shaft_w, y: gap },
4126 ]
4127 } else if direction == "right" {
4128 vec![
4129 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4130 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4131 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4132 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4133 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4134 ]
4135 } else {
4136 vec![
4137 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4138 PathCommand::LineTo { x: 0.0, y: 0.0 },
4139 PathCommand::MoveTo { x: ah, y: -ah },
4140 PathCommand::LineTo { x: 0.0, y: 0.0 },
4141 PathCommand::LineTo { x: ah, y: ah },
4142 ]
4143 };
4144 (cmds, arrow_h, false)
4145 }
4146 };
4147
4148 let arrow_half = actual_arrow_h / 2.0;
4150 let arrow_box = LayoutBox {
4151 width: shaft_w,
4152 height: arrow_half,
4153 depth: arrow_half,
4154 content: BoxContent::SvgPath {
4155 commands,
4156 fill: fill_arrow,
4157 },
4158 color: options.color,
4159 };
4160
4161 let gap = 0.111;
4163 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4164 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4165 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4169 sup_d
4170 } else {
4171 0.0
4172 };
4173 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4174 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4175 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4176 let depth = if below_box.is_some() {
4177 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4178 } else {
4179 (arrow_half - axis).max(0.0)
4180 };
4181
4182 let inner = LayoutBox {
4183 width: shaft_w,
4184 height,
4185 depth,
4186 content: BoxContent::OpLimits {
4187 base: Box::new(arrow_box),
4188 sup: above_box.map(Box::new),
4189 sub: below_box.map(Box::new),
4190 base_shift: -axis,
4191 sup_kern: gap,
4192 sub_kern: gap,
4193 slant: 0.0,
4194 sup_scale: sup_ratio,
4195 sub_scale: sub_ratio,
4196 },
4197 color: options.color,
4198 };
4199
4200 if target_col_width > inner.width + 1e-6 {
4204 let extra = target_col_width - inner.width;
4205 let kl = extra / 2.0;
4206 let kr = extra - kl;
4207 cd_wrap_hpad(inner, kl, kr, options.color)
4208 } else {
4209 inner
4210 }
4211 }
4212
4213 "down" | "up" | "vert_eq" => {
4214 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4218 "vert_eq" if target_size > 0.0 => {
4219 make_vert_delim_box(target_size.max(big_total), true, options)
4220 }
4221 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4222 "down" if target_size > 0.0 => {
4223 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4224 }
4225 "up" if target_size > 0.0 => {
4226 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4227 }
4228 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4229 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4230 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4231 };
4232 let box_h = shaft_box.height;
4233 let box_d = shaft_box.depth;
4234 let shaft_w = shaft_box.width;
4235
4236 let left_box = label_above.map(|n| {
4239 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4240 });
4241 let right_box = label_below.map(|n| {
4242 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4243 });
4244
4245 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4246 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4247 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4248 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4249 let inner_w = left_part + shaft_w + right_part;
4250
4251 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4253 let extra = target_col_width - inner_w;
4254 let kl = extra / 2.0;
4255 let kr = extra - kl;
4256 (kl, kr, target_col_width)
4257 } else {
4258 (0.0, 0.0, inner_w)
4259 };
4260
4261 let mut children: Vec<LayoutBox> = Vec::new();
4262 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4263 if let Some(lb) = left_box {
4264 children.push(lb);
4265 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4266 }
4267 children.push(shaft_box);
4268 if let Some(rb) = right_box {
4269 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4270 children.push(rb);
4271 }
4272 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4273
4274 LayoutBox {
4275 width: total_w,
4276 height: box_h,
4277 depth: box_d,
4278 content: BoxContent::HBox(children),
4279 color: options.color,
4280 }
4281 }
4282
4283 _ => LayoutBox::new_empty(),
4285 }
4286}
4287
4288fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4290 let metrics = options.metrics();
4291 let pt = 1.0 / metrics.pt_per_em;
4292 let baselineskip = 3.0 * metrics.x_height;
4294 let arstrut_h = 0.7 * baselineskip;
4295 let arstrut_d = 0.3 * baselineskip;
4296
4297 let num_rows = body.len();
4298 if num_rows == 0 {
4299 return LayoutBox::new_empty();
4300 }
4301 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4302 if num_cols == 0 {
4303 return LayoutBox::new_empty();
4304 }
4305
4306 let jot = 3.0 * pt;
4308
4309 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4311 let mut col_widths = vec![0.0_f64; num_cols];
4312 let mut row_heights = vec![arstrut_h; num_rows];
4313 let mut row_depths = vec![arstrut_d; num_rows];
4314
4315 for (r, row) in body.iter().enumerate() {
4316 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4317
4318 for (c, cell) in row.iter().enumerate() {
4319 let cbox = match cell {
4320 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4321 layout_cd_arrow(
4322 direction,
4323 label_above.as_deref(),
4324 label_below.as_deref(),
4325 0.0, 0.0, 0.0, options,
4329 )
4330 }
4331 ParseNode::OrdGroup { body: cell_body, .. } => {
4335 layout_expression(cell_body, options, false)
4336 }
4337 other => layout_node(other, options),
4338 };
4339
4340 row_heights[r] = row_heights[r].max(cbox.height);
4341 row_depths[r] = row_depths[r].max(cbox.depth);
4342 col_widths[c] = col_widths[c].max(cbox.width);
4343 row_boxes.push(cbox);
4344 }
4345
4346 while row_boxes.len() < num_cols {
4348 row_boxes.push(LayoutBox::new_empty());
4349 }
4350 cell_boxes.push(row_boxes);
4351 }
4352
4353 let col_target_w: Vec<f64> = col_widths.clone();
4357
4358 #[cfg(debug_assertions)]
4359 {
4360 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4361 for (r, row) in cell_boxes.iter().enumerate() {
4362 for (c, b) in row.iter().enumerate() {
4363 if b.width > 0.0 {
4364 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4365 }
4366 }
4367 }
4368 }
4369
4370 for (r, row) in body.iter().enumerate() {
4372 let is_arrow_row = r % 2 == 1;
4373 for (c, cell) in row.iter().enumerate() {
4374 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4375 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4376 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4377 let b = layout_cd_arrow(
4378 direction,
4379 label_above.as_deref(),
4380 label_below.as_deref(),
4381 cell_boxes[r][c].width,
4382 col_target_w[c],
4383 0.0,
4384 options,
4385 );
4386 let w = b.width;
4387 (b, w)
4388 } else if is_arrow_row && c % 2 == 0 {
4389 let v_span = row_heights[r] + row_depths[r];
4393 let b = layout_cd_arrow(
4394 direction,
4395 label_above.as_deref(),
4396 label_below.as_deref(),
4397 v_span,
4398 col_widths[c],
4399 0.0,
4400 options,
4401 );
4402 let w = b.width;
4403 (b, w)
4404 } else {
4405 continue;
4406 };
4407 col_widths[c] = col_widths[c].max(col_w);
4408 cell_boxes[r][c] = new_box;
4409 }
4410 }
4411 }
4412
4413 #[cfg(debug_assertions)]
4414 {
4415 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4416 }
4417
4418 for rd in &mut row_depths {
4421 *rd += jot;
4422 }
4423
4424 let col_gap = 0.5;
4429
4430 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4432
4433 let col_separators = vec![None; num_cols + 1];
4435
4436 let mut total_height = 0.0_f64;
4437 let mut row_positions = Vec::with_capacity(num_rows);
4438 for r in 0..num_rows {
4439 total_height += row_heights[r];
4440 row_positions.push(total_height);
4441 total_height += row_depths[r];
4442 }
4443
4444 let offset = total_height / 2.0 + metrics.axis_height;
4445 let height = offset;
4446 let depth = total_height - offset;
4447
4448 let total_width = col_widths.iter().sum::<f64>()
4450 + col_gap * (num_cols.saturating_sub(1)) as f64;
4451
4452 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4454
4455 LayoutBox {
4456 width: total_width,
4457 height,
4458 depth,
4459 content: BoxContent::Array {
4460 cells: cell_boxes,
4461 col_widths,
4462 col_aligns,
4463 row_heights,
4464 row_depths,
4465 col_gap,
4466 offset,
4467 content_x_offset: 0.0,
4468 col_separators,
4469 hlines_before_row,
4470 rule_thickness: 0.04 * pt,
4471 double_rule_sep: metrics.double_rule_sep,
4472 array_inner_width: total_width,
4473 tag_gap_em: 0.0,
4474 tag_col_width: 0.0,
4475 row_tags: (0..num_rows).map(|_| None).collect(),
4476 tags_left: false,
4477 },
4478 color: options.color,
4479 }
4480}
4481
4482fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4483 let mid = width / 2.0;
4484 let q = height * 0.6;
4485 if is_over {
4486 vec![
4487 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4488 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4489 PathCommand::LineTo { x: mid - 0.05, y: -q },
4490 PathCommand::LineTo { x: mid, y: -height },
4491 PathCommand::LineTo { x: mid + 0.05, y: -q },
4492 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4493 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4494 ]
4495 } else {
4496 vec![
4497 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4498 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4499 PathCommand::LineTo { x: mid - 0.05, y: q },
4500 PathCommand::LineTo { x: mid, y: height },
4501 PathCommand::LineTo { x: mid + 0.05, y: q },
4502 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4503 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4504 ]
4505 }
4506}