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 {
1811 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1812 if !is_below =>
1813 {
1814 if inner_accent.height <= 0.001 {
1818 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1824 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1825 katex_pos + correction
1826 } else {
1827 if label == "\\bar" || label == "\\=" {
1833 body_box.height
1834 } else {
1835 let inner_visual_top = inner_cl + 0.35_f64.min(inner_accent.height);
1840 let h_for_kern = if body_box.height > inner_visual_top + 0.002 {
1841 inner_visual_top
1842 } else {
1843 body_box.height
1844 };
1845 let katex_pos = (h_for_kern - options.metrics().x_height).max(0.0);
1846 let correction =
1847 (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1848 katex_pos + correction
1849 }
1850 }
1851 }
1852 _ => {
1853 if label == "\\bar" || label == "\\=" {
1866 body_box.height
1867 } else {
1868 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1869 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1870 katex_pos + correction
1871 }
1872 }
1873 };
1874 let base_clearance = base_clearance + accent_box.depth;
1879 if label == "\\bar" || label == "\\=" {
1880 (base_clearance - 0.12).max(0.0)
1881 } else {
1882 base_clearance
1883 }
1884 };
1885
1886 let (height, depth) = if is_below {
1887 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1888 } else if use_arrow_path {
1889 (body_box.height + gap + accent_box.height, body_box.depth)
1890 } else {
1891 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1898 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1899 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1900 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1901 } else {
1902 body_box.height.max(accent_visual_top)
1903 };
1904 (h, body_box.depth)
1905 };
1906
1907 LayoutBox {
1908 width: body_box.width,
1909 height,
1910 depth,
1911 content: BoxContent::Accent {
1912 base: Box::new(body_box),
1913 accent: Box::new(accent_box),
1914 clearance,
1915 skew,
1916 is_below,
1917 under_gap_em: 0.0,
1918 },
1919 color: options.color,
1920 }
1921}
1922
1923fn node_contains_middle(node: &ParseNode) -> bool {
1929 match node {
1930 ParseNode::Middle { .. } => true,
1931 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1932 body.iter().any(node_contains_middle)
1933 }
1934 ParseNode::SupSub { base, sup, sub, .. } => {
1935 base.as_deref().is_some_and(node_contains_middle)
1936 || sup.as_deref().is_some_and(node_contains_middle)
1937 || sub.as_deref().is_some_and(node_contains_middle)
1938 }
1939 ParseNode::GenFrac { numer, denom, .. } => {
1940 node_contains_middle(numer) || node_contains_middle(denom)
1941 }
1942 ParseNode::Sqrt { body, index, .. } => {
1943 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1944 }
1945 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1946 node_contains_middle(base)
1947 }
1948 ParseNode::Op { body, .. } => body
1949 .as_ref()
1950 .is_some_and(|b| b.iter().any(node_contains_middle)),
1951 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1952 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1953 ParseNode::Font { body, .. } => node_contains_middle(body),
1954 ParseNode::Text { body, .. }
1955 | ParseNode::Color { body, .. }
1956 | ParseNode::Styling { body, .. }
1957 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1958 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1959 node_contains_middle(body)
1960 }
1961 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1962 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1963 node_contains_middle(body)
1964 }
1965 ParseNode::Array { body, .. } => body
1966 .iter()
1967 .any(|row| row.iter().any(node_contains_middle)),
1968 ParseNode::Enclose { body, .. }
1969 | ParseNode::Lap { body, .. }
1970 | ParseNode::RaiseBox { body, .. }
1971 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1972 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1973 ParseNode::XArrow { body, below, .. } => {
1974 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1975 }
1976 ParseNode::CdArrow { label_above, label_below, .. } => {
1977 label_above.as_deref().is_some_and(node_contains_middle)
1978 || label_below.as_deref().is_some_and(node_contains_middle)
1979 }
1980 ParseNode::MathChoice {
1981 display,
1982 text,
1983 script,
1984 scriptscript,
1985 ..
1986 } => {
1987 display.iter().any(node_contains_middle)
1988 || text.iter().any(node_contains_middle)
1989 || script.iter().any(node_contains_middle)
1990 || scriptscript.iter().any(node_contains_middle)
1991 }
1992 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1993 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1994 _ => false,
1995 }
1996}
1997
1998fn body_contains_middle(nodes: &[ParseNode]) -> bool {
2000 nodes.iter().any(node_contains_middle)
2001}
2002
2003fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
2006 let m = options.metrics();
2007 if options.style.is_display() {
2008 m.delim1
2009 } else if matches!(
2010 options.style,
2011 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
2012 ) {
2013 options
2014 .with_style(MathStyle::Script)
2015 .metrics()
2016 .delim2
2017 } else {
2018 m.delim2
2019 }
2020}
2021
2022fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2024 let metrics = options.metrics();
2025 let inner_height = inner.height;
2026 let inner_depth = inner.depth;
2027 let axis = metrics.axis_height;
2028 let max_dist = (inner_height - axis).max(inner_depth + axis);
2029 let delim_factor = 901.0;
2030 let delim_extend = 5.0 / metrics.pt_per_em;
2031 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2032 from_formula.max(inner_height + inner_depth)
2034}
2035
2036fn layout_left_right(
2037 body: &[ParseNode],
2038 left_delim: &str,
2039 right_delim: &str,
2040 options: &LayoutOptions,
2041) -> LayoutBox {
2042 let (inner, total_height) = if body_contains_middle(body) {
2043 let opts_first = LayoutOptions {
2045 leftright_delim_height: None,
2046 ..options.clone()
2047 };
2048 let inner_first = layout_expression(body, &opts_first, true);
2049 let total_height = left_right_delim_total_height(&inner_first, options);
2050 let opts_second = LayoutOptions {
2052 leftright_delim_height: Some(total_height),
2053 ..options.clone()
2054 };
2055 let inner_second = layout_expression(body, &opts_second, true);
2056 (inner_second, total_height)
2057 } else {
2058 let inner = layout_expression(body, options, true);
2059 let total_height = left_right_delim_total_height(&inner, options);
2060 (inner, total_height)
2061 };
2062
2063 let inner_height = inner.height;
2064 let inner_depth = inner.depth;
2065
2066 let left_box = make_stretchy_delim(left_delim, total_height, options);
2067 let right_box = make_stretchy_delim(right_delim, total_height, options);
2068
2069 let width = left_box.width + inner.width + right_box.width;
2070 let height = left_box.height.max(right_box.height).max(inner_height);
2071 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2072
2073 LayoutBox {
2074 width,
2075 height,
2076 depth,
2077 content: BoxContent::LeftRight {
2078 left: Box::new(left_box),
2079 right: Box::new(right_box),
2080 inner: Box::new(inner),
2081 },
2082 color: options.color,
2083 }
2084}
2085
2086const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2087 FontId::MainRegular,
2088 FontId::Size1Regular,
2089 FontId::Size2Regular,
2090 FontId::Size3Regular,
2091 FontId::Size4Regular,
2092];
2093
2094fn normalize_delim(delim: &str) -> &str {
2096 match delim {
2097 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2098 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2099 _ => delim,
2100 }
2101}
2102
2103fn is_vert_delim(delim: &str) -> bool {
2105 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2106}
2107
2108fn is_double_vert_delim(delim: &str) -> bool {
2110 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2111}
2112
2113fn vert_repeat_piece_height(is_double: bool) -> f64 {
2115 let code = if is_double { 8741_u32 } else { 8739 };
2116 get_char_metrics(FontId::Size1Regular, code)
2117 .map(|m| m.height + m.depth)
2118 .unwrap_or(0.5)
2119}
2120
2121fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2123 let piece = vert_repeat_piece_height(is_double);
2124 let min_h = 2.0 * piece;
2125 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2126 let mut h = min_h + repeat_count * piece;
2127 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2131 h *= 1.135;
2132 }
2133 h
2134}
2135
2136fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2138 let neg = -mid_th;
2139 if !is_double {
2140 format!(
2141 "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"
2142 )
2143 } else {
2144 format!(
2145 "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"
2146 )
2147 }
2148}
2149
2150fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2151 let s = 0.001_f64;
2152 cmds.iter()
2153 .map(|c| match *c {
2154 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2155 x: x * s,
2156 y: y * s,
2157 },
2158 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2159 x: x * s,
2160 y: y * s,
2161 },
2162 PathCommand::CubicTo {
2163 x1,
2164 y1,
2165 x2,
2166 y2,
2167 x,
2168 y,
2169 } => PathCommand::CubicTo {
2170 x1: x1 * s,
2171 y1: y1 * s,
2172 x2: x2 * s,
2173 y2: y2 * s,
2174 x: x * s,
2175 y: y * s,
2176 },
2177 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2178 x1: x1 * s,
2179 y1: y1 * s,
2180 x: x * s,
2181 y: y * s,
2182 },
2183 PathCommand::Close => PathCommand::Close,
2184 })
2185 .collect()
2186}
2187
2188fn map_vert_path_y_to_baseline(
2190 cmds: Vec<PathCommand>,
2191 height: f64,
2192 depth: f64,
2193 view_box_height: i64,
2194) -> Vec<PathCommand> {
2195 let span_em = view_box_height as f64 / 1000.0;
2196 let total = height + depth;
2197 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2198 cmds.into_iter()
2199 .map(|c| match c {
2200 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2201 x,
2202 y: -height + y * scale_y,
2203 },
2204 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2205 x,
2206 y: -height + y * scale_y,
2207 },
2208 PathCommand::CubicTo {
2209 x1,
2210 y1,
2211 x2,
2212 y2,
2213 x,
2214 y,
2215 } => PathCommand::CubicTo {
2216 x1,
2217 y1: -height + y1 * scale_y,
2218 x2,
2219 y2: -height + y2 * scale_y,
2220 x,
2221 y: -height + y * scale_y,
2222 },
2223 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2224 x1,
2225 y1: -height + y1 * scale_y,
2226 x,
2227 y: -height + y * scale_y,
2228 },
2229 PathCommand::Close => PathCommand::Close,
2230 })
2231 .collect()
2232}
2233
2234fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2237 let real_h = katex_vert_real_height(total_height, is_double);
2238 let axis = options.metrics().axis_height;
2239 let depth = (real_h / 2.0 - axis).max(0.0);
2240 let height = real_h - depth;
2241 let width = if is_double { 0.556 } else { 0.333 };
2242
2243 let piece = vert_repeat_piece_height(is_double);
2244 let mid_em = (real_h - 2.0 * piece).max(0.0);
2245 let mid_th = (mid_em * 1000.0).round() as i64;
2246 let view_box_height = (real_h * 1000.0).round() as i64;
2247
2248 let d = tall_vert_svg_path_data(mid_th, is_double);
2249 let raw = parse_svg_path_data(&d);
2250 let scaled = scale_svg_path_to_em(&raw);
2251 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2252
2253 LayoutBox {
2254 width,
2255 height,
2256 depth,
2257 content: BoxContent::SvgPath { commands, fill: true },
2258 color: options.color,
2259 }
2260}
2261
2262fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2264 if delim == "." || delim.is_empty() {
2265 return LayoutBox::new_kern(0.0);
2266 }
2267
2268 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2273 return make_vert_delim_box(total_height, false, options);
2274 }
2275 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2276 return make_vert_delim_box(total_height, true, options);
2277 }
2278
2279 let delim = normalize_delim(delim);
2281
2282 let ch = resolve_symbol_char(delim, Mode::Math);
2283 let char_code = ch as u32;
2284
2285 let mut best_font = FontId::MainRegular;
2286 let mut best_w = 0.4;
2287 let mut best_h = 0.7;
2288 let mut best_d = 0.2;
2289
2290 for &font_id in &DELIM_FONT_SEQUENCE {
2291 if let Some(m) = get_char_metrics(font_id, char_code) {
2292 best_font = font_id;
2293 best_w = m.width;
2294 best_h = m.height;
2295 best_d = m.depth;
2296 if best_h + best_d >= total_height {
2297 break;
2298 }
2299 }
2300 }
2301
2302 let best_total = best_h + best_d;
2303 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2304 return stacked;
2305 }
2306
2307 LayoutBox {
2308 width: best_w,
2309 height: best_h,
2310 depth: best_d,
2311 content: BoxContent::Glyph {
2312 font_id: best_font,
2313 char_code,
2314 },
2315 color: options.color,
2316 }
2317}
2318
2319const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2321
2322fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2324 if delim == "." || delim.is_empty() {
2325 return LayoutBox::new_kern(0.0);
2326 }
2327
2328 if is_vert_delim(delim) {
2330 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2331 return make_vert_delim_box(total, false, options);
2332 }
2333 if is_double_vert_delim(delim) {
2334 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2335 return make_vert_delim_box(total, true, options);
2336 }
2337
2338 let delim = normalize_delim(delim);
2340
2341 let ch = resolve_symbol_char(delim, Mode::Math);
2342 let char_code = ch as u32;
2343
2344 let font_id = match size {
2345 1 => FontId::Size1Regular,
2346 2 => FontId::Size2Regular,
2347 3 => FontId::Size3Regular,
2348 4 => FontId::Size4Regular,
2349 _ => FontId::Size1Regular,
2350 };
2351
2352 let metrics = get_char_metrics(font_id, char_code);
2353 let (width, height, depth, actual_font) = match metrics {
2354 Some(m) => (m.width, m.height, m.depth, font_id),
2355 None => {
2356 let m = get_char_metrics(FontId::MainRegular, char_code);
2357 match m {
2358 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2359 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2360 }
2361 }
2362 };
2363
2364 LayoutBox {
2365 width,
2366 height,
2367 depth,
2368 content: BoxContent::Glyph {
2369 font_id: actual_font,
2370 char_code,
2371 },
2372 color: options.color,
2373 }
2374}
2375
2376#[allow(clippy::too_many_arguments)]
2381fn layout_array(
2382 body: &[Vec<ParseNode>],
2383 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2384 arraystretch: f64,
2385 add_jot: bool,
2386 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2387 hlines: &[Vec<bool>],
2388 col_sep_type: Option<&str>,
2389 hskip: bool,
2390 tags: Option<&[ArrayTag]>,
2391 _leqno: bool,
2392 options: &LayoutOptions,
2393) -> LayoutBox {
2394 let metrics = options.metrics();
2395 let pt = 1.0 / metrics.pt_per_em;
2396 let baselineskip = 12.0 * pt;
2397 let jot = 3.0 * pt;
2398 let arrayskip = arraystretch * baselineskip;
2399 let arstrut_h = 0.7 * arrayskip;
2400 let arstrut_d = 0.3 * arrayskip;
2401 const ALIGN_RELATION_MU: f64 = 3.0;
2404 let col_gap = match col_sep_type {
2405 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2406 Some("alignat") => 0.0,
2407 Some("small") => {
2408 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2411 / options.size_multiplier()
2412 }
2413 _ => 2.0 * 5.0 * pt, };
2415 let cell_options = match col_sep_type {
2416 Some("align") | Some("alignat") => LayoutOptions {
2417 align_relation_spacing: Some(ALIGN_RELATION_MU),
2418 ..options.clone()
2419 },
2420 _ => options.clone(),
2421 };
2422
2423 let num_rows = body.len();
2424 if num_rows == 0 {
2425 return LayoutBox::new_empty();
2426 }
2427
2428 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2429
2430 use ratex_parser::parse_node::AlignType;
2432 let col_aligns: Vec<u8> = {
2433 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2434 .map(|cs| {
2435 cs.iter()
2436 .filter(|s| matches!(s.align_type, AlignType::Align))
2437 .collect()
2438 })
2439 .unwrap_or_default();
2440 (0..num_cols)
2441 .map(|c| {
2442 align_specs
2443 .get(c)
2444 .and_then(|s| s.align.as_deref())
2445 .and_then(|a| a.bytes().next())
2446 .unwrap_or(b'c')
2447 })
2448 .collect()
2449 };
2450
2451 let col_separators: Vec<Option<bool>> = {
2454 let mut seps = vec![None; num_cols + 1];
2455 let mut align_count = 0usize;
2456 if let Some(cs) = cols {
2457 for spec in cs {
2458 match spec.align_type {
2459 AlignType::Align => align_count += 1,
2460 AlignType::Separator if spec.align.as_deref() == Some("|") => {
2461 if align_count <= num_cols {
2462 seps[align_count] = Some(false);
2463 }
2464 }
2465 AlignType::Separator if spec.align.as_deref() == Some(":") => {
2466 if align_count <= num_cols {
2467 seps[align_count] = Some(true);
2468 }
2469 }
2470 _ => {}
2471 }
2472 }
2473 }
2474 seps
2475 };
2476
2477 let rule_thickness = 0.4 * pt;
2478 let double_rule_sep = metrics.double_rule_sep;
2479
2480 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2482 let mut col_widths = vec![0.0_f64; num_cols];
2483 let mut row_heights = Vec::with_capacity(num_rows);
2484 let mut row_depths = Vec::with_capacity(num_rows);
2485
2486 for row in body {
2487 let mut row_boxes = Vec::with_capacity(num_cols);
2488 let mut rh = arstrut_h;
2489 let mut rd = arstrut_d;
2490
2491 for (c, cell) in row.iter().enumerate() {
2492 let cell_nodes = match cell {
2493 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2494 other => std::slice::from_ref(other),
2495 };
2496 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2497 rh = rh.max(cell_box.height);
2498 rd = rd.max(cell_box.depth);
2499 if c < num_cols {
2500 col_widths[c] = col_widths[c].max(cell_box.width);
2501 }
2502 row_boxes.push(cell_box);
2503 }
2504
2505 while row_boxes.len() < num_cols {
2507 row_boxes.push(LayoutBox::new_empty());
2508 }
2509
2510 if add_jot {
2511 rd += jot;
2512 }
2513
2514 row_heights.push(rh);
2515 row_depths.push(rd);
2516 cell_boxes.push(row_boxes);
2517 }
2518
2519 for (r, gap) in row_gaps.iter().enumerate() {
2521 if r < row_depths.len() {
2522 if let Some(m) = gap {
2523 let gap_em = measurement_to_em(m, options);
2524 if gap_em > 0.0 {
2525 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2526 }
2527 }
2528 }
2529 }
2530
2531 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2533 while hlines_before_row.len() < num_rows + 1 {
2534 hlines_before_row.push(vec![]);
2535 }
2536
2537 for r in 0..=num_rows {
2543 let n = hlines_before_row[r].len();
2544 if n > 1 {
2545 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2546 if r == 0 {
2547 if num_rows > 0 {
2548 row_heights[0] += extra;
2549 }
2550 } else {
2551 row_depths[r - 1] += extra;
2552 }
2553 }
2554 }
2555
2556 let mut total_height = 0.0;
2558 let mut row_positions = Vec::with_capacity(num_rows);
2559 for r in 0..num_rows {
2560 total_height += row_heights[r];
2561 row_positions.push(total_height);
2562 total_height += row_depths[r];
2563 }
2564
2565 let offset = total_height / 2.0 + metrics.axis_height;
2566
2567 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2569
2570 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2572 + col_gap * (num_cols.saturating_sub(1)) as f64
2573 + 2.0 * content_x_offset;
2574
2575 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2576 let mut tag_col_width = 0.0_f64;
2577 let text_opts = options.with_style(options.style.text());
2578 if let Some(tag_slice) = tags {
2579 if tag_slice.len() == num_rows {
2580 for (r, t) in tag_slice.iter().enumerate() {
2581 if let ArrayTag::Explicit(nodes) = t {
2582 if !nodes.is_empty() {
2583 let tb = layout_expression(nodes, &text_opts, true);
2584 tag_col_width = tag_col_width.max(tb.width);
2585 row_tag_boxes[r] = Some(tb);
2586 }
2587 }
2588 }
2589 }
2590 }
2591 let tag_gap_em = if tag_col_width > 0.0 {
2592 text_opts.metrics().quad
2593 } else {
2594 0.0
2595 };
2596 let tags_left = false;
2598
2599 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2600
2601 let height = offset;
2602 let depth = total_height - offset;
2603
2604 LayoutBox {
2605 width: total_width,
2606 height,
2607 depth,
2608 content: BoxContent::Array {
2609 cells: cell_boxes,
2610 col_widths: col_widths.clone(),
2611 col_aligns,
2612 row_heights: row_heights.clone(),
2613 row_depths: row_depths.clone(),
2614 col_gap,
2615 offset,
2616 content_x_offset,
2617 col_separators,
2618 hlines_before_row,
2619 rule_thickness,
2620 double_rule_sep,
2621 array_inner_width,
2622 tag_gap_em,
2623 tag_col_width,
2624 row_tags: row_tag_boxes,
2625 tags_left,
2626 },
2627 color: options.color,
2628 }
2629}
2630
2631fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2636 let multiplier = match size {
2638 1 => 0.5,
2639 2 => 0.6,
2640 3 => 0.7,
2641 4 => 0.8,
2642 5 => 0.9,
2643 6 => 1.0,
2644 7 => 1.2,
2645 8 => 1.44,
2646 9 => 1.728,
2647 10 => 2.074,
2648 11 => 2.488,
2649 _ => 1.0,
2650 };
2651
2652 let inner_opts = options.with_style(options.style.text());
2654 let inner = layout_expression(body, &inner_opts, true);
2655 let ratio = multiplier / options.size_multiplier();
2656 if (ratio - 1.0).abs() < 0.001 {
2657 inner
2658 } else {
2659 LayoutBox {
2660 width: inner.width * ratio,
2661 height: inner.height * ratio,
2662 depth: inner.depth * ratio,
2663 content: BoxContent::Scaled {
2664 body: Box::new(inner),
2665 child_scale: ratio,
2666 },
2667 color: options.color,
2668 }
2669 }
2670}
2671
2672fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2675 let metrics = options.metrics();
2676 let mut children = Vec::new();
2677 for c in body.chars() {
2678 let ch = if star && c == ' ' {
2679 '\u{2423}' } else {
2681 c
2682 };
2683 let code = ch as u32;
2684 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2685 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2686 None => match get_char_metrics(FontId::MainRegular, code) {
2687 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2688 None => (
2689 FontId::TypewriterRegular,
2690 0.5,
2691 metrics.x_height,
2692 0.0,
2693 ),
2694 },
2695 };
2696 children.push(LayoutBox {
2697 width: w,
2698 height: h,
2699 depth: d,
2700 content: BoxContent::Glyph {
2701 font_id,
2702 char_code: code,
2703 },
2704 color: options.color,
2705 });
2706 }
2707 let mut hbox = make_hbox(children);
2708 hbox.color = options.color;
2709 hbox
2710}
2711
2712fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2720 let mut children = Vec::new();
2721 for node in body {
2722 match node {
2723 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2724 children.push(layout_symbol(text, *mode, options));
2725 }
2726 ParseNode::SpacingNode { text, .. } => {
2727 children.push(layout_spacing_command(text, options));
2728 }
2729 _ => {
2730 children.push(layout_node(node, options));
2731 }
2732 }
2733 }
2734 make_hbox(children)
2735}
2736
2737fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2740 let base = layout_expression(body, options, true);
2741 let w = base.width;
2742 let h = base.height;
2743 let d = base.depth;
2744
2745 let shadow = layout_expression(body, options, true);
2747 let shadow_shift_x = 0.02_f64;
2748 let _shadow_shift_y = 0.01_f64;
2749
2750 let kern_back = LayoutBox::new_kern(-w);
2754 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2755
2756 let children = vec![
2763 kern_x,
2764 shadow,
2765 kern_back,
2766 base,
2767 ];
2768 let hbox = make_hbox(children);
2770 LayoutBox {
2772 width: w,
2773 height: h,
2774 depth: d,
2775 content: hbox.content,
2776 color: options.color,
2777 }
2778}
2779
2780fn layout_enclose(
2783 label: &str,
2784 background_color: Option<&str>,
2785 border_color: Option<&str>,
2786 body: &ParseNode,
2787 options: &LayoutOptions,
2788) -> LayoutBox {
2789 use crate::layout_box::BoxContent;
2790 use ratex_types::color::Color;
2791
2792 if label == "\\phase" {
2794 return layout_phase(body, options);
2795 }
2796
2797 if label == "\\angl" {
2799 return layout_angl(body, options);
2800 }
2801
2802 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2804 return layout_cancel(label, body, options);
2805 }
2806
2807 let metrics = options.metrics();
2809 let padding = 3.0 / metrics.pt_per_em;
2810 let border_thickness = 0.4 / metrics.pt_per_em;
2811
2812 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2813
2814 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2815 let border = border_color
2816 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2817 .unwrap_or(Color::BLACK);
2818
2819 let inner = layout_node(body, options);
2820 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2821
2822 let width = inner.width + 2.0 * outer_pad;
2823 let height = inner.height + outer_pad;
2824 let depth = inner.depth + outer_pad;
2825
2826 LayoutBox {
2827 width,
2828 height,
2829 depth,
2830 content: BoxContent::Framed {
2831 body: Box::new(inner),
2832 padding,
2833 border_thickness,
2834 has_border,
2835 bg_color: bg,
2836 border_color: border,
2837 },
2838 color: options.color,
2839 }
2840}
2841
2842fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2844 use crate::layout_box::BoxContent;
2845 let inner = layout_node(body, options);
2846 let height = inner.height + shift;
2848 let depth = (inner.depth - shift).max(0.0);
2849 let width = inner.width;
2850 LayoutBox {
2851 width,
2852 height,
2853 depth,
2854 content: BoxContent::RaiseBox {
2855 body: Box::new(inner),
2856 shift,
2857 },
2858 color: options.color,
2859 }
2860}
2861
2862fn is_single_char_body(node: &ParseNode) -> bool {
2865 use ratex_parser::parse_node::ParseNode as PN;
2866 match node {
2867 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2869 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2870 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2872 _ => false,
2873 }
2874}
2875
2876fn layout_cancel(
2882 label: &str,
2883 body: &ParseNode,
2884 options: &LayoutOptions,
2885) -> LayoutBox {
2886 use crate::layout_box::BoxContent;
2887 let inner = layout_node(body, options);
2888 let w = inner.width.max(0.01);
2889 let h = inner.height;
2890 let d = inner.depth;
2891
2892 let single = is_single_char_body(body);
2895 let (v_pad, h_pad) = if label == "\\sout" {
2896 (0.0, 0.0)
2897 } else if single {
2898 (0.2, 0.0)
2899 } else {
2900 (0.0, 0.2)
2901 };
2902
2903 let commands: Vec<PathCommand> = match label {
2907 "\\cancel" => vec![
2908 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2911 "\\bcancel" => vec![
2912 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2915 "\\xcancel" => vec![
2916 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2917 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2918 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2919 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2920 ],
2921 "\\sout" => {
2922 let mid_y = -0.5 * options.metrics().x_height;
2924 vec![
2925 PathCommand::MoveTo { x: 0.0, y: mid_y },
2926 PathCommand::LineTo { x: w, y: mid_y },
2927 ]
2928 }
2929 _ => vec![],
2930 };
2931
2932 let line_w = w + 2.0 * h_pad;
2933 let line_h = h + v_pad;
2934 let line_d = d + v_pad;
2935 let line_box = LayoutBox {
2936 width: line_w,
2937 height: line_h,
2938 depth: line_d,
2939 content: BoxContent::SvgPath { commands, fill: false },
2940 color: options.color,
2941 };
2942
2943 let body_kern = -(line_w - h_pad);
2945 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2946 LayoutBox {
2947 width: w,
2948 height: h,
2949 depth: d,
2950 content: BoxContent::HBox(vec![line_box, body_shifted]),
2951 color: options.color,
2952 }
2953}
2954
2955fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2958 use crate::layout_box::BoxContent;
2959 let metrics = options.metrics();
2960 let inner = layout_node(body, options);
2961 let line_weight = 0.6_f64 / metrics.pt_per_em;
2963 let clearance = 0.35_f64 * metrics.x_height;
2964 let angle_height = inner.height + inner.depth + line_weight + clearance;
2965 let left_pad = angle_height / 2.0 + line_weight;
2966 let width = inner.width + left_pad;
2967
2968 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2970
2971 let sy = angle_height / y_svg;
2973 let sx = sy;
2976 let right_x = (400_000.0_f64 * sx).min(width);
2977
2978 let bottom_y = inner.depth + line_weight + clearance;
2980 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2981
2982 let x_peak = y_svg / 2.0;
2984 let commands = vec![
2985 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2986 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2987 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2988 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2989 PathCommand::LineTo {
2990 x: 145.0 * sx,
2991 y: vy(y_svg - 80.0),
2992 },
2993 PathCommand::LineTo {
2994 x: right_x,
2995 y: vy(y_svg - 80.0),
2996 },
2997 PathCommand::Close,
2998 ];
2999
3000 let body_shifted = make_hbox(vec![
3001 LayoutBox::new_kern(left_pad),
3002 inner.clone(),
3003 ]);
3004
3005 let path_height = inner.height;
3006 let path_depth = bottom_y;
3007
3008 LayoutBox {
3009 width,
3010 height: path_height,
3011 depth: path_depth,
3012 content: BoxContent::HBox(vec![
3013 LayoutBox {
3014 width,
3015 height: path_height,
3016 depth: path_depth,
3017 content: BoxContent::SvgPath { commands, fill: true },
3018 color: options.color,
3019 },
3020 LayoutBox::new_kern(-width),
3021 body_shifted,
3022 ]),
3023 color: options.color,
3024 }
3025}
3026
3027fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3030 use crate::layout_box::BoxContent;
3031 let inner = layout_node(body, options);
3032 let w = inner.width.max(0.3);
3033 let clearance = 0.1_f64;
3035 let arc_h = inner.height + clearance;
3036
3037 let path_commands = vec![
3039 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3040 PathCommand::LineTo { x: w, y: -arc_h },
3041 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
3042 ];
3043
3044 let height = arc_h;
3045 LayoutBox {
3046 width: w,
3047 height,
3048 depth: inner.depth,
3049 content: BoxContent::Angl {
3050 path_commands,
3051 body: Box::new(inner),
3052 },
3053 color: options.color,
3054 }
3055}
3056
3057fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3058 let font_id = match font {
3059 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3060 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3061 "mathit" | "\\mathit" | "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3062 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3063 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3064 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3065 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3066 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3067 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3068 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3069 _ => None,
3070 };
3071
3072 if let Some(fid) = font_id {
3073 layout_with_font(body, fid, options)
3074 } else {
3075 layout_node(body, options)
3076 }
3077}
3078
3079fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3080 match node {
3081 ParseNode::OrdGroup { body, .. } => {
3082 let kern = options.inter_glyph_kern_em;
3083 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3084 for (i, n) in body.iter().enumerate() {
3085 if i > 0 && kern > 0.0 {
3086 children.push(LayoutBox::new_kern(kern));
3087 }
3088 children.push(layout_with_font(n, font_id, options));
3089 }
3090 make_hbox(children)
3091 }
3092 ParseNode::SupSub {
3093 base, sup, sub, ..
3094 } => {
3095 if let Some(base_node) = base.as_deref() {
3096 if should_use_op_limits(base_node, options) {
3097 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3098 }
3099 }
3100 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3101 }
3102 ParseNode::MathOrd { text, mode, .. }
3103 | ParseNode::TextOrd { text, mode, .. }
3104 | ParseNode::Atom { text, mode, .. } => {
3105 let ch = resolve_symbol_char(text, *mode);
3106 let char_code = ch as u32;
3107 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3108 .map(|(_, m)| m)
3109 .unwrap_or(char_code);
3110 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3111 LayoutBox {
3112 width: math_glyph_advance_em(&m, *mode),
3114 height: m.height,
3115 depth: m.depth,
3116 content: BoxContent::Glyph { font_id, char_code },
3117 color: options.color,
3118 }
3119 } else {
3120 layout_node(node, options)
3122 }
3123 }
3124 _ => layout_node(node, options),
3125 }
3126}
3127
3128fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3133 let cramped = options.with_style(options.style.cramped());
3134 let body_box = layout_node(body, &cramped);
3135 let metrics = options.metrics();
3136 let rule = metrics.default_rule_thickness;
3137
3138 let height = body_box.height + 3.0 * rule;
3140 LayoutBox {
3141 width: body_box.width,
3142 height,
3143 depth: body_box.depth,
3144 content: BoxContent::Overline {
3145 body: Box::new(body_box),
3146 rule_thickness: rule,
3147 },
3148 color: options.color,
3149 }
3150}
3151
3152fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3153 let body_box = layout_node(body, options);
3154 let metrics = options.metrics();
3155 let rule = metrics.default_rule_thickness;
3156
3157 let depth = body_box.depth + 3.0 * rule;
3159 LayoutBox {
3160 width: body_box.width,
3161 height: body_box.height,
3162 depth,
3163 content: BoxContent::Underline {
3164 body: Box::new(body_box),
3165 rule_thickness: rule,
3166 },
3167 color: options.color,
3168 }
3169}
3170
3171fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3173 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3174 let body_opts = options
3176 .with_color(link_color)
3177 .with_inter_glyph_kern(0.024);
3178 let body_box = layout_expression(body, &body_opts, true);
3179 layout_underline_laid_out(body_box, options, link_color)
3180}
3181
3182fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3184 let metrics = options.metrics();
3185 let rule = metrics.default_rule_thickness;
3186 let depth = body_box.depth + 3.0 * rule;
3187 LayoutBox {
3188 width: body_box.width,
3189 height: body_box.height,
3190 depth,
3191 content: BoxContent::Underline {
3192 body: Box::new(body_box),
3193 rule_thickness: rule,
3194 },
3195 color,
3196 }
3197}
3198
3199fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3204 let metrics = options.metrics();
3205 let mu = metrics.css_em_per_mu();
3206
3207 let width = match text {
3208 "\\," | "\\thinspace" => 3.0 * mu,
3209 "\\:" | "\\medspace" => 4.0 * mu,
3210 "\\;" | "\\thickspace" => 5.0 * mu,
3211 "\\!" | "\\negthinspace" => -3.0 * mu,
3212 "\\negmedspace" => -4.0 * mu,
3213 "\\negthickspace" => -5.0 * mu,
3214 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3215 get_char_metrics(FontId::MainRegular, 160)
3219 .map(|m| m.width)
3220 .unwrap_or(0.25)
3221 }
3222 "\\quad" => metrics.quad,
3223 "\\qquad" => 2.0 * metrics.quad,
3224 "\\enspace" => metrics.quad / 2.0,
3225 _ => 0.0,
3226 };
3227
3228 LayoutBox::new_kern(width)
3229}
3230
3231fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3236 let metrics = options.metrics();
3237 match m.unit.as_str() {
3238 "em" => m.number,
3239 "ex" => m.number * metrics.x_height,
3240 "mu" => m.number * metrics.css_em_per_mu(),
3241 "pt" => m.number / metrics.pt_per_em,
3242 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3243 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3244 "in" => m.number * 72.27 / metrics.pt_per_em,
3245 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3246 "pc" => m.number * 12.0 / metrics.pt_per_em,
3247 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3248 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3249 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3250 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3251 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3252 _ => m.number,
3253 }
3254}
3255
3256fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3262 match node {
3263 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3264 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3265 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3266 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3267 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3269 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3270 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3271 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3272 }
3273 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3274 ParseNode::SupSub { base, .. } => {
3275 base.as_ref().and_then(|b| node_math_class(b))
3276 }
3277 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3278 ParseNode::SpacingNode { .. } => None,
3279 ParseNode::Kern { .. } => None,
3280 ParseNode::HtmlMathMl { html, .. } => {
3281 for child in html {
3283 if let Some(cls) = node_math_class(child) {
3284 return Some(cls);
3285 }
3286 }
3287 None
3288 }
3289 ParseNode::Lap { .. } => None,
3290 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3291 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3292 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3294 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3296 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3297 ParseNode::Middle { .. } => Some(MathClass::Ord),
3298 _ => Some(MathClass::Ord),
3299 }
3300}
3301
3302fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3303 match mclass {
3304 "mord" => MathClass::Ord,
3305 "mop" => MathClass::Op,
3306 "mbin" => MathClass::Bin,
3307 "mrel" => MathClass::Rel,
3308 "mopen" => MathClass::Open,
3309 "mclose" => MathClass::Close,
3310 "mpunct" => MathClass::Punct,
3311 "minner" => MathClass::Inner,
3312 _ => MathClass::Ord,
3313 }
3314}
3315
3316fn get_base_elem(node: &ParseNode) -> &ParseNode {
3320 match node {
3321 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3322 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3323 ParseNode::Font { body, .. } => get_base_elem(body),
3324 _ => node,
3325 }
3326}
3327
3328fn is_character_box(node: &ParseNode) -> bool {
3329 matches!(
3330 get_base_elem(node),
3331 ParseNode::MathOrd { .. }
3332 | ParseNode::TextOrd { .. }
3333 | ParseNode::Atom { .. }
3334 | ParseNode::AccentToken { .. }
3335 )
3336}
3337
3338fn family_to_math_class(family: AtomFamily) -> MathClass {
3339 match family {
3340 AtomFamily::Bin => MathClass::Bin,
3341 AtomFamily::Rel => MathClass::Rel,
3342 AtomFamily::Open => MathClass::Open,
3343 AtomFamily::Close => MathClass::Close,
3344 AtomFamily::Punct => MathClass::Punct,
3345 AtomFamily::Inner => MathClass::Inner,
3346 }
3347}
3348
3349fn layout_horiz_brace(
3354 base: &ParseNode,
3355 is_over: bool,
3356 func_label: &str,
3357 options: &LayoutOptions,
3358) -> LayoutBox {
3359 let body_box = layout_node(base, options);
3360 let w = body_box.width.max(0.5);
3361
3362 let is_bracket = func_label
3363 .trim_start_matches('\\')
3364 .ends_with("bracket");
3365
3366 let stretch_key = if is_bracket {
3368 if is_over {
3369 "overbracket"
3370 } else {
3371 "underbracket"
3372 }
3373 } else if is_over {
3374 "overbrace"
3375 } else {
3376 "underbrace"
3377 };
3378
3379 let (raw_commands, brace_h, brace_fill) =
3380 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3381 Some((c, h)) => (c, h, true),
3382 None => {
3383 let h = 0.35_f64;
3384 (horiz_brace_path(w, h, is_over), h, false)
3385 }
3386 };
3387
3388 let y_shift = brace_h / 2.0;
3394 let commands = shift_path_y(raw_commands, y_shift);
3395
3396 let brace_box = LayoutBox {
3397 width: w,
3398 height: 0.0,
3399 depth: brace_h,
3400 content: BoxContent::SvgPath {
3401 commands,
3402 fill: brace_fill,
3403 },
3404 color: options.color,
3405 };
3406
3407 let gap = 0.1;
3408 let (height, depth) = if is_over {
3409 (body_box.height + brace_h + gap, body_box.depth)
3410 } else {
3411 (body_box.height, body_box.depth + brace_h + gap)
3412 };
3413
3414 let clearance = if is_over {
3415 height - brace_h
3416 } else {
3417 body_box.height + body_box.depth + gap
3418 };
3419 let total_w = body_box.width;
3420
3421 LayoutBox {
3422 width: total_w,
3423 height,
3424 depth,
3425 content: BoxContent::Accent {
3426 base: Box::new(body_box),
3427 accent: Box::new(brace_box),
3428 clearance,
3429 skew: 0.0,
3430 is_below: !is_over,
3431 under_gap_em: 0.0,
3432 },
3433 color: options.color,
3434 }
3435}
3436
3437fn layout_xarrow(
3442 label: &str,
3443 body: &ParseNode,
3444 below: Option<&ParseNode>,
3445 options: &LayoutOptions,
3446) -> LayoutBox {
3447 let sup_style = options.style.superscript();
3448 let sub_style = options.style.subscript();
3449 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3450 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3451
3452 let sup_opts = options.with_style(sup_style);
3453 let body_box = layout_node(body, &sup_opts);
3454 let body_w = body_box.width * sup_ratio;
3455
3456 let below_box = below.map(|b| {
3457 let sub_opts = options.with_style(sub_style);
3458 layout_node(b, &sub_opts)
3459 });
3460 let below_w = below_box
3461 .as_ref()
3462 .map(|b| b.width * sub_ratio)
3463 .unwrap_or(0.0);
3464
3465 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3468 let upper_w = body_w + sup_ratio;
3469 let lower_w = if below_box.is_some() {
3470 below_w + sub_ratio
3471 } else {
3472 0.0
3473 };
3474 let arrow_w = upper_w.max(lower_w).max(min_w);
3475 let arrow_h = 0.3;
3476
3477 let (commands, actual_arrow_h, fill_arrow) =
3478 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3479 Some((c, h)) => (c, h, true),
3480 None => (
3481 stretchy_accent_path(label, arrow_w, arrow_h),
3482 arrow_h,
3483 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3484 ),
3485 };
3486 let arrow_box = LayoutBox {
3487 width: arrow_w,
3488 height: actual_arrow_h / 2.0,
3489 depth: actual_arrow_h / 2.0,
3490 content: BoxContent::SvgPath {
3491 commands,
3492 fill: fill_arrow,
3493 },
3494 color: options.color,
3495 };
3496
3497 let metrics = options.metrics();
3500 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3502 let gap = 0.111; let base_shift = -axis;
3506
3507 let sup_kern = gap;
3515 let sub_kern = gap;
3516
3517 let sup_h = body_box.height * sup_ratio;
3518 let sup_d = body_box.depth * sup_ratio;
3519
3520 let height = axis + arrow_half + gap + sup_h + sup_d;
3522 let mut depth = (arrow_half - axis).max(0.0);
3524
3525 if let Some(ref bel) = below_box {
3526 let sub_h = bel.height * sub_ratio;
3527 let sub_d = bel.depth * sub_ratio;
3528 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3530 }
3531
3532 LayoutBox {
3533 width: arrow_w,
3534 height,
3535 depth,
3536 content: BoxContent::OpLimits {
3537 base: Box::new(arrow_box),
3538 sup: Some(Box::new(body_box)),
3539 sub: below_box.map(Box::new),
3540 base_shift,
3541 sup_kern,
3542 sub_kern,
3543 slant: 0.0,
3544 sup_scale: sup_ratio,
3545 sub_scale: sub_ratio,
3546 },
3547 color: options.color,
3548 }
3549}
3550
3551fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3556 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3559 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3560 let diameter = radius * 2.0;
3561
3562 let cx = radius;
3564 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3567
3568 let circle_commands = vec![
3569 PathCommand::MoveTo { x: cx + r, y: cy },
3570 PathCommand::CubicTo {
3571 x1: cx + r, y1: cy - k * r,
3572 x2: cx + k * r, y2: cy - r,
3573 x: cx, y: cy - r,
3574 },
3575 PathCommand::CubicTo {
3576 x1: cx - k * r, y1: cy - r,
3577 x2: cx - r, y2: cy - k * r,
3578 x: cx - r, y: cy,
3579 },
3580 PathCommand::CubicTo {
3581 x1: cx - r, y1: cy + k * r,
3582 x2: cx - k * r, y2: cy + r,
3583 x: cx, y: cy + r,
3584 },
3585 PathCommand::CubicTo {
3586 x1: cx + k * r, y1: cy + r,
3587 x2: cx + r, y2: cy + k * r,
3588 x: cx + r, y: cy,
3589 },
3590 PathCommand::Close,
3591 ];
3592
3593 let circle_box = LayoutBox {
3594 width: diameter,
3595 height: r - cy.min(0.0),
3596 depth: (r + cy).max(0.0),
3597 content: BoxContent::SvgPath {
3598 commands: circle_commands,
3599 fill: false,
3600 },
3601 color: options.color,
3602 };
3603
3604 let content_shift = (diameter - body_box.width) / 2.0;
3606 let children = vec![
3608 circle_box,
3609 LayoutBox::new_kern(-(diameter) + content_shift),
3610 body_box.clone(),
3611 ];
3612
3613 let height = r - cy.min(0.0);
3614 let depth = (r + cy).max(0.0);
3615
3616 LayoutBox {
3617 width: diameter,
3618 height,
3619 depth,
3620 content: BoxContent::HBox(children),
3621 color: options.color,
3622 }
3623}
3624
3625fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3649 let r: f64 = 0.1125;
3651 let cy: f64 = -0.2625;
3655 let k: f64 = 0.5523;
3657 let cx: f64 = r;
3659
3660 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3663
3664 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3673 vec![
3674 PathCommand::MoveTo { x: ox + rad, y: cy },
3675 PathCommand::CubicTo {
3676 x1: ox + rad, y1: cy - k * rad,
3677 x2: ox + k * rad, y2: cy - rad,
3678 x: ox, y: cy - rad,
3679 },
3680 PathCommand::CubicTo {
3681 x1: ox - k * rad, y1: cy - rad,
3682 x2: ox - rad, y2: cy - k * rad,
3683 x: ox - rad, y: cy,
3684 },
3685 PathCommand::CubicTo {
3686 x1: ox - rad, y1: cy + k * rad,
3687 x2: ox - k * rad, y2: cy + rad,
3688 x: ox, y: cy + rad,
3689 },
3690 PathCommand::CubicTo {
3691 x1: ox + k * rad, y1: cy + rad,
3692 x2: ox + rad, y2: cy + k * rad,
3693 x: ox + rad, y: cy,
3694 },
3695 PathCommand::Close,
3696 ]
3697 };
3698
3699 let disk = LayoutBox {
3700 width: 2.0 * r,
3701 height: h,
3702 depth: d,
3703 content: BoxContent::SvgPath {
3704 commands: circle_commands(cx, r),
3705 fill: true,
3706 },
3707 color: options.color,
3708 };
3709
3710 let ring = LayoutBox {
3711 width: 2.0 * r,
3712 height: h,
3713 depth: d,
3714 content: BoxContent::SvgPath {
3715 commands: circle_commands(cx, r_ring),
3716 fill: false,
3717 },
3718 color: options.color,
3719 };
3720
3721 let bar_len: f64 = 0.25;
3725 let bar_th: f64 = 0.04;
3726 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3729
3730 let children = if imageof {
3731 vec![disk, bar, ring]
3732 } else {
3733 vec![ring, bar, disk]
3734 };
3735
3736 let total_width = 4.0 * r + bar_len;
3738 LayoutBox {
3739 width: total_width,
3740 height: h,
3741 depth: d,
3742 content: BoxContent::HBox(children),
3743 color: options.color,
3744 }
3745}
3746
3747fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3751 let cx = width / 2.0;
3752 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3757 PathCommand::MoveTo { x: cx + a, y: cy },
3758 PathCommand::CubicTo {
3759 x1: cx + a,
3760 y1: cy - k * b,
3761 x2: cx + k * a,
3762 y2: cy - b,
3763 x: cx,
3764 y: cy - b,
3765 },
3766 PathCommand::CubicTo {
3767 x1: cx - k * a,
3768 y1: cy - b,
3769 x2: cx - a,
3770 y2: cy - k * b,
3771 x: cx - a,
3772 y: cy,
3773 },
3774 PathCommand::CubicTo {
3775 x1: cx - a,
3776 y1: cy + k * b,
3777 x2: cx - k * a,
3778 y2: cy + b,
3779 x: cx,
3780 y: cy + b,
3781 },
3782 PathCommand::CubicTo {
3783 x1: cx + k * a,
3784 y1: cy + b,
3785 x2: cx + a,
3786 y2: cy + k * b,
3787 x: cx + a,
3788 y: cy,
3789 },
3790 PathCommand::Close,
3791 ]
3792}
3793
3794fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3795 cmds.into_iter().map(|c| match c {
3796 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3797 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3798 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3799 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3800 },
3801 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3802 x1, y1: y1 + dy, x, y: y + dy,
3803 },
3804 PathCommand::Close => PathCommand::Close,
3805 }).collect()
3806}
3807
3808fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3809 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3810 return commands;
3811 }
3812 let ah = height * 0.35; let mid_y = -height / 2.0;
3814
3815 match label {
3816 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3817 vec![
3818 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3819 PathCommand::LineTo { x: 0.0, y: mid_y },
3820 PathCommand::LineTo { x: ah, y: mid_y + ah },
3821 PathCommand::MoveTo { x: 0.0, y: mid_y },
3822 PathCommand::LineTo { x: width, y: mid_y },
3823 ]
3824 }
3825 "\\overleftrightarrow" | "\\underleftrightarrow"
3826 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3827 vec![
3828 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3829 PathCommand::LineTo { x: 0.0, y: mid_y },
3830 PathCommand::LineTo { x: ah, y: mid_y + ah },
3831 PathCommand::MoveTo { x: 0.0, y: mid_y },
3832 PathCommand::LineTo { x: width, y: mid_y },
3833 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3834 PathCommand::LineTo { x: width, y: mid_y },
3835 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3836 ]
3837 }
3838 "\\xlongequal" => {
3839 let gap = 0.04;
3840 vec![
3841 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3842 PathCommand::LineTo { x: width, y: mid_y - gap },
3843 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3844 PathCommand::LineTo { x: width, y: mid_y + gap },
3845 ]
3846 }
3847 "\\xhookleftarrow" => {
3848 vec![
3849 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3850 PathCommand::LineTo { x: 0.0, y: mid_y },
3851 PathCommand::LineTo { x: ah, y: mid_y + ah },
3852 PathCommand::MoveTo { x: 0.0, y: mid_y },
3853 PathCommand::LineTo { x: width, y: mid_y },
3854 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3855 ]
3856 }
3857 "\\xhookrightarrow" => {
3858 vec![
3859 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3860 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3861 PathCommand::LineTo { x: width, y: mid_y },
3862 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3863 PathCommand::LineTo { x: width, y: mid_y },
3864 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3865 ]
3866 }
3867 "\\xrightharpoonup" | "\\xleftharpoonup" => {
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 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3885 let right = label.contains("right");
3886 if right {
3887 vec![
3888 PathCommand::MoveTo { x: 0.0, y: mid_y },
3889 PathCommand::LineTo { x: width, y: mid_y },
3890 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3891 PathCommand::LineTo { x: width, y: mid_y },
3892 ]
3893 } else {
3894 vec![
3895 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3896 PathCommand::LineTo { x: 0.0, y: mid_y },
3897 PathCommand::LineTo { x: width, y: mid_y },
3898 ]
3899 }
3900 }
3901 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3902 let gap = 0.06;
3903 vec![
3904 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3905 PathCommand::LineTo { x: width, y: mid_y - gap },
3906 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3907 PathCommand::LineTo { x: width, y: mid_y - gap },
3908 PathCommand::MoveTo { x: width, y: mid_y + gap },
3909 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3910 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3911 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3912 ]
3913 }
3914 "\\xtofrom" | "\\xrightleftarrows" => {
3915 let gap = 0.06;
3916 vec![
3917 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3918 PathCommand::LineTo { x: width, y: mid_y - gap },
3919 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3920 PathCommand::LineTo { x: width, y: mid_y - gap },
3921 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3922 PathCommand::MoveTo { x: width, y: mid_y + gap },
3923 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3924 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3925 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3926 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3927 ]
3928 }
3929 "\\overlinesegment" | "\\underlinesegment" => {
3930 vec![
3931 PathCommand::MoveTo { x: 0.0, y: mid_y },
3932 PathCommand::LineTo { x: width, y: mid_y },
3933 ]
3934 }
3935 _ => {
3936 vec![
3937 PathCommand::MoveTo { x: 0.0, y: mid_y },
3938 PathCommand::LineTo { x: width, y: mid_y },
3939 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3940 PathCommand::LineTo { x: width, y: mid_y },
3941 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3942 ]
3943 }
3944 }
3945}
3946
3947fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
3953 let h = inner.height;
3954 let d = inner.depth;
3955 let w = inner.width + pad_l + pad_r;
3956 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
3957 if pad_l > 0.0 {
3958 children.push(LayoutBox::new_kern(pad_l));
3959 }
3960 children.push(inner);
3961 if pad_r > 0.0 {
3962 children.push(LayoutBox::new_kern(pad_r));
3963 }
3964 LayoutBox {
3965 width: w,
3966 height: h,
3967 depth: d,
3968 content: BoxContent::HBox(children),
3969 color,
3970 }
3971}
3972
3973fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
3984 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
3985 LayoutBox {
3986 width: label.width,
3987 height: box_h,
3988 depth: box_d,
3989 content: BoxContent::RaiseBox {
3990 body: Box::new(label),
3991 shift,
3992 },
3993 color,
3994 }
3995}
3996
3997fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
4001 let sup_style = options.style.superscript();
4002 let sup_opts = options.with_style(sup_style);
4003 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4004 let inner = layout_node(body, &sup_opts);
4005 if (sup_ratio - 1.0).abs() < 1e-6 {
4006 inner
4007 } else {
4008 LayoutBox {
4009 width: inner.width * sup_ratio,
4010 height: inner.height * sup_ratio,
4011 depth: inner.depth * sup_ratio,
4012 content: BoxContent::Scaled {
4013 body: Box::new(inner),
4014 child_scale: sup_ratio,
4015 },
4016 color: options.color,
4017 }
4018 }
4019}
4020
4021fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4027 let axis = options.metrics().axis_height;
4028 let depth = (total_height / 2.0 - axis).max(0.0);
4029 let height = total_height - depth;
4030 if let Some((commands, w)) =
4031 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4032 {
4033 return LayoutBox {
4034 width: w,
4035 height,
4036 depth,
4037 content: BoxContent::SvgPath {
4038 commands,
4039 fill: true,
4040 },
4041 color: options.color,
4042 };
4043 }
4044 if down {
4046 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4047 } else {
4048 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4049 }
4050}
4051
4052fn layout_cd_arrow(
4068 direction: &str,
4069 label_above: Option<&ParseNode>,
4070 label_below: Option<&ParseNode>,
4071 target_size: f64,
4072 target_col_width: f64,
4073 _target_depth: f64,
4074 options: &LayoutOptions,
4075) -> LayoutBox {
4076 let metrics = options.metrics();
4077 let axis = metrics.axis_height;
4078
4079 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4082
4083 match direction {
4084 "right" | "left" | "horiz_eq" => {
4085 let sup_style = options.style.superscript();
4087 let sub_style = options.style.subscript();
4088 let sup_opts = options.with_style(sup_style);
4089 let sub_opts = options.with_style(sub_style);
4090 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4091 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4092
4093 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4094 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4095
4096 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4097 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4098
4099 let path_label = if direction == "right" {
4101 "\\cdrightarrow"
4102 } else if direction == "left" {
4103 "\\cdleftarrow"
4104 } else {
4105 "\\cdlongequal"
4106 };
4107 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4108 const CD_LABEL_PAD_L: f64 = 0.22;
4111 const CD_LABEL_PAD_R: f64 = 0.48;
4112 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4113 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4114 let upper_need = above_box
4115 .as_ref()
4116 .map(|_| above_w + cd_pad_sup)
4117 .unwrap_or(0.0);
4118 let lower_need = below_box
4119 .as_ref()
4120 .map(|_| below_w + cd_pad_sub)
4121 .unwrap_or(0.0);
4122 let natural_w = upper_need.max(lower_need).max(0.0);
4123 let shaft_w = if target_size > 0.0 {
4124 target_size
4125 } else {
4126 natural_w.max(min_shaft_w)
4127 };
4128
4129 let (commands, actual_arrow_h, fill_arrow) =
4130 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4131 Some((c, h)) => (c, h, true),
4132 None => {
4133 let arrow_h = 0.3_f64;
4135 let ah = 0.12_f64;
4136 let cmds = if direction == "horiz_eq" {
4137 let gap = 0.06;
4138 vec![
4139 PathCommand::MoveTo { x: 0.0, y: -gap },
4140 PathCommand::LineTo { x: shaft_w, y: -gap },
4141 PathCommand::MoveTo { x: 0.0, y: gap },
4142 PathCommand::LineTo { x: shaft_w, y: gap },
4143 ]
4144 } else if direction == "right" {
4145 vec![
4146 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4147 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4148 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4149 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4150 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4151 ]
4152 } else {
4153 vec![
4154 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4155 PathCommand::LineTo { x: 0.0, y: 0.0 },
4156 PathCommand::MoveTo { x: ah, y: -ah },
4157 PathCommand::LineTo { x: 0.0, y: 0.0 },
4158 PathCommand::LineTo { x: ah, y: ah },
4159 ]
4160 };
4161 (cmds, arrow_h, false)
4162 }
4163 };
4164
4165 let arrow_half = actual_arrow_h / 2.0;
4167 let arrow_box = LayoutBox {
4168 width: shaft_w,
4169 height: arrow_half,
4170 depth: arrow_half,
4171 content: BoxContent::SvgPath {
4172 commands,
4173 fill: fill_arrow,
4174 },
4175 color: options.color,
4176 };
4177
4178 let gap = 0.111;
4180 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4181 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4182 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4186 sup_d
4187 } else {
4188 0.0
4189 };
4190 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4191 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4192 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4193 let depth = if below_box.is_some() {
4194 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4195 } else {
4196 (arrow_half - axis).max(0.0)
4197 };
4198
4199 let inner = LayoutBox {
4200 width: shaft_w,
4201 height,
4202 depth,
4203 content: BoxContent::OpLimits {
4204 base: Box::new(arrow_box),
4205 sup: above_box.map(Box::new),
4206 sub: below_box.map(Box::new),
4207 base_shift: -axis,
4208 sup_kern: gap,
4209 sub_kern: gap,
4210 slant: 0.0,
4211 sup_scale: sup_ratio,
4212 sub_scale: sub_ratio,
4213 },
4214 color: options.color,
4215 };
4216
4217 if target_col_width > inner.width + 1e-6 {
4221 let extra = target_col_width - inner.width;
4222 let kl = extra / 2.0;
4223 let kr = extra - kl;
4224 cd_wrap_hpad(inner, kl, kr, options.color)
4225 } else {
4226 inner
4227 }
4228 }
4229
4230 "down" | "up" | "vert_eq" => {
4231 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4235 "vert_eq" if target_size > 0.0 => {
4236 make_vert_delim_box(target_size.max(big_total), true, options)
4237 }
4238 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4239 "down" if target_size > 0.0 => {
4240 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4241 }
4242 "up" if target_size > 0.0 => {
4243 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4244 }
4245 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4246 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4247 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4248 };
4249 let box_h = shaft_box.height;
4250 let box_d = shaft_box.depth;
4251 let shaft_w = shaft_box.width;
4252
4253 let left_box = label_above.map(|n| {
4256 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4257 });
4258 let right_box = label_below.map(|n| {
4259 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4260 });
4261
4262 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4263 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4264 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4265 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4266 let inner_w = left_part + shaft_w + right_part;
4267
4268 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4270 let extra = target_col_width - inner_w;
4271 let kl = extra / 2.0;
4272 let kr = extra - kl;
4273 (kl, kr, target_col_width)
4274 } else {
4275 (0.0, 0.0, inner_w)
4276 };
4277
4278 let mut children: Vec<LayoutBox> = Vec::new();
4279 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4280 if let Some(lb) = left_box {
4281 children.push(lb);
4282 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4283 }
4284 children.push(shaft_box);
4285 if let Some(rb) = right_box {
4286 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4287 children.push(rb);
4288 }
4289 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4290
4291 LayoutBox {
4292 width: total_w,
4293 height: box_h,
4294 depth: box_d,
4295 content: BoxContent::HBox(children),
4296 color: options.color,
4297 }
4298 }
4299
4300 _ => LayoutBox::new_empty(),
4302 }
4303}
4304
4305fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4307 let metrics = options.metrics();
4308 let pt = 1.0 / metrics.pt_per_em;
4309 let baselineskip = 3.0 * metrics.x_height;
4311 let arstrut_h = 0.7 * baselineskip;
4312 let arstrut_d = 0.3 * baselineskip;
4313
4314 let num_rows = body.len();
4315 if num_rows == 0 {
4316 return LayoutBox::new_empty();
4317 }
4318 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4319 if num_cols == 0 {
4320 return LayoutBox::new_empty();
4321 }
4322
4323 let jot = 3.0 * pt;
4325
4326 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4328 let mut col_widths = vec![0.0_f64; num_cols];
4329 let mut row_heights = vec![arstrut_h; num_rows];
4330 let mut row_depths = vec![arstrut_d; num_rows];
4331
4332 for (r, row) in body.iter().enumerate() {
4333 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4334
4335 for (c, cell) in row.iter().enumerate() {
4336 let cbox = match cell {
4337 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4338 layout_cd_arrow(
4339 direction,
4340 label_above.as_deref(),
4341 label_below.as_deref(),
4342 0.0, 0.0, 0.0, options,
4346 )
4347 }
4348 ParseNode::OrdGroup { body: cell_body, .. } => {
4352 layout_expression(cell_body, options, false)
4353 }
4354 other => layout_node(other, options),
4355 };
4356
4357 row_heights[r] = row_heights[r].max(cbox.height);
4358 row_depths[r] = row_depths[r].max(cbox.depth);
4359 col_widths[c] = col_widths[c].max(cbox.width);
4360 row_boxes.push(cbox);
4361 }
4362
4363 while row_boxes.len() < num_cols {
4365 row_boxes.push(LayoutBox::new_empty());
4366 }
4367 cell_boxes.push(row_boxes);
4368 }
4369
4370 let col_target_w: Vec<f64> = col_widths.clone();
4374
4375 #[cfg(debug_assertions)]
4376 {
4377 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4378 for (r, row) in cell_boxes.iter().enumerate() {
4379 for (c, b) in row.iter().enumerate() {
4380 if b.width > 0.0 {
4381 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4382 }
4383 }
4384 }
4385 }
4386
4387 for (r, row) in body.iter().enumerate() {
4389 let is_arrow_row = r % 2 == 1;
4390 for (c, cell) in row.iter().enumerate() {
4391 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4392 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4393 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4394 let b = layout_cd_arrow(
4395 direction,
4396 label_above.as_deref(),
4397 label_below.as_deref(),
4398 cell_boxes[r][c].width,
4399 col_target_w[c],
4400 0.0,
4401 options,
4402 );
4403 let w = b.width;
4404 (b, w)
4405 } else if is_arrow_row && c % 2 == 0 {
4406 let v_span = row_heights[r] + row_depths[r];
4410 let b = layout_cd_arrow(
4411 direction,
4412 label_above.as_deref(),
4413 label_below.as_deref(),
4414 v_span,
4415 col_widths[c],
4416 0.0,
4417 options,
4418 );
4419 let w = b.width;
4420 (b, w)
4421 } else {
4422 continue;
4423 };
4424 col_widths[c] = col_widths[c].max(col_w);
4425 cell_boxes[r][c] = new_box;
4426 }
4427 }
4428 }
4429
4430 #[cfg(debug_assertions)]
4431 {
4432 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4433 }
4434
4435 for rd in &mut row_depths {
4438 *rd += jot;
4439 }
4440
4441 let col_gap = 0.5;
4446
4447 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4449
4450 let col_separators = vec![None; num_cols + 1];
4452
4453 let mut total_height = 0.0_f64;
4454 let mut row_positions = Vec::with_capacity(num_rows);
4455 for r in 0..num_rows {
4456 total_height += row_heights[r];
4457 row_positions.push(total_height);
4458 total_height += row_depths[r];
4459 }
4460
4461 let offset = total_height / 2.0 + metrics.axis_height;
4462 let height = offset;
4463 let depth = total_height - offset;
4464
4465 let total_width = col_widths.iter().sum::<f64>()
4467 + col_gap * (num_cols.saturating_sub(1)) as f64;
4468
4469 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4471
4472 LayoutBox {
4473 width: total_width,
4474 height,
4475 depth,
4476 content: BoxContent::Array {
4477 cells: cell_boxes,
4478 col_widths,
4479 col_aligns,
4480 row_heights,
4481 row_depths,
4482 col_gap,
4483 offset,
4484 content_x_offset: 0.0,
4485 col_separators,
4486 hlines_before_row,
4487 rule_thickness: 0.04 * pt,
4488 double_rule_sep: metrics.double_rule_sep,
4489 array_inner_width: total_width,
4490 tag_gap_em: 0.0,
4491 tag_col_width: 0.0,
4492 row_tags: (0..num_rows).map(|_| None).collect(),
4493 tags_left: false,
4494 },
4495 color: options.color,
4496 }
4497}
4498
4499fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4500 let mid = width / 2.0;
4501 let q = height * 0.6;
4502 if is_over {
4503 vec![
4504 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4505 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4506 PathCommand::LineTo { x: mid - 0.05, y: -q },
4507 PathCommand::LineTo { x: mid, y: -height },
4508 PathCommand::LineTo { x: mid + 0.05, y: -q },
4509 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4510 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4511 ]
4512 } else {
4513 vec![
4514 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4515 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4516 PathCommand::LineTo { x: mid - 0.05, y: q },
4517 PathCommand::LineTo { x: mid, y: height },
4518 PathCommand::LineTo { x: mid + 0.05, y: q },
4519 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4520 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4521 ]
4522 }
4523}