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 += sup_style_metrics.sup_drop * sup_ratio;
977 sup_shift += metrics.big_op_spacing1;
980 }
981 if horiz_brace_under && sub_box.is_some() {
982 sub_shift += sub_style_metrics.sub_drop * sub_ratio;
983 sub_shift += metrics.big_op_spacing2 + 0.2;
984 }
985
986 let italic_correction = 0.0;
989
990 let sub_h_kern = if sub_box.is_some() && !center_scripts {
993 -glyph_italic(&base_box)
994 } else {
995 0.0
996 };
997
998 let mut height = base_box.height;
1000 let mut depth = base_box.depth;
1001 let mut total_width = base_box.width;
1002
1003 if let Some(ref sup_b) = sup_box {
1004 height = height.max(sup_shift + sup_height_scaled);
1005 if center_scripts {
1006 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1007 } else {
1008 total_width = total_width.max(
1009 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1010 );
1011 }
1012 }
1013 if let Some(ref sub_b) = sub_box {
1014 depth = depth.max(sub_shift + sub_depth_scaled);
1015 if center_scripts {
1016 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1017 } else {
1018 total_width = total_width.max(
1019 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1020 );
1021 }
1022 }
1023
1024 LayoutBox {
1025 width: total_width,
1026 height,
1027 depth,
1028 content: BoxContent::SupSub {
1029 base: Box::new(base_box),
1030 sup: sup_box.map(Box::new),
1031 sub: sub_box.map(Box::new),
1032 sup_shift,
1033 sub_shift,
1034 sup_scale: sup_ratio,
1035 sub_scale: sub_ratio,
1036 center_scripts,
1037 italic_correction,
1038 sub_h_kern,
1039 },
1040 color: options.color,
1041 }
1042}
1043
1044fn layout_radical(
1049 body: &ParseNode,
1050 index: Option<&ParseNode>,
1051 options: &LayoutOptions,
1052) -> LayoutBox {
1053 let cramped = options.style.cramped();
1054 let cramped_opts = options.with_style(cramped);
1055 let mut body_box = layout_node(body, &cramped_opts);
1056
1057 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1059 body_box.height *= body_ratio;
1060 body_box.depth *= body_ratio;
1061 body_box.width *= body_ratio;
1062
1063 if body_box.height == 0.0 {
1065 body_box.height = options.metrics().x_height;
1066 }
1067
1068 let metrics = options.metrics();
1069 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1074 metrics.x_height
1075 } else {
1076 theta
1077 };
1078
1079 let mut line_clearance = theta + phi / 4.0;
1080
1081 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1083
1084 let tex_height = select_surd_height(min_delim_height);
1087 let rule_width = theta;
1088 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1089 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1090 .map(|m| m.width)
1091 .unwrap_or(0.833);
1092
1093 let delim_depth = tex_height - rule_width;
1095 if delim_depth > body_box.height + body_box.depth + line_clearance {
1096 line_clearance =
1097 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1098 }
1099
1100 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1101
1102 let height = tex_height + rule_width - img_shift;
1105 let depth = if img_shift > body_box.depth {
1106 img_shift
1107 } else {
1108 body_box.depth
1109 };
1110
1111 const INDEX_KERN: f64 = 0.05;
1113 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1114 let root_style = options.style.superscript().superscript();
1115 let root_opts = options.with_style(root_style);
1116 let idx = layout_node(index_node, &root_opts);
1117 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1118 let offset = idx.width * index_ratio + INDEX_KERN;
1119 (Some(Box::new(idx)), offset, index_ratio)
1120 } else {
1121 (None, 0.0, 1.0)
1122 };
1123
1124 let width = index_offset + advance_width + body_box.width;
1125
1126 LayoutBox {
1127 width,
1128 height,
1129 depth,
1130 content: BoxContent::Radical {
1131 body: Box::new(body_box),
1132 index: index_box,
1133 index_offset,
1134 index_scale,
1135 rule_thickness: rule_width,
1136 inner_height: tex_height,
1137 },
1138 color: options.color,
1139 }
1140}
1141
1142fn select_surd_height(min_height: f64) -> f64 {
1145 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1146 for &h in &SURD_HEIGHTS {
1147 if h >= min_height {
1148 return h;
1149 }
1150 }
1151 SURD_HEIGHTS[4].max(min_height)
1153}
1154
1155const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1160
1161fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1163 match base {
1164 ParseNode::Op {
1165 limits,
1166 always_handle_sup_sub,
1167 ..
1168 } => {
1169 *limits
1170 && (options.style.is_display()
1171 || always_handle_sup_sub.unwrap_or(false))
1172 }
1173 ParseNode::OperatorName {
1174 always_handle_sup_sub,
1175 limits,
1176 ..
1177 } => {
1178 *always_handle_sup_sub
1179 && (options.style.is_display() || *limits)
1180 }
1181 _ => false,
1182 }
1183}
1184
1185fn layout_op(
1191 name: Option<&str>,
1192 symbol: bool,
1193 body: Option<&[ParseNode]>,
1194 _limits: bool,
1195 suppress_base_shift: bool,
1196 options: &LayoutOptions,
1197) -> LayoutBox {
1198 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1199
1200 if symbol && !suppress_base_shift {
1202 let axis = options.metrics().axis_height;
1203 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1204 if shift.abs() > 0.001 {
1205 base_box.height -= shift;
1206 base_box.depth += shift;
1207 }
1208 }
1209
1210 if !suppress_base_shift && !symbol && body.is_some() {
1215 let axis = options.metrics().axis_height;
1216 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1217 if delta.abs() > 0.001 {
1218 let w = base_box.width;
1219 let raise = -delta;
1221 base_box = LayoutBox {
1222 width: w,
1223 height: (base_box.height + raise).max(0.0),
1224 depth: (base_box.depth - raise).max(0.0),
1225 content: BoxContent::RaiseBox {
1226 body: Box::new(base_box),
1227 shift: raise,
1228 },
1229 color: options.color,
1230 };
1231 }
1232 }
1233
1234 base_box
1235}
1236
1237fn build_op_base(
1240 name: Option<&str>,
1241 symbol: bool,
1242 body: Option<&[ParseNode]>,
1243 options: &LayoutOptions,
1244) -> (LayoutBox, f64) {
1245 if symbol {
1246 let large = options.style.is_display()
1247 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1248 let font_id = if large {
1249 FontId::Size2Regular
1250 } else {
1251 FontId::Size1Regular
1252 };
1253
1254 let op_name = name.unwrap_or("");
1255 let ch = resolve_op_char(op_name);
1256 let char_code = ch as u32;
1257
1258 let metrics = get_char_metrics(font_id, char_code);
1259 let (width, height, depth, italic) = match metrics {
1260 Some(m) => (m.width, m.height, m.depth, m.italic),
1261 None => (1.0, 0.75, 0.25, 0.0),
1262 };
1263 let width_with_italic = width + italic;
1266
1267 let base = LayoutBox {
1268 width: width_with_italic,
1269 height,
1270 depth,
1271 content: BoxContent::Glyph {
1272 font_id,
1273 char_code,
1274 },
1275 color: options.color,
1276 };
1277
1278 if op_name == "\\oiint" || op_name == "\\oiiint" {
1281 let w = base.width;
1282 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1283 let overlay_box = LayoutBox {
1284 width: w,
1285 height: base.height,
1286 depth: base.depth,
1287 content: BoxContent::SvgPath {
1288 commands: ellipse_commands,
1289 fill: false,
1290 },
1291 color: options.color,
1292 };
1293 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1294 return (with_overlay, italic);
1295 }
1296
1297 (base, italic)
1298 } else if let Some(body_nodes) = body {
1299 let base = layout_expression(body_nodes, options, true);
1300 (base, 0.0)
1301 } else {
1302 let base = layout_op_text(name.unwrap_or(""), options);
1303 (base, 0.0)
1304 }
1305}
1306
1307fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1309 let text = name.strip_prefix('\\').unwrap_or(name);
1310 let mut children = Vec::new();
1311 for ch in text.chars() {
1312 let char_code = ch as u32;
1313 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1314 let (width, height, depth) = match metrics {
1315 Some(m) => (m.width, m.height, m.depth),
1316 None => (0.5, 0.43, 0.0),
1317 };
1318 children.push(LayoutBox {
1319 width,
1320 height,
1321 depth,
1322 content: BoxContent::Glyph {
1323 font_id: FontId::MainRegular,
1324 char_code,
1325 },
1326 color: options.color,
1327 });
1328 }
1329 make_hbox(children)
1330}
1331
1332fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1334 let metrics = options.metrics();
1335 (base.height - base.depth) / 2.0 - metrics.axis_height
1336}
1337
1338fn resolve_op_char(name: &str) -> char {
1340 match name {
1343 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1346 }
1347 let font_mode = ratex_font::Mode::Math;
1348 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1349 if let Some(cp) = info.codepoint {
1350 return cp;
1351 }
1352 }
1353 name.chars().next().unwrap_or('?')
1354}
1355
1356fn layout_op_with_limits(
1358 base_node: &ParseNode,
1359 sup_node: Option<&ParseNode>,
1360 sub_node: Option<&ParseNode>,
1361 options: &LayoutOptions,
1362) -> LayoutBox {
1363 let (name, symbol, body, suppress_base_shift) = match base_node {
1364 ParseNode::Op {
1365 name,
1366 symbol,
1367 body,
1368 suppress_base_shift,
1369 ..
1370 } => (
1371 name.as_deref(),
1372 *symbol,
1373 body.as_deref(),
1374 suppress_base_shift.unwrap_or(false),
1375 ),
1376 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1377 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1378 };
1379
1380 let (base_box, slant) = build_op_base(name, symbol, body, options);
1381 let base_shift = if symbol && !suppress_base_shift {
1383 compute_op_base_shift(&base_box, options)
1384 } else {
1385 0.0
1386 };
1387
1388 layout_op_limits_inner(&base_box, sup_node, sub_node, slant, base_shift, options)
1389}
1390
1391fn layout_op_limits_inner(
1393 base: &LayoutBox,
1394 sup_node: Option<&ParseNode>,
1395 sub_node: Option<&ParseNode>,
1396 slant: f64,
1397 base_shift: f64,
1398 options: &LayoutOptions,
1399) -> LayoutBox {
1400 let metrics = options.metrics();
1401 let sup_style = options.style.superscript();
1402 let sub_style = options.style.subscript();
1403
1404 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1405 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1406
1407 let extra_clearance = 0.08_f64;
1409
1410 let sup_data = sup_node.map(|s| {
1411 let sup_opts = options.with_style(sup_style);
1412 let elem = layout_node(s, &sup_opts);
1413 let kern = (metrics.big_op_spacing1 + extra_clearance)
1414 .max(metrics.big_op_spacing3 - elem.depth * sup_ratio + extra_clearance);
1415 (elem, kern)
1416 });
1417
1418 let sub_data = sub_node.map(|s| {
1419 let sub_opts = options.with_style(sub_style);
1420 let elem = layout_node(s, &sub_opts);
1421 let kern = (metrics.big_op_spacing2 + extra_clearance)
1422 .max(metrics.big_op_spacing4 - elem.height * sub_ratio + extra_clearance);
1423 (elem, kern)
1424 });
1425
1426 let sp5 = metrics.big_op_spacing5;
1427
1428 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1429 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1430 let sup_h = sup_elem.height * sup_ratio;
1433 let sup_d = sup_elem.depth * sup_ratio;
1434 let sub_h = sub_elem.height * sub_ratio;
1435 let sub_d = sub_elem.depth * sub_ratio;
1436
1437 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1438
1439 let height = bottom
1440 + base.height - base_shift
1441 + sup_kern
1442 + sup_h + sup_d
1443 + sp5
1444 - (base.height + base.depth);
1445
1446 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1447 let total_d = bottom;
1448
1449 let w = base
1450 .width
1451 .max(sup_elem.width * sup_ratio)
1452 .max(sub_elem.width * sub_ratio);
1453 let _ = height; (total_h, total_d, w)
1455 }
1456 (None, Some((sub_elem, sub_kern))) => {
1457 let sub_h = sub_elem.height * sub_ratio;
1460 let sub_d = sub_elem.depth * sub_ratio;
1461
1462 let total_h = base.height - base_shift;
1463 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1464
1465 let w = base.width.max(sub_elem.width * sub_ratio);
1466 (total_h, total_d, w)
1467 }
1468 (Some((sup_elem, sup_kern)), None) => {
1469 let sup_h = sup_elem.height * sup_ratio;
1472 let sup_d = sup_elem.depth * sup_ratio;
1473
1474 let total_h =
1475 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1476 let total_d = base.depth + base_shift;
1477
1478 let w = base.width.max(sup_elem.width * sup_ratio);
1479 (total_h, total_d, w)
1480 }
1481 (None, None) => {
1482 return base.clone();
1483 }
1484 };
1485
1486 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1487 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1488
1489 LayoutBox {
1490 width: total_width,
1491 height: total_height,
1492 depth: total_depth,
1493 content: BoxContent::OpLimits {
1494 base: Box::new(base.clone()),
1495 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1496 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1497 base_shift,
1498 sup_kern: sup_kern_val,
1499 sub_kern: sub_kern_val,
1500 slant,
1501 sup_scale: sup_ratio,
1502 sub_scale: sub_ratio,
1503 },
1504 color: options.color,
1505 }
1506}
1507
1508fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1510 let mut children = Vec::new();
1511 for node in body {
1512 match node {
1513 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1514 let ch = text.chars().next().unwrap_or('?');
1515 let char_code = ch as u32;
1516 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1517 let (width, height, depth) = match metrics {
1518 Some(m) => (m.width, m.height, m.depth),
1519 None => (0.5, 0.43, 0.0),
1520 };
1521 children.push(LayoutBox {
1522 width,
1523 height,
1524 depth,
1525 content: BoxContent::Glyph {
1526 font_id: FontId::MainRegular,
1527 char_code,
1528 },
1529 color: options.color,
1530 });
1531 }
1532 _ => {
1533 children.push(layout_node(node, options));
1534 }
1535 }
1536 }
1537 make_hbox(children)
1538}
1539
1540const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1546
1547fn glyph_italic(lb: &LayoutBox) -> f64 {
1551 match &lb.content {
1552 BoxContent::Glyph { font_id, char_code } => {
1553 get_char_metrics(*font_id, *char_code)
1554 .map(|m| m.italic)
1555 .unwrap_or(0.0)
1556 }
1557 BoxContent::HBox(children) => {
1558 children.last().map(glyph_italic).unwrap_or(0.0)
1559 }
1560 _ => 0.0,
1561 }
1562}
1563
1564fn accent_ordgroup_len(base: &ParseNode) -> usize {
1569 match base {
1570 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1571 _ => 1,
1572 }
1573}
1574
1575fn glyph_skew(lb: &LayoutBox) -> f64 {
1576 match &lb.content {
1577 BoxContent::Glyph { font_id, char_code } => {
1578 get_char_metrics(*font_id, *char_code)
1579 .map(|m| m.skew)
1580 .unwrap_or(0.0)
1581 }
1582 BoxContent::HBox(children) => {
1583 children.last().map(glyph_skew).unwrap_or(0.0)
1584 }
1585 _ => 0.0,
1586 }
1587}
1588
1589fn layout_accent(
1590 label: &str,
1591 base: &ParseNode,
1592 is_stretchy: bool,
1593 is_shifty: bool,
1594 is_below: bool,
1595 options: &LayoutOptions,
1596) -> LayoutBox {
1597 let body_box = layout_node(base, options);
1598 let base_w = body_box.width.max(0.5);
1599
1600 if label == "\\textcircled" {
1602 return layout_textcircled(body_box, options);
1603 }
1604
1605 if let Some((commands, w, h, fill)) =
1607 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1608 {
1609 let accent_box = LayoutBox {
1611 width: w,
1612 height: 0.0,
1613 depth: h,
1614 content: BoxContent::SvgPath { commands, fill },
1615 color: options.color,
1616 };
1617 let gap = 0.065;
1622 let under_gap_em = if is_below && label == "\\utilde" {
1623 0.12
1624 } else {
1625 0.0
1626 };
1627 let clearance = if is_below {
1628 body_box.height + body_box.depth + gap
1629 } else if label == "\\vec" {
1630 (body_box.height - options.metrics().x_height).max(0.0)
1633 } else {
1634 body_box.height + gap
1635 };
1636 let (height, depth) = if is_below {
1637 (body_box.height, body_box.depth + h + gap + under_gap_em)
1638 } else if label == "\\vec" {
1639 (clearance + h, body_box.depth)
1641 } else {
1642 (body_box.height + gap + h, body_box.depth)
1643 };
1644 let vec_skew = if label == "\\vec" {
1645 (if is_shifty {
1646 glyph_skew(&body_box)
1647 } else {
1648 0.0
1649 }) + VEC_SKEW_EXTRA_RIGHT_EM
1650 } else {
1651 0.0
1652 };
1653 return LayoutBox {
1654 width: body_box.width,
1655 height,
1656 depth,
1657 content: BoxContent::Accent {
1658 base: Box::new(body_box),
1659 accent: Box::new(accent_box),
1660 clearance,
1661 skew: vec_skew,
1662 is_below,
1663 under_gap_em,
1664 },
1665 color: options.color,
1666 };
1667 }
1668
1669 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1671
1672 let accent_box = if use_arrow_path {
1673 let (commands, arrow_h, fill_arrow) =
1674 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1675 Some((c, h)) => (c, h, true),
1676 None => {
1677 let h = 0.3_f64;
1678 let c = stretchy_accent_path(label, base_w, h);
1679 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1680 (c, h, fill)
1681 }
1682 };
1683 LayoutBox {
1684 width: base_w,
1685 height: arrow_h / 2.0,
1686 depth: arrow_h / 2.0,
1687 content: BoxContent::SvgPath {
1688 commands,
1689 fill: fill_arrow,
1690 },
1691 color: options.color,
1692 }
1693 } else {
1694 let accent_char = {
1696 let ch = resolve_symbol_char(label, Mode::Text);
1697 if ch == label.chars().next().unwrap_or('?') {
1698 resolve_symbol_char(label, Mode::Math)
1701 } else {
1702 ch
1703 }
1704 };
1705 let accent_code = accent_char as u32;
1706 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1707 let (accent_w, accent_h, accent_d) = match accent_metrics {
1708 Some(m) => (m.width, m.height, m.depth),
1709 None => (body_box.width, 0.25, 0.0),
1710 };
1711 LayoutBox {
1712 width: accent_w,
1713 height: accent_h,
1714 depth: accent_d,
1715 content: BoxContent::Glyph {
1716 font_id: FontId::MainRegular,
1717 char_code: accent_code,
1718 },
1719 color: options.color,
1720 }
1721 };
1722
1723 let skew = if use_arrow_path {
1724 0.0
1725 } else if is_shifty {
1726 glyph_skew(&body_box)
1729 } else {
1730 0.0
1731 };
1732
1733 let gap = if use_arrow_path {
1742 if label == "\\Overrightarrow" {
1743 0.21
1744 } else {
1745 0.26
1746 }
1747 } else {
1748 0.0
1749 };
1750
1751 let clearance = if is_below {
1752 body_box.height + body_box.depth + accent_box.depth + gap
1753 } else if use_arrow_path {
1754 body_box.height + gap
1755 } else {
1756 let base_clearance = match &body_box.content {
1766 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1767 if !is_below =>
1768 {
1769 if inner_accent.height <= 0.001 {
1775 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1781 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1782 katex_pos + correction
1783 } else {
1784 inner_cl + 0.3
1785 }
1786 }
1787 _ => {
1788 if label == "\\bar" || label == "\\=" {
1801 body_box.height
1802 } else {
1803 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1804 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1805 katex_pos + correction
1806 }
1807 }
1808 };
1809 let base_clearance = base_clearance + accent_box.depth;
1814 if label == "\\bar" || label == "\\=" {
1815 (base_clearance - 0.12).max(0.0)
1816 } else {
1817 base_clearance
1818 }
1819 };
1820
1821 let (height, depth) = if is_below {
1822 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1823 } else if use_arrow_path {
1824 (body_box.height + gap + accent_box.height, body_box.depth)
1825 } else {
1826 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1833 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1834 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1835 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1836 } else {
1837 body_box.height.max(accent_visual_top)
1838 };
1839 (h, body_box.depth)
1840 };
1841
1842 LayoutBox {
1843 width: body_box.width,
1844 height,
1845 depth,
1846 content: BoxContent::Accent {
1847 base: Box::new(body_box),
1848 accent: Box::new(accent_box),
1849 clearance,
1850 skew,
1851 is_below,
1852 under_gap_em: 0.0,
1853 },
1854 color: options.color,
1855 }
1856}
1857
1858fn node_contains_middle(node: &ParseNode) -> bool {
1864 match node {
1865 ParseNode::Middle { .. } => true,
1866 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1867 body.iter().any(node_contains_middle)
1868 }
1869 ParseNode::SupSub { base, sup, sub, .. } => {
1870 base.as_deref().is_some_and(node_contains_middle)
1871 || sup.as_deref().is_some_and(node_contains_middle)
1872 || sub.as_deref().is_some_and(node_contains_middle)
1873 }
1874 ParseNode::GenFrac { numer, denom, .. } => {
1875 node_contains_middle(numer) || node_contains_middle(denom)
1876 }
1877 ParseNode::Sqrt { body, index, .. } => {
1878 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1879 }
1880 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1881 node_contains_middle(base)
1882 }
1883 ParseNode::Op { body, .. } => body
1884 .as_ref()
1885 .is_some_and(|b| b.iter().any(node_contains_middle)),
1886 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1887 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1888 ParseNode::Font { body, .. } => node_contains_middle(body),
1889 ParseNode::Text { body, .. }
1890 | ParseNode::Color { body, .. }
1891 | ParseNode::Styling { body, .. }
1892 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1893 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1894 node_contains_middle(body)
1895 }
1896 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1897 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1898 node_contains_middle(body)
1899 }
1900 ParseNode::Array { body, .. } => body
1901 .iter()
1902 .any(|row| row.iter().any(node_contains_middle)),
1903 ParseNode::Enclose { body, .. }
1904 | ParseNode::Lap { body, .. }
1905 | ParseNode::RaiseBox { body, .. }
1906 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1907 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1908 ParseNode::XArrow { body, below, .. } => {
1909 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1910 }
1911 ParseNode::CdArrow { label_above, label_below, .. } => {
1912 label_above.as_deref().is_some_and(node_contains_middle)
1913 || label_below.as_deref().is_some_and(node_contains_middle)
1914 }
1915 ParseNode::MathChoice {
1916 display,
1917 text,
1918 script,
1919 scriptscript,
1920 ..
1921 } => {
1922 display.iter().any(node_contains_middle)
1923 || text.iter().any(node_contains_middle)
1924 || script.iter().any(node_contains_middle)
1925 || scriptscript.iter().any(node_contains_middle)
1926 }
1927 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1928 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1929 _ => false,
1930 }
1931}
1932
1933fn body_contains_middle(nodes: &[ParseNode]) -> bool {
1935 nodes.iter().any(node_contains_middle)
1936}
1937
1938fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
1941 let m = options.metrics();
1942 if options.style.is_display() {
1943 m.delim1
1944 } else if matches!(
1945 options.style,
1946 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
1947 ) {
1948 options
1949 .with_style(MathStyle::Script)
1950 .metrics()
1951 .delim2
1952 } else {
1953 m.delim2
1954 }
1955}
1956
1957fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
1959 let metrics = options.metrics();
1960 let inner_height = inner.height;
1961 let inner_depth = inner.depth;
1962 let axis = metrics.axis_height;
1963 let max_dist = (inner_height - axis).max(inner_depth + axis);
1964 let delim_factor = 901.0;
1965 let delim_extend = 5.0 / metrics.pt_per_em;
1966 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
1967 from_formula.max(inner_height + inner_depth)
1969}
1970
1971fn layout_left_right(
1972 body: &[ParseNode],
1973 left_delim: &str,
1974 right_delim: &str,
1975 options: &LayoutOptions,
1976) -> LayoutBox {
1977 let (inner, total_height) = if body_contains_middle(body) {
1978 let opts_first = LayoutOptions {
1980 leftright_delim_height: None,
1981 ..options.clone()
1982 };
1983 let inner_first = layout_expression(body, &opts_first, true);
1984 let total_height = left_right_delim_total_height(&inner_first, options);
1985 let opts_second = LayoutOptions {
1987 leftright_delim_height: Some(total_height),
1988 ..options.clone()
1989 };
1990 let inner_second = layout_expression(body, &opts_second, true);
1991 (inner_second, total_height)
1992 } else {
1993 let inner = layout_expression(body, options, true);
1994 let total_height = left_right_delim_total_height(&inner, options);
1995 (inner, total_height)
1996 };
1997
1998 let inner_height = inner.height;
1999 let inner_depth = inner.depth;
2000
2001 let left_box = make_stretchy_delim(left_delim, total_height, options);
2002 let right_box = make_stretchy_delim(right_delim, total_height, options);
2003
2004 let width = left_box.width + inner.width + right_box.width;
2005 let height = left_box.height.max(right_box.height).max(inner_height);
2006 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2007
2008 LayoutBox {
2009 width,
2010 height,
2011 depth,
2012 content: BoxContent::LeftRight {
2013 left: Box::new(left_box),
2014 right: Box::new(right_box),
2015 inner: Box::new(inner),
2016 },
2017 color: options.color,
2018 }
2019}
2020
2021const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2022 FontId::MainRegular,
2023 FontId::Size1Regular,
2024 FontId::Size2Regular,
2025 FontId::Size3Regular,
2026 FontId::Size4Regular,
2027];
2028
2029fn normalize_delim(delim: &str) -> &str {
2031 match delim {
2032 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2033 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2034 _ => delim,
2035 }
2036}
2037
2038fn is_vert_delim(delim: &str) -> bool {
2040 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2041}
2042
2043fn is_double_vert_delim(delim: &str) -> bool {
2045 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2046}
2047
2048fn vert_repeat_piece_height(is_double: bool) -> f64 {
2050 let code = if is_double { 8741_u32 } else { 8739 };
2051 get_char_metrics(FontId::Size1Regular, code)
2052 .map(|m| m.height + m.depth)
2053 .unwrap_or(0.5)
2054}
2055
2056fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2058 let piece = vert_repeat_piece_height(is_double);
2059 let min_h = 2.0 * piece;
2060 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2061 let mut h = min_h + repeat_count * piece;
2062 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2066 h *= 1.135;
2067 }
2068 h
2069}
2070
2071fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2073 let neg = -mid_th;
2074 if !is_double {
2075 format!(
2076 "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"
2077 )
2078 } else {
2079 format!(
2080 "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"
2081 )
2082 }
2083}
2084
2085fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2086 let s = 0.001_f64;
2087 cmds.iter()
2088 .map(|c| match *c {
2089 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2090 x: x * s,
2091 y: y * s,
2092 },
2093 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2094 x: x * s,
2095 y: y * s,
2096 },
2097 PathCommand::CubicTo {
2098 x1,
2099 y1,
2100 x2,
2101 y2,
2102 x,
2103 y,
2104 } => PathCommand::CubicTo {
2105 x1: x1 * s,
2106 y1: y1 * s,
2107 x2: x2 * s,
2108 y2: y2 * s,
2109 x: x * s,
2110 y: y * s,
2111 },
2112 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2113 x1: x1 * s,
2114 y1: y1 * s,
2115 x: x * s,
2116 y: y * s,
2117 },
2118 PathCommand::Close => PathCommand::Close,
2119 })
2120 .collect()
2121}
2122
2123fn map_vert_path_y_to_baseline(
2125 cmds: Vec<PathCommand>,
2126 height: f64,
2127 depth: f64,
2128 view_box_height: i64,
2129) -> Vec<PathCommand> {
2130 let span_em = view_box_height as f64 / 1000.0;
2131 let total = height + depth;
2132 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2133 cmds.into_iter()
2134 .map(|c| match c {
2135 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2136 x,
2137 y: -height + y * scale_y,
2138 },
2139 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2140 x,
2141 y: -height + y * scale_y,
2142 },
2143 PathCommand::CubicTo {
2144 x1,
2145 y1,
2146 x2,
2147 y2,
2148 x,
2149 y,
2150 } => PathCommand::CubicTo {
2151 x1,
2152 y1: -height + y1 * scale_y,
2153 x2,
2154 y2: -height + y2 * scale_y,
2155 x,
2156 y: -height + y * scale_y,
2157 },
2158 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2159 x1,
2160 y1: -height + y1 * scale_y,
2161 x,
2162 y: -height + y * scale_y,
2163 },
2164 PathCommand::Close => PathCommand::Close,
2165 })
2166 .collect()
2167}
2168
2169fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2172 let real_h = katex_vert_real_height(total_height, is_double);
2173 let axis = options.metrics().axis_height;
2174 let depth = (real_h / 2.0 - axis).max(0.0);
2175 let height = real_h - depth;
2176 let width = if is_double { 0.556 } else { 0.333 };
2177
2178 let piece = vert_repeat_piece_height(is_double);
2179 let mid_em = (real_h - 2.0 * piece).max(0.0);
2180 let mid_th = (mid_em * 1000.0).round() as i64;
2181 let view_box_height = (real_h * 1000.0).round() as i64;
2182
2183 let d = tall_vert_svg_path_data(mid_th, is_double);
2184 let raw = parse_svg_path_data(&d);
2185 let scaled = scale_svg_path_to_em(&raw);
2186 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2187
2188 LayoutBox {
2189 width,
2190 height,
2191 depth,
2192 content: BoxContent::SvgPath { commands, fill: true },
2193 color: options.color,
2194 }
2195}
2196
2197fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2199 if delim == "." || delim.is_empty() {
2200 return LayoutBox::new_kern(0.0);
2201 }
2202
2203 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2208 return make_vert_delim_box(total_height, false, options);
2209 }
2210 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2211 return make_vert_delim_box(total_height, true, options);
2212 }
2213
2214 let delim = normalize_delim(delim);
2216
2217 let ch = resolve_symbol_char(delim, Mode::Math);
2218 let char_code = ch as u32;
2219
2220 let mut best_font = FontId::MainRegular;
2221 let mut best_w = 0.4;
2222 let mut best_h = 0.7;
2223 let mut best_d = 0.2;
2224
2225 for &font_id in &DELIM_FONT_SEQUENCE {
2226 if let Some(m) = get_char_metrics(font_id, char_code) {
2227 best_font = font_id;
2228 best_w = m.width;
2229 best_h = m.height;
2230 best_d = m.depth;
2231 if best_h + best_d >= total_height {
2232 break;
2233 }
2234 }
2235 }
2236
2237 let best_total = best_h + best_d;
2238 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2239 return stacked;
2240 }
2241
2242 LayoutBox {
2243 width: best_w,
2244 height: best_h,
2245 depth: best_d,
2246 content: BoxContent::Glyph {
2247 font_id: best_font,
2248 char_code,
2249 },
2250 color: options.color,
2251 }
2252}
2253
2254const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2256
2257fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2259 if delim == "." || delim.is_empty() {
2260 return LayoutBox::new_kern(0.0);
2261 }
2262
2263 if is_vert_delim(delim) {
2265 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2266 return make_vert_delim_box(total, false, options);
2267 }
2268 if is_double_vert_delim(delim) {
2269 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2270 return make_vert_delim_box(total, true, options);
2271 }
2272
2273 let delim = normalize_delim(delim);
2275
2276 let ch = resolve_symbol_char(delim, Mode::Math);
2277 let char_code = ch as u32;
2278
2279 let font_id = match size {
2280 1 => FontId::Size1Regular,
2281 2 => FontId::Size2Regular,
2282 3 => FontId::Size3Regular,
2283 4 => FontId::Size4Regular,
2284 _ => FontId::Size1Regular,
2285 };
2286
2287 let metrics = get_char_metrics(font_id, char_code);
2288 let (width, height, depth, actual_font) = match metrics {
2289 Some(m) => (m.width, m.height, m.depth, font_id),
2290 None => {
2291 let m = get_char_metrics(FontId::MainRegular, char_code);
2292 match m {
2293 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2294 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2295 }
2296 }
2297 };
2298
2299 LayoutBox {
2300 width,
2301 height,
2302 depth,
2303 content: BoxContent::Glyph {
2304 font_id: actual_font,
2305 char_code,
2306 },
2307 color: options.color,
2308 }
2309}
2310
2311#[allow(clippy::too_many_arguments)]
2316fn layout_array(
2317 body: &[Vec<ParseNode>],
2318 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2319 arraystretch: f64,
2320 add_jot: bool,
2321 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2322 hlines: &[Vec<bool>],
2323 col_sep_type: Option<&str>,
2324 hskip: bool,
2325 options: &LayoutOptions,
2326) -> LayoutBox {
2327 let metrics = options.metrics();
2328 let pt = 1.0 / metrics.pt_per_em;
2329 let baselineskip = 12.0 * pt;
2330 let jot = 3.0 * pt;
2331 let arrayskip = arraystretch * baselineskip;
2332 let arstrut_h = 0.7 * arrayskip;
2333 let arstrut_d = 0.3 * arrayskip;
2334 const ALIGN_RELATION_MU: f64 = 3.0;
2337 let col_gap = match col_sep_type {
2338 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2339 Some("alignat") => 0.0,
2340 Some("small") => {
2341 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2344 / options.size_multiplier()
2345 }
2346 _ => 2.0 * 5.0 * pt, };
2348 let cell_options = match col_sep_type {
2349 Some("align") | Some("alignat") => LayoutOptions {
2350 align_relation_spacing: Some(ALIGN_RELATION_MU),
2351 ..options.clone()
2352 },
2353 _ => options.clone(),
2354 };
2355
2356 let num_rows = body.len();
2357 if num_rows == 0 {
2358 return LayoutBox::new_empty();
2359 }
2360
2361 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2362
2363 use ratex_parser::parse_node::AlignType;
2365 let col_aligns: Vec<u8> = {
2366 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2367 .map(|cs| {
2368 cs.iter()
2369 .filter(|s| matches!(s.align_type, AlignType::Align))
2370 .collect()
2371 })
2372 .unwrap_or_default();
2373 (0..num_cols)
2374 .map(|c| {
2375 align_specs
2376 .get(c)
2377 .and_then(|s| s.align.as_deref())
2378 .and_then(|a| a.bytes().next())
2379 .unwrap_or(b'c')
2380 })
2381 .collect()
2382 };
2383
2384 let col_separators: Vec<Option<bool>> = {
2387 let mut seps = vec![None; num_cols + 1];
2388 let mut align_count = 0usize;
2389 if let Some(cs) = cols {
2390 for spec in cs {
2391 match spec.align_type {
2392 AlignType::Align => align_count += 1,
2393 AlignType::Separator if spec.align.as_deref() == Some("|") => {
2394 if align_count <= num_cols {
2395 seps[align_count] = Some(false);
2396 }
2397 }
2398 AlignType::Separator if spec.align.as_deref() == Some(":") => {
2399 if align_count <= num_cols {
2400 seps[align_count] = Some(true);
2401 }
2402 }
2403 _ => {}
2404 }
2405 }
2406 }
2407 seps
2408 };
2409
2410 let rule_thickness = 0.4 * pt;
2411 let double_rule_sep = metrics.double_rule_sep;
2412
2413 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2415 let mut col_widths = vec![0.0_f64; num_cols];
2416 let mut row_heights = Vec::with_capacity(num_rows);
2417 let mut row_depths = Vec::with_capacity(num_rows);
2418
2419 for row in body {
2420 let mut row_boxes = Vec::with_capacity(num_cols);
2421 let mut rh = arstrut_h;
2422 let mut rd = arstrut_d;
2423
2424 for (c, cell) in row.iter().enumerate() {
2425 let cell_nodes = match cell {
2426 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2427 other => std::slice::from_ref(other),
2428 };
2429 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2430 rh = rh.max(cell_box.height);
2431 rd = rd.max(cell_box.depth);
2432 if c < num_cols {
2433 col_widths[c] = col_widths[c].max(cell_box.width);
2434 }
2435 row_boxes.push(cell_box);
2436 }
2437
2438 while row_boxes.len() < num_cols {
2440 row_boxes.push(LayoutBox::new_empty());
2441 }
2442
2443 if add_jot {
2444 rd += jot;
2445 }
2446
2447 row_heights.push(rh);
2448 row_depths.push(rd);
2449 cell_boxes.push(row_boxes);
2450 }
2451
2452 for (r, gap) in row_gaps.iter().enumerate() {
2454 if r < row_depths.len() {
2455 if let Some(m) = gap {
2456 let gap_em = measurement_to_em(m, options);
2457 if gap_em > 0.0 {
2458 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2459 }
2460 }
2461 }
2462 }
2463
2464 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2466 while hlines_before_row.len() < num_rows + 1 {
2467 hlines_before_row.push(vec![]);
2468 }
2469
2470 for r in 0..=num_rows {
2476 let n = hlines_before_row[r].len();
2477 if n > 1 {
2478 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2479 if r == 0 {
2480 if num_rows > 0 {
2481 row_heights[0] += extra;
2482 }
2483 } else {
2484 row_depths[r - 1] += extra;
2485 }
2486 }
2487 }
2488
2489 let mut total_height = 0.0;
2491 let mut row_positions = Vec::with_capacity(num_rows);
2492 for r in 0..num_rows {
2493 total_height += row_heights[r];
2494 row_positions.push(total_height);
2495 total_height += row_depths[r];
2496 }
2497
2498 let offset = total_height / 2.0 + metrics.axis_height;
2499
2500 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2502
2503 let total_width: f64 = col_widths.iter().sum::<f64>()
2505 + col_gap * (num_cols.saturating_sub(1)) as f64
2506 + 2.0 * content_x_offset;
2507
2508 let height = offset;
2509 let depth = total_height - offset;
2510
2511 LayoutBox {
2512 width: total_width,
2513 height,
2514 depth,
2515 content: BoxContent::Array {
2516 cells: cell_boxes,
2517 col_widths: col_widths.clone(),
2518 col_aligns,
2519 row_heights: row_heights.clone(),
2520 row_depths: row_depths.clone(),
2521 col_gap,
2522 offset,
2523 content_x_offset,
2524 col_separators,
2525 hlines_before_row,
2526 rule_thickness,
2527 double_rule_sep,
2528 },
2529 color: options.color,
2530 }
2531}
2532
2533fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2538 let multiplier = match size {
2540 1 => 0.5,
2541 2 => 0.6,
2542 3 => 0.7,
2543 4 => 0.8,
2544 5 => 0.9,
2545 6 => 1.0,
2546 7 => 1.2,
2547 8 => 1.44,
2548 9 => 1.728,
2549 10 => 2.074,
2550 11 => 2.488,
2551 _ => 1.0,
2552 };
2553
2554 let inner = layout_expression(body, options, true);
2555 let ratio = multiplier / options.size_multiplier();
2556 if (ratio - 1.0).abs() < 0.001 {
2557 inner
2558 } else {
2559 LayoutBox {
2560 width: inner.width * ratio,
2561 height: inner.height * ratio,
2562 depth: inner.depth * ratio,
2563 content: BoxContent::Scaled {
2564 body: Box::new(inner),
2565 child_scale: ratio,
2566 },
2567 color: options.color,
2568 }
2569 }
2570}
2571
2572fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2575 let metrics = options.metrics();
2576 let mut children = Vec::new();
2577 for c in body.chars() {
2578 let ch = if star && c == ' ' {
2579 '\u{2423}' } else {
2581 c
2582 };
2583 let code = ch as u32;
2584 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2585 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2586 None => match get_char_metrics(FontId::MainRegular, code) {
2587 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2588 None => (
2589 FontId::TypewriterRegular,
2590 0.5,
2591 metrics.x_height,
2592 0.0,
2593 ),
2594 },
2595 };
2596 children.push(LayoutBox {
2597 width: w,
2598 height: h,
2599 depth: d,
2600 content: BoxContent::Glyph {
2601 font_id,
2602 char_code: code,
2603 },
2604 color: options.color,
2605 });
2606 }
2607 let mut hbox = make_hbox(children);
2608 hbox.color = options.color;
2609 hbox
2610}
2611
2612fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2613 let mut children = Vec::new();
2614 for node in body {
2615 match node {
2616 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2617 children.push(layout_symbol(text, *mode, options));
2618 }
2619 ParseNode::SpacingNode { text, .. } => {
2620 children.push(layout_spacing_command(text, options));
2621 }
2622 _ => {
2623 children.push(layout_node(node, options));
2624 }
2625 }
2626 }
2627 make_hbox(children)
2628}
2629
2630fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2633 let base = layout_expression(body, options, true);
2634 let w = base.width;
2635 let h = base.height;
2636 let d = base.depth;
2637
2638 let shadow = layout_expression(body, options, true);
2640 let shadow_shift_x = 0.02_f64;
2641 let _shadow_shift_y = 0.01_f64;
2642
2643 let kern_back = LayoutBox::new_kern(-w);
2647 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2648
2649 let children = vec![
2656 kern_x,
2657 shadow,
2658 kern_back,
2659 base,
2660 ];
2661 let hbox = make_hbox(children);
2663 LayoutBox {
2665 width: w,
2666 height: h,
2667 depth: d,
2668 content: hbox.content,
2669 color: options.color,
2670 }
2671}
2672
2673fn layout_enclose(
2676 label: &str,
2677 background_color: Option<&str>,
2678 border_color: Option<&str>,
2679 body: &ParseNode,
2680 options: &LayoutOptions,
2681) -> LayoutBox {
2682 use crate::layout_box::BoxContent;
2683 use ratex_types::color::Color;
2684
2685 if label == "\\phase" {
2687 return layout_phase(body, options);
2688 }
2689
2690 if label == "\\angl" {
2692 return layout_angl(body, options);
2693 }
2694
2695 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2697 return layout_cancel(label, body, options);
2698 }
2699
2700 let metrics = options.metrics();
2702 let padding = 3.0 / metrics.pt_per_em;
2703 let border_thickness = 0.4 / metrics.pt_per_em;
2704
2705 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2706
2707 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2708 let border = border_color
2709 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2710 .unwrap_or(Color::BLACK);
2711
2712 let inner = layout_node(body, options);
2713 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2714
2715 let width = inner.width + 2.0 * outer_pad;
2716 let height = inner.height + outer_pad;
2717 let depth = inner.depth + outer_pad;
2718
2719 LayoutBox {
2720 width,
2721 height,
2722 depth,
2723 content: BoxContent::Framed {
2724 body: Box::new(inner),
2725 padding,
2726 border_thickness,
2727 has_border,
2728 bg_color: bg,
2729 border_color: border,
2730 },
2731 color: options.color,
2732 }
2733}
2734
2735fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2737 use crate::layout_box::BoxContent;
2738 let inner = layout_node(body, options);
2739 let height = inner.height + shift;
2741 let depth = (inner.depth - shift).max(0.0);
2742 let width = inner.width;
2743 LayoutBox {
2744 width,
2745 height,
2746 depth,
2747 content: BoxContent::RaiseBox {
2748 body: Box::new(inner),
2749 shift,
2750 },
2751 color: options.color,
2752 }
2753}
2754
2755fn is_single_char_body(node: &ParseNode) -> bool {
2758 use ratex_parser::parse_node::ParseNode as PN;
2759 match node {
2760 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2762 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2763 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2765 _ => false,
2766 }
2767}
2768
2769fn layout_cancel(
2775 label: &str,
2776 body: &ParseNode,
2777 options: &LayoutOptions,
2778) -> LayoutBox {
2779 use crate::layout_box::BoxContent;
2780 let inner = layout_node(body, options);
2781 let w = inner.width.max(0.01);
2782 let h = inner.height;
2783 let d = inner.depth;
2784
2785 let single = is_single_char_body(body);
2788 let (v_pad, h_pad) = if label == "\\sout" {
2789 (0.0, 0.0)
2790 } else if single {
2791 (0.2, 0.0)
2792 } else {
2793 (0.0, 0.2)
2794 };
2795
2796 let commands: Vec<PathCommand> = match label {
2800 "\\cancel" => vec![
2801 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2804 "\\bcancel" => vec![
2805 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2808 "\\xcancel" => vec![
2809 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2810 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2811 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2812 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2813 ],
2814 "\\sout" => {
2815 let mid_y = -0.5 * options.metrics().x_height;
2817 vec![
2818 PathCommand::MoveTo { x: 0.0, y: mid_y },
2819 PathCommand::LineTo { x: w, y: mid_y },
2820 ]
2821 }
2822 _ => vec![],
2823 };
2824
2825 let line_w = w + 2.0 * h_pad;
2826 let line_h = h + v_pad;
2827 let line_d = d + v_pad;
2828 let line_box = LayoutBox {
2829 width: line_w,
2830 height: line_h,
2831 depth: line_d,
2832 content: BoxContent::SvgPath { commands, fill: false },
2833 color: options.color,
2834 };
2835
2836 let body_kern = -(line_w - h_pad);
2838 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2839 LayoutBox {
2840 width: w,
2841 height: h,
2842 depth: d,
2843 content: BoxContent::HBox(vec![line_box, body_shifted]),
2844 color: options.color,
2845 }
2846}
2847
2848fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2851 use crate::layout_box::BoxContent;
2852 let metrics = options.metrics();
2853 let inner = layout_node(body, options);
2854 let line_weight = 0.6_f64 / metrics.pt_per_em;
2856 let clearance = 0.35_f64 * metrics.x_height;
2857 let angle_height = inner.height + inner.depth + line_weight + clearance;
2858 let left_pad = angle_height / 2.0 + line_weight;
2859 let width = inner.width + left_pad;
2860
2861 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2863
2864 let sy = angle_height / y_svg;
2866 let sx = sy;
2869 let right_x = (400_000.0_f64 * sx).min(width);
2870
2871 let bottom_y = inner.depth + line_weight + clearance;
2873 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2874
2875 let x_peak = y_svg / 2.0;
2877 let commands = vec![
2878 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2879 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2880 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2881 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2882 PathCommand::LineTo {
2883 x: 145.0 * sx,
2884 y: vy(y_svg - 80.0),
2885 },
2886 PathCommand::LineTo {
2887 x: right_x,
2888 y: vy(y_svg - 80.0),
2889 },
2890 PathCommand::Close,
2891 ];
2892
2893 let body_shifted = make_hbox(vec![
2894 LayoutBox::new_kern(left_pad),
2895 inner.clone(),
2896 ]);
2897
2898 let path_height = inner.height;
2899 let path_depth = bottom_y;
2900
2901 LayoutBox {
2902 width,
2903 height: path_height,
2904 depth: path_depth,
2905 content: BoxContent::HBox(vec![
2906 LayoutBox {
2907 width,
2908 height: path_height,
2909 depth: path_depth,
2910 content: BoxContent::SvgPath { commands, fill: true },
2911 color: options.color,
2912 },
2913 LayoutBox::new_kern(-width),
2914 body_shifted,
2915 ]),
2916 color: options.color,
2917 }
2918}
2919
2920fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2923 use crate::layout_box::BoxContent;
2924 let inner = layout_node(body, options);
2925 let w = inner.width.max(0.3);
2926 let clearance = 0.1_f64;
2928 let arc_h = inner.height + clearance;
2929
2930 let path_commands = vec![
2932 PathCommand::MoveTo { x: 0.0, y: -arc_h },
2933 PathCommand::LineTo { x: w, y: -arc_h },
2934 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
2935 ];
2936
2937 let height = arc_h;
2938 LayoutBox {
2939 width: w,
2940 height,
2941 depth: inner.depth,
2942 content: BoxContent::Angl {
2943 path_commands,
2944 body: Box::new(inner),
2945 },
2946 color: options.color,
2947 }
2948}
2949
2950fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2951 let font_id = match font {
2952 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
2953 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
2954 "mathit" | "\\mathit" | "textit" | "\\textit" => Some(FontId::MainItalic),
2955 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
2956 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
2957 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
2958 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
2959 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
2960 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
2961 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
2962 _ => None,
2963 };
2964
2965 if let Some(fid) = font_id {
2966 layout_with_font(body, fid, options)
2967 } else {
2968 layout_node(body, options)
2969 }
2970}
2971
2972fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
2973 match node {
2974 ParseNode::OrdGroup { body, .. } => {
2975 let kern = options.inter_glyph_kern_em;
2976 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
2977 for (i, n) in body.iter().enumerate() {
2978 if i > 0 && kern > 0.0 {
2979 children.push(LayoutBox::new_kern(kern));
2980 }
2981 children.push(layout_with_font(n, font_id, options));
2982 }
2983 make_hbox(children)
2984 }
2985 ParseNode::SupSub {
2986 base, sup, sub, ..
2987 } => {
2988 if let Some(base_node) = base.as_deref() {
2989 if should_use_op_limits(base_node, options) {
2990 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
2991 }
2992 }
2993 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
2994 }
2995 ParseNode::MathOrd { text, .. }
2996 | ParseNode::TextOrd { text, .. }
2997 | ParseNode::Atom { text, .. } => {
2998 let ch = resolve_symbol_char(text, Mode::Math);
2999 let char_code = ch as u32;
3000 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3001 .map(|(_, m)| m)
3002 .unwrap_or(char_code);
3003 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3004 LayoutBox {
3005 width: math_glyph_advance_em(&m, Mode::Math),
3006 height: m.height,
3007 depth: m.depth,
3008 content: BoxContent::Glyph { font_id, char_code },
3009 color: options.color,
3010 }
3011 } else {
3012 layout_node(node, options)
3014 }
3015 }
3016 _ => layout_node(node, options),
3017 }
3018}
3019
3020fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3025 let cramped = options.with_style(options.style.cramped());
3026 let body_box = layout_node(body, &cramped);
3027 let metrics = options.metrics();
3028 let rule = metrics.default_rule_thickness;
3029
3030 let height = body_box.height + 3.0 * rule;
3032 LayoutBox {
3033 width: body_box.width,
3034 height,
3035 depth: body_box.depth,
3036 content: BoxContent::Overline {
3037 body: Box::new(body_box),
3038 rule_thickness: rule,
3039 },
3040 color: options.color,
3041 }
3042}
3043
3044fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3045 let body_box = layout_node(body, options);
3046 let metrics = options.metrics();
3047 let rule = metrics.default_rule_thickness;
3048
3049 let depth = body_box.depth + 3.0 * rule;
3051 LayoutBox {
3052 width: body_box.width,
3053 height: body_box.height,
3054 depth,
3055 content: BoxContent::Underline {
3056 body: Box::new(body_box),
3057 rule_thickness: rule,
3058 },
3059 color: options.color,
3060 }
3061}
3062
3063fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3065 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3066 let body_opts = options
3068 .with_color(link_color)
3069 .with_inter_glyph_kern(0.024);
3070 let body_box = layout_expression(body, &body_opts, true);
3071 layout_underline_laid_out(body_box, options, link_color)
3072}
3073
3074fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3076 let metrics = options.metrics();
3077 let rule = metrics.default_rule_thickness;
3078 let depth = body_box.depth + 3.0 * rule;
3079 LayoutBox {
3080 width: body_box.width,
3081 height: body_box.height,
3082 depth,
3083 content: BoxContent::Underline {
3084 body: Box::new(body_box),
3085 rule_thickness: rule,
3086 },
3087 color,
3088 }
3089}
3090
3091fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3096 let metrics = options.metrics();
3097 let mu = metrics.css_em_per_mu();
3098
3099 let width = match text {
3100 "\\," | "\\thinspace" => 3.0 * mu,
3101 "\\:" | "\\medspace" => 4.0 * mu,
3102 "\\;" | "\\thickspace" => 5.0 * mu,
3103 "\\!" | "\\negthinspace" => -3.0 * mu,
3104 "\\negmedspace" => -4.0 * mu,
3105 "\\negthickspace" => -5.0 * mu,
3106 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3107 get_char_metrics(FontId::MainRegular, 160)
3111 .map(|m| m.width)
3112 .unwrap_or(0.25)
3113 }
3114 "\\quad" => metrics.quad,
3115 "\\qquad" => 2.0 * metrics.quad,
3116 "\\enspace" => metrics.quad / 2.0,
3117 _ => 0.0,
3118 };
3119
3120 LayoutBox::new_kern(width)
3121}
3122
3123fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3128 let metrics = options.metrics();
3129 match m.unit.as_str() {
3130 "em" => m.number,
3131 "ex" => m.number * metrics.x_height,
3132 "mu" => m.number * metrics.css_em_per_mu(),
3133 "pt" => m.number / metrics.pt_per_em,
3134 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3135 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3136 "in" => m.number * 72.27 / metrics.pt_per_em,
3137 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3138 "pc" => m.number * 12.0 / metrics.pt_per_em,
3139 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3140 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3141 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3142 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3143 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3144 _ => m.number,
3145 }
3146}
3147
3148fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3154 match node {
3155 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3156 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3157 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3158 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3159 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3161 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3162 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3163 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3164 }
3165 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3166 ParseNode::SupSub { base, .. } => {
3167 base.as_ref().and_then(|b| node_math_class(b))
3168 }
3169 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3170 ParseNode::SpacingNode { .. } => None,
3171 ParseNode::Kern { .. } => None,
3172 ParseNode::HtmlMathMl { html, .. } => {
3173 for child in html {
3175 if let Some(cls) = node_math_class(child) {
3176 return Some(cls);
3177 }
3178 }
3179 None
3180 }
3181 ParseNode::Lap { .. } => None,
3182 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3183 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3184 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3186 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3188 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3189 ParseNode::Middle { .. } => Some(MathClass::Ord),
3190 _ => Some(MathClass::Ord),
3191 }
3192}
3193
3194fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3195 match mclass {
3196 "mord" => MathClass::Ord,
3197 "mop" => MathClass::Op,
3198 "mbin" => MathClass::Bin,
3199 "mrel" => MathClass::Rel,
3200 "mopen" => MathClass::Open,
3201 "mclose" => MathClass::Close,
3202 "mpunct" => MathClass::Punct,
3203 "minner" => MathClass::Inner,
3204 _ => MathClass::Ord,
3205 }
3206}
3207
3208fn is_character_box(node: &ParseNode) -> bool {
3210 matches!(
3211 node,
3212 ParseNode::MathOrd { .. }
3213 | ParseNode::TextOrd { .. }
3214 | ParseNode::Atom { .. }
3215 | ParseNode::AccentToken { .. }
3216 )
3217}
3218
3219fn family_to_math_class(family: AtomFamily) -> MathClass {
3220 match family {
3221 AtomFamily::Bin => MathClass::Bin,
3222 AtomFamily::Rel => MathClass::Rel,
3223 AtomFamily::Open => MathClass::Open,
3224 AtomFamily::Close => MathClass::Close,
3225 AtomFamily::Punct => MathClass::Punct,
3226 AtomFamily::Inner => MathClass::Inner,
3227 }
3228}
3229
3230fn layout_horiz_brace(
3235 base: &ParseNode,
3236 is_over: bool,
3237 func_label: &str,
3238 options: &LayoutOptions,
3239) -> LayoutBox {
3240 let body_box = layout_node(base, options);
3241 let w = body_box.width.max(0.5);
3242
3243 let is_bracket = func_label
3244 .trim_start_matches('\\')
3245 .ends_with("bracket");
3246
3247 let stretch_key = if is_bracket {
3249 if is_over {
3250 "overbracket"
3251 } else {
3252 "underbracket"
3253 }
3254 } else if is_over {
3255 "overbrace"
3256 } else {
3257 "underbrace"
3258 };
3259
3260 let (raw_commands, brace_h, brace_fill) =
3261 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3262 Some((c, h)) => (c, h, true),
3263 None => {
3264 let h = 0.35_f64;
3265 (horiz_brace_path(w, h, is_over), h, false)
3266 }
3267 };
3268
3269 let y_shift = if is_over {
3273 -brace_h / 2.0
3274 } else {
3275 brace_h / 2.0
3276 };
3277 let commands = shift_path_y(raw_commands, y_shift);
3278
3279 let brace_box = LayoutBox {
3280 width: w,
3281 height: if is_over { brace_h } else { 0.0 },
3282 depth: if is_over { 0.0 } else { brace_h },
3283 content: BoxContent::SvgPath {
3284 commands,
3285 fill: brace_fill,
3286 },
3287 color: options.color,
3288 };
3289
3290 let gap = 0.1;
3291 let (height, depth) = if is_over {
3292 (body_box.height + brace_h + gap, body_box.depth)
3293 } else {
3294 (body_box.height, body_box.depth + brace_h + gap)
3295 };
3296
3297 let clearance = if is_over {
3298 height - brace_h
3299 } else {
3300 body_box.height + body_box.depth + gap
3301 };
3302 let total_w = body_box.width;
3303
3304 LayoutBox {
3305 width: total_w,
3306 height,
3307 depth,
3308 content: BoxContent::Accent {
3309 base: Box::new(body_box),
3310 accent: Box::new(brace_box),
3311 clearance,
3312 skew: 0.0,
3313 is_below: !is_over,
3314 under_gap_em: 0.0,
3315 },
3316 color: options.color,
3317 }
3318}
3319
3320fn layout_xarrow(
3325 label: &str,
3326 body: &ParseNode,
3327 below: Option<&ParseNode>,
3328 options: &LayoutOptions,
3329) -> LayoutBox {
3330 let sup_style = options.style.superscript();
3331 let sub_style = options.style.subscript();
3332 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3333 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3334
3335 let sup_opts = options.with_style(sup_style);
3336 let body_box = layout_node(body, &sup_opts);
3337 let body_w = body_box.width * sup_ratio;
3338
3339 let below_box = below.map(|b| {
3340 let sub_opts = options.with_style(sub_style);
3341 layout_node(b, &sub_opts)
3342 });
3343 let below_w = below_box
3344 .as_ref()
3345 .map(|b| b.width * sub_ratio)
3346 .unwrap_or(0.0);
3347
3348 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3351 let upper_w = body_w + sup_ratio;
3352 let lower_w = if below_box.is_some() {
3353 below_w + sub_ratio
3354 } else {
3355 0.0
3356 };
3357 let arrow_w = upper_w.max(lower_w).max(min_w);
3358 let arrow_h = 0.3;
3359
3360 let (commands, actual_arrow_h, fill_arrow) =
3361 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3362 Some((c, h)) => (c, h, true),
3363 None => (
3364 stretchy_accent_path(label, arrow_w, arrow_h),
3365 arrow_h,
3366 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3367 ),
3368 };
3369 let arrow_box = LayoutBox {
3370 width: arrow_w,
3371 height: actual_arrow_h / 2.0,
3372 depth: actual_arrow_h / 2.0,
3373 content: BoxContent::SvgPath {
3374 commands,
3375 fill: fill_arrow,
3376 },
3377 color: options.color,
3378 };
3379
3380 let metrics = options.metrics();
3383 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3385 let gap = 0.111; let base_shift = -axis;
3389
3390 let sup_kern = gap;
3398 let sub_kern = gap;
3399
3400 let sup_h = body_box.height * sup_ratio;
3401 let sup_d = body_box.depth * sup_ratio;
3402
3403 let height = axis + arrow_half + gap + sup_h + sup_d;
3405 let mut depth = (arrow_half - axis).max(0.0);
3407
3408 if let Some(ref bel) = below_box {
3409 let sub_h = bel.height * sub_ratio;
3410 let sub_d = bel.depth * sub_ratio;
3411 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3413 }
3414
3415 LayoutBox {
3416 width: arrow_w,
3417 height,
3418 depth,
3419 content: BoxContent::OpLimits {
3420 base: Box::new(arrow_box),
3421 sup: Some(Box::new(body_box)),
3422 sub: below_box.map(Box::new),
3423 base_shift,
3424 sup_kern,
3425 sub_kern,
3426 slant: 0.0,
3427 sup_scale: sup_ratio,
3428 sub_scale: sub_ratio,
3429 },
3430 color: options.color,
3431 }
3432}
3433
3434fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3439 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3442 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3443 let diameter = radius * 2.0;
3444
3445 let cx = radius;
3447 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3450
3451 let circle_commands = vec![
3452 PathCommand::MoveTo { x: cx + r, y: cy },
3453 PathCommand::CubicTo {
3454 x1: cx + r, y1: cy - k * r,
3455 x2: cx + k * r, y2: cy - r,
3456 x: cx, y: cy - r,
3457 },
3458 PathCommand::CubicTo {
3459 x1: cx - k * r, y1: cy - r,
3460 x2: cx - r, y2: cy - k * r,
3461 x: cx - r, y: cy,
3462 },
3463 PathCommand::CubicTo {
3464 x1: cx - r, y1: cy + k * r,
3465 x2: cx - k * r, y2: cy + r,
3466 x: cx, y: cy + r,
3467 },
3468 PathCommand::CubicTo {
3469 x1: cx + k * r, y1: cy + r,
3470 x2: cx + r, y2: cy + k * r,
3471 x: cx + r, y: cy,
3472 },
3473 PathCommand::Close,
3474 ];
3475
3476 let circle_box = LayoutBox {
3477 width: diameter,
3478 height: r - cy.min(0.0),
3479 depth: (r + cy).max(0.0),
3480 content: BoxContent::SvgPath {
3481 commands: circle_commands,
3482 fill: false,
3483 },
3484 color: options.color,
3485 };
3486
3487 let content_shift = (diameter - body_box.width) / 2.0;
3489 let children = vec![
3491 circle_box,
3492 LayoutBox::new_kern(-(diameter) + content_shift),
3493 body_box.clone(),
3494 ];
3495
3496 let height = r - cy.min(0.0);
3497 let depth = (r + cy).max(0.0);
3498
3499 LayoutBox {
3500 width: diameter,
3501 height,
3502 depth,
3503 content: BoxContent::HBox(children),
3504 color: options.color,
3505 }
3506}
3507
3508fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3532 let r: f64 = 0.1125;
3534 let cy: f64 = -0.2625;
3538 let k: f64 = 0.5523;
3540 let cx: f64 = r;
3542
3543 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3546
3547 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3556 vec![
3557 PathCommand::MoveTo { x: ox + rad, y: cy },
3558 PathCommand::CubicTo {
3559 x1: ox + rad, y1: cy - k * rad,
3560 x2: ox + k * rad, y2: cy - rad,
3561 x: ox, y: cy - rad,
3562 },
3563 PathCommand::CubicTo {
3564 x1: ox - k * rad, y1: cy - rad,
3565 x2: ox - rad, y2: cy - k * rad,
3566 x: ox - rad, y: cy,
3567 },
3568 PathCommand::CubicTo {
3569 x1: ox - rad, y1: cy + k * rad,
3570 x2: ox - k * rad, y2: cy + rad,
3571 x: ox, y: cy + rad,
3572 },
3573 PathCommand::CubicTo {
3574 x1: ox + k * rad, y1: cy + rad,
3575 x2: ox + rad, y2: cy + k * rad,
3576 x: ox + rad, y: cy,
3577 },
3578 PathCommand::Close,
3579 ]
3580 };
3581
3582 let disk = LayoutBox {
3583 width: 2.0 * r,
3584 height: h,
3585 depth: d,
3586 content: BoxContent::SvgPath {
3587 commands: circle_commands(cx, r),
3588 fill: true,
3589 },
3590 color: options.color,
3591 };
3592
3593 let ring = LayoutBox {
3594 width: 2.0 * r,
3595 height: h,
3596 depth: d,
3597 content: BoxContent::SvgPath {
3598 commands: circle_commands(cx, r_ring),
3599 fill: false,
3600 },
3601 color: options.color,
3602 };
3603
3604 let bar_len: f64 = 0.25;
3608 let bar_th: f64 = 0.04;
3609 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3612
3613 let children = if imageof {
3614 vec![disk, bar, ring]
3615 } else {
3616 vec![ring, bar, disk]
3617 };
3618
3619 let total_width = 4.0 * r + bar_len;
3621 LayoutBox {
3622 width: total_width,
3623 height: h,
3624 depth: d,
3625 content: BoxContent::HBox(children),
3626 color: options.color,
3627 }
3628}
3629
3630fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3634 let cx = width / 2.0;
3635 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3640 PathCommand::MoveTo { x: cx + a, y: cy },
3641 PathCommand::CubicTo {
3642 x1: cx + a,
3643 y1: cy - k * b,
3644 x2: cx + k * a,
3645 y2: cy - b,
3646 x: cx,
3647 y: cy - b,
3648 },
3649 PathCommand::CubicTo {
3650 x1: cx - k * a,
3651 y1: cy - b,
3652 x2: cx - a,
3653 y2: cy - k * b,
3654 x: cx - a,
3655 y: cy,
3656 },
3657 PathCommand::CubicTo {
3658 x1: cx - a,
3659 y1: cy + k * b,
3660 x2: cx - k * a,
3661 y2: cy + b,
3662 x: cx,
3663 y: cy + b,
3664 },
3665 PathCommand::CubicTo {
3666 x1: cx + k * a,
3667 y1: cy + b,
3668 x2: cx + a,
3669 y2: cy + k * b,
3670 x: cx + a,
3671 y: cy,
3672 },
3673 PathCommand::Close,
3674 ]
3675}
3676
3677fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3678 cmds.into_iter().map(|c| match c {
3679 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3680 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3681 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3682 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3683 },
3684 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3685 x1, y1: y1 + dy, x, y: y + dy,
3686 },
3687 PathCommand::Close => PathCommand::Close,
3688 }).collect()
3689}
3690
3691fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3692 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3693 return commands;
3694 }
3695 let ah = height * 0.35; let mid_y = -height / 2.0;
3697
3698 match label {
3699 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3700 vec![
3701 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3702 PathCommand::LineTo { x: 0.0, y: mid_y },
3703 PathCommand::LineTo { x: ah, y: mid_y + ah },
3704 PathCommand::MoveTo { x: 0.0, y: mid_y },
3705 PathCommand::LineTo { x: width, y: mid_y },
3706 ]
3707 }
3708 "\\overleftrightarrow" | "\\underleftrightarrow"
3709 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3710 vec![
3711 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3712 PathCommand::LineTo { x: 0.0, y: mid_y },
3713 PathCommand::LineTo { x: ah, y: mid_y + ah },
3714 PathCommand::MoveTo { x: 0.0, y: mid_y },
3715 PathCommand::LineTo { x: width, y: mid_y },
3716 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3717 PathCommand::LineTo { x: width, y: mid_y },
3718 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3719 ]
3720 }
3721 "\\xlongequal" => {
3722 let gap = 0.04;
3723 vec![
3724 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3725 PathCommand::LineTo { x: width, y: mid_y - gap },
3726 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3727 PathCommand::LineTo { x: width, y: mid_y + gap },
3728 ]
3729 }
3730 "\\xhookleftarrow" => {
3731 vec![
3732 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3733 PathCommand::LineTo { x: 0.0, y: mid_y },
3734 PathCommand::LineTo { x: ah, y: mid_y + ah },
3735 PathCommand::MoveTo { x: 0.0, y: mid_y },
3736 PathCommand::LineTo { x: width, y: mid_y },
3737 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3738 ]
3739 }
3740 "\\xhookrightarrow" => {
3741 vec![
3742 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3743 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3744 PathCommand::LineTo { x: width, y: mid_y },
3745 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3746 PathCommand::LineTo { x: width, y: mid_y },
3747 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3748 ]
3749 }
3750 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3751 let right = label.contains("right");
3752 if right {
3753 vec![
3754 PathCommand::MoveTo { x: 0.0, y: mid_y },
3755 PathCommand::LineTo { x: width, y: mid_y },
3756 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3757 PathCommand::LineTo { x: width, y: mid_y },
3758 ]
3759 } else {
3760 vec![
3761 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3762 PathCommand::LineTo { x: 0.0, y: mid_y },
3763 PathCommand::LineTo { x: width, y: mid_y },
3764 ]
3765 }
3766 }
3767 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3768 let right = label.contains("right");
3769 if right {
3770 vec![
3771 PathCommand::MoveTo { x: 0.0, y: mid_y },
3772 PathCommand::LineTo { x: width, y: mid_y },
3773 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3774 PathCommand::LineTo { x: width, y: mid_y },
3775 ]
3776 } else {
3777 vec![
3778 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3779 PathCommand::LineTo { x: 0.0, y: mid_y },
3780 PathCommand::LineTo { x: width, y: mid_y },
3781 ]
3782 }
3783 }
3784 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3785 let gap = 0.06;
3786 vec![
3787 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3788 PathCommand::LineTo { x: width, y: mid_y - gap },
3789 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3790 PathCommand::LineTo { x: width, y: mid_y - gap },
3791 PathCommand::MoveTo { x: width, y: mid_y + gap },
3792 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3793 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3794 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3795 ]
3796 }
3797 "\\xtofrom" | "\\xrightleftarrows" => {
3798 let gap = 0.06;
3799 vec![
3800 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3801 PathCommand::LineTo { x: width, y: mid_y - gap },
3802 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3803 PathCommand::LineTo { x: width, y: mid_y - gap },
3804 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3805 PathCommand::MoveTo { x: width, y: mid_y + gap },
3806 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3807 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3808 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3809 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3810 ]
3811 }
3812 "\\overlinesegment" | "\\underlinesegment" => {
3813 vec![
3814 PathCommand::MoveTo { x: 0.0, y: mid_y },
3815 PathCommand::LineTo { x: width, y: mid_y },
3816 ]
3817 }
3818 _ => {
3819 vec![
3820 PathCommand::MoveTo { x: 0.0, y: mid_y },
3821 PathCommand::LineTo { x: width, y: mid_y },
3822 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3823 PathCommand::LineTo { x: width, y: mid_y },
3824 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3825 ]
3826 }
3827 }
3828}
3829
3830fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
3836 let h = inner.height;
3837 let d = inner.depth;
3838 let w = inner.width + pad_l + pad_r;
3839 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
3840 if pad_l > 0.0 {
3841 children.push(LayoutBox::new_kern(pad_l));
3842 }
3843 children.push(inner);
3844 if pad_r > 0.0 {
3845 children.push(LayoutBox::new_kern(pad_r));
3846 }
3847 LayoutBox {
3848 width: w,
3849 height: h,
3850 depth: d,
3851 content: BoxContent::HBox(children),
3852 color,
3853 }
3854}
3855
3856fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
3867 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
3868 LayoutBox {
3869 width: label.width,
3870 height: box_h,
3871 depth: box_d,
3872 content: BoxContent::RaiseBox {
3873 body: Box::new(label),
3874 shift,
3875 },
3876 color,
3877 }
3878}
3879
3880fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3884 let sup_style = options.style.superscript();
3885 let sup_opts = options.with_style(sup_style);
3886 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3887 let inner = layout_node(body, &sup_opts);
3888 if (sup_ratio - 1.0).abs() < 1e-6 {
3889 inner
3890 } else {
3891 LayoutBox {
3892 width: inner.width * sup_ratio,
3893 height: inner.height * sup_ratio,
3894 depth: inner.depth * sup_ratio,
3895 content: BoxContent::Scaled {
3896 body: Box::new(inner),
3897 child_scale: sup_ratio,
3898 },
3899 color: options.color,
3900 }
3901 }
3902}
3903
3904fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
3910 let axis = options.metrics().axis_height;
3911 let depth = (total_height / 2.0 - axis).max(0.0);
3912 let height = total_height - depth;
3913 if let Some((commands, w)) =
3914 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
3915 {
3916 return LayoutBox {
3917 width: w,
3918 height,
3919 depth,
3920 content: BoxContent::SvgPath {
3921 commands,
3922 fill: true,
3923 },
3924 color: options.color,
3925 };
3926 }
3927 if down {
3929 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
3930 } else {
3931 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
3932 }
3933}
3934
3935fn layout_cd_arrow(
3951 direction: &str,
3952 label_above: Option<&ParseNode>,
3953 label_below: Option<&ParseNode>,
3954 target_size: f64,
3955 target_col_width: f64,
3956 _target_depth: f64,
3957 options: &LayoutOptions,
3958) -> LayoutBox {
3959 let metrics = options.metrics();
3960 let axis = metrics.axis_height;
3961
3962 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
3965
3966 match direction {
3967 "right" | "left" | "horiz_eq" => {
3968 let sup_style = options.style.superscript();
3970 let sub_style = options.style.subscript();
3971 let sup_opts = options.with_style(sup_style);
3972 let sub_opts = options.with_style(sub_style);
3973 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3974 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3975
3976 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
3977 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
3978
3979 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
3980 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
3981
3982 let path_label = if direction == "right" {
3984 "\\cdrightarrow"
3985 } else if direction == "left" {
3986 "\\cdleftarrow"
3987 } else {
3988 "\\cdlongequal"
3989 };
3990 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
3991 const CD_LABEL_PAD_L: f64 = 0.22;
3994 const CD_LABEL_PAD_R: f64 = 0.48;
3995 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
3996 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
3997 let upper_need = above_box
3998 .as_ref()
3999 .map(|_| above_w + cd_pad_sup)
4000 .unwrap_or(0.0);
4001 let lower_need = below_box
4002 .as_ref()
4003 .map(|_| below_w + cd_pad_sub)
4004 .unwrap_or(0.0);
4005 let natural_w = upper_need.max(lower_need).max(0.0);
4006 let shaft_w = if target_size > 0.0 {
4007 target_size
4008 } else {
4009 natural_w.max(min_shaft_w)
4010 };
4011
4012 let (commands, actual_arrow_h, fill_arrow) =
4013 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4014 Some((c, h)) => (c, h, true),
4015 None => {
4016 let arrow_h = 0.3_f64;
4018 let ah = 0.12_f64;
4019 let cmds = if direction == "horiz_eq" {
4020 let gap = 0.06;
4021 vec![
4022 PathCommand::MoveTo { x: 0.0, y: -gap },
4023 PathCommand::LineTo { x: shaft_w, y: -gap },
4024 PathCommand::MoveTo { x: 0.0, y: gap },
4025 PathCommand::LineTo { x: shaft_w, y: gap },
4026 ]
4027 } else if direction == "right" {
4028 vec![
4029 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4030 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4031 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4032 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4033 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4034 ]
4035 } else {
4036 vec![
4037 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4038 PathCommand::LineTo { x: 0.0, y: 0.0 },
4039 PathCommand::MoveTo { x: ah, y: -ah },
4040 PathCommand::LineTo { x: 0.0, y: 0.0 },
4041 PathCommand::LineTo { x: ah, y: ah },
4042 ]
4043 };
4044 (cmds, arrow_h, false)
4045 }
4046 };
4047
4048 let arrow_half = actual_arrow_h / 2.0;
4050 let arrow_box = LayoutBox {
4051 width: shaft_w,
4052 height: arrow_half,
4053 depth: arrow_half,
4054 content: BoxContent::SvgPath {
4055 commands,
4056 fill: fill_arrow,
4057 },
4058 color: options.color,
4059 };
4060
4061 let gap = 0.111;
4063 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4064 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4065 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4069 sup_d
4070 } else {
4071 0.0
4072 };
4073 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4074 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4075 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4076 let depth = if below_box.is_some() {
4077 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4078 } else {
4079 (arrow_half - axis).max(0.0)
4080 };
4081
4082 let inner = LayoutBox {
4083 width: shaft_w,
4084 height,
4085 depth,
4086 content: BoxContent::OpLimits {
4087 base: Box::new(arrow_box),
4088 sup: above_box.map(Box::new),
4089 sub: below_box.map(Box::new),
4090 base_shift: -axis,
4091 sup_kern: gap,
4092 sub_kern: gap,
4093 slant: 0.0,
4094 sup_scale: sup_ratio,
4095 sub_scale: sub_ratio,
4096 },
4097 color: options.color,
4098 };
4099
4100 if target_col_width > inner.width + 1e-6 {
4104 let extra = target_col_width - inner.width;
4105 let kl = extra / 2.0;
4106 let kr = extra - kl;
4107 cd_wrap_hpad(inner, kl, kr, options.color)
4108 } else {
4109 inner
4110 }
4111 }
4112
4113 "down" | "up" | "vert_eq" => {
4114 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4118 "vert_eq" if target_size > 0.0 => {
4119 make_vert_delim_box(target_size.max(big_total), true, options)
4120 }
4121 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4122 "down" if target_size > 0.0 => {
4123 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4124 }
4125 "up" if target_size > 0.0 => {
4126 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4127 }
4128 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4129 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4130 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4131 };
4132 let box_h = shaft_box.height;
4133 let box_d = shaft_box.depth;
4134 let shaft_w = shaft_box.width;
4135
4136 let left_box = label_above.map(|n| {
4139 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4140 });
4141 let right_box = label_below.map(|n| {
4142 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4143 });
4144
4145 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4146 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4147 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4148 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4149 let inner_w = left_part + shaft_w + right_part;
4150
4151 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4153 let extra = target_col_width - inner_w;
4154 let kl = extra / 2.0;
4155 let kr = extra - kl;
4156 (kl, kr, target_col_width)
4157 } else {
4158 (0.0, 0.0, inner_w)
4159 };
4160
4161 let mut children: Vec<LayoutBox> = Vec::new();
4162 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4163 if let Some(lb) = left_box {
4164 children.push(lb);
4165 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4166 }
4167 children.push(shaft_box);
4168 if let Some(rb) = right_box {
4169 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4170 children.push(rb);
4171 }
4172 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4173
4174 LayoutBox {
4175 width: total_w,
4176 height: box_h,
4177 depth: box_d,
4178 content: BoxContent::HBox(children),
4179 color: options.color,
4180 }
4181 }
4182
4183 _ => LayoutBox::new_empty(),
4185 }
4186}
4187
4188fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4190 let metrics = options.metrics();
4191 let pt = 1.0 / metrics.pt_per_em;
4192 let baselineskip = 3.0 * metrics.x_height;
4194 let arstrut_h = 0.7 * baselineskip;
4195 let arstrut_d = 0.3 * baselineskip;
4196
4197 let num_rows = body.len();
4198 if num_rows == 0 {
4199 return LayoutBox::new_empty();
4200 }
4201 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4202 if num_cols == 0 {
4203 return LayoutBox::new_empty();
4204 }
4205
4206 let jot = 3.0 * pt;
4208
4209 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4211 let mut col_widths = vec![0.0_f64; num_cols];
4212 let mut row_heights = vec![arstrut_h; num_rows];
4213 let mut row_depths = vec![arstrut_d; num_rows];
4214
4215 for (r, row) in body.iter().enumerate() {
4216 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4217
4218 for (c, cell) in row.iter().enumerate() {
4219 let cbox = match cell {
4220 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4221 layout_cd_arrow(
4222 direction,
4223 label_above.as_deref(),
4224 label_below.as_deref(),
4225 0.0, 0.0, 0.0, options,
4229 )
4230 }
4231 ParseNode::OrdGroup { body: cell_body, .. } => {
4232 layout_expression(cell_body, options, true)
4233 }
4234 other => layout_node(other, options),
4235 };
4236
4237 row_heights[r] = row_heights[r].max(cbox.height);
4238 row_depths[r] = row_depths[r].max(cbox.depth);
4239 col_widths[c] = col_widths[c].max(cbox.width);
4240 row_boxes.push(cbox);
4241 }
4242
4243 while row_boxes.len() < num_cols {
4245 row_boxes.push(LayoutBox::new_empty());
4246 }
4247 cell_boxes.push(row_boxes);
4248 }
4249
4250 let col_target_w: Vec<f64> = col_widths.clone();
4254
4255 #[cfg(debug_assertions)]
4256 {
4257 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4258 for (r, row) in cell_boxes.iter().enumerate() {
4259 for (c, b) in row.iter().enumerate() {
4260 if b.width > 0.0 {
4261 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4262 }
4263 }
4264 }
4265 }
4266
4267 for (r, row) in body.iter().enumerate() {
4269 let is_arrow_row = r % 2 == 1;
4270 for (c, cell) in row.iter().enumerate() {
4271 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4272 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4273 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4274 let b = layout_cd_arrow(
4275 direction,
4276 label_above.as_deref(),
4277 label_below.as_deref(),
4278 cell_boxes[r][c].width,
4279 col_target_w[c],
4280 0.0,
4281 options,
4282 );
4283 let w = b.width;
4284 (b, w)
4285 } else if is_arrow_row && c % 2 == 0 {
4286 let v_span = row_heights[r] + row_depths[r];
4290 let b = layout_cd_arrow(
4291 direction,
4292 label_above.as_deref(),
4293 label_below.as_deref(),
4294 v_span,
4295 col_widths[c],
4296 0.0,
4297 options,
4298 );
4299 let w = b.width;
4300 (b, w)
4301 } else {
4302 continue;
4303 };
4304 col_widths[c] = col_widths[c].max(col_w);
4305 cell_boxes[r][c] = new_box;
4306 }
4307 }
4308 }
4309
4310 #[cfg(debug_assertions)]
4311 {
4312 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4313 }
4314
4315 for rd in &mut row_depths {
4318 *rd += jot;
4319 }
4320
4321 let col_gap = 0.5;
4326
4327 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4329
4330 let col_separators = vec![None; num_cols + 1];
4332
4333 let mut total_height = 0.0_f64;
4334 let mut row_positions = Vec::with_capacity(num_rows);
4335 for r in 0..num_rows {
4336 total_height += row_heights[r];
4337 row_positions.push(total_height);
4338 total_height += row_depths[r];
4339 }
4340
4341 let offset = total_height / 2.0 + metrics.axis_height;
4342 let height = offset;
4343 let depth = total_height - offset;
4344
4345 let total_width = col_widths.iter().sum::<f64>()
4347 + col_gap * (num_cols.saturating_sub(1)) as f64;
4348
4349 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4351
4352 LayoutBox {
4353 width: total_width,
4354 height,
4355 depth,
4356 content: BoxContent::Array {
4357 cells: cell_boxes,
4358 col_widths,
4359 col_aligns,
4360 row_heights,
4361 row_depths,
4362 col_gap,
4363 offset,
4364 content_x_offset: 0.0,
4365 col_separators,
4366 hlines_before_row,
4367 rule_thickness: 0.04 * pt,
4368 double_rule_sep: metrics.double_rule_sep,
4369 },
4370 color: options.color,
4371 }
4372}
4373
4374fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4375 let mid = width / 2.0;
4376 let q = height * 0.6;
4377 if is_over {
4378 vec![
4379 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4380 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4381 PathCommand::LineTo { x: mid - 0.05, y: -q },
4382 PathCommand::LineTo { x: mid, y: -height },
4383 PathCommand::LineTo { x: mid + 0.05, y: -q },
4384 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4385 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4386 ]
4387 } else {
4388 vec![
4389 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4390 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4391 PathCommand::LineTo { x: mid - 0.05, y: q },
4392 PathCommand::LineTo { x: mid, y: height },
4393 PathCommand::LineTo { x: mid + 0.05, y: q },
4394 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4395 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4396 ]
4397 }
4398}