1use ratex_font::{get_char_metrics, get_global_metrics, FontId};
2use ratex_parser::parse_node::{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 ..
364 } => {
365 if is_cd.unwrap_or(false) {
366 layout_cd(body, options)
367 } else {
368 layout_array(
369 body,
370 cols.as_deref(),
371 *arraystretch,
372 add_jot.unwrap_or(false),
373 row_gaps,
374 hlines_before_row,
375 col_separation_type.as_deref(),
376 hskip_before_and_after.unwrap_or(false),
377 options,
378 )
379 }
380 }
381
382 ParseNode::CdArrow {
383 direction,
384 label_above,
385 label_below,
386 ..
387 } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
388
389 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
390
391 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
392 Some(f) => {
393 let group = ParseNode::OrdGroup {
394 mode: *mode,
395 body: body.clone(),
396 semisimple: None,
397 loc: None,
398 };
399 layout_font(f, &group, options)
400 }
401 None => layout_text(body, options),
402 },
403
404 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
405
406 ParseNode::Href { body, .. } => layout_href(body, options),
407
408 ParseNode::Overline { body, .. } => layout_overline(body, options),
409 ParseNode::Underline { body, .. } => layout_underline(body, options),
410
411 ParseNode::Rule {
412 width: w,
413 height: h,
414 shift,
415 ..
416 } => {
417 let width = measurement_to_em(w, options);
418 let ink_h = measurement_to_em(h, options);
419 let raise = shift
420 .as_ref()
421 .map(|s| measurement_to_em(s, options))
422 .unwrap_or(0.0);
423 let box_height = (raise + ink_h).max(0.0);
424 let box_depth = (-raise).max(0.0);
425 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
426 }
427
428 ParseNode::Phantom { body, .. } => {
429 let inner = layout_expression(body, options, true);
430 LayoutBox {
431 width: inner.width,
432 height: inner.height,
433 depth: inner.depth,
434 content: BoxContent::Empty,
435 color: Color::BLACK,
436 }
437 }
438
439 ParseNode::VPhantom { body, .. } => {
440 let inner = layout_node(body, options);
441 LayoutBox {
442 width: 0.0,
443 height: inner.height,
444 depth: inner.depth,
445 content: BoxContent::Empty,
446 color: Color::BLACK,
447 }
448 }
449
450 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
451 let mut inner = layout_node(body, options);
452 if *smash_height { inner.height = 0.0; }
453 if *smash_depth { inner.depth = 0.0; }
454 inner
455 }
456
457 ParseNode::Middle { delim, .. } => {
458 match options.leftright_delim_height {
459 Some(h) => make_stretchy_delim(delim, h, options),
460 None => {
461 let placeholder = make_stretchy_delim(delim, 1.0, options);
463 LayoutBox {
464 width: placeholder.width,
465 height: 0.0,
466 depth: 0.0,
467 content: BoxContent::Empty,
468 color: options.color,
469 }
470 }
471 }
472 }
473
474 ParseNode::HtmlMathMl { html, .. } => {
475 layout_expression(html, options, true)
476 }
477
478 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
479
480 ParseNode::MathChoice {
481 display, text, script, scriptscript, ..
482 } => {
483 let branch = match options.style {
484 MathStyle::Display | MathStyle::DisplayCramped => display,
485 MathStyle::Text | MathStyle::TextCramped => text,
486 MathStyle::Script | MathStyle::ScriptCramped => script,
487 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
488 };
489 layout_expression(branch, options, true)
490 }
491
492 ParseNode::Lap { alignment, body, .. } => {
493 let inner = layout_node(body, options);
494 let shift = match alignment.as_str() {
495 "llap" => -inner.width,
496 "clap" => -inner.width / 2.0,
497 _ => 0.0, };
499 let mut children = Vec::new();
500 if shift != 0.0 {
501 children.push(LayoutBox::new_kern(shift));
502 }
503 let h = inner.height;
504 let d = inner.depth;
505 children.push(inner);
506 LayoutBox {
507 width: 0.0,
508 height: h,
509 depth: d,
510 content: BoxContent::HBox(children),
511 color: options.color,
512 }
513 }
514
515 ParseNode::HorizBrace {
516 base,
517 is_over,
518 label,
519 ..
520 } => layout_horiz_brace(base, *is_over, label, options),
521
522 ParseNode::XArrow {
523 label, body, below, ..
524 } => layout_xarrow(label, body, below.as_deref(), options),
525
526 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
527
528 ParseNode::HBox { body, .. } => layout_text(body, options),
529
530 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
531 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
532 }
533
534 ParseNode::RaiseBox { dy, body, .. } => {
535 let shift = measurement_to_em(dy, options);
536 layout_raisebox(shift, body, options)
537 }
538
539 ParseNode::VCenter { body, .. } => {
540 let inner = layout_node(body, options);
542 let axis = options.metrics().axis_height;
543 let total = inner.height + inner.depth;
544 let height = total / 2.0 + axis;
545 let depth = total - height;
546 LayoutBox {
547 width: inner.width,
548 height,
549 depth,
550 content: inner.content,
551 color: inner.color,
552 }
553 }
554
555 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
556
557 _ => LayoutBox::new_empty(),
559 }
560}
561
562fn missing_glyph_width_em(ch: char) -> f64 {
572 match ch as u32 {
573 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
575 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
577 0xAC00..=0xD7AF => 1.0,
579 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
581 _ => 0.5,
582 }
583}
584
585fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
586 let m = get_global_metrics(options.style.size_index());
587 let w = missing_glyph_width_em(ch);
588 if w >= 0.99 {
589 let h = (m.quad * 0.92).max(m.x_height);
590 (w, h, 0.0)
591 } else {
592 (w, m.x_height, 0.0)
593 }
594}
595
596#[inline]
598fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
599 if mode == Mode::Math {
600 m.width + m.italic
601 } else {
602 m.width
603 }
604}
605
606fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
607 let ch = resolve_symbol_char(text, mode);
608
609 match ch as u32 {
611 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
614 }
615
616 let char_code = ch as u32;
617
618 if let Some((font_id, metric_cp)) =
619 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
620 {
621 let m = get_char_metrics(font_id, metric_cp);
622 let (width, height, depth) = match m {
623 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
624 None => missing_glyph_metrics_fallback(ch, options),
625 };
626 return LayoutBox {
627 width,
628 height,
629 depth,
630 content: BoxContent::Glyph {
631 font_id,
632 char_code,
633 },
634 color: options.color,
635 };
636 }
637
638 let mut font_id = select_font(text, ch, mode, options);
639 let mut metrics = get_char_metrics(font_id, char_code);
640
641 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
642 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
643 font_id = FontId::MathItalic;
644 metrics = Some(m);
645 }
646 }
647
648 let (width, height, depth) = match metrics {
649 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
650 None => missing_glyph_metrics_fallback(ch, options),
651 };
652
653 LayoutBox {
654 width,
655 height,
656 depth,
657 content: BoxContent::Glyph {
658 font_id,
659 char_code,
660 },
661 color: options.color,
662 }
663}
664
665fn resolve_symbol_char(text: &str, mode: Mode) -> char {
667 let font_mode = match mode {
668 Mode::Math => ratex_font::Mode::Math,
669 Mode::Text => ratex_font::Mode::Text,
670 };
671
672 if let Some(raw) = text.chars().next() {
673 let ru = raw as u32;
674 if (0x1D400..=0x1D7FF).contains(&ru) {
675 return raw;
676 }
677 }
678
679 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
680 if let Some(cp) = info.codepoint {
681 return cp;
682 }
683 }
684
685 text.chars().next().unwrap_or('?')
686}
687
688fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
692 let font_mode = match mode {
693 Mode::Math => ratex_font::Mode::Math,
694 Mode::Text => ratex_font::Mode::Text,
695 };
696
697 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
698 if info.font == ratex_font::SymbolFont::Ams {
699 return FontId::AmsRegular;
700 }
701 }
702
703 match mode {
704 Mode::Math => {
705 if resolved_char.is_ascii_lowercase()
706 || resolved_char.is_ascii_uppercase()
707 || is_math_italic_greek(resolved_char)
708 {
709 FontId::MathItalic
710 } else {
711 FontId::MainRegular
712 }
713 }
714 Mode::Text => FontId::MainRegular,
715 }
716}
717
718fn is_math_italic_greek(ch: char) -> bool {
721 matches!(ch,
722 '\u{03B1}'..='\u{03C9}' |
723 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
724 '\u{03F1}' | '\u{03F5}'
725 )
726}
727
728fn is_arrow_accent(label: &str) -> bool {
729 matches!(
730 label,
731 "\\overrightarrow"
732 | "\\overleftarrow"
733 | "\\Overrightarrow"
734 | "\\overleftrightarrow"
735 | "\\underrightarrow"
736 | "\\underleftarrow"
737 | "\\underleftrightarrow"
738 | "\\overleftharpoon"
739 | "\\overrightharpoon"
740 | "\\overlinesegment"
741 | "\\underlinesegment"
742 )
743}
744
745fn layout_fraction(
750 numer: &ParseNode,
751 denom: &ParseNode,
752 bar_thickness: f64,
753 continued: bool,
754 options: &LayoutOptions,
755) -> LayoutBox {
756 let numer_s = options.style.numerator();
757 let denom_s = options.style.denominator();
758 let numer_style = options.with_style(numer_s);
759 let denom_style = options.with_style(denom_s);
760
761 let mut numer_box = layout_node(numer, &numer_style);
762 if continued {
764 let pt = options.metrics().pt_per_em;
765 let h_min = 8.5 / pt;
766 let d_min = 3.5 / pt;
767 if numer_box.height < h_min {
768 numer_box.height = h_min;
769 }
770 if numer_box.depth < d_min {
771 numer_box.depth = d_min;
772 }
773 }
774 let denom_box = layout_node(denom, &denom_style);
775
776 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
778 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
779
780 let numer_height = numer_box.height * numer_ratio;
781 let numer_depth = numer_box.depth * numer_ratio;
782 let denom_height = denom_box.height * denom_ratio;
783 let denom_depth = denom_box.depth * denom_ratio;
784 let numer_width = numer_box.width * numer_ratio;
785 let denom_width = denom_box.width * denom_ratio;
786
787 let metrics = options.metrics();
788 let axis = metrics.axis_height;
789 let rule = bar_thickness;
790
791 let (mut num_shift, mut den_shift) = if options.style.is_display() {
793 (metrics.num1, metrics.denom1)
794 } else if bar_thickness > 0.0 {
795 (metrics.num2, metrics.denom2)
796 } else {
797 (metrics.num3, metrics.denom2)
798 };
799
800 if bar_thickness > 0.0 {
801 let min_clearance = if options.style.is_display() {
802 3.0 * rule
803 } else {
804 rule
805 };
806
807 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
808 if num_clearance < min_clearance {
809 num_shift += min_clearance - num_clearance;
810 }
811
812 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
813 if den_clearance < min_clearance {
814 den_shift += min_clearance - den_clearance;
815 }
816 } else {
817 let min_gap = if options.style.is_display() {
818 7.0 * metrics.default_rule_thickness
819 } else {
820 3.0 * metrics.default_rule_thickness
821 };
822
823 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
824 if gap < min_gap {
825 let adjust = (min_gap - gap) / 2.0;
826 num_shift += adjust;
827 den_shift += adjust;
828 }
829 }
830
831 let total_width = numer_width.max(denom_width);
832 let height = numer_height + num_shift;
833 let depth = denom_depth + den_shift;
834
835 LayoutBox {
836 width: total_width,
837 height,
838 depth,
839 content: BoxContent::Fraction {
840 numer: Box::new(numer_box),
841 denom: Box::new(denom_box),
842 numer_shift: num_shift,
843 denom_shift: den_shift,
844 bar_thickness: rule,
845 numer_scale: numer_ratio,
846 denom_scale: denom_ratio,
847 },
848 color: options.color,
849 }
850}
851
852fn layout_supsub(
857 base: Option<&ParseNode>,
858 sup: Option<&ParseNode>,
859 sub: Option<&ParseNode>,
860 options: &LayoutOptions,
861 inherited_font: Option<FontId>,
862) -> LayoutBox {
863 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
864 Some(fid) => layout_with_font(n, fid, opts),
865 None => layout_node(n, opts),
866 };
867
868 let horiz_brace_over = matches!(
869 base,
870 Some(ParseNode::HorizBrace {
871 is_over: true,
872 ..
873 })
874 );
875 let horiz_brace_under = matches!(
876 base,
877 Some(ParseNode::HorizBrace {
878 is_over: false,
879 ..
880 })
881 );
882 let center_scripts = horiz_brace_over || horiz_brace_under;
883
884 let base_box = base
885 .map(|b| layout_child(b, options))
886 .unwrap_or_else(LayoutBox::new_empty);
887
888 let is_char_box = base.is_some_and(is_character_box);
889 let metrics = options.metrics();
890 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
894
895 let sup_style = options.style.superscript();
896 let sub_style = options.style.subscript();
897
898 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
899 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
900
901 let sup_box = sup.map(|s| {
902 let sup_opts = options.with_style(sup_style);
903 layout_child(s, &sup_opts)
904 });
905
906 let sub_box = sub.map(|s| {
907 let sub_opts = options.with_style(sub_style);
908 layout_child(s, &sub_opts)
909 });
910
911 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
912 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
913 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
914 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
915
916 let sup_style_metrics = get_global_metrics(sup_style.size_index());
918 let sub_style_metrics = get_global_metrics(sub_style.size_index());
919
920 let mut sup_shift = if !is_char_box && sup_box.is_some() {
923 base_box.height - sup_style_metrics.sup_drop * sup_ratio
924 } else {
925 0.0
926 };
927
928 let mut sub_shift = if !is_char_box && sub_box.is_some() {
929 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
930 } else {
931 0.0
932 };
933
934 let min_sup_shift = if options.style.is_cramped() {
935 metrics.sup3
936 } else if options.style.is_display() {
937 metrics.sup1
938 } else {
939 metrics.sup2
940 };
941
942 if sup_box.is_some() && sub_box.is_some() {
943 sup_shift = sup_shift
945 .max(min_sup_shift)
946 .max(sup_depth_scaled + 0.25 * metrics.x_height);
947 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
950 let max_width = 4.0 * rule_width;
951 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
952 if gap < max_width {
953 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
954 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
955 if psi > 0.0 {
956 sup_shift += psi;
957 sub_shift -= psi;
958 }
959 }
960 } else if sub_box.is_some() {
961 sub_shift = sub_shift
963 .max(metrics.sub1)
964 .max(sub_height_scaled - 0.8 * metrics.x_height);
965 } else if sup_box.is_some() {
966 sup_shift = sup_shift
968 .max(min_sup_shift)
969 .max(sup_depth_scaled + 0.25 * metrics.x_height);
970 }
971
972 if horiz_brace_over && sup_box.is_some() {
976 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
977 }
978 if horiz_brace_under && sub_box.is_some() {
979 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
980 }
981
982 let italic_correction = 0.0;
985
986 let sub_h_kern = if sub_box.is_some() && !center_scripts {
989 -glyph_italic(&base_box)
990 } else {
991 0.0
992 };
993
994 let mut height = base_box.height;
996 let mut depth = base_box.depth;
997 let mut total_width = base_box.width;
998
999 if let Some(ref sup_b) = sup_box {
1000 height = height.max(sup_shift + sup_height_scaled);
1001 if center_scripts {
1002 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1003 } else {
1004 total_width = total_width.max(
1005 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1006 );
1007 }
1008 }
1009 if let Some(ref sub_b) = sub_box {
1010 depth = depth.max(sub_shift + sub_depth_scaled);
1011 if center_scripts {
1012 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1013 } else {
1014 total_width = total_width.max(
1015 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1016 );
1017 }
1018 }
1019
1020 LayoutBox {
1021 width: total_width,
1022 height,
1023 depth,
1024 content: BoxContent::SupSub {
1025 base: Box::new(base_box),
1026 sup: sup_box.map(Box::new),
1027 sub: sub_box.map(Box::new),
1028 sup_shift,
1029 sub_shift,
1030 sup_scale: sup_ratio,
1031 sub_scale: sub_ratio,
1032 center_scripts,
1033 italic_correction,
1034 sub_h_kern,
1035 },
1036 color: options.color,
1037 }
1038}
1039
1040fn layout_radical(
1045 body: &ParseNode,
1046 index: Option<&ParseNode>,
1047 options: &LayoutOptions,
1048) -> LayoutBox {
1049 let cramped = options.style.cramped();
1050 let cramped_opts = options.with_style(cramped);
1051 let mut body_box = layout_node(body, &cramped_opts);
1052
1053 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1055 body_box.height *= body_ratio;
1056 body_box.depth *= body_ratio;
1057 body_box.width *= body_ratio;
1058
1059 if body_box.height == 0.0 {
1061 body_box.height = options.metrics().x_height;
1062 }
1063
1064 let metrics = options.metrics();
1065 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1070 metrics.x_height
1071 } else {
1072 theta
1073 };
1074
1075 let mut line_clearance = theta + phi / 4.0;
1076
1077 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1079
1080 let tex_height = select_surd_height(min_delim_height);
1083 let rule_width = theta;
1084 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1085 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1086 .map(|m| m.width)
1087 .unwrap_or(0.833);
1088
1089 let delim_depth = tex_height - rule_width;
1091 if delim_depth > body_box.height + body_box.depth + line_clearance {
1092 line_clearance =
1093 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1094 }
1095
1096 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1097
1098 let height = tex_height + rule_width - img_shift;
1101 let depth = if img_shift > body_box.depth {
1102 img_shift
1103 } else {
1104 body_box.depth
1105 };
1106
1107 const INDEX_KERN: f64 = 0.05;
1109 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1110 let root_style = options.style.superscript().superscript();
1111 let root_opts = options.with_style(root_style);
1112 let idx = layout_node(index_node, &root_opts);
1113 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1114 let offset = idx.width * index_ratio + INDEX_KERN;
1115 (Some(Box::new(idx)), offset, index_ratio)
1116 } else {
1117 (None, 0.0, 1.0)
1118 };
1119
1120 let width = index_offset + advance_width + body_box.width;
1121
1122 LayoutBox {
1123 width,
1124 height,
1125 depth,
1126 content: BoxContent::Radical {
1127 body: Box::new(body_box),
1128 index: index_box,
1129 index_offset,
1130 index_scale,
1131 rule_thickness: rule_width,
1132 inner_height: tex_height,
1133 },
1134 color: options.color,
1135 }
1136}
1137
1138fn select_surd_height(min_height: f64) -> f64 {
1141 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1142 for &h in &SURD_HEIGHTS {
1143 if h >= min_height {
1144 return h;
1145 }
1146 }
1147 SURD_HEIGHTS[4].max(min_height)
1149}
1150
1151const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1156
1157fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1159 match base {
1160 ParseNode::Op {
1161 limits,
1162 always_handle_sup_sub,
1163 ..
1164 } => {
1165 *limits
1166 && (options.style.is_display()
1167 || always_handle_sup_sub.unwrap_or(false))
1168 }
1169 ParseNode::OperatorName {
1170 always_handle_sup_sub,
1171 limits,
1172 ..
1173 } => {
1174 *always_handle_sup_sub
1175 && (options.style.is_display() || *limits)
1176 }
1177 _ => false,
1178 }
1179}
1180
1181fn layout_op(
1187 name: Option<&str>,
1188 symbol: bool,
1189 body: Option<&[ParseNode]>,
1190 _limits: bool,
1191 suppress_base_shift: bool,
1192 options: &LayoutOptions,
1193) -> LayoutBox {
1194 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1195
1196 if symbol && !suppress_base_shift {
1198 let axis = options.metrics().axis_height;
1199 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1200 if shift.abs() > 0.001 {
1201 base_box.height -= shift;
1202 base_box.depth += shift;
1203 }
1204 }
1205
1206 if !suppress_base_shift && !symbol && body.is_some() {
1211 let axis = options.metrics().axis_height;
1212 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1213 if delta.abs() > 0.001 {
1214 let w = base_box.width;
1215 let raise = -delta;
1217 base_box = LayoutBox {
1218 width: w,
1219 height: (base_box.height + raise).max(0.0),
1220 depth: (base_box.depth - raise).max(0.0),
1221 content: BoxContent::RaiseBox {
1222 body: Box::new(base_box),
1223 shift: raise,
1224 },
1225 color: options.color,
1226 };
1227 }
1228 }
1229
1230 base_box
1231}
1232
1233fn build_op_base(
1236 name: Option<&str>,
1237 symbol: bool,
1238 body: Option<&[ParseNode]>,
1239 options: &LayoutOptions,
1240) -> (LayoutBox, f64) {
1241 if symbol {
1242 let large = options.style.is_display()
1243 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1244 let font_id = if large {
1245 FontId::Size2Regular
1246 } else {
1247 FontId::Size1Regular
1248 };
1249
1250 let op_name = name.unwrap_or("");
1251 let ch = resolve_op_char(op_name);
1252 let char_code = ch as u32;
1253
1254 let metrics = get_char_metrics(font_id, char_code);
1255 let (width, height, depth, italic) = match metrics {
1256 Some(m) => (m.width, m.height, m.depth, m.italic),
1257 None => (1.0, 0.75, 0.25, 0.0),
1258 };
1259 let width_with_italic = width + italic;
1262
1263 let base = LayoutBox {
1264 width: width_with_italic,
1265 height,
1266 depth,
1267 content: BoxContent::Glyph {
1268 font_id,
1269 char_code,
1270 },
1271 color: options.color,
1272 };
1273
1274 if op_name == "\\oiint" || op_name == "\\oiiint" {
1277 let w = base.width;
1278 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1279 let overlay_box = LayoutBox {
1280 width: w,
1281 height: base.height,
1282 depth: base.depth,
1283 content: BoxContent::SvgPath {
1284 commands: ellipse_commands,
1285 fill: false,
1286 },
1287 color: options.color,
1288 };
1289 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1290 return (with_overlay, italic);
1291 }
1292
1293 (base, italic)
1294 } else if let Some(body_nodes) = body {
1295 let base = layout_expression(body_nodes, options, true);
1296 (base, 0.0)
1297 } else {
1298 let base = layout_op_text(name.unwrap_or(""), options);
1299 (base, 0.0)
1300 }
1301}
1302
1303fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1305 let text = name.strip_prefix('\\').unwrap_or(name);
1306 let mut children = Vec::new();
1307 for ch in text.chars() {
1308 let char_code = ch as u32;
1309 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1310 let (width, height, depth) = match metrics {
1311 Some(m) => (m.width, m.height, m.depth),
1312 None => (0.5, 0.43, 0.0),
1313 };
1314 children.push(LayoutBox {
1315 width,
1316 height,
1317 depth,
1318 content: BoxContent::Glyph {
1319 font_id: FontId::MainRegular,
1320 char_code,
1321 },
1322 color: options.color,
1323 });
1324 }
1325 make_hbox(children)
1326}
1327
1328fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1330 let metrics = options.metrics();
1331 (base.height - base.depth) / 2.0 - metrics.axis_height
1332}
1333
1334fn resolve_op_char(name: &str) -> char {
1336 match name {
1339 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1342 }
1343 let font_mode = ratex_font::Mode::Math;
1344 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1345 if let Some(cp) = info.codepoint {
1346 return cp;
1347 }
1348 }
1349 name.chars().next().unwrap_or('?')
1350}
1351
1352fn layout_op_with_limits(
1354 base_node: &ParseNode,
1355 sup_node: Option<&ParseNode>,
1356 sub_node: Option<&ParseNode>,
1357 options: &LayoutOptions,
1358) -> LayoutBox {
1359 let (name, symbol, body, suppress_base_shift) = match base_node {
1360 ParseNode::Op {
1361 name,
1362 symbol,
1363 body,
1364 suppress_base_shift,
1365 ..
1366 } => (
1367 name.as_deref(),
1368 *symbol,
1369 body.as_deref(),
1370 suppress_base_shift.unwrap_or(false),
1371 ),
1372 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1373 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1374 };
1375
1376 let legacy_limit_kern_padding = !suppress_base_shift;
1378
1379 let (base_box, slant) = build_op_base(name, symbol, body, options);
1380 let base_shift = if symbol && !suppress_base_shift {
1382 compute_op_base_shift(&base_box, options)
1383 } else {
1384 0.0
1385 };
1386
1387 layout_op_limits_inner(
1388 &base_box,
1389 sup_node,
1390 sub_node,
1391 slant,
1392 base_shift,
1393 legacy_limit_kern_padding,
1394 options,
1395 )
1396}
1397
1398fn layout_op_limits_inner(
1403 base: &LayoutBox,
1404 sup_node: Option<&ParseNode>,
1405 sub_node: Option<&ParseNode>,
1406 slant: f64,
1407 base_shift: f64,
1408 legacy_limit_kern_padding: bool,
1409 options: &LayoutOptions,
1410) -> LayoutBox {
1411 let metrics = options.metrics();
1412 let sup_style = options.style.superscript();
1413 let sub_style = options.style.subscript();
1414
1415 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1416 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1417
1418 let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 };
1419
1420 let sup_data = sup_node.map(|s| {
1421 let sup_opts = options.with_style(sup_style);
1422 let elem = layout_node(s, &sup_opts);
1423 let d = if legacy_limit_kern_padding {
1427 elem.depth * sup_ratio
1428 } else {
1429 elem.depth
1430 };
1431 let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1432 (elem, kern)
1433 });
1434
1435 let sub_data = sub_node.map(|s| {
1436 let sub_opts = options.with_style(sub_style);
1437 let elem = layout_node(s, &sub_opts);
1438 let h = if legacy_limit_kern_padding {
1439 elem.height * sub_ratio
1440 } else {
1441 elem.height
1442 };
1443 let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1444 (elem, kern)
1445 });
1446
1447 let sp5 = metrics.big_op_spacing5;
1448
1449 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1450 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1451 let sup_h = sup_elem.height * sup_ratio;
1454 let sup_d = sup_elem.depth * sup_ratio;
1455 let sub_h = sub_elem.height * sub_ratio;
1456 let sub_d = sub_elem.depth * sub_ratio;
1457
1458 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1459
1460 let height = bottom
1461 + base.height - base_shift
1462 + sup_kern
1463 + sup_h + sup_d
1464 + sp5
1465 - (base.height + base.depth);
1466
1467 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1468 let total_d = bottom;
1469
1470 let w = base
1471 .width
1472 .max(sup_elem.width * sup_ratio)
1473 .max(sub_elem.width * sub_ratio);
1474 let _ = height; (total_h, total_d, w)
1476 }
1477 (None, Some((sub_elem, sub_kern))) => {
1478 let sub_h = sub_elem.height * sub_ratio;
1481 let sub_d = sub_elem.depth * sub_ratio;
1482
1483 let total_h = base.height - base_shift;
1484 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1485
1486 let w = base.width.max(sub_elem.width * sub_ratio);
1487 (total_h, total_d, w)
1488 }
1489 (Some((sup_elem, sup_kern)), None) => {
1490 let sup_h = sup_elem.height * sup_ratio;
1493 let sup_d = sup_elem.depth * sup_ratio;
1494
1495 let total_h =
1496 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1497 let total_d = base.depth + base_shift;
1498
1499 let w = base.width.max(sup_elem.width * sup_ratio);
1500 (total_h, total_d, w)
1501 }
1502 (None, None) => {
1503 return base.clone();
1504 }
1505 };
1506
1507 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1508 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1509
1510 LayoutBox {
1511 width: total_width,
1512 height: total_height,
1513 depth: total_depth,
1514 content: BoxContent::OpLimits {
1515 base: Box::new(base.clone()),
1516 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1517 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1518 base_shift,
1519 sup_kern: sup_kern_val,
1520 sub_kern: sub_kern_val,
1521 slant,
1522 sup_scale: sup_ratio,
1523 sub_scale: sub_ratio,
1524 },
1525 color: options.color,
1526 }
1527}
1528
1529fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1531 let mut children = Vec::new();
1532 for node in body {
1533 match node {
1534 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1535 let ch = text.chars().next().unwrap_or('?');
1536 let char_code = ch as u32;
1537 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1538 let (width, height, depth) = match metrics {
1539 Some(m) => (m.width, m.height, m.depth),
1540 None => (0.5, 0.43, 0.0),
1541 };
1542 children.push(LayoutBox {
1543 width,
1544 height,
1545 depth,
1546 content: BoxContent::Glyph {
1547 font_id: FontId::MainRegular,
1548 char_code,
1549 },
1550 color: options.color,
1551 });
1552 }
1553 _ => {
1554 children.push(layout_node(node, options));
1555 }
1556 }
1557 }
1558 make_hbox(children)
1559}
1560
1561const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1567
1568fn glyph_italic(lb: &LayoutBox) -> f64 {
1572 match &lb.content {
1573 BoxContent::Glyph { font_id, char_code } => {
1574 get_char_metrics(*font_id, *char_code)
1575 .map(|m| m.italic)
1576 .unwrap_or(0.0)
1577 }
1578 BoxContent::HBox(children) => {
1579 children.last().map(glyph_italic).unwrap_or(0.0)
1580 }
1581 _ => 0.0,
1582 }
1583}
1584
1585fn accent_ordgroup_len(base: &ParseNode) -> usize {
1590 match base {
1591 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1592 _ => 1,
1593 }
1594}
1595
1596fn glyph_skew(lb: &LayoutBox) -> f64 {
1597 match &lb.content {
1598 BoxContent::Glyph { font_id, char_code } => {
1599 get_char_metrics(*font_id, *char_code)
1600 .map(|m| m.skew)
1601 .unwrap_or(0.0)
1602 }
1603 BoxContent::HBox(children) => {
1604 children.last().map(glyph_skew).unwrap_or(0.0)
1605 }
1606 _ => 0.0,
1607 }
1608}
1609
1610fn layout_accent(
1611 label: &str,
1612 base: &ParseNode,
1613 is_stretchy: bool,
1614 is_shifty: bool,
1615 is_below: bool,
1616 options: &LayoutOptions,
1617) -> LayoutBox {
1618 let body_box = layout_node(base, options);
1619 let base_w = body_box.width.max(0.5);
1620
1621 if label == "\\textcircled" {
1623 return layout_textcircled(body_box, options);
1624 }
1625
1626 if let Some((commands, w, h, fill)) =
1628 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1629 {
1630 let accent_box = LayoutBox {
1632 width: w,
1633 height: 0.0,
1634 depth: h,
1635 content: BoxContent::SvgPath { commands, fill },
1636 color: options.color,
1637 };
1638 let gap = 0.065;
1643 let under_gap_em = if is_below && label == "\\utilde" {
1644 0.12
1645 } else {
1646 0.0
1647 };
1648 let clearance = if is_below {
1649 body_box.height + body_box.depth + gap
1650 } else if label == "\\vec" {
1651 (body_box.height - options.metrics().x_height).max(0.0)
1654 } else {
1655 body_box.height + gap
1656 };
1657 let (height, depth) = if is_below {
1658 (body_box.height, body_box.depth + h + gap + under_gap_em)
1659 } else if label == "\\vec" {
1660 (clearance + h, body_box.depth)
1662 } else {
1663 (body_box.height + gap + h, body_box.depth)
1664 };
1665 let vec_skew = if label == "\\vec" {
1666 (if is_shifty {
1667 glyph_skew(&body_box)
1668 } else {
1669 0.0
1670 }) + VEC_SKEW_EXTRA_RIGHT_EM
1671 } else {
1672 0.0
1673 };
1674 return LayoutBox {
1675 width: body_box.width,
1676 height,
1677 depth,
1678 content: BoxContent::Accent {
1679 base: Box::new(body_box),
1680 accent: Box::new(accent_box),
1681 clearance,
1682 skew: vec_skew,
1683 is_below,
1684 under_gap_em,
1685 },
1686 color: options.color,
1687 };
1688 }
1689
1690 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1692
1693 let accent_box = if use_arrow_path {
1694 let (commands, arrow_h, fill_arrow) =
1695 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1696 Some((c, h)) => (c, h, true),
1697 None => {
1698 let h = 0.3_f64;
1699 let c = stretchy_accent_path(label, base_w, h);
1700 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1701 (c, h, fill)
1702 }
1703 };
1704 LayoutBox {
1705 width: base_w,
1706 height: arrow_h / 2.0,
1707 depth: arrow_h / 2.0,
1708 content: BoxContent::SvgPath {
1709 commands,
1710 fill: fill_arrow,
1711 },
1712 color: options.color,
1713 }
1714 } else {
1715 let accent_char = {
1717 let ch = resolve_symbol_char(label, Mode::Text);
1718 if ch == label.chars().next().unwrap_or('?') {
1719 resolve_symbol_char(label, Mode::Math)
1722 } else {
1723 ch
1724 }
1725 };
1726 let accent_code = accent_char as u32;
1727 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1728 let (accent_w, accent_h, accent_d) = match accent_metrics {
1729 Some(m) => (m.width, m.height, m.depth),
1730 None => (body_box.width, 0.25, 0.0),
1731 };
1732 LayoutBox {
1733 width: accent_w,
1734 height: accent_h,
1735 depth: accent_d,
1736 content: BoxContent::Glyph {
1737 font_id: FontId::MainRegular,
1738 char_code: accent_code,
1739 },
1740 color: options.color,
1741 }
1742 };
1743
1744 let skew = if use_arrow_path {
1745 0.0
1746 } else if is_shifty {
1747 glyph_skew(&body_box)
1750 } else {
1751 0.0
1752 };
1753
1754 let gap = if use_arrow_path {
1763 if label == "\\Overrightarrow" {
1764 0.21
1765 } else {
1766 0.26
1767 }
1768 } else {
1769 0.0
1770 };
1771
1772 let clearance = if is_below {
1773 body_box.height + body_box.depth + accent_box.depth + gap
1774 } else if use_arrow_path {
1775 body_box.height + gap
1776 } else {
1777 let base_clearance = match &body_box.content {
1787 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1788 if !is_below =>
1789 {
1790 if inner_accent.height <= 0.001 {
1796 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1802 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1803 katex_pos + correction
1804 } else {
1805 inner_cl + 0.3
1806 }
1807 }
1808 _ => {
1809 if label == "\\bar" || label == "\\=" {
1822 body_box.height
1823 } else {
1824 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1825 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1826 katex_pos + correction
1827 }
1828 }
1829 };
1830 let base_clearance = base_clearance + accent_box.depth;
1835 if label == "\\bar" || label == "\\=" {
1836 (base_clearance - 0.12).max(0.0)
1837 } else {
1838 base_clearance
1839 }
1840 };
1841
1842 let (height, depth) = if is_below {
1843 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1844 } else if use_arrow_path {
1845 (body_box.height + gap + accent_box.height, body_box.depth)
1846 } else {
1847 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1854 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1855 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1856 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1857 } else {
1858 body_box.height.max(accent_visual_top)
1859 };
1860 (h, body_box.depth)
1861 };
1862
1863 LayoutBox {
1864 width: body_box.width,
1865 height,
1866 depth,
1867 content: BoxContent::Accent {
1868 base: Box::new(body_box),
1869 accent: Box::new(accent_box),
1870 clearance,
1871 skew,
1872 is_below,
1873 under_gap_em: 0.0,
1874 },
1875 color: options.color,
1876 }
1877}
1878
1879fn node_contains_middle(node: &ParseNode) -> bool {
1885 match node {
1886 ParseNode::Middle { .. } => true,
1887 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1888 body.iter().any(node_contains_middle)
1889 }
1890 ParseNode::SupSub { base, sup, sub, .. } => {
1891 base.as_deref().is_some_and(node_contains_middle)
1892 || sup.as_deref().is_some_and(node_contains_middle)
1893 || sub.as_deref().is_some_and(node_contains_middle)
1894 }
1895 ParseNode::GenFrac { numer, denom, .. } => {
1896 node_contains_middle(numer) || node_contains_middle(denom)
1897 }
1898 ParseNode::Sqrt { body, index, .. } => {
1899 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1900 }
1901 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1902 node_contains_middle(base)
1903 }
1904 ParseNode::Op { body, .. } => body
1905 .as_ref()
1906 .is_some_and(|b| b.iter().any(node_contains_middle)),
1907 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1908 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1909 ParseNode::Font { body, .. } => node_contains_middle(body),
1910 ParseNode::Text { body, .. }
1911 | ParseNode::Color { body, .. }
1912 | ParseNode::Styling { body, .. }
1913 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1914 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1915 node_contains_middle(body)
1916 }
1917 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1918 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1919 node_contains_middle(body)
1920 }
1921 ParseNode::Array { body, .. } => body
1922 .iter()
1923 .any(|row| row.iter().any(node_contains_middle)),
1924 ParseNode::Enclose { body, .. }
1925 | ParseNode::Lap { body, .. }
1926 | ParseNode::RaiseBox { body, .. }
1927 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1928 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1929 ParseNode::XArrow { body, below, .. } => {
1930 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1931 }
1932 ParseNode::CdArrow { label_above, label_below, .. } => {
1933 label_above.as_deref().is_some_and(node_contains_middle)
1934 || label_below.as_deref().is_some_and(node_contains_middle)
1935 }
1936 ParseNode::MathChoice {
1937 display,
1938 text,
1939 script,
1940 scriptscript,
1941 ..
1942 } => {
1943 display.iter().any(node_contains_middle)
1944 || text.iter().any(node_contains_middle)
1945 || script.iter().any(node_contains_middle)
1946 || scriptscript.iter().any(node_contains_middle)
1947 }
1948 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1949 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1950 _ => false,
1951 }
1952}
1953
1954fn body_contains_middle(nodes: &[ParseNode]) -> bool {
1956 nodes.iter().any(node_contains_middle)
1957}
1958
1959fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
1962 let m = options.metrics();
1963 if options.style.is_display() {
1964 m.delim1
1965 } else if matches!(
1966 options.style,
1967 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
1968 ) {
1969 options
1970 .with_style(MathStyle::Script)
1971 .metrics()
1972 .delim2
1973 } else {
1974 m.delim2
1975 }
1976}
1977
1978fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
1980 let metrics = options.metrics();
1981 let inner_height = inner.height;
1982 let inner_depth = inner.depth;
1983 let axis = metrics.axis_height;
1984 let max_dist = (inner_height - axis).max(inner_depth + axis);
1985 let delim_factor = 901.0;
1986 let delim_extend = 5.0 / metrics.pt_per_em;
1987 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
1988 from_formula.max(inner_height + inner_depth)
1990}
1991
1992fn layout_left_right(
1993 body: &[ParseNode],
1994 left_delim: &str,
1995 right_delim: &str,
1996 options: &LayoutOptions,
1997) -> LayoutBox {
1998 let (inner, total_height) = if body_contains_middle(body) {
1999 let opts_first = LayoutOptions {
2001 leftright_delim_height: None,
2002 ..options.clone()
2003 };
2004 let inner_first = layout_expression(body, &opts_first, true);
2005 let total_height = left_right_delim_total_height(&inner_first, options);
2006 let opts_second = LayoutOptions {
2008 leftright_delim_height: Some(total_height),
2009 ..options.clone()
2010 };
2011 let inner_second = layout_expression(body, &opts_second, true);
2012 (inner_second, total_height)
2013 } else {
2014 let inner = layout_expression(body, options, true);
2015 let total_height = left_right_delim_total_height(&inner, options);
2016 (inner, total_height)
2017 };
2018
2019 let inner_height = inner.height;
2020 let inner_depth = inner.depth;
2021
2022 let left_box = make_stretchy_delim(left_delim, total_height, options);
2023 let right_box = make_stretchy_delim(right_delim, total_height, options);
2024
2025 let width = left_box.width + inner.width + right_box.width;
2026 let height = left_box.height.max(right_box.height).max(inner_height);
2027 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2028
2029 LayoutBox {
2030 width,
2031 height,
2032 depth,
2033 content: BoxContent::LeftRight {
2034 left: Box::new(left_box),
2035 right: Box::new(right_box),
2036 inner: Box::new(inner),
2037 },
2038 color: options.color,
2039 }
2040}
2041
2042const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2043 FontId::MainRegular,
2044 FontId::Size1Regular,
2045 FontId::Size2Regular,
2046 FontId::Size3Regular,
2047 FontId::Size4Regular,
2048];
2049
2050fn normalize_delim(delim: &str) -> &str {
2052 match delim {
2053 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2054 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2055 _ => delim,
2056 }
2057}
2058
2059fn is_vert_delim(delim: &str) -> bool {
2061 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2062}
2063
2064fn is_double_vert_delim(delim: &str) -> bool {
2066 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2067}
2068
2069fn vert_repeat_piece_height(is_double: bool) -> f64 {
2071 let code = if is_double { 8741_u32 } else { 8739 };
2072 get_char_metrics(FontId::Size1Regular, code)
2073 .map(|m| m.height + m.depth)
2074 .unwrap_or(0.5)
2075}
2076
2077fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2079 let piece = vert_repeat_piece_height(is_double);
2080 let min_h = 2.0 * piece;
2081 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2082 let mut h = min_h + repeat_count * piece;
2083 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2087 h *= 1.135;
2088 }
2089 h
2090}
2091
2092fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2094 let neg = -mid_th;
2095 if !is_double {
2096 format!(
2097 "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"
2098 )
2099 } else {
2100 format!(
2101 "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"
2102 )
2103 }
2104}
2105
2106fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2107 let s = 0.001_f64;
2108 cmds.iter()
2109 .map(|c| match *c {
2110 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2111 x: x * s,
2112 y: y * s,
2113 },
2114 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2115 x: x * s,
2116 y: y * s,
2117 },
2118 PathCommand::CubicTo {
2119 x1,
2120 y1,
2121 x2,
2122 y2,
2123 x,
2124 y,
2125 } => PathCommand::CubicTo {
2126 x1: x1 * s,
2127 y1: y1 * s,
2128 x2: x2 * s,
2129 y2: y2 * s,
2130 x: x * s,
2131 y: y * s,
2132 },
2133 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2134 x1: x1 * s,
2135 y1: y1 * s,
2136 x: x * s,
2137 y: y * s,
2138 },
2139 PathCommand::Close => PathCommand::Close,
2140 })
2141 .collect()
2142}
2143
2144fn map_vert_path_y_to_baseline(
2146 cmds: Vec<PathCommand>,
2147 height: f64,
2148 depth: f64,
2149 view_box_height: i64,
2150) -> Vec<PathCommand> {
2151 let span_em = view_box_height as f64 / 1000.0;
2152 let total = height + depth;
2153 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2154 cmds.into_iter()
2155 .map(|c| match c {
2156 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2157 x,
2158 y: -height + y * scale_y,
2159 },
2160 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2161 x,
2162 y: -height + y * scale_y,
2163 },
2164 PathCommand::CubicTo {
2165 x1,
2166 y1,
2167 x2,
2168 y2,
2169 x,
2170 y,
2171 } => PathCommand::CubicTo {
2172 x1,
2173 y1: -height + y1 * scale_y,
2174 x2,
2175 y2: -height + y2 * scale_y,
2176 x,
2177 y: -height + y * scale_y,
2178 },
2179 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2180 x1,
2181 y1: -height + y1 * scale_y,
2182 x,
2183 y: -height + y * scale_y,
2184 },
2185 PathCommand::Close => PathCommand::Close,
2186 })
2187 .collect()
2188}
2189
2190fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2193 let real_h = katex_vert_real_height(total_height, is_double);
2194 let axis = options.metrics().axis_height;
2195 let depth = (real_h / 2.0 - axis).max(0.0);
2196 let height = real_h - depth;
2197 let width = if is_double { 0.556 } else { 0.333 };
2198
2199 let piece = vert_repeat_piece_height(is_double);
2200 let mid_em = (real_h - 2.0 * piece).max(0.0);
2201 let mid_th = (mid_em * 1000.0).round() as i64;
2202 let view_box_height = (real_h * 1000.0).round() as i64;
2203
2204 let d = tall_vert_svg_path_data(mid_th, is_double);
2205 let raw = parse_svg_path_data(&d);
2206 let scaled = scale_svg_path_to_em(&raw);
2207 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2208
2209 LayoutBox {
2210 width,
2211 height,
2212 depth,
2213 content: BoxContent::SvgPath { commands, fill: true },
2214 color: options.color,
2215 }
2216}
2217
2218fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2220 if delim == "." || delim.is_empty() {
2221 return LayoutBox::new_kern(0.0);
2222 }
2223
2224 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2229 return make_vert_delim_box(total_height, false, options);
2230 }
2231 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2232 return make_vert_delim_box(total_height, true, options);
2233 }
2234
2235 let delim = normalize_delim(delim);
2237
2238 let ch = resolve_symbol_char(delim, Mode::Math);
2239 let char_code = ch as u32;
2240
2241 let mut best_font = FontId::MainRegular;
2242 let mut best_w = 0.4;
2243 let mut best_h = 0.7;
2244 let mut best_d = 0.2;
2245
2246 for &font_id in &DELIM_FONT_SEQUENCE {
2247 if let Some(m) = get_char_metrics(font_id, char_code) {
2248 best_font = font_id;
2249 best_w = m.width;
2250 best_h = m.height;
2251 best_d = m.depth;
2252 if best_h + best_d >= total_height {
2253 break;
2254 }
2255 }
2256 }
2257
2258 let best_total = best_h + best_d;
2259 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2260 return stacked;
2261 }
2262
2263 LayoutBox {
2264 width: best_w,
2265 height: best_h,
2266 depth: best_d,
2267 content: BoxContent::Glyph {
2268 font_id: best_font,
2269 char_code,
2270 },
2271 color: options.color,
2272 }
2273}
2274
2275const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2277
2278fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2280 if delim == "." || delim.is_empty() {
2281 return LayoutBox::new_kern(0.0);
2282 }
2283
2284 if is_vert_delim(delim) {
2286 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2287 return make_vert_delim_box(total, false, options);
2288 }
2289 if is_double_vert_delim(delim) {
2290 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2291 return make_vert_delim_box(total, true, options);
2292 }
2293
2294 let delim = normalize_delim(delim);
2296
2297 let ch = resolve_symbol_char(delim, Mode::Math);
2298 let char_code = ch as u32;
2299
2300 let font_id = match size {
2301 1 => FontId::Size1Regular,
2302 2 => FontId::Size2Regular,
2303 3 => FontId::Size3Regular,
2304 4 => FontId::Size4Regular,
2305 _ => FontId::Size1Regular,
2306 };
2307
2308 let metrics = get_char_metrics(font_id, char_code);
2309 let (width, height, depth, actual_font) = match metrics {
2310 Some(m) => (m.width, m.height, m.depth, font_id),
2311 None => {
2312 let m = get_char_metrics(FontId::MainRegular, char_code);
2313 match m {
2314 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2315 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2316 }
2317 }
2318 };
2319
2320 LayoutBox {
2321 width,
2322 height,
2323 depth,
2324 content: BoxContent::Glyph {
2325 font_id: actual_font,
2326 char_code,
2327 },
2328 color: options.color,
2329 }
2330}
2331
2332#[allow(clippy::too_many_arguments)]
2337fn layout_array(
2338 body: &[Vec<ParseNode>],
2339 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2340 arraystretch: f64,
2341 add_jot: bool,
2342 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2343 hlines: &[Vec<bool>],
2344 col_sep_type: Option<&str>,
2345 hskip: bool,
2346 options: &LayoutOptions,
2347) -> LayoutBox {
2348 let metrics = options.metrics();
2349 let pt = 1.0 / metrics.pt_per_em;
2350 let baselineskip = 12.0 * pt;
2351 let jot = 3.0 * pt;
2352 let arrayskip = arraystretch * baselineskip;
2353 let arstrut_h = 0.7 * arrayskip;
2354 let arstrut_d = 0.3 * arrayskip;
2355 const ALIGN_RELATION_MU: f64 = 3.0;
2358 let col_gap = match col_sep_type {
2359 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2360 Some("alignat") => 0.0,
2361 Some("small") => {
2362 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2365 / options.size_multiplier()
2366 }
2367 _ => 2.0 * 5.0 * pt, };
2369 let cell_options = match col_sep_type {
2370 Some("align") | Some("alignat") => LayoutOptions {
2371 align_relation_spacing: Some(ALIGN_RELATION_MU),
2372 ..options.clone()
2373 },
2374 _ => options.clone(),
2375 };
2376
2377 let num_rows = body.len();
2378 if num_rows == 0 {
2379 return LayoutBox::new_empty();
2380 }
2381
2382 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2383
2384 use ratex_parser::parse_node::AlignType;
2386 let col_aligns: Vec<u8> = {
2387 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2388 .map(|cs| {
2389 cs.iter()
2390 .filter(|s| matches!(s.align_type, AlignType::Align))
2391 .collect()
2392 })
2393 .unwrap_or_default();
2394 (0..num_cols)
2395 .map(|c| {
2396 align_specs
2397 .get(c)
2398 .and_then(|s| s.align.as_deref())
2399 .and_then(|a| a.bytes().next())
2400 .unwrap_or(b'c')
2401 })
2402 .collect()
2403 };
2404
2405 let col_separators: Vec<Option<bool>> = {
2408 let mut seps = vec![None; num_cols + 1];
2409 let mut align_count = 0usize;
2410 if let Some(cs) = cols {
2411 for spec in cs {
2412 match spec.align_type {
2413 AlignType::Align => align_count += 1,
2414 AlignType::Separator if spec.align.as_deref() == Some("|") => {
2415 if align_count <= num_cols {
2416 seps[align_count] = Some(false);
2417 }
2418 }
2419 AlignType::Separator if spec.align.as_deref() == Some(":") => {
2420 if align_count <= num_cols {
2421 seps[align_count] = Some(true);
2422 }
2423 }
2424 _ => {}
2425 }
2426 }
2427 }
2428 seps
2429 };
2430
2431 let rule_thickness = 0.4 * pt;
2432 let double_rule_sep = metrics.double_rule_sep;
2433
2434 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2436 let mut col_widths = vec![0.0_f64; num_cols];
2437 let mut row_heights = Vec::with_capacity(num_rows);
2438 let mut row_depths = Vec::with_capacity(num_rows);
2439
2440 for row in body {
2441 let mut row_boxes = Vec::with_capacity(num_cols);
2442 let mut rh = arstrut_h;
2443 let mut rd = arstrut_d;
2444
2445 for (c, cell) in row.iter().enumerate() {
2446 let cell_nodes = match cell {
2447 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2448 other => std::slice::from_ref(other),
2449 };
2450 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2451 rh = rh.max(cell_box.height);
2452 rd = rd.max(cell_box.depth);
2453 if c < num_cols {
2454 col_widths[c] = col_widths[c].max(cell_box.width);
2455 }
2456 row_boxes.push(cell_box);
2457 }
2458
2459 while row_boxes.len() < num_cols {
2461 row_boxes.push(LayoutBox::new_empty());
2462 }
2463
2464 if add_jot {
2465 rd += jot;
2466 }
2467
2468 row_heights.push(rh);
2469 row_depths.push(rd);
2470 cell_boxes.push(row_boxes);
2471 }
2472
2473 for (r, gap) in row_gaps.iter().enumerate() {
2475 if r < row_depths.len() {
2476 if let Some(m) = gap {
2477 let gap_em = measurement_to_em(m, options);
2478 if gap_em > 0.0 {
2479 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2480 }
2481 }
2482 }
2483 }
2484
2485 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2487 while hlines_before_row.len() < num_rows + 1 {
2488 hlines_before_row.push(vec![]);
2489 }
2490
2491 for r in 0..=num_rows {
2497 let n = hlines_before_row[r].len();
2498 if n > 1 {
2499 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2500 if r == 0 {
2501 if num_rows > 0 {
2502 row_heights[0] += extra;
2503 }
2504 } else {
2505 row_depths[r - 1] += extra;
2506 }
2507 }
2508 }
2509
2510 let mut total_height = 0.0;
2512 let mut row_positions = Vec::with_capacity(num_rows);
2513 for r in 0..num_rows {
2514 total_height += row_heights[r];
2515 row_positions.push(total_height);
2516 total_height += row_depths[r];
2517 }
2518
2519 let offset = total_height / 2.0 + metrics.axis_height;
2520
2521 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2523
2524 let total_width: f64 = col_widths.iter().sum::<f64>()
2526 + col_gap * (num_cols.saturating_sub(1)) as f64
2527 + 2.0 * content_x_offset;
2528
2529 let height = offset;
2530 let depth = total_height - offset;
2531
2532 LayoutBox {
2533 width: total_width,
2534 height,
2535 depth,
2536 content: BoxContent::Array {
2537 cells: cell_boxes,
2538 col_widths: col_widths.clone(),
2539 col_aligns,
2540 row_heights: row_heights.clone(),
2541 row_depths: row_depths.clone(),
2542 col_gap,
2543 offset,
2544 content_x_offset,
2545 col_separators,
2546 hlines_before_row,
2547 rule_thickness,
2548 double_rule_sep,
2549 },
2550 color: options.color,
2551 }
2552}
2553
2554fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2559 let multiplier = match size {
2561 1 => 0.5,
2562 2 => 0.6,
2563 3 => 0.7,
2564 4 => 0.8,
2565 5 => 0.9,
2566 6 => 1.0,
2567 7 => 1.2,
2568 8 => 1.44,
2569 9 => 1.728,
2570 10 => 2.074,
2571 11 => 2.488,
2572 _ => 1.0,
2573 };
2574
2575 let inner_opts = options.with_style(options.style.text());
2577 let inner = layout_expression(body, &inner_opts, true);
2578 let ratio = multiplier / options.size_multiplier();
2579 if (ratio - 1.0).abs() < 0.001 {
2580 inner
2581 } else {
2582 LayoutBox {
2583 width: inner.width * ratio,
2584 height: inner.height * ratio,
2585 depth: inner.depth * ratio,
2586 content: BoxContent::Scaled {
2587 body: Box::new(inner),
2588 child_scale: ratio,
2589 },
2590 color: options.color,
2591 }
2592 }
2593}
2594
2595fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2598 let metrics = options.metrics();
2599 let mut children = Vec::new();
2600 for c in body.chars() {
2601 let ch = if star && c == ' ' {
2602 '\u{2423}' } else {
2604 c
2605 };
2606 let code = ch as u32;
2607 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2608 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2609 None => match get_char_metrics(FontId::MainRegular, code) {
2610 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2611 None => (
2612 FontId::TypewriterRegular,
2613 0.5,
2614 metrics.x_height,
2615 0.0,
2616 ),
2617 },
2618 };
2619 children.push(LayoutBox {
2620 width: w,
2621 height: h,
2622 depth: d,
2623 content: BoxContent::Glyph {
2624 font_id,
2625 char_code: code,
2626 },
2627 color: options.color,
2628 });
2629 }
2630 let mut hbox = make_hbox(children);
2631 hbox.color = options.color;
2632 hbox
2633}
2634
2635fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2643 let mut children = Vec::new();
2644 for node in body {
2645 match node {
2646 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2647 children.push(layout_symbol(text, *mode, options));
2648 }
2649 ParseNode::SpacingNode { text, .. } => {
2650 children.push(layout_spacing_command(text, options));
2651 }
2652 _ => {
2653 children.push(layout_node(node, options));
2654 }
2655 }
2656 }
2657 make_hbox(children)
2658}
2659
2660fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2663 let base = layout_expression(body, options, true);
2664 let w = base.width;
2665 let h = base.height;
2666 let d = base.depth;
2667
2668 let shadow = layout_expression(body, options, true);
2670 let shadow_shift_x = 0.02_f64;
2671 let _shadow_shift_y = 0.01_f64;
2672
2673 let kern_back = LayoutBox::new_kern(-w);
2677 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2678
2679 let children = vec![
2686 kern_x,
2687 shadow,
2688 kern_back,
2689 base,
2690 ];
2691 let hbox = make_hbox(children);
2693 LayoutBox {
2695 width: w,
2696 height: h,
2697 depth: d,
2698 content: hbox.content,
2699 color: options.color,
2700 }
2701}
2702
2703fn layout_enclose(
2706 label: &str,
2707 background_color: Option<&str>,
2708 border_color: Option<&str>,
2709 body: &ParseNode,
2710 options: &LayoutOptions,
2711) -> LayoutBox {
2712 use crate::layout_box::BoxContent;
2713 use ratex_types::color::Color;
2714
2715 if label == "\\phase" {
2717 return layout_phase(body, options);
2718 }
2719
2720 if label == "\\angl" {
2722 return layout_angl(body, options);
2723 }
2724
2725 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2727 return layout_cancel(label, body, options);
2728 }
2729
2730 let metrics = options.metrics();
2732 let padding = 3.0 / metrics.pt_per_em;
2733 let border_thickness = 0.4 / metrics.pt_per_em;
2734
2735 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2736
2737 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2738 let border = border_color
2739 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2740 .unwrap_or(Color::BLACK);
2741
2742 let inner = layout_node(body, options);
2743 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2744
2745 let width = inner.width + 2.0 * outer_pad;
2746 let height = inner.height + outer_pad;
2747 let depth = inner.depth + outer_pad;
2748
2749 LayoutBox {
2750 width,
2751 height,
2752 depth,
2753 content: BoxContent::Framed {
2754 body: Box::new(inner),
2755 padding,
2756 border_thickness,
2757 has_border,
2758 bg_color: bg,
2759 border_color: border,
2760 },
2761 color: options.color,
2762 }
2763}
2764
2765fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2767 use crate::layout_box::BoxContent;
2768 let inner = layout_node(body, options);
2769 let height = inner.height + shift;
2771 let depth = (inner.depth - shift).max(0.0);
2772 let width = inner.width;
2773 LayoutBox {
2774 width,
2775 height,
2776 depth,
2777 content: BoxContent::RaiseBox {
2778 body: Box::new(inner),
2779 shift,
2780 },
2781 color: options.color,
2782 }
2783}
2784
2785fn is_single_char_body(node: &ParseNode) -> bool {
2788 use ratex_parser::parse_node::ParseNode as PN;
2789 match node {
2790 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2792 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2793 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2795 _ => false,
2796 }
2797}
2798
2799fn layout_cancel(
2805 label: &str,
2806 body: &ParseNode,
2807 options: &LayoutOptions,
2808) -> LayoutBox {
2809 use crate::layout_box::BoxContent;
2810 let inner = layout_node(body, options);
2811 let w = inner.width.max(0.01);
2812 let h = inner.height;
2813 let d = inner.depth;
2814
2815 let single = is_single_char_body(body);
2818 let (v_pad, h_pad) = if label == "\\sout" {
2819 (0.0, 0.0)
2820 } else if single {
2821 (0.2, 0.0)
2822 } else {
2823 (0.0, 0.2)
2824 };
2825
2826 let commands: Vec<PathCommand> = match label {
2830 "\\cancel" => vec![
2831 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2834 "\\bcancel" => vec![
2835 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2838 "\\xcancel" => vec![
2839 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2840 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2841 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2842 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2843 ],
2844 "\\sout" => {
2845 let mid_y = -0.5 * options.metrics().x_height;
2847 vec![
2848 PathCommand::MoveTo { x: 0.0, y: mid_y },
2849 PathCommand::LineTo { x: w, y: mid_y },
2850 ]
2851 }
2852 _ => vec![],
2853 };
2854
2855 let line_w = w + 2.0 * h_pad;
2856 let line_h = h + v_pad;
2857 let line_d = d + v_pad;
2858 let line_box = LayoutBox {
2859 width: line_w,
2860 height: line_h,
2861 depth: line_d,
2862 content: BoxContent::SvgPath { commands, fill: false },
2863 color: options.color,
2864 };
2865
2866 let body_kern = -(line_w - h_pad);
2868 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2869 LayoutBox {
2870 width: w,
2871 height: h,
2872 depth: d,
2873 content: BoxContent::HBox(vec![line_box, body_shifted]),
2874 color: options.color,
2875 }
2876}
2877
2878fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2881 use crate::layout_box::BoxContent;
2882 let metrics = options.metrics();
2883 let inner = layout_node(body, options);
2884 let line_weight = 0.6_f64 / metrics.pt_per_em;
2886 let clearance = 0.35_f64 * metrics.x_height;
2887 let angle_height = inner.height + inner.depth + line_weight + clearance;
2888 let left_pad = angle_height / 2.0 + line_weight;
2889 let width = inner.width + left_pad;
2890
2891 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2893
2894 let sy = angle_height / y_svg;
2896 let sx = sy;
2899 let right_x = (400_000.0_f64 * sx).min(width);
2900
2901 let bottom_y = inner.depth + line_weight + clearance;
2903 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2904
2905 let x_peak = y_svg / 2.0;
2907 let commands = vec![
2908 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2909 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2910 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2911 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2912 PathCommand::LineTo {
2913 x: 145.0 * sx,
2914 y: vy(y_svg - 80.0),
2915 },
2916 PathCommand::LineTo {
2917 x: right_x,
2918 y: vy(y_svg - 80.0),
2919 },
2920 PathCommand::Close,
2921 ];
2922
2923 let body_shifted = make_hbox(vec![
2924 LayoutBox::new_kern(left_pad),
2925 inner.clone(),
2926 ]);
2927
2928 let path_height = inner.height;
2929 let path_depth = bottom_y;
2930
2931 LayoutBox {
2932 width,
2933 height: path_height,
2934 depth: path_depth,
2935 content: BoxContent::HBox(vec![
2936 LayoutBox {
2937 width,
2938 height: path_height,
2939 depth: path_depth,
2940 content: BoxContent::SvgPath { commands, fill: true },
2941 color: options.color,
2942 },
2943 LayoutBox::new_kern(-width),
2944 body_shifted,
2945 ]),
2946 color: options.color,
2947 }
2948}
2949
2950fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2953 use crate::layout_box::BoxContent;
2954 let inner = layout_node(body, options);
2955 let w = inner.width.max(0.3);
2956 let clearance = 0.1_f64;
2958 let arc_h = inner.height + clearance;
2959
2960 let path_commands = vec![
2962 PathCommand::MoveTo { x: 0.0, y: -arc_h },
2963 PathCommand::LineTo { x: w, y: -arc_h },
2964 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
2965 ];
2966
2967 let height = arc_h;
2968 LayoutBox {
2969 width: w,
2970 height,
2971 depth: inner.depth,
2972 content: BoxContent::Angl {
2973 path_commands,
2974 body: Box::new(inner),
2975 },
2976 color: options.color,
2977 }
2978}
2979
2980fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2981 let font_id = match font {
2982 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
2983 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
2984 "mathit" | "\\mathit" | "textit" | "\\textit" => Some(FontId::MainItalic),
2985 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
2986 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
2987 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
2988 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
2989 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
2990 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
2991 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
2992 _ => None,
2993 };
2994
2995 if let Some(fid) = font_id {
2996 layout_with_font(body, fid, options)
2997 } else {
2998 layout_node(body, options)
2999 }
3000}
3001
3002fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3003 match node {
3004 ParseNode::OrdGroup { body, .. } => {
3005 let kern = options.inter_glyph_kern_em;
3006 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3007 for (i, n) in body.iter().enumerate() {
3008 if i > 0 && kern > 0.0 {
3009 children.push(LayoutBox::new_kern(kern));
3010 }
3011 children.push(layout_with_font(n, font_id, options));
3012 }
3013 make_hbox(children)
3014 }
3015 ParseNode::SupSub {
3016 base, sup, sub, ..
3017 } => {
3018 if let Some(base_node) = base.as_deref() {
3019 if should_use_op_limits(base_node, options) {
3020 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3021 }
3022 }
3023 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3024 }
3025 ParseNode::MathOrd { text, .. }
3026 | ParseNode::TextOrd { text, .. }
3027 | ParseNode::Atom { text, .. } => {
3028 let ch = resolve_symbol_char(text, Mode::Math);
3029 let char_code = ch as u32;
3030 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3031 .map(|(_, m)| m)
3032 .unwrap_or(char_code);
3033 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3034 LayoutBox {
3035 width: math_glyph_advance_em(&m, Mode::Math),
3036 height: m.height,
3037 depth: m.depth,
3038 content: BoxContent::Glyph { font_id, char_code },
3039 color: options.color,
3040 }
3041 } else {
3042 layout_node(node, options)
3044 }
3045 }
3046 _ => layout_node(node, options),
3047 }
3048}
3049
3050fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3055 let cramped = options.with_style(options.style.cramped());
3056 let body_box = layout_node(body, &cramped);
3057 let metrics = options.metrics();
3058 let rule = metrics.default_rule_thickness;
3059
3060 let height = body_box.height + 3.0 * rule;
3062 LayoutBox {
3063 width: body_box.width,
3064 height,
3065 depth: body_box.depth,
3066 content: BoxContent::Overline {
3067 body: Box::new(body_box),
3068 rule_thickness: rule,
3069 },
3070 color: options.color,
3071 }
3072}
3073
3074fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3075 let body_box = layout_node(body, options);
3076 let metrics = options.metrics();
3077 let rule = metrics.default_rule_thickness;
3078
3079 let depth = body_box.depth + 3.0 * rule;
3081 LayoutBox {
3082 width: body_box.width,
3083 height: body_box.height,
3084 depth,
3085 content: BoxContent::Underline {
3086 body: Box::new(body_box),
3087 rule_thickness: rule,
3088 },
3089 color: options.color,
3090 }
3091}
3092
3093fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3095 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3096 let body_opts = options
3098 .with_color(link_color)
3099 .with_inter_glyph_kern(0.024);
3100 let body_box = layout_expression(body, &body_opts, true);
3101 layout_underline_laid_out(body_box, options, link_color)
3102}
3103
3104fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3106 let metrics = options.metrics();
3107 let rule = metrics.default_rule_thickness;
3108 let depth = body_box.depth + 3.0 * rule;
3109 LayoutBox {
3110 width: body_box.width,
3111 height: body_box.height,
3112 depth,
3113 content: BoxContent::Underline {
3114 body: Box::new(body_box),
3115 rule_thickness: rule,
3116 },
3117 color,
3118 }
3119}
3120
3121fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3126 let metrics = options.metrics();
3127 let mu = metrics.css_em_per_mu();
3128
3129 let width = match text {
3130 "\\," | "\\thinspace" => 3.0 * mu,
3131 "\\:" | "\\medspace" => 4.0 * mu,
3132 "\\;" | "\\thickspace" => 5.0 * mu,
3133 "\\!" | "\\negthinspace" => -3.0 * mu,
3134 "\\negmedspace" => -4.0 * mu,
3135 "\\negthickspace" => -5.0 * mu,
3136 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3137 get_char_metrics(FontId::MainRegular, 160)
3141 .map(|m| m.width)
3142 .unwrap_or(0.25)
3143 }
3144 "\\quad" => metrics.quad,
3145 "\\qquad" => 2.0 * metrics.quad,
3146 "\\enspace" => metrics.quad / 2.0,
3147 _ => 0.0,
3148 };
3149
3150 LayoutBox::new_kern(width)
3151}
3152
3153fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3158 let metrics = options.metrics();
3159 match m.unit.as_str() {
3160 "em" => m.number,
3161 "ex" => m.number * metrics.x_height,
3162 "mu" => m.number * metrics.css_em_per_mu(),
3163 "pt" => m.number / metrics.pt_per_em,
3164 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3165 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3166 "in" => m.number * 72.27 / metrics.pt_per_em,
3167 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3168 "pc" => m.number * 12.0 / metrics.pt_per_em,
3169 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3170 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3171 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3172 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3173 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3174 _ => m.number,
3175 }
3176}
3177
3178fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3184 match node {
3185 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3186 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3187 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3188 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3189 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3191 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3192 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3193 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3194 }
3195 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3196 ParseNode::SupSub { base, .. } => {
3197 base.as_ref().and_then(|b| node_math_class(b))
3198 }
3199 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3200 ParseNode::SpacingNode { .. } => None,
3201 ParseNode::Kern { .. } => None,
3202 ParseNode::HtmlMathMl { html, .. } => {
3203 for child in html {
3205 if let Some(cls) = node_math_class(child) {
3206 return Some(cls);
3207 }
3208 }
3209 None
3210 }
3211 ParseNode::Lap { .. } => None,
3212 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3213 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3214 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3216 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3218 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3219 ParseNode::Middle { .. } => Some(MathClass::Ord),
3220 _ => Some(MathClass::Ord),
3221 }
3222}
3223
3224fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3225 match mclass {
3226 "mord" => MathClass::Ord,
3227 "mop" => MathClass::Op,
3228 "mbin" => MathClass::Bin,
3229 "mrel" => MathClass::Rel,
3230 "mopen" => MathClass::Open,
3231 "mclose" => MathClass::Close,
3232 "mpunct" => MathClass::Punct,
3233 "minner" => MathClass::Inner,
3234 _ => MathClass::Ord,
3235 }
3236}
3237
3238fn get_base_elem(node: &ParseNode) -> &ParseNode {
3242 match node {
3243 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3244 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3245 ParseNode::Font { body, .. } => get_base_elem(body),
3246 _ => node,
3247 }
3248}
3249
3250fn is_character_box(node: &ParseNode) -> bool {
3251 matches!(
3252 get_base_elem(node),
3253 ParseNode::MathOrd { .. }
3254 | ParseNode::TextOrd { .. }
3255 | ParseNode::Atom { .. }
3256 | ParseNode::AccentToken { .. }
3257 )
3258}
3259
3260fn family_to_math_class(family: AtomFamily) -> MathClass {
3261 match family {
3262 AtomFamily::Bin => MathClass::Bin,
3263 AtomFamily::Rel => MathClass::Rel,
3264 AtomFamily::Open => MathClass::Open,
3265 AtomFamily::Close => MathClass::Close,
3266 AtomFamily::Punct => MathClass::Punct,
3267 AtomFamily::Inner => MathClass::Inner,
3268 }
3269}
3270
3271fn layout_horiz_brace(
3276 base: &ParseNode,
3277 is_over: bool,
3278 func_label: &str,
3279 options: &LayoutOptions,
3280) -> LayoutBox {
3281 let body_box = layout_node(base, options);
3282 let w = body_box.width.max(0.5);
3283
3284 let is_bracket = func_label
3285 .trim_start_matches('\\')
3286 .ends_with("bracket");
3287
3288 let stretch_key = if is_bracket {
3290 if is_over {
3291 "overbracket"
3292 } else {
3293 "underbracket"
3294 }
3295 } else if is_over {
3296 "overbrace"
3297 } else {
3298 "underbrace"
3299 };
3300
3301 let (raw_commands, brace_h, brace_fill) =
3302 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3303 Some((c, h)) => (c, h, true),
3304 None => {
3305 let h = 0.35_f64;
3306 (horiz_brace_path(w, h, is_over), h, false)
3307 }
3308 };
3309
3310 let y_shift = brace_h / 2.0;
3316 let commands = shift_path_y(raw_commands, y_shift);
3317
3318 let brace_box = LayoutBox {
3319 width: w,
3320 height: 0.0,
3321 depth: brace_h,
3322 content: BoxContent::SvgPath {
3323 commands,
3324 fill: brace_fill,
3325 },
3326 color: options.color,
3327 };
3328
3329 let gap = 0.1;
3330 let (height, depth) = if is_over {
3331 (body_box.height + brace_h + gap, body_box.depth)
3332 } else {
3333 (body_box.height, body_box.depth + brace_h + gap)
3334 };
3335
3336 let clearance = if is_over {
3337 height - brace_h
3338 } else {
3339 body_box.height + body_box.depth + gap
3340 };
3341 let total_w = body_box.width;
3342
3343 LayoutBox {
3344 width: total_w,
3345 height,
3346 depth,
3347 content: BoxContent::Accent {
3348 base: Box::new(body_box),
3349 accent: Box::new(brace_box),
3350 clearance,
3351 skew: 0.0,
3352 is_below: !is_over,
3353 under_gap_em: 0.0,
3354 },
3355 color: options.color,
3356 }
3357}
3358
3359fn layout_xarrow(
3364 label: &str,
3365 body: &ParseNode,
3366 below: Option<&ParseNode>,
3367 options: &LayoutOptions,
3368) -> LayoutBox {
3369 let sup_style = options.style.superscript();
3370 let sub_style = options.style.subscript();
3371 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3372 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3373
3374 let sup_opts = options.with_style(sup_style);
3375 let body_box = layout_node(body, &sup_opts);
3376 let body_w = body_box.width * sup_ratio;
3377
3378 let below_box = below.map(|b| {
3379 let sub_opts = options.with_style(sub_style);
3380 layout_node(b, &sub_opts)
3381 });
3382 let below_w = below_box
3383 .as_ref()
3384 .map(|b| b.width * sub_ratio)
3385 .unwrap_or(0.0);
3386
3387 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3390 let upper_w = body_w + sup_ratio;
3391 let lower_w = if below_box.is_some() {
3392 below_w + sub_ratio
3393 } else {
3394 0.0
3395 };
3396 let arrow_w = upper_w.max(lower_w).max(min_w);
3397 let arrow_h = 0.3;
3398
3399 let (commands, actual_arrow_h, fill_arrow) =
3400 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3401 Some((c, h)) => (c, h, true),
3402 None => (
3403 stretchy_accent_path(label, arrow_w, arrow_h),
3404 arrow_h,
3405 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3406 ),
3407 };
3408 let arrow_box = LayoutBox {
3409 width: arrow_w,
3410 height: actual_arrow_h / 2.0,
3411 depth: actual_arrow_h / 2.0,
3412 content: BoxContent::SvgPath {
3413 commands,
3414 fill: fill_arrow,
3415 },
3416 color: options.color,
3417 };
3418
3419 let metrics = options.metrics();
3422 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3424 let gap = 0.111; let base_shift = -axis;
3428
3429 let sup_kern = gap;
3437 let sub_kern = gap;
3438
3439 let sup_h = body_box.height * sup_ratio;
3440 let sup_d = body_box.depth * sup_ratio;
3441
3442 let height = axis + arrow_half + gap + sup_h + sup_d;
3444 let mut depth = (arrow_half - axis).max(0.0);
3446
3447 if let Some(ref bel) = below_box {
3448 let sub_h = bel.height * sub_ratio;
3449 let sub_d = bel.depth * sub_ratio;
3450 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3452 }
3453
3454 LayoutBox {
3455 width: arrow_w,
3456 height,
3457 depth,
3458 content: BoxContent::OpLimits {
3459 base: Box::new(arrow_box),
3460 sup: Some(Box::new(body_box)),
3461 sub: below_box.map(Box::new),
3462 base_shift,
3463 sup_kern,
3464 sub_kern,
3465 slant: 0.0,
3466 sup_scale: sup_ratio,
3467 sub_scale: sub_ratio,
3468 },
3469 color: options.color,
3470 }
3471}
3472
3473fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3478 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3481 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3482 let diameter = radius * 2.0;
3483
3484 let cx = radius;
3486 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3489
3490 let circle_commands = vec![
3491 PathCommand::MoveTo { x: cx + r, y: cy },
3492 PathCommand::CubicTo {
3493 x1: cx + r, y1: cy - k * r,
3494 x2: cx + k * r, y2: cy - r,
3495 x: cx, y: cy - r,
3496 },
3497 PathCommand::CubicTo {
3498 x1: cx - k * r, y1: cy - r,
3499 x2: cx - r, y2: cy - k * r,
3500 x: cx - r, y: cy,
3501 },
3502 PathCommand::CubicTo {
3503 x1: cx - r, y1: cy + k * r,
3504 x2: cx - k * r, y2: cy + r,
3505 x: cx, y: cy + r,
3506 },
3507 PathCommand::CubicTo {
3508 x1: cx + k * r, y1: cy + r,
3509 x2: cx + r, y2: cy + k * r,
3510 x: cx + r, y: cy,
3511 },
3512 PathCommand::Close,
3513 ];
3514
3515 let circle_box = LayoutBox {
3516 width: diameter,
3517 height: r - cy.min(0.0),
3518 depth: (r + cy).max(0.0),
3519 content: BoxContent::SvgPath {
3520 commands: circle_commands,
3521 fill: false,
3522 },
3523 color: options.color,
3524 };
3525
3526 let content_shift = (diameter - body_box.width) / 2.0;
3528 let children = vec![
3530 circle_box,
3531 LayoutBox::new_kern(-(diameter) + content_shift),
3532 body_box.clone(),
3533 ];
3534
3535 let height = r - cy.min(0.0);
3536 let depth = (r + cy).max(0.0);
3537
3538 LayoutBox {
3539 width: diameter,
3540 height,
3541 depth,
3542 content: BoxContent::HBox(children),
3543 color: options.color,
3544 }
3545}
3546
3547fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3571 let r: f64 = 0.1125;
3573 let cy: f64 = -0.2625;
3577 let k: f64 = 0.5523;
3579 let cx: f64 = r;
3581
3582 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3585
3586 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3595 vec![
3596 PathCommand::MoveTo { x: ox + rad, y: cy },
3597 PathCommand::CubicTo {
3598 x1: ox + rad, y1: cy - k * rad,
3599 x2: ox + k * rad, y2: cy - rad,
3600 x: ox, y: cy - rad,
3601 },
3602 PathCommand::CubicTo {
3603 x1: ox - k * rad, y1: cy - rad,
3604 x2: ox - rad, y2: cy - k * rad,
3605 x: ox - rad, y: cy,
3606 },
3607 PathCommand::CubicTo {
3608 x1: ox - rad, y1: cy + k * rad,
3609 x2: ox - k * rad, y2: cy + rad,
3610 x: ox, y: cy + rad,
3611 },
3612 PathCommand::CubicTo {
3613 x1: ox + k * rad, y1: cy + rad,
3614 x2: ox + rad, y2: cy + k * rad,
3615 x: ox + rad, y: cy,
3616 },
3617 PathCommand::Close,
3618 ]
3619 };
3620
3621 let disk = LayoutBox {
3622 width: 2.0 * r,
3623 height: h,
3624 depth: d,
3625 content: BoxContent::SvgPath {
3626 commands: circle_commands(cx, r),
3627 fill: true,
3628 },
3629 color: options.color,
3630 };
3631
3632 let ring = LayoutBox {
3633 width: 2.0 * r,
3634 height: h,
3635 depth: d,
3636 content: BoxContent::SvgPath {
3637 commands: circle_commands(cx, r_ring),
3638 fill: false,
3639 },
3640 color: options.color,
3641 };
3642
3643 let bar_len: f64 = 0.25;
3647 let bar_th: f64 = 0.04;
3648 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3651
3652 let children = if imageof {
3653 vec![disk, bar, ring]
3654 } else {
3655 vec![ring, bar, disk]
3656 };
3657
3658 let total_width = 4.0 * r + bar_len;
3660 LayoutBox {
3661 width: total_width,
3662 height: h,
3663 depth: d,
3664 content: BoxContent::HBox(children),
3665 color: options.color,
3666 }
3667}
3668
3669fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3673 let cx = width / 2.0;
3674 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3679 PathCommand::MoveTo { x: cx + a, y: cy },
3680 PathCommand::CubicTo {
3681 x1: cx + a,
3682 y1: cy - k * b,
3683 x2: cx + k * a,
3684 y2: cy - b,
3685 x: cx,
3686 y: cy - b,
3687 },
3688 PathCommand::CubicTo {
3689 x1: cx - k * a,
3690 y1: cy - b,
3691 x2: cx - a,
3692 y2: cy - k * b,
3693 x: cx - a,
3694 y: cy,
3695 },
3696 PathCommand::CubicTo {
3697 x1: cx - a,
3698 y1: cy + k * b,
3699 x2: cx - k * a,
3700 y2: cy + b,
3701 x: cx,
3702 y: cy + b,
3703 },
3704 PathCommand::CubicTo {
3705 x1: cx + k * a,
3706 y1: cy + b,
3707 x2: cx + a,
3708 y2: cy + k * b,
3709 x: cx + a,
3710 y: cy,
3711 },
3712 PathCommand::Close,
3713 ]
3714}
3715
3716fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3717 cmds.into_iter().map(|c| match c {
3718 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3719 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3720 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3721 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3722 },
3723 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3724 x1, y1: y1 + dy, x, y: y + dy,
3725 },
3726 PathCommand::Close => PathCommand::Close,
3727 }).collect()
3728}
3729
3730fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3731 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3732 return commands;
3733 }
3734 let ah = height * 0.35; let mid_y = -height / 2.0;
3736
3737 match label {
3738 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3739 vec![
3740 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3741 PathCommand::LineTo { x: 0.0, y: mid_y },
3742 PathCommand::LineTo { x: ah, y: mid_y + ah },
3743 PathCommand::MoveTo { x: 0.0, y: mid_y },
3744 PathCommand::LineTo { x: width, y: mid_y },
3745 ]
3746 }
3747 "\\overleftrightarrow" | "\\underleftrightarrow"
3748 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3749 vec![
3750 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3751 PathCommand::LineTo { x: 0.0, y: mid_y },
3752 PathCommand::LineTo { x: ah, y: mid_y + ah },
3753 PathCommand::MoveTo { x: 0.0, y: mid_y },
3754 PathCommand::LineTo { x: width, y: mid_y },
3755 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3756 PathCommand::LineTo { x: width, y: mid_y },
3757 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3758 ]
3759 }
3760 "\\xlongequal" => {
3761 let gap = 0.04;
3762 vec![
3763 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3764 PathCommand::LineTo { x: width, y: mid_y - gap },
3765 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3766 PathCommand::LineTo { x: width, y: mid_y + gap },
3767 ]
3768 }
3769 "\\xhookleftarrow" => {
3770 vec![
3771 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3772 PathCommand::LineTo { x: 0.0, y: mid_y },
3773 PathCommand::LineTo { x: ah, y: mid_y + ah },
3774 PathCommand::MoveTo { x: 0.0, y: mid_y },
3775 PathCommand::LineTo { x: width, y: mid_y },
3776 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3777 ]
3778 }
3779 "\\xhookrightarrow" => {
3780 vec![
3781 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3782 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3783 PathCommand::LineTo { x: width, y: mid_y },
3784 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3785 PathCommand::LineTo { x: width, y: mid_y },
3786 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3787 ]
3788 }
3789 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3790 let right = label.contains("right");
3791 if right {
3792 vec![
3793 PathCommand::MoveTo { x: 0.0, y: mid_y },
3794 PathCommand::LineTo { x: width, y: mid_y },
3795 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3796 PathCommand::LineTo { x: width, y: mid_y },
3797 ]
3798 } else {
3799 vec![
3800 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3801 PathCommand::LineTo { x: 0.0, y: mid_y },
3802 PathCommand::LineTo { x: width, y: mid_y },
3803 ]
3804 }
3805 }
3806 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3807 let right = label.contains("right");
3808 if right {
3809 vec![
3810 PathCommand::MoveTo { x: 0.0, y: mid_y },
3811 PathCommand::LineTo { x: width, y: mid_y },
3812 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3813 PathCommand::LineTo { x: width, y: mid_y },
3814 ]
3815 } else {
3816 vec![
3817 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3818 PathCommand::LineTo { x: 0.0, y: mid_y },
3819 PathCommand::LineTo { x: width, y: mid_y },
3820 ]
3821 }
3822 }
3823 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3824 let gap = 0.06;
3825 vec![
3826 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3827 PathCommand::LineTo { x: width, y: mid_y - gap },
3828 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3829 PathCommand::LineTo { x: width, y: mid_y - gap },
3830 PathCommand::MoveTo { x: width, y: mid_y + gap },
3831 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3832 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3833 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3834 ]
3835 }
3836 "\\xtofrom" | "\\xrightleftarrows" => {
3837 let gap = 0.06;
3838 vec![
3839 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3840 PathCommand::LineTo { x: width, y: mid_y - gap },
3841 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3842 PathCommand::LineTo { x: width, y: mid_y - gap },
3843 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3844 PathCommand::MoveTo { x: width, y: mid_y + gap },
3845 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3846 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3847 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3848 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3849 ]
3850 }
3851 "\\overlinesegment" | "\\underlinesegment" => {
3852 vec![
3853 PathCommand::MoveTo { x: 0.0, y: mid_y },
3854 PathCommand::LineTo { x: width, y: mid_y },
3855 ]
3856 }
3857 _ => {
3858 vec![
3859 PathCommand::MoveTo { x: 0.0, y: mid_y },
3860 PathCommand::LineTo { x: width, y: mid_y },
3861 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3862 PathCommand::LineTo { x: width, y: mid_y },
3863 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3864 ]
3865 }
3866 }
3867}
3868
3869fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
3875 let h = inner.height;
3876 let d = inner.depth;
3877 let w = inner.width + pad_l + pad_r;
3878 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
3879 if pad_l > 0.0 {
3880 children.push(LayoutBox::new_kern(pad_l));
3881 }
3882 children.push(inner);
3883 if pad_r > 0.0 {
3884 children.push(LayoutBox::new_kern(pad_r));
3885 }
3886 LayoutBox {
3887 width: w,
3888 height: h,
3889 depth: d,
3890 content: BoxContent::HBox(children),
3891 color,
3892 }
3893}
3894
3895fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
3906 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
3907 LayoutBox {
3908 width: label.width,
3909 height: box_h,
3910 depth: box_d,
3911 content: BoxContent::RaiseBox {
3912 body: Box::new(label),
3913 shift,
3914 },
3915 color,
3916 }
3917}
3918
3919fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3923 let sup_style = options.style.superscript();
3924 let sup_opts = options.with_style(sup_style);
3925 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3926 let inner = layout_node(body, &sup_opts);
3927 if (sup_ratio - 1.0).abs() < 1e-6 {
3928 inner
3929 } else {
3930 LayoutBox {
3931 width: inner.width * sup_ratio,
3932 height: inner.height * sup_ratio,
3933 depth: inner.depth * sup_ratio,
3934 content: BoxContent::Scaled {
3935 body: Box::new(inner),
3936 child_scale: sup_ratio,
3937 },
3938 color: options.color,
3939 }
3940 }
3941}
3942
3943fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
3949 let axis = options.metrics().axis_height;
3950 let depth = (total_height / 2.0 - axis).max(0.0);
3951 let height = total_height - depth;
3952 if let Some((commands, w)) =
3953 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
3954 {
3955 return LayoutBox {
3956 width: w,
3957 height,
3958 depth,
3959 content: BoxContent::SvgPath {
3960 commands,
3961 fill: true,
3962 },
3963 color: options.color,
3964 };
3965 }
3966 if down {
3968 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
3969 } else {
3970 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
3971 }
3972}
3973
3974fn layout_cd_arrow(
3990 direction: &str,
3991 label_above: Option<&ParseNode>,
3992 label_below: Option<&ParseNode>,
3993 target_size: f64,
3994 target_col_width: f64,
3995 _target_depth: f64,
3996 options: &LayoutOptions,
3997) -> LayoutBox {
3998 let metrics = options.metrics();
3999 let axis = metrics.axis_height;
4000
4001 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4004
4005 match direction {
4006 "right" | "left" | "horiz_eq" => {
4007 let sup_style = options.style.superscript();
4009 let sub_style = options.style.subscript();
4010 let sup_opts = options.with_style(sup_style);
4011 let sub_opts = options.with_style(sub_style);
4012 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4013 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4014
4015 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4016 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4017
4018 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4019 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4020
4021 let path_label = if direction == "right" {
4023 "\\cdrightarrow"
4024 } else if direction == "left" {
4025 "\\cdleftarrow"
4026 } else {
4027 "\\cdlongequal"
4028 };
4029 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4030 const CD_LABEL_PAD_L: f64 = 0.22;
4033 const CD_LABEL_PAD_R: f64 = 0.48;
4034 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4035 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4036 let upper_need = above_box
4037 .as_ref()
4038 .map(|_| above_w + cd_pad_sup)
4039 .unwrap_or(0.0);
4040 let lower_need = below_box
4041 .as_ref()
4042 .map(|_| below_w + cd_pad_sub)
4043 .unwrap_or(0.0);
4044 let natural_w = upper_need.max(lower_need).max(0.0);
4045 let shaft_w = if target_size > 0.0 {
4046 target_size
4047 } else {
4048 natural_w.max(min_shaft_w)
4049 };
4050
4051 let (commands, actual_arrow_h, fill_arrow) =
4052 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4053 Some((c, h)) => (c, h, true),
4054 None => {
4055 let arrow_h = 0.3_f64;
4057 let ah = 0.12_f64;
4058 let cmds = if direction == "horiz_eq" {
4059 let gap = 0.06;
4060 vec![
4061 PathCommand::MoveTo { x: 0.0, y: -gap },
4062 PathCommand::LineTo { x: shaft_w, y: -gap },
4063 PathCommand::MoveTo { x: 0.0, y: gap },
4064 PathCommand::LineTo { x: shaft_w, y: gap },
4065 ]
4066 } else if direction == "right" {
4067 vec![
4068 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4069 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4070 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4071 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4072 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4073 ]
4074 } else {
4075 vec![
4076 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4077 PathCommand::LineTo { x: 0.0, y: 0.0 },
4078 PathCommand::MoveTo { x: ah, y: -ah },
4079 PathCommand::LineTo { x: 0.0, y: 0.0 },
4080 PathCommand::LineTo { x: ah, y: ah },
4081 ]
4082 };
4083 (cmds, arrow_h, false)
4084 }
4085 };
4086
4087 let arrow_half = actual_arrow_h / 2.0;
4089 let arrow_box = LayoutBox {
4090 width: shaft_w,
4091 height: arrow_half,
4092 depth: arrow_half,
4093 content: BoxContent::SvgPath {
4094 commands,
4095 fill: fill_arrow,
4096 },
4097 color: options.color,
4098 };
4099
4100 let gap = 0.111;
4102 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4103 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4104 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4108 sup_d
4109 } else {
4110 0.0
4111 };
4112 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4113 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4114 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4115 let depth = if below_box.is_some() {
4116 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4117 } else {
4118 (arrow_half - axis).max(0.0)
4119 };
4120
4121 let inner = LayoutBox {
4122 width: shaft_w,
4123 height,
4124 depth,
4125 content: BoxContent::OpLimits {
4126 base: Box::new(arrow_box),
4127 sup: above_box.map(Box::new),
4128 sub: below_box.map(Box::new),
4129 base_shift: -axis,
4130 sup_kern: gap,
4131 sub_kern: gap,
4132 slant: 0.0,
4133 sup_scale: sup_ratio,
4134 sub_scale: sub_ratio,
4135 },
4136 color: options.color,
4137 };
4138
4139 if target_col_width > inner.width + 1e-6 {
4143 let extra = target_col_width - inner.width;
4144 let kl = extra / 2.0;
4145 let kr = extra - kl;
4146 cd_wrap_hpad(inner, kl, kr, options.color)
4147 } else {
4148 inner
4149 }
4150 }
4151
4152 "down" | "up" | "vert_eq" => {
4153 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4157 "vert_eq" if target_size > 0.0 => {
4158 make_vert_delim_box(target_size.max(big_total), true, options)
4159 }
4160 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4161 "down" if target_size > 0.0 => {
4162 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4163 }
4164 "up" if target_size > 0.0 => {
4165 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4166 }
4167 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4168 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4169 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4170 };
4171 let box_h = shaft_box.height;
4172 let box_d = shaft_box.depth;
4173 let shaft_w = shaft_box.width;
4174
4175 let left_box = label_above.map(|n| {
4178 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4179 });
4180 let right_box = label_below.map(|n| {
4181 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4182 });
4183
4184 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4185 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4186 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4187 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4188 let inner_w = left_part + shaft_w + right_part;
4189
4190 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4192 let extra = target_col_width - inner_w;
4193 let kl = extra / 2.0;
4194 let kr = extra - kl;
4195 (kl, kr, target_col_width)
4196 } else {
4197 (0.0, 0.0, inner_w)
4198 };
4199
4200 let mut children: Vec<LayoutBox> = Vec::new();
4201 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4202 if let Some(lb) = left_box {
4203 children.push(lb);
4204 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4205 }
4206 children.push(shaft_box);
4207 if let Some(rb) = right_box {
4208 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4209 children.push(rb);
4210 }
4211 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4212
4213 LayoutBox {
4214 width: total_w,
4215 height: box_h,
4216 depth: box_d,
4217 content: BoxContent::HBox(children),
4218 color: options.color,
4219 }
4220 }
4221
4222 _ => LayoutBox::new_empty(),
4224 }
4225}
4226
4227fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4229 let metrics = options.metrics();
4230 let pt = 1.0 / metrics.pt_per_em;
4231 let baselineskip = 3.0 * metrics.x_height;
4233 let arstrut_h = 0.7 * baselineskip;
4234 let arstrut_d = 0.3 * baselineskip;
4235
4236 let num_rows = body.len();
4237 if num_rows == 0 {
4238 return LayoutBox::new_empty();
4239 }
4240 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4241 if num_cols == 0 {
4242 return LayoutBox::new_empty();
4243 }
4244
4245 let jot = 3.0 * pt;
4247
4248 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4250 let mut col_widths = vec![0.0_f64; num_cols];
4251 let mut row_heights = vec![arstrut_h; num_rows];
4252 let mut row_depths = vec![arstrut_d; num_rows];
4253
4254 for (r, row) in body.iter().enumerate() {
4255 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4256
4257 for (c, cell) in row.iter().enumerate() {
4258 let cbox = match cell {
4259 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4260 layout_cd_arrow(
4261 direction,
4262 label_above.as_deref(),
4263 label_below.as_deref(),
4264 0.0, 0.0, 0.0, options,
4268 )
4269 }
4270 ParseNode::OrdGroup { body: cell_body, .. } => {
4274 layout_expression(cell_body, options, false)
4275 }
4276 other => layout_node(other, options),
4277 };
4278
4279 row_heights[r] = row_heights[r].max(cbox.height);
4280 row_depths[r] = row_depths[r].max(cbox.depth);
4281 col_widths[c] = col_widths[c].max(cbox.width);
4282 row_boxes.push(cbox);
4283 }
4284
4285 while row_boxes.len() < num_cols {
4287 row_boxes.push(LayoutBox::new_empty());
4288 }
4289 cell_boxes.push(row_boxes);
4290 }
4291
4292 let col_target_w: Vec<f64> = col_widths.clone();
4296
4297 #[cfg(debug_assertions)]
4298 {
4299 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4300 for (r, row) in cell_boxes.iter().enumerate() {
4301 for (c, b) in row.iter().enumerate() {
4302 if b.width > 0.0 {
4303 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4304 }
4305 }
4306 }
4307 }
4308
4309 for (r, row) in body.iter().enumerate() {
4311 let is_arrow_row = r % 2 == 1;
4312 for (c, cell) in row.iter().enumerate() {
4313 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4314 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4315 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4316 let b = layout_cd_arrow(
4317 direction,
4318 label_above.as_deref(),
4319 label_below.as_deref(),
4320 cell_boxes[r][c].width,
4321 col_target_w[c],
4322 0.0,
4323 options,
4324 );
4325 let w = b.width;
4326 (b, w)
4327 } else if is_arrow_row && c % 2 == 0 {
4328 let v_span = row_heights[r] + row_depths[r];
4332 let b = layout_cd_arrow(
4333 direction,
4334 label_above.as_deref(),
4335 label_below.as_deref(),
4336 v_span,
4337 col_widths[c],
4338 0.0,
4339 options,
4340 );
4341 let w = b.width;
4342 (b, w)
4343 } else {
4344 continue;
4345 };
4346 col_widths[c] = col_widths[c].max(col_w);
4347 cell_boxes[r][c] = new_box;
4348 }
4349 }
4350 }
4351
4352 #[cfg(debug_assertions)]
4353 {
4354 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4355 }
4356
4357 for rd in &mut row_depths {
4360 *rd += jot;
4361 }
4362
4363 let col_gap = 0.5;
4368
4369 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4371
4372 let col_separators = vec![None; num_cols + 1];
4374
4375 let mut total_height = 0.0_f64;
4376 let mut row_positions = Vec::with_capacity(num_rows);
4377 for r in 0..num_rows {
4378 total_height += row_heights[r];
4379 row_positions.push(total_height);
4380 total_height += row_depths[r];
4381 }
4382
4383 let offset = total_height / 2.0 + metrics.axis_height;
4384 let height = offset;
4385 let depth = total_height - offset;
4386
4387 let total_width = col_widths.iter().sum::<f64>()
4389 + col_gap * (num_cols.saturating_sub(1)) as f64;
4390
4391 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4393
4394 LayoutBox {
4395 width: total_width,
4396 height,
4397 depth,
4398 content: BoxContent::Array {
4399 cells: cell_boxes,
4400 col_widths,
4401 col_aligns,
4402 row_heights,
4403 row_depths,
4404 col_gap,
4405 offset,
4406 content_x_offset: 0.0,
4407 col_separators,
4408 hlines_before_row,
4409 rule_thickness: 0.04 * pt,
4410 double_rule_sep: metrics.double_rule_sep,
4411 },
4412 color: options.color,
4413 }
4414}
4415
4416fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4417 let mid = width / 2.0;
4418 let q = height * 0.6;
4419 if is_over {
4420 vec![
4421 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4422 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4423 PathCommand::LineTo { x: mid - 0.05, y: -q },
4424 PathCommand::LineTo { x: mid, y: -height },
4425 PathCommand::LineTo { x: mid + 0.05, y: -q },
4426 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4427 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4428 ]
4429 } else {
4430 vec![
4431 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4432 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4433 PathCommand::LineTo { x: mid - 0.05, y: q },
4434 PathCommand::LineTo { x: mid, y: height },
4435 PathCommand::LineTo { x: mid + 0.05, y: q },
4436 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4437 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4438 ]
4439 }
4440}