1use std::collections::HashMap;
2
3use ratex_font::{get_char_metrics, get_global_metrics, FontId};
4use ratex_parser::parse_node::{
5 ArrayTag, AtomFamily, Mode, ParseNode, ProofBranch, ProofLineStyle,
6};
7use ratex_types::color::Color;
8use ratex_types::math_style::MathStyle;
9use ratex_types::path_command::PathCommand;
10
11use crate::hbox::make_hbox;
12use crate::layout_box::{BoxContent, LayoutBox, PlacedBox, ProofRule};
13use crate::layout_options::LayoutOptions;
14
15use crate::katex_svg::parse_svg_path_data;
16use crate::spacing::{atom_spacing, mu_to_em, MathClass};
17use crate::stacked_delim::make_stacked_delim_if_needed;
18
19const NULL_DELIMITER_SPACE: f64 = 0.12;
22
23pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
25 layout_expression(nodes, options, true)
26}
27
28fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
31 let n = raw.len();
32 let mut eff = raw.to_vec();
33 for i in 0..n {
34 if raw[i] != Some(MathClass::Bin) {
35 continue;
36 }
37 let prev = if i == 0 { None } else { raw[i - 1] };
38 let left_cancel = matches!(
39 prev,
40 None
41 | Some(MathClass::Bin)
42 | Some(MathClass::Open)
43 | Some(MathClass::Rel)
44 | Some(MathClass::Op)
45 | Some(MathClass::Punct)
46 );
47 if left_cancel {
48 eff[i] = Some(MathClass::Ord);
49 }
50 }
51 for i in 0..n {
52 if raw[i] != Some(MathClass::Bin) {
53 continue;
54 }
55 let next = if i + 1 < n { raw[i + 1] } else { None };
56 let right_cancel = matches!(
57 next,
58 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
59 );
60 if right_cancel {
61 eff[i] = Some(MathClass::Ord);
62 }
63 }
64 eff
65}
66
67fn node_is_middle_fence(node: &ParseNode) -> bool {
72 matches!(node, ParseNode::Middle { .. })
73}
74
75fn layout_expression(
77 nodes: &[ParseNode],
78 options: &LayoutOptions,
79 is_real_group: bool,
80) -> LayoutBox {
81 if nodes.is_empty() {
82 return LayoutBox::new_empty();
83 }
84
85 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
87 if has_cr {
88 return layout_multiline(nodes, options, is_real_group);
89 }
90
91 let raw_classes: Vec<Option<MathClass>> =
92 nodes.iter().map(node_math_class).collect();
93 let eff_classes = apply_bin_cancellation(&raw_classes);
94
95 let mut children = Vec::new();
96 let mut prev_class: Option<MathClass> = None;
97 let mut prev_class_node_idx: Option<usize> = None;
99
100 for (i, node) in nodes.iter().enumerate() {
101 let lbox = layout_node(node, options);
102 let cur_class = eff_classes.get(i).copied().flatten();
103
104 if is_real_group {
105 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
106 let prev_middle = prev_class_node_idx
107 .is_some_and(|j| node_is_middle_fence(&nodes[j]));
108 let cur_middle = node_is_middle_fence(node);
109 let mu = if prev_middle || cur_middle {
110 0.0
111 } else {
112 atom_spacing(prev, cur, options.style.is_tight())
113 };
114 let mu = if let Some(cap) = options.align_relation_spacing {
115 if prev == MathClass::Rel || cur == MathClass::Rel {
116 mu.min(cap)
117 } else {
118 mu
119 }
120 } else {
121 mu
122 };
123 if mu > 0.0 {
124 let em = mu_to_em(mu, options.metrics().quad);
125 children.push(LayoutBox::new_kern(em));
126 }
127 }
128 }
129
130 if cur_class.is_some() {
131 prev_class = cur_class;
132 prev_class_node_idx = Some(i);
133 }
134
135 children.push(lbox);
136 }
137
138 make_hbox(children)
139}
140
141fn layout_multiline(
143 nodes: &[ParseNode],
144 options: &LayoutOptions,
145 is_real_group: bool,
146) -> LayoutBox {
147 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
148 let metrics = options.metrics();
149 let pt = 1.0 / metrics.pt_per_em;
150 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
155 let mut start = 0;
156 for (i, node) in nodes.iter().enumerate() {
157 if matches!(node, ParseNode::Cr { .. }) {
158 rows.push(&nodes[start..i]);
159 start = i + 1;
160 }
161 }
162 rows.push(&nodes[start..]);
163
164 let row_boxes: Vec<LayoutBox> = rows
165 .iter()
166 .map(|row| layout_expression(row, options, is_real_group))
167 .collect();
168
169 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
170
171 let mut vchildren: Vec<VBoxChild> = Vec::new();
172 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
173 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
174 for (i, row) in row_boxes.iter().enumerate() {
175 if i > 0 {
176 let prev_depth = row_boxes[i - 1].depth;
178 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
179 vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
180 h += gap + row.height + prev_depth;
181 }
182 vchildren.push(VBoxChild {
183 kind: VBoxChildKind::Box(Box::new(row.clone())),
184 shift: 0.0,
185 });
186 }
187
188 LayoutBox {
189 width: total_width,
190 height: h,
191 depth: d,
192 content: BoxContent::VBox(vchildren),
193 color: options.color,
194 }
195}
196
197
198fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
200 match node {
201 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
202 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
203 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
204 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
205
206 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
207
208 ParseNode::SupSub {
209 base, sup, sub, ..
210 } => {
211 if let Some(base_node) = base.as_deref() {
212 if should_use_op_limits(base_node, options) {
213 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
214 }
215 }
216 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
217 }
218
219 ParseNode::GenFrac {
220 numer,
221 denom,
222 has_bar_line,
223 bar_size,
224 left_delim,
225 right_delim,
226 continued,
227 ..
228 } => {
229 let bar_thickness = if *has_bar_line {
230 bar_size
231 .as_ref()
232 .map(|m| measurement_to_em(m, options))
233 .unwrap_or(options.metrics().default_rule_thickness)
234 } else {
235 0.0
236 };
237 let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
238
239 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
240 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
241
242 if has_left || has_right {
243 let total_h = genfrac_delim_target_height(options);
244 let left_d = left_delim.as_deref().unwrap_or(".");
245 let right_d = right_delim.as_deref().unwrap_or(".");
246 let left_box = make_stretchy_delim(left_d, total_h, options);
247 let right_box = make_stretchy_delim(right_d, total_h, options);
248
249 let width = left_box.width + frac.width + right_box.width;
250 let height = frac.height.max(left_box.height).max(right_box.height);
251 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
252
253 LayoutBox {
254 width,
255 height,
256 depth,
257 content: BoxContent::LeftRight {
258 left: Box::new(left_box),
259 right: Box::new(right_box),
260 inner: Box::new(frac),
261 },
262 color: options.color,
263 }
264 } else {
265 let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE };
266 make_hbox(vec![
267 LayoutBox::new_kern(NULL_DELIMITER_SPACE),
268 frac,
269 LayoutBox::new_kern(right_nds),
270 ])
271 }
272 }
273
274 ParseNode::Sqrt { body, index, .. } => {
275 layout_radical(body, index.as_deref(), options)
276 }
277
278 ParseNode::Op {
279 name,
280 symbol,
281 body,
282 limits,
283 suppress_base_shift,
284 ..
285 } => layout_op(
286 name.as_deref(),
287 *symbol,
288 body.as_deref(),
289 *limits,
290 suppress_base_shift.unwrap_or(false),
291 options,
292 ),
293
294 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
295
296 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
297
298 ParseNode::Kern { dimension, .. } => {
299 let em = measurement_to_em(dimension, options);
300 LayoutBox::new_kern(em)
301 }
302
303 ParseNode::Color { color, body, .. } => {
304 let new_color = Color::parse(color).unwrap_or(options.color);
305 let new_opts = options.with_color(new_color);
306 let mut lbox = layout_expression(body, &new_opts, true);
307 lbox.color = new_color;
308 lbox
309 }
310
311 ParseNode::Styling { style, body, .. } => {
312 let new_style = match style {
313 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
314 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
315 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
316 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
317 };
318 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
319 let new_opts = options.with_style(new_style);
320 let inner = layout_expression(body, &new_opts, true);
321 if (ratio - 1.0).abs() < 0.001 {
322 inner
323 } else {
324 LayoutBox {
325 width: inner.width * ratio,
326 height: inner.height * ratio,
327 depth: inner.depth * ratio,
328 content: BoxContent::Scaled {
329 body: Box::new(inner),
330 child_scale: ratio,
331 },
332 color: options.color,
333 }
334 }
335 }
336
337 ParseNode::Accent {
338 label, base, is_stretchy, is_shifty, ..
339 } => {
340 let is_below = matches!(label.as_str(), "\\c");
342 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
343 }
344
345 ParseNode::AccentUnder {
346 label, base, is_stretchy, ..
347 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
348
349 ParseNode::LeftRight {
350 body, left, right, ..
351 } => layout_left_right(body, left, right, options),
352
353 ParseNode::DelimSizing {
354 size, delim, ..
355 } => layout_delim_sizing(*size, delim, options),
356
357 ParseNode::Array {
358 body,
359 cols,
360 arraystretch,
361 add_jot,
362 row_gaps,
363 hlines_before_row,
364 col_separation_type,
365 hskip_before_and_after,
366 is_cd,
367 tags,
368 leqno,
369 ..
370 } => {
371 if is_cd.unwrap_or(false) {
372 layout_cd(body, options)
373 } else {
374 layout_array(
375 body,
376 cols.as_deref(),
377 *arraystretch,
378 add_jot.unwrap_or(false),
379 row_gaps,
380 hlines_before_row,
381 col_separation_type.as_deref(),
382 hskip_before_and_after.unwrap_or(false),
383 tags.as_deref(),
384 leqno.unwrap_or(false),
385 options,
386 )
387 }
388 }
389
390 ParseNode::CdArrow {
391 direction,
392 label_above,
393 label_below,
394 ..
395 } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
396
397 ParseNode::ProofTree { tree, .. } => layout_proof_tree(tree, options),
398
399 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
400
401 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
402 Some(f) => {
403 let group = ParseNode::OrdGroup {
404 mode: *mode,
405 body: body.clone(),
406 semisimple: None,
407 loc: None,
408 };
409 layout_font(f, &group, options)
410 }
411 None => layout_text(body, options),
412 },
413
414 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
415
416 ParseNode::Href { body, .. } => layout_href(body, options),
417
418 ParseNode::Overline { body, .. } => layout_overline(body, options),
419 ParseNode::Underline { body, .. } => layout_underline(body, options),
420
421 ParseNode::Rule {
422 width: w,
423 height: h,
424 shift,
425 ..
426 } => {
427 let width = measurement_to_em(w, options);
428 let ink_h = measurement_to_em(h, options);
429 let raise = shift
430 .as_ref()
431 .map(|s| measurement_to_em(s, options))
432 .unwrap_or(0.0);
433 let box_height = (raise + ink_h).max(0.0);
434 let box_depth = (-raise).max(0.0);
435 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
436 }
437
438 ParseNode::Phantom { body, .. } => {
439 let inner = layout_expression(body, options, true);
440 LayoutBox {
441 width: inner.width,
442 height: inner.height,
443 depth: inner.depth,
444 content: BoxContent::Empty,
445 color: Color::BLACK,
446 }
447 }
448
449 ParseNode::VPhantom { body, .. } => {
450 let inner = layout_node(body, options);
451 LayoutBox {
452 width: 0.0,
453 height: inner.height,
454 depth: inner.depth,
455 content: BoxContent::Empty,
456 color: Color::BLACK,
457 }
458 }
459
460 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
461 let mut inner = layout_node(body, options);
462 if *smash_height { inner.height = 0.0; }
463 if *smash_depth { inner.depth = 0.0; }
464 inner
465 }
466
467 ParseNode::Middle { delim, .. } => {
468 match options.leftright_delim_height {
469 Some(h) => make_stretchy_delim(delim, h, options),
470 None => {
471 let placeholder = make_stretchy_delim(delim, 1.0, options);
473 LayoutBox {
474 width: placeholder.width,
475 height: 0.0,
476 depth: 0.0,
477 content: BoxContent::Empty,
478 color: options.color,
479 }
480 }
481 }
482 }
483
484 ParseNode::HtmlMathMl { html, .. } => {
485 layout_expression(html, options, true)
486 }
487
488 ParseNode::Html { attributes, body, .. } => layout_html(attributes, body, options),
489
490 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
491
492 ParseNode::MathChoice {
493 display, text, script, scriptscript, ..
494 } => {
495 let branch = match options.style {
496 MathStyle::Display | MathStyle::DisplayCramped => display,
497 MathStyle::Text | MathStyle::TextCramped => text,
498 MathStyle::Script | MathStyle::ScriptCramped => script,
499 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
500 };
501 layout_expression(branch, options, true)
502 }
503
504 ParseNode::Lap { alignment, body, .. } => {
505 let inner = layout_node(body, options);
506 let shift = match alignment.as_str() {
507 "llap" => -inner.width,
508 "clap" => -inner.width / 2.0,
509 _ => 0.0, };
511 let mut children = Vec::new();
512 if shift != 0.0 {
513 children.push(LayoutBox::new_kern(shift));
514 }
515 let h = inner.height;
516 let d = inner.depth;
517 children.push(inner);
518 LayoutBox {
519 width: 0.0,
520 height: h,
521 depth: d,
522 content: BoxContent::HBox(children),
523 color: options.color,
524 }
525 }
526
527 ParseNode::HorizBrace {
528 base,
529 is_over,
530 label,
531 ..
532 } => layout_horiz_brace(base, *is_over, label, options),
533
534 ParseNode::XArrow {
535 label, body, below, ..
536 } => layout_xarrow(label, body, below.as_deref(), options),
537
538 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
539
540 ParseNode::HBox { body, .. } => layout_text(body, options),
541
542 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
543 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
544 }
545
546 ParseNode::RaiseBox { dy, body, .. } => {
547 let shift = measurement_to_em(dy, options);
548 layout_raisebox(shift, body, options)
549 }
550
551 ParseNode::VCenter { body, .. } => {
552 let inner = layout_node(body, options);
554 let axis = options.metrics().axis_height;
555 let total = inner.height + inner.depth;
556 let height = total / 2.0 + axis;
557 let depth = total - height;
558 LayoutBox {
559 width: inner.width,
560 height,
561 depth,
562 content: inner.content,
563 color: inner.color,
564 }
565 }
566
567 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
568
569 ParseNode::Tag { tag, .. } => {
570 let text_opts = options.with_style(options.style.text());
571 layout_expression(tag, &text_opts, true)
572 },
573
574 _ => LayoutBox::new_empty(),
576 }
577}
578
579fn missing_glyph_width_em(ch: char) -> f64 {
589 match ch as u32 {
590 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
592 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
594 0xAC00..=0xD7AF => 1.0,
596 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
598 0x1F000..=0x1FAFF => 1.0,
601 0x2700..=0x27BF => 1.0,
603 0x2600..=0x26FF => 1.0,
605 0x2B00..=0x2BFF => 1.0,
607 _ => 0.5,
608 }
609}
610
611fn missing_glyph_height_em(ch: char, m: &ratex_font::MathConstants) -> f64 {
612 let ru = ch as u32;
613 if (0x1F000..=0x1FAFF).contains(&ru) {
614 (m.quad * 0.74).max(m.x_height)
619 } else {
620 (m.quad * 0.92).max(m.x_height)
621 }
622}
623
624fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
625 let m = get_global_metrics(options.style.size_index());
626 let w = missing_glyph_width_em(ch);
627 if w >= 0.99 {
628 let h = missing_glyph_height_em(ch, m);
629 (w, h, 0.0)
630 } else {
631 (w, m.x_height, 0.0)
632 }
633}
634
635#[inline]
637fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
638 if mode == Mode::Math {
639 m.width + m.italic
640 } else {
641 m.width
642 }
643}
644
645fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
646 let ch = resolve_symbol_char(text, mode);
647
648 match ch as u32 {
650 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
653 }
654
655 let char_code = ch as u32;
656
657 if let Some((font_id, metric_cp)) =
658 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
659 {
660 let m = get_char_metrics(font_id, metric_cp);
661 let (width, height, depth) = match m {
662 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
663 None => missing_glyph_metrics_fallback(ch, options),
664 };
665 return LayoutBox {
666 width,
667 height,
668 depth,
669 content: BoxContent::Glyph {
670 font_id,
671 char_code,
672 },
673 color: options.color,
674 };
675 }
676
677 let mut font_id = select_font(text, ch, mode, options);
678 let mut metrics = get_char_metrics(font_id, char_code);
679
680 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
681 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
682 font_id = FontId::MathItalic;
683 metrics = Some(m);
684 }
685 }
686
687 let (width, height, depth) = if let Some(m) = metrics {
693 (math_glyph_advance_em(&m, mode), m.height, m.depth)
694 } else if mode == Mode::Math {
695 let size_font = if options.style.is_display() {
696 FontId::Size2Regular
697 } else {
698 FontId::Size1Regular
699 };
700 match get_char_metrics(size_font, char_code)
701 .or_else(|| get_char_metrics(FontId::Size1Regular, char_code))
702 {
703 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
704 None => missing_glyph_metrics_fallback(ch, options),
705 }
706 } else {
707 missing_glyph_metrics_fallback(ch, options)
708 };
709
710 if metrics.is_none() && missing_glyph_width_em(ch) >= 0.99 {
713 font_id = FontId::CjkRegular;
714 }
715
716 LayoutBox {
717 width,
718 height,
719 depth,
720 content: BoxContent::Glyph {
721 font_id,
722 char_code,
723 },
724 color: options.color,
725 }
726}
727
728fn resolve_symbol_char(text: &str, mode: Mode) -> char {
730 let font_mode = match mode {
731 Mode::Math => ratex_font::Mode::Math,
732 Mode::Text => ratex_font::Mode::Text,
733 };
734
735 if let Some(raw) = text.chars().next() {
736 let ru = raw as u32;
737 if (0x1D400..=0x1D7FF).contains(&ru) {
738 return raw;
739 }
740 }
741
742 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
743 if let Some(cp) = info.codepoint {
744 return cp;
745 }
746 }
747
748 text.chars().next().unwrap_or('?')
749}
750
751fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
755 let font_mode = match mode {
756 Mode::Math => ratex_font::Mode::Math,
757 Mode::Text => ratex_font::Mode::Text,
758 };
759
760 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
761 if info.font == ratex_font::SymbolFont::Ams {
762 return FontId::AmsRegular;
763 }
764 }
765
766 match mode {
767 Mode::Math => {
768 if resolved_char.is_ascii_lowercase()
769 || resolved_char.is_ascii_uppercase()
770 || is_math_italic_greek(resolved_char)
771 {
772 FontId::MathItalic
773 } else {
774 FontId::MainRegular
775 }
776 }
777 Mode::Text => FontId::MainRegular,
778 }
779}
780
781fn is_math_italic_greek(ch: char) -> bool {
784 matches!(ch,
785 '\u{03B1}'..='\u{03C9}' |
786 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
787 '\u{03F1}' | '\u{03F5}'
788 )
789}
790
791fn is_arrow_accent(label: &str) -> bool {
792 matches!(
793 label,
794 "\\overrightarrow"
795 | "\\overleftarrow"
796 | "\\Overrightarrow"
797 | "\\overleftrightarrow"
798 | "\\underrightarrow"
799 | "\\underleftarrow"
800 | "\\underleftrightarrow"
801 | "\\overleftharpoon"
802 | "\\overrightharpoon"
803 | "\\overlinesegment"
804 | "\\underlinesegment"
805 )
806}
807
808fn layout_fraction(
813 numer: &ParseNode,
814 denom: &ParseNode,
815 bar_thickness: f64,
816 continued: bool,
817 options: &LayoutOptions,
818) -> LayoutBox {
819 let numer_s = options.style.numerator();
820 let denom_s = options.style.denominator();
821 let numer_style = options.with_style(numer_s);
822 let denom_style = options.with_style(denom_s);
823
824 let mut numer_box = layout_node(numer, &numer_style);
825 if continued {
827 let pt = options.metrics().pt_per_em;
828 let h_min = 8.5 / pt;
829 let d_min = 3.5 / pt;
830 if numer_box.height < h_min {
831 numer_box.height = h_min;
832 }
833 if numer_box.depth < d_min {
834 numer_box.depth = d_min;
835 }
836 }
837 let denom_box = layout_node(denom, &denom_style);
838
839 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
841 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
842
843 let numer_height = numer_box.height * numer_ratio;
844 let numer_depth = numer_box.depth * numer_ratio;
845 let denom_height = denom_box.height * denom_ratio;
846 let denom_depth = denom_box.depth * denom_ratio;
847 let numer_width = numer_box.width * numer_ratio;
848 let denom_width = denom_box.width * denom_ratio;
849
850 let metrics = options.metrics();
851 let axis = metrics.axis_height;
852 let rule = bar_thickness;
853
854 let (mut num_shift, mut den_shift) = if options.style.is_display() {
856 (metrics.num1, metrics.denom1)
857 } else if bar_thickness > 0.0 {
858 (metrics.num2, metrics.denom2)
859 } else {
860 (metrics.num3, metrics.denom2)
861 };
862
863 if bar_thickness > 0.0 {
864 let min_clearance = if options.style.is_display() {
865 3.0 * rule
866 } else {
867 rule
868 };
869
870 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
871 if num_clearance < min_clearance {
872 num_shift += min_clearance - num_clearance;
873 }
874
875 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
876 if den_clearance < min_clearance {
877 den_shift += min_clearance - den_clearance;
878 }
879 } else {
880 let min_gap = if options.style.is_display() {
881 7.0 * metrics.default_rule_thickness
882 } else {
883 3.0 * metrics.default_rule_thickness
884 };
885
886 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
887 if gap < min_gap {
888 let adjust = (min_gap - gap) / 2.0;
889 num_shift += adjust;
890 den_shift += adjust;
891 }
892 }
893
894 let total_width = numer_width.max(denom_width);
895 let height = numer_height + num_shift;
896 let depth = denom_depth + den_shift;
897
898 LayoutBox {
899 width: total_width,
900 height,
901 depth,
902 content: BoxContent::Fraction {
903 numer: Box::new(numer_box),
904 denom: Box::new(denom_box),
905 numer_shift: num_shift,
906 denom_shift: den_shift,
907 bar_thickness: rule,
908 numer_scale: numer_ratio,
909 denom_scale: denom_ratio,
910 },
911 color: options.color,
912 }
913}
914
915fn layout_supsub(
920 base: Option<&ParseNode>,
921 sup: Option<&ParseNode>,
922 sub: Option<&ParseNode>,
923 options: &LayoutOptions,
924 inherited_font: Option<FontId>,
925) -> LayoutBox {
926 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
927 Some(fid) => layout_with_font(n, fid, opts),
928 None => layout_node(n, opts),
929 };
930
931 let horiz_brace_over = matches!(
932 base,
933 Some(ParseNode::HorizBrace {
934 is_over: true,
935 ..
936 })
937 );
938 let horiz_brace_under = matches!(
939 base,
940 Some(ParseNode::HorizBrace {
941 is_over: false,
942 ..
943 })
944 );
945 let center_scripts = horiz_brace_over || horiz_brace_under;
946
947 let base_box = base
948 .map(|b| layout_child(b, options))
949 .unwrap_or_else(LayoutBox::new_empty);
950
951 let is_char_box = base.is_some_and(is_character_box);
952 let metrics = options.metrics();
953 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
957
958 let sup_style = options.style.superscript();
959 let sub_style = options.style.subscript();
960
961 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
962 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
963
964 let sup_box = sup.map(|s| {
965 let sup_opts = options.with_style(sup_style);
966 layout_child(s, &sup_opts)
967 });
968
969 let sub_box = sub.map(|s| {
970 let sub_opts = options.with_style(sub_style);
971 layout_child(s, &sub_opts)
972 });
973
974 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
975 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
976 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
977 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
978
979 let sup_style_metrics = get_global_metrics(sup_style.size_index());
981 let sub_style_metrics = get_global_metrics(sub_style.size_index());
982
983 let mut sup_shift = if !is_char_box && sup_box.is_some() {
986 base_box.height - sup_style_metrics.sup_drop * sup_ratio
987 } else {
988 0.0
989 };
990
991 let mut sub_shift = if !is_char_box && sub_box.is_some() {
992 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
993 } else {
994 0.0
995 };
996
997 let min_sup_shift = if options.style.is_cramped() {
998 metrics.sup3
999 } else if options.style.is_display() {
1000 metrics.sup1
1001 } else {
1002 metrics.sup2
1003 };
1004
1005 if sup_box.is_some() && sub_box.is_some() {
1006 sup_shift = sup_shift
1008 .max(min_sup_shift)
1009 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1010 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
1013 let max_width = 4.0 * rule_width;
1014 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
1015 if gap < max_width {
1016 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
1017 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
1018 if psi > 0.0 {
1019 sup_shift += psi;
1020 sub_shift -= psi;
1021 }
1022 }
1023 } else if sub_box.is_some() {
1024 sub_shift = sub_shift
1026 .max(metrics.sub1)
1027 .max(sub_height_scaled - 0.8 * metrics.x_height);
1028 } else if sup_box.is_some() {
1029 sup_shift = sup_shift
1031 .max(min_sup_shift)
1032 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1033 }
1034
1035 if horiz_brace_over && sup_box.is_some() {
1039 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
1040 }
1041 if horiz_brace_under && sub_box.is_some() {
1042 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
1043 }
1044
1045 let italic_correction = 0.0;
1048
1049 let sub_h_kern = if sub_box.is_some() && !center_scripts {
1052 -glyph_italic(&base_box)
1053 } else {
1054 0.0
1055 };
1056
1057 let mut height = base_box.height;
1059 let mut depth = base_box.depth;
1060 let mut total_width = base_box.width;
1061
1062 if let Some(ref sup_b) = sup_box {
1063 height = height.max(sup_shift + sup_height_scaled);
1064 if center_scripts {
1065 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1066 } else {
1067 total_width = total_width.max(
1068 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1069 );
1070 }
1071 }
1072 if let Some(ref sub_b) = sub_box {
1073 depth = depth.max(sub_shift + sub_depth_scaled);
1074 if center_scripts {
1075 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1076 } else {
1077 total_width = total_width.max(
1078 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1079 );
1080 }
1081 }
1082
1083 LayoutBox {
1084 width: total_width,
1085 height,
1086 depth,
1087 content: BoxContent::SupSub {
1088 base: Box::new(base_box),
1089 sup: sup_box.map(Box::new),
1090 sub: sub_box.map(Box::new),
1091 sup_shift,
1092 sub_shift,
1093 sup_scale: sup_ratio,
1094 sub_scale: sub_ratio,
1095 center_scripts,
1096 italic_correction,
1097 sub_h_kern,
1098 },
1099 color: options.color,
1100 }
1101}
1102
1103fn layout_radical(
1108 body: &ParseNode,
1109 index: Option<&ParseNode>,
1110 options: &LayoutOptions,
1111) -> LayoutBox {
1112 let cramped = options.style.cramped();
1113 let cramped_opts = options.with_style(cramped);
1114 let mut body_box = layout_node(body, &cramped_opts);
1115
1116 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1118 body_box.height *= body_ratio;
1119 body_box.depth *= body_ratio;
1120 body_box.width *= body_ratio;
1121
1122 if body_box.height == 0.0 {
1124 body_box.height = options.metrics().x_height;
1125 }
1126
1127 let metrics = options.metrics();
1128 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1133 metrics.x_height
1134 } else {
1135 theta
1136 };
1137
1138 let mut line_clearance = theta + phi / 4.0;
1139
1140 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1142
1143 let tex_height = select_surd_height(min_delim_height);
1146 let rule_width = theta;
1147 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1148 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1149 .map(|m| m.width)
1150 .unwrap_or(0.833);
1151
1152 let delim_depth = tex_height - rule_width;
1154 if delim_depth > body_box.height + body_box.depth + line_clearance {
1155 line_clearance =
1156 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1157 }
1158
1159 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1160
1161 let height = tex_height + rule_width - img_shift;
1164 let depth = if img_shift > body_box.depth {
1165 img_shift
1166 } else {
1167 body_box.depth
1168 };
1169
1170 const INDEX_KERN: f64 = 0.05;
1172 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1173 let root_style = options.style.superscript().superscript();
1174 let root_opts = options.with_style(root_style);
1175 let idx = layout_node(index_node, &root_opts);
1176 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1177 let offset = idx.width * index_ratio + INDEX_KERN;
1178 (Some(Box::new(idx)), offset, index_ratio)
1179 } else {
1180 (None, 0.0, 1.0)
1181 };
1182
1183 let width = index_offset + advance_width + body_box.width;
1184
1185 LayoutBox {
1186 width,
1187 height,
1188 depth,
1189 content: BoxContent::Radical {
1190 body: Box::new(body_box),
1191 index: index_box,
1192 index_offset,
1193 index_scale,
1194 rule_thickness: rule_width,
1195 inner_height: tex_height,
1196 },
1197 color: options.color,
1198 }
1199}
1200
1201fn select_surd_height(min_height: f64) -> f64 {
1204 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1205 for &h in &SURD_HEIGHTS {
1206 if h >= min_height {
1207 return h;
1208 }
1209 }
1210 SURD_HEIGHTS[4].max(min_height)
1212}
1213
1214const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1219
1220fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1222 match base {
1223 ParseNode::Op {
1224 limits,
1225 always_handle_sup_sub,
1226 ..
1227 } => {
1228 *limits
1229 && (options.style.is_display()
1230 || always_handle_sup_sub.unwrap_or(false))
1231 }
1232 ParseNode::OperatorName {
1233 always_handle_sup_sub,
1234 limits,
1235 ..
1236 } => {
1237 *always_handle_sup_sub
1238 && (options.style.is_display() || *limits)
1239 }
1240 _ => false,
1241 }
1242}
1243
1244fn layout_op(
1250 name: Option<&str>,
1251 symbol: bool,
1252 body: Option<&[ParseNode]>,
1253 _limits: bool,
1254 suppress_base_shift: bool,
1255 options: &LayoutOptions,
1256) -> LayoutBox {
1257 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1258
1259 if symbol && !suppress_base_shift {
1261 let axis = options.metrics().axis_height;
1262 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1263 if shift.abs() > 0.001 {
1264 base_box.height -= shift;
1265 base_box.depth += shift;
1266 }
1267 }
1268
1269 if !suppress_base_shift && !symbol && body.is_some() {
1274 let axis = options.metrics().axis_height;
1275 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1276 if delta.abs() > 0.001 {
1277 let w = base_box.width;
1278 let raise = -delta;
1280 base_box = LayoutBox {
1281 width: w,
1282 height: (base_box.height + raise).max(0.0),
1283 depth: (base_box.depth - raise).max(0.0),
1284 content: BoxContent::RaiseBox {
1285 body: Box::new(base_box),
1286 shift: raise,
1287 },
1288 color: options.color,
1289 };
1290 }
1291 }
1292
1293 base_box
1294}
1295
1296fn build_op_base(
1299 name: Option<&str>,
1300 symbol: bool,
1301 body: Option<&[ParseNode]>,
1302 options: &LayoutOptions,
1303) -> (LayoutBox, f64) {
1304 if symbol {
1305 let large = options.style.is_display()
1306 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1307 let font_id = if large {
1308 FontId::Size2Regular
1309 } else {
1310 FontId::Size1Regular
1311 };
1312
1313 let op_name = name.unwrap_or("");
1314 let ch = resolve_op_char(op_name);
1315 let char_code = ch as u32;
1316
1317 let metrics = get_char_metrics(font_id, char_code);
1318 let (width, height, depth, italic) = match metrics {
1319 Some(m) => (m.width, m.height, m.depth, m.italic),
1320 None => (1.0, 0.75, 0.25, 0.0),
1321 };
1322 let width_with_italic = width + italic;
1325
1326 let base = LayoutBox {
1327 width: width_with_italic,
1328 height,
1329 depth,
1330 content: BoxContent::Glyph {
1331 font_id,
1332 char_code,
1333 },
1334 color: options.color,
1335 };
1336
1337 if op_name == "\\oiint" || op_name == "\\oiiint" {
1340 let w = base.width;
1341 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1342 let overlay_box = LayoutBox {
1343 width: w,
1344 height: base.height,
1345 depth: base.depth,
1346 content: BoxContent::SvgPath {
1347 commands: ellipse_commands,
1348 fill: false,
1349 },
1350 color: options.color,
1351 };
1352 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1353 return (with_overlay, italic);
1354 }
1355
1356 (base, italic)
1357 } else if let Some(body_nodes) = body {
1358 let base = layout_expression(body_nodes, options, true);
1359 (base, 0.0)
1360 } else {
1361 let base = layout_op_text(name.unwrap_or(""), options);
1362 (base, 0.0)
1363 }
1364}
1365
1366fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1368 let text = name.strip_prefix('\\').unwrap_or(name);
1369 let mut children = Vec::new();
1370 for ch in text.chars() {
1371 let char_code = ch as u32;
1372 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1373 let (width, height, depth) = match metrics {
1374 Some(m) => (m.width, m.height, m.depth),
1375 None => (0.5, 0.43, 0.0),
1376 };
1377 children.push(LayoutBox {
1378 width,
1379 height,
1380 depth,
1381 content: BoxContent::Glyph {
1382 font_id: FontId::MainRegular,
1383 char_code,
1384 },
1385 color: options.color,
1386 });
1387 }
1388 make_hbox(children)
1389}
1390
1391fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1393 let metrics = options.metrics();
1394 (base.height - base.depth) / 2.0 - metrics.axis_height
1395}
1396
1397fn resolve_op_char(name: &str) -> char {
1399 match name {
1402 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1405 }
1406 let font_mode = ratex_font::Mode::Math;
1407 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1408 if let Some(cp) = info.codepoint {
1409 return cp;
1410 }
1411 }
1412 name.chars().next().unwrap_or('?')
1413}
1414
1415fn layout_op_with_limits(
1417 base_node: &ParseNode,
1418 sup_node: Option<&ParseNode>,
1419 sub_node: Option<&ParseNode>,
1420 options: &LayoutOptions,
1421) -> LayoutBox {
1422 let (name, symbol, body, suppress_base_shift) = match base_node {
1423 ParseNode::Op {
1424 name,
1425 symbol,
1426 body,
1427 suppress_base_shift,
1428 ..
1429 } => (
1430 name.as_deref(),
1431 *symbol,
1432 body.as_deref(),
1433 suppress_base_shift.unwrap_or(false),
1434 ),
1435 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1436 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1437 };
1438
1439 let legacy_limit_kern_padding = !suppress_base_shift;
1441
1442 let (base_box, slant) = build_op_base(name, symbol, body, options);
1443 let base_shift = if symbol && !suppress_base_shift {
1445 compute_op_base_shift(&base_box, options)
1446 } else {
1447 0.0
1448 };
1449
1450 layout_op_limits_inner(
1451 &base_box,
1452 sup_node,
1453 sub_node,
1454 slant,
1455 base_shift,
1456 legacy_limit_kern_padding,
1457 options,
1458 )
1459}
1460
1461fn layout_op_limits_inner(
1466 base: &LayoutBox,
1467 sup_node: Option<&ParseNode>,
1468 sub_node: Option<&ParseNode>,
1469 slant: f64,
1470 base_shift: f64,
1471 legacy_limit_kern_padding: bool,
1472 options: &LayoutOptions,
1473) -> LayoutBox {
1474 let metrics = options.metrics();
1475 let sup_style = options.style.superscript();
1476 let sub_style = options.style.subscript();
1477
1478 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1479 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1480
1481 let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 };
1482
1483 let sup_data = sup_node.map(|s| {
1484 let sup_opts = options.with_style(sup_style);
1485 let elem = layout_node(s, &sup_opts);
1486 let d = if legacy_limit_kern_padding {
1490 elem.depth * sup_ratio
1491 } else {
1492 elem.depth
1493 };
1494 let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1495 (elem, kern)
1496 });
1497
1498 let sub_data = sub_node.map(|s| {
1499 let sub_opts = options.with_style(sub_style);
1500 let elem = layout_node(s, &sub_opts);
1501 let h = if legacy_limit_kern_padding {
1502 elem.height * sub_ratio
1503 } else {
1504 elem.height
1505 };
1506 let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1507 (elem, kern)
1508 });
1509
1510 let sp5 = metrics.big_op_spacing5;
1511
1512 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1513 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1514 let sup_h = sup_elem.height * sup_ratio;
1517 let sup_d = sup_elem.depth * sup_ratio;
1518 let sub_h = sub_elem.height * sub_ratio;
1519 let sub_d = sub_elem.depth * sub_ratio;
1520
1521 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1522
1523 let height = bottom
1524 + base.height - base_shift
1525 + sup_kern
1526 + sup_h + sup_d
1527 + sp5
1528 - (base.height + base.depth);
1529
1530 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1531 let total_d = bottom;
1532
1533 let w = base
1534 .width
1535 .max(sup_elem.width * sup_ratio)
1536 .max(sub_elem.width * sub_ratio);
1537 let _ = height; (total_h, total_d, w)
1539 }
1540 (None, Some((sub_elem, sub_kern))) => {
1541 let sub_h = sub_elem.height * sub_ratio;
1544 let sub_d = sub_elem.depth * sub_ratio;
1545
1546 let total_h = base.height - base_shift;
1547 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1548
1549 let w = base.width.max(sub_elem.width * sub_ratio);
1550 (total_h, total_d, w)
1551 }
1552 (Some((sup_elem, sup_kern)), None) => {
1553 let sup_h = sup_elem.height * sup_ratio;
1556 let sup_d = sup_elem.depth * sup_ratio;
1557
1558 let total_h =
1559 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1560 let total_d = base.depth + base_shift;
1561
1562 let w = base.width.max(sup_elem.width * sup_ratio);
1563 (total_h, total_d, w)
1564 }
1565 (None, None) => {
1566 return base.clone();
1567 }
1568 };
1569
1570 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1571 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1572
1573 LayoutBox {
1574 width: total_width,
1575 height: total_height,
1576 depth: total_depth,
1577 content: BoxContent::OpLimits {
1578 base: Box::new(base.clone()),
1579 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1580 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1581 base_shift,
1582 sup_kern: sup_kern_val,
1583 sub_kern: sub_kern_val,
1584 slant,
1585 sup_scale: sup_ratio,
1586 sub_scale: sub_ratio,
1587 },
1588 color: options.color,
1589 }
1590}
1591
1592fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1594 let mut children = Vec::new();
1595 for node in body {
1596 match node {
1597 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1598 let ch = text.chars().next().unwrap_or('?');
1599 let char_code = ch as u32;
1600 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1601 let (width, height, depth) = match metrics {
1602 Some(m) => (m.width, m.height, m.depth),
1603 None => (0.5, 0.43, 0.0),
1604 };
1605 children.push(LayoutBox {
1606 width,
1607 height,
1608 depth,
1609 content: BoxContent::Glyph {
1610 font_id: FontId::MainRegular,
1611 char_code,
1612 },
1613 color: options.color,
1614 });
1615 }
1616 _ => {
1617 children.push(layout_node(node, options));
1618 }
1619 }
1620 }
1621 make_hbox(children)
1622}
1623
1624const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1630
1631fn glyph_italic(lb: &LayoutBox) -> f64 {
1635 match &lb.content {
1636 BoxContent::Glyph { font_id, char_code } => {
1637 get_char_metrics(*font_id, *char_code)
1638 .map(|m| m.italic)
1639 .unwrap_or(0.0)
1640 }
1641 BoxContent::HBox(children) => {
1642 children.last().map(glyph_italic).unwrap_or(0.0)
1643 }
1644 _ => 0.0,
1645 }
1646}
1647
1648fn accent_ordgroup_len(base: &ParseNode) -> usize {
1653 match base {
1654 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1655 _ => 1,
1656 }
1657}
1658
1659fn glyph_skew(lb: &LayoutBox) -> f64 {
1660 match &lb.content {
1661 BoxContent::Glyph { font_id, char_code } => {
1662 get_char_metrics(*font_id, *char_code)
1663 .map(|m| m.skew)
1664 .unwrap_or(0.0)
1665 }
1666 BoxContent::HBox(children) => {
1667 children.last().map(glyph_skew).unwrap_or(0.0)
1668 }
1669 _ => 0.0,
1670 }
1671}
1672
1673fn layout_accent(
1674 label: &str,
1675 base: &ParseNode,
1676 is_stretchy: bool,
1677 is_shifty: bool,
1678 is_below: bool,
1679 options: &LayoutOptions,
1680) -> LayoutBox {
1681 let body_box = layout_node(base, options);
1682 let base_w = body_box.width.max(0.5);
1683
1684 if label == "\\textcircled" {
1686 return layout_textcircled(body_box, options);
1687 }
1688
1689 if let Some((commands, w, h, fill)) =
1691 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1692 {
1693 let accent_box = LayoutBox {
1695 width: w,
1696 height: 0.0,
1697 depth: h,
1698 content: BoxContent::SvgPath { commands, fill },
1699 color: options.color,
1700 };
1701 let gap = 0.065;
1706 let under_gap_em = if is_below && label == "\\utilde" {
1707 0.12
1708 } else {
1709 0.0
1710 };
1711 let clearance = if is_below {
1712 body_box.height + body_box.depth + gap
1713 } else if label == "\\vec" {
1714 (body_box.height - options.metrics().x_height).max(0.0)
1717 } else {
1718 body_box.height + gap
1719 };
1720 let (height, depth) = if is_below {
1721 (body_box.height, body_box.depth + h + gap + under_gap_em)
1722 } else if label == "\\vec" {
1723 (clearance + h, body_box.depth)
1725 } else {
1726 (body_box.height + gap + h, body_box.depth)
1727 };
1728 let vec_skew = if label == "\\vec" {
1729 (if is_shifty {
1730 glyph_skew(&body_box)
1731 } else {
1732 0.0
1733 }) + VEC_SKEW_EXTRA_RIGHT_EM
1734 } else {
1735 0.0
1736 };
1737 return LayoutBox {
1738 width: body_box.width,
1739 height,
1740 depth,
1741 content: BoxContent::Accent {
1742 base: Box::new(body_box),
1743 accent: Box::new(accent_box),
1744 clearance,
1745 skew: vec_skew,
1746 is_below,
1747 under_gap_em,
1748 },
1749 color: options.color,
1750 };
1751 }
1752
1753 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1755
1756 let accent_box = if use_arrow_path {
1757 let (commands, arrow_h, fill_arrow) =
1758 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1759 Some((c, h)) => (c, h, true),
1760 None => {
1761 let h = 0.3_f64;
1762 let c = stretchy_accent_path(label, base_w, h);
1763 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1764 (c, h, fill)
1765 }
1766 };
1767 LayoutBox {
1768 width: base_w,
1769 height: arrow_h / 2.0,
1770 depth: arrow_h / 2.0,
1771 content: BoxContent::SvgPath {
1772 commands,
1773 fill: fill_arrow,
1774 },
1775 color: options.color,
1776 }
1777 } else {
1778 let accent_char = {
1780 let ch = resolve_symbol_char(label, Mode::Text);
1781 if ch == label.chars().next().unwrap_or('?') {
1782 resolve_symbol_char(label, Mode::Math)
1785 } else {
1786 ch
1787 }
1788 };
1789 let accent_code = accent_char as u32;
1790 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1791 let (accent_w, accent_h, accent_d) = match accent_metrics {
1792 Some(m) => (m.width, m.height, m.depth),
1793 None => (body_box.width, 0.25, 0.0),
1794 };
1795 LayoutBox {
1796 width: accent_w,
1797 height: accent_h,
1798 depth: accent_d,
1799 content: BoxContent::Glyph {
1800 font_id: FontId::MainRegular,
1801 char_code: accent_code,
1802 },
1803 color: options.color,
1804 }
1805 };
1806
1807 let skew = if use_arrow_path {
1808 0.0
1809 } else if is_shifty {
1810 glyph_skew(&body_box)
1813 } else {
1814 0.0
1815 };
1816
1817 let gap = if use_arrow_path {
1826 if label == "\\Overrightarrow" {
1827 0.21
1828 } else {
1829 0.26
1830 }
1831 } else {
1832 0.0
1833 };
1834
1835 let clearance = if is_below {
1836 body_box.height + body_box.depth + accent_box.depth + gap
1837 } else if use_arrow_path {
1838 body_box.height + gap
1839 } else {
1840 let base_clearance = match &body_box.content {
1847 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1848 if !is_below =>
1849 {
1850 if inner_accent.height <= 0.001 {
1854 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1860 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1861 katex_pos + correction
1862 } else {
1863 if label == "\\bar" || label == "\\=" {
1869 body_box.height
1870 } else {
1871 let inner_visual_top = inner_cl + 0.35_f64.min(inner_accent.height);
1876 let h_for_kern = if body_box.height > inner_visual_top + 0.002 {
1877 inner_visual_top
1878 } else {
1879 body_box.height
1880 };
1881 let katex_pos = (h_for_kern - options.metrics().x_height).max(0.0);
1882 let correction =
1883 (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1884 katex_pos + correction
1885 }
1886 }
1887 }
1888 _ => {
1889 if label == "\\bar" || label == "\\=" {
1902 body_box.height
1903 } else {
1904 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1905 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1906 katex_pos + correction
1907 }
1908 }
1909 };
1910 let base_clearance = base_clearance + accent_box.depth;
1915 if label == "\\bar" || label == "\\=" {
1916 (base_clearance - 0.12).max(0.0)
1917 } else {
1918 base_clearance
1919 }
1920 };
1921
1922 let (height, depth) = if is_below {
1923 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1924 } else if use_arrow_path {
1925 (body_box.height + gap + accent_box.height, body_box.depth)
1926 } else {
1927 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1934 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1935 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1936 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1937 } else {
1938 body_box.height.max(accent_visual_top)
1939 };
1940 (h, body_box.depth)
1941 };
1942
1943 LayoutBox {
1944 width: body_box.width,
1945 height,
1946 depth,
1947 content: BoxContent::Accent {
1948 base: Box::new(body_box),
1949 accent: Box::new(accent_box),
1950 clearance,
1951 skew,
1952 is_below,
1953 under_gap_em: 0.0,
1954 },
1955 color: options.color,
1956 }
1957}
1958
1959fn node_contains_middle(node: &ParseNode) -> bool {
1965 match node {
1966 ParseNode::Middle { .. } => true,
1967 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1968 body.iter().any(node_contains_middle)
1969 }
1970 ParseNode::SupSub { base, sup, sub, .. } => {
1971 base.as_deref().is_some_and(node_contains_middle)
1972 || sup.as_deref().is_some_and(node_contains_middle)
1973 || sub.as_deref().is_some_and(node_contains_middle)
1974 }
1975 ParseNode::GenFrac { numer, denom, .. } => {
1976 node_contains_middle(numer) || node_contains_middle(denom)
1977 }
1978 ParseNode::Sqrt { body, index, .. } => {
1979 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1980 }
1981 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1982 node_contains_middle(base)
1983 }
1984 ParseNode::Op { body, .. } => body
1985 .as_ref()
1986 .is_some_and(|b| b.iter().any(node_contains_middle)),
1987 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1988 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1989 ParseNode::Font { body, .. } => node_contains_middle(body),
1990 ParseNode::Text { body, .. }
1991 | ParseNode::Color { body, .. }
1992 | ParseNode::Styling { body, .. }
1993 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1994 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1995 node_contains_middle(body)
1996 }
1997 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1998 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1999 node_contains_middle(body)
2000 }
2001 ParseNode::Array { body, .. } => body
2002 .iter()
2003 .any(|row| row.iter().any(node_contains_middle)),
2004 ParseNode::Enclose { body, .. }
2005 | ParseNode::Lap { body, .. }
2006 | ParseNode::RaiseBox { body, .. }
2007 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
2008 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
2009 ParseNode::XArrow { body, below, .. } => {
2010 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
2011 }
2012 ParseNode::CdArrow { label_above, label_below, .. } => {
2013 label_above.as_deref().is_some_and(node_contains_middle)
2014 || label_below.as_deref().is_some_and(node_contains_middle)
2015 }
2016 ParseNode::MathChoice {
2017 display,
2018 text,
2019 script,
2020 scriptscript,
2021 ..
2022 } => {
2023 display.iter().any(node_contains_middle)
2024 || text.iter().any(node_contains_middle)
2025 || script.iter().any(node_contains_middle)
2026 || scriptscript.iter().any(node_contains_middle)
2027 }
2028 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
2029 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
2030 ParseNode::Html { body, .. } => body.iter().any(node_contains_middle),
2031 _ => false,
2032 }
2033}
2034
2035fn body_contains_middle(nodes: &[ParseNode]) -> bool {
2037 nodes.iter().any(node_contains_middle)
2038}
2039
2040fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
2043 let m = options.metrics();
2044 if options.style.is_display() {
2045 m.delim1
2046 } else if matches!(
2047 options.style,
2048 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
2049 ) {
2050 options
2051 .with_style(MathStyle::Script)
2052 .metrics()
2053 .delim2
2054 } else {
2055 m.delim2
2056 }
2057}
2058
2059fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2061 let metrics = options.metrics();
2062 let inner_height = inner.height;
2063 let inner_depth = inner.depth;
2064 let axis = metrics.axis_height;
2065 let max_dist = (inner_height - axis).max(inner_depth + axis);
2066 let delim_factor = 901.0;
2067 let delim_extend = 5.0 / metrics.pt_per_em;
2068 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2069 from_formula.max(inner_height + inner_depth)
2071}
2072
2073fn layout_left_right(
2074 body: &[ParseNode],
2075 left_delim: &str,
2076 right_delim: &str,
2077 options: &LayoutOptions,
2078) -> LayoutBox {
2079 let (inner, total_height) = if body_contains_middle(body) {
2080 let opts_first = LayoutOptions {
2082 leftright_delim_height: None,
2083 ..options.clone()
2084 };
2085 let inner_first = layout_expression(body, &opts_first, true);
2086 let total_height = left_right_delim_total_height(&inner_first, options);
2087 let opts_second = LayoutOptions {
2089 leftright_delim_height: Some(total_height),
2090 ..options.clone()
2091 };
2092 let inner_second = layout_expression(body, &opts_second, true);
2093 (inner_second, total_height)
2094 } else {
2095 let inner = layout_expression(body, options, true);
2096 let total_height = left_right_delim_total_height(&inner, options);
2097 (inner, total_height)
2098 };
2099
2100 let inner_height = inner.height;
2101 let inner_depth = inner.depth;
2102
2103 let left_box = make_stretchy_delim(left_delim, total_height, options);
2104 let right_box = make_stretchy_delim(right_delim, total_height, options);
2105
2106 let width = left_box.width + inner.width + right_box.width;
2107 let height = left_box.height.max(right_box.height).max(inner_height);
2108 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2109
2110 LayoutBox {
2111 width,
2112 height,
2113 depth,
2114 content: BoxContent::LeftRight {
2115 left: Box::new(left_box),
2116 right: Box::new(right_box),
2117 inner: Box::new(inner),
2118 },
2119 color: options.color,
2120 }
2121}
2122
2123const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2124 FontId::MainRegular,
2125 FontId::Size1Regular,
2126 FontId::Size2Regular,
2127 FontId::Size3Regular,
2128 FontId::Size4Regular,
2129];
2130
2131fn normalize_delim(delim: &str) -> &str {
2133 match delim {
2134 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2135 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2136 _ => delim,
2137 }
2138}
2139
2140fn is_vert_delim(delim: &str) -> bool {
2142 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2143}
2144
2145fn is_double_vert_delim(delim: &str) -> bool {
2147 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2148}
2149
2150fn vert_repeat_piece_height(is_double: bool) -> f64 {
2152 let code = if is_double { 8741_u32 } else { 8739 };
2153 get_char_metrics(FontId::Size1Regular, code)
2154 .map(|m| m.height + m.depth)
2155 .unwrap_or(0.5)
2156}
2157
2158fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2160 let piece = vert_repeat_piece_height(is_double);
2161 let min_h = 2.0 * piece;
2162 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2163 let mut h = min_h + repeat_count * piece;
2164 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2168 h *= 1.135;
2169 }
2170 h
2171}
2172
2173fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2175 let neg = -mid_th;
2176 if !is_double {
2177 format!(
2178 "M145 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v{mid_th} v585 h43z"
2179 )
2180 } else {
2181 format!(
2182 "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"
2183 )
2184 }
2185}
2186
2187fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2188 let s = 0.001_f64;
2189 cmds.iter()
2190 .map(|c| match *c {
2191 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2192 x: x * s,
2193 y: y * s,
2194 },
2195 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2196 x: x * s,
2197 y: y * s,
2198 },
2199 PathCommand::CubicTo {
2200 x1,
2201 y1,
2202 x2,
2203 y2,
2204 x,
2205 y,
2206 } => PathCommand::CubicTo {
2207 x1: x1 * s,
2208 y1: y1 * s,
2209 x2: x2 * s,
2210 y2: y2 * s,
2211 x: x * s,
2212 y: y * s,
2213 },
2214 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2215 x1: x1 * s,
2216 y1: y1 * s,
2217 x: x * s,
2218 y: y * s,
2219 },
2220 PathCommand::Close => PathCommand::Close,
2221 })
2222 .collect()
2223}
2224
2225fn map_vert_path_y_to_baseline(
2227 cmds: Vec<PathCommand>,
2228 height: f64,
2229 depth: f64,
2230 view_box_height: i64,
2231) -> Vec<PathCommand> {
2232 let span_em = view_box_height as f64 / 1000.0;
2233 let total = height + depth;
2234 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2235 cmds.into_iter()
2236 .map(|c| match c {
2237 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2238 x,
2239 y: -height + y * scale_y,
2240 },
2241 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2242 x,
2243 y: -height + y * scale_y,
2244 },
2245 PathCommand::CubicTo {
2246 x1,
2247 y1,
2248 x2,
2249 y2,
2250 x,
2251 y,
2252 } => PathCommand::CubicTo {
2253 x1,
2254 y1: -height + y1 * scale_y,
2255 x2,
2256 y2: -height + y2 * scale_y,
2257 x,
2258 y: -height + y * scale_y,
2259 },
2260 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2261 x1,
2262 y1: -height + y1 * scale_y,
2263 x,
2264 y: -height + y * scale_y,
2265 },
2266 PathCommand::Close => PathCommand::Close,
2267 })
2268 .collect()
2269}
2270
2271fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2274 let real_h = katex_vert_real_height(total_height, is_double);
2275 let axis = options.metrics().axis_height;
2276 let depth = (real_h / 2.0 - axis).max(0.0);
2277 let height = real_h - depth;
2278 let width = if is_double { 0.556 } else { 0.333 };
2279
2280 let piece = vert_repeat_piece_height(is_double);
2281 let mid_em = (real_h - 2.0 * piece).max(0.0);
2282 let mid_th = (mid_em * 1000.0).round() as i64;
2283 let view_box_height = (real_h * 1000.0).round() as i64;
2284
2285 let d = tall_vert_svg_path_data(mid_th, is_double);
2286 let raw = parse_svg_path_data(&d);
2287 let scaled = scale_svg_path_to_em(&raw);
2288 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2289
2290 LayoutBox {
2291 width,
2292 height,
2293 depth,
2294 content: BoxContent::SvgPath { commands, fill: true },
2295 color: options.color,
2296 }
2297}
2298
2299fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2301 if delim == "." || delim.is_empty() {
2302 return LayoutBox::new_kern(0.0);
2303 }
2304
2305 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2310 return make_vert_delim_box(total_height, false, options);
2311 }
2312 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2313 return make_vert_delim_box(total_height, true, options);
2314 }
2315
2316 let delim = normalize_delim(delim);
2318
2319 let ch = resolve_symbol_char(delim, Mode::Math);
2320 let char_code = ch as u32;
2321
2322 let mut best_font = FontId::MainRegular;
2323 let mut best_w = 0.4;
2324 let mut best_h = 0.7;
2325 let mut best_d = 0.2;
2326
2327 for &font_id in &DELIM_FONT_SEQUENCE {
2328 if let Some(m) = get_char_metrics(font_id, char_code) {
2329 best_font = font_id;
2330 best_w = m.width;
2331 best_h = m.height;
2332 best_d = m.depth;
2333 if best_h + best_d >= total_height {
2334 break;
2335 }
2336 }
2337 }
2338
2339 let best_total = best_h + best_d;
2340 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2341 return stacked;
2342 }
2343
2344 LayoutBox {
2345 width: best_w,
2346 height: best_h,
2347 depth: best_d,
2348 content: BoxContent::Glyph {
2349 font_id: best_font,
2350 char_code,
2351 },
2352 color: options.color,
2353 }
2354}
2355
2356const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2358
2359fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2361 if delim == "." || delim.is_empty() {
2362 return LayoutBox::new_kern(0.0);
2363 }
2364
2365 if is_vert_delim(delim) {
2367 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2368 return make_vert_delim_box(total, false, options);
2369 }
2370 if is_double_vert_delim(delim) {
2371 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2372 return make_vert_delim_box(total, true, options);
2373 }
2374
2375 let delim = normalize_delim(delim);
2377
2378 let ch = resolve_symbol_char(delim, Mode::Math);
2379 let char_code = ch as u32;
2380
2381 let font_id = match size {
2382 1 => FontId::Size1Regular,
2383 2 => FontId::Size2Regular,
2384 3 => FontId::Size3Regular,
2385 4 => FontId::Size4Regular,
2386 _ => FontId::Size1Regular,
2387 };
2388
2389 let metrics = get_char_metrics(font_id, char_code);
2390 let (width, height, depth, actual_font) = match metrics {
2391 Some(m) => (m.width, m.height, m.depth, font_id),
2392 None => {
2393 let m = get_char_metrics(FontId::MainRegular, char_code);
2394 match m {
2395 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2396 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2397 }
2398 }
2399 };
2400
2401 LayoutBox {
2402 width,
2403 height,
2404 depth,
2405 content: BoxContent::Glyph {
2406 font_id: actual_font,
2407 char_code,
2408 },
2409 color: options.color,
2410 }
2411}
2412
2413#[allow(clippy::too_many_arguments)]
2418fn layout_array(
2419 body: &[Vec<ParseNode>],
2420 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2421 arraystretch: f64,
2422 add_jot: bool,
2423 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2424 hlines: &[Vec<bool>],
2425 col_sep_type: Option<&str>,
2426 hskip: bool,
2427 tags: Option<&[ArrayTag]>,
2428 _leqno: bool,
2429 options: &LayoutOptions,
2430) -> LayoutBox {
2431 let metrics = options.metrics();
2432 let pt = 1.0 / metrics.pt_per_em;
2433 let baselineskip = 12.0 * pt;
2434 let jot = 3.0 * pt;
2435 let arrayskip = arraystretch * baselineskip;
2436 let arstrut_h = 0.7 * arrayskip;
2437 let arstrut_d = 0.3 * arrayskip;
2438 const ALIGN_RELATION_MU: f64 = 3.0;
2441 let col_gap = match col_sep_type {
2442 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2443 Some("alignat") => 0.0,
2444 Some("small") => {
2445 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2448 / options.size_multiplier()
2449 }
2450 _ => 2.0 * 5.0 * pt, };
2452 let cell_options = match col_sep_type {
2453 Some("align") | Some("alignat") => LayoutOptions {
2454 align_relation_spacing: Some(ALIGN_RELATION_MU),
2455 ..options.clone()
2456 },
2457 _ => options.clone(),
2458 };
2459
2460 let num_rows = body.len();
2461 if num_rows == 0 {
2462 return LayoutBox::new_empty();
2463 }
2464
2465 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2466
2467 use ratex_parser::parse_node::AlignType;
2469 let col_aligns: Vec<u8> = {
2470 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2471 .map(|cs| {
2472 cs.iter()
2473 .filter(|s| matches!(s.align_type, AlignType::Align))
2474 .collect()
2475 })
2476 .unwrap_or_default();
2477 (0..num_cols)
2478 .map(|c| {
2479 align_specs
2480 .get(c)
2481 .and_then(|s| s.align.as_deref())
2482 .and_then(|a| a.bytes().next())
2483 .unwrap_or(b'c')
2484 })
2485 .collect()
2486 };
2487
2488 let col_separators: Vec<Option<bool>> = {
2491 let mut seps = vec![None; num_cols + 1];
2492 let mut align_count = 0usize;
2493 if let Some(cs) = cols {
2494 for spec in cs {
2495 match spec.align_type {
2496 AlignType::Align => align_count += 1,
2497 AlignType::Separator
2498 if spec.align.as_deref() == Some("|") && align_count <= num_cols =>
2499 {
2500 seps[align_count] = Some(false);
2501 }
2502 AlignType::Separator
2503 if spec.align.as_deref() == Some(":") && align_count <= num_cols =>
2504 {
2505 seps[align_count] = Some(true);
2506 }
2507 _ => {}
2508 }
2509 }
2510 }
2511 seps
2512 };
2513
2514 let rule_thickness = 0.4 * pt;
2515 let double_rule_sep = metrics.double_rule_sep;
2516
2517 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2519 let mut col_widths = vec![0.0_f64; num_cols];
2520 let mut row_heights = Vec::with_capacity(num_rows);
2521 let mut row_depths = Vec::with_capacity(num_rows);
2522
2523 for row in body {
2524 let mut row_boxes = Vec::with_capacity(num_cols);
2525 let mut rh = arstrut_h;
2526 let mut rd = arstrut_d;
2527
2528 for (c, cell) in row.iter().enumerate() {
2529 let cell_nodes = match cell {
2530 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2531 other => std::slice::from_ref(other),
2532 };
2533 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2534 rh = rh.max(cell_box.height);
2535 rd = rd.max(cell_box.depth);
2536 if c < num_cols {
2537 col_widths[c] = col_widths[c].max(cell_box.width);
2538 }
2539 row_boxes.push(cell_box);
2540 }
2541
2542 while row_boxes.len() < num_cols {
2544 row_boxes.push(LayoutBox::new_empty());
2545 }
2546
2547 if add_jot {
2548 rd += jot;
2549 }
2550
2551 row_heights.push(rh);
2552 row_depths.push(rd);
2553 cell_boxes.push(row_boxes);
2554 }
2555
2556 for (r, gap) in row_gaps.iter().enumerate() {
2558 if r < row_depths.len() {
2559 if let Some(m) = gap {
2560 let gap_em = measurement_to_em(m, options);
2561 if gap_em > 0.0 {
2562 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2563 }
2564 }
2565 }
2566 }
2567
2568 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2570 while hlines_before_row.len() < num_rows + 1 {
2571 hlines_before_row.push(vec![]);
2572 }
2573
2574 for r in 0..=num_rows {
2580 let n = hlines_before_row[r].len();
2581 if n > 1 {
2582 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2583 if r == 0 {
2584 if num_rows > 0 {
2585 row_heights[0] += extra;
2586 }
2587 } else {
2588 row_depths[r - 1] += extra;
2589 }
2590 }
2591 }
2592
2593 let mut total_height = 0.0;
2595 let mut row_positions = Vec::with_capacity(num_rows);
2596 for r in 0..num_rows {
2597 total_height += row_heights[r];
2598 row_positions.push(total_height);
2599 total_height += row_depths[r];
2600 }
2601
2602 let offset = total_height / 2.0 + metrics.axis_height;
2603
2604 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2606
2607 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2609 + col_gap * (num_cols.saturating_sub(1)) as f64
2610 + 2.0 * content_x_offset;
2611
2612 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2613 let mut tag_col_width = 0.0_f64;
2614 let text_opts = options.with_style(options.style.text());
2615 if let Some(tag_slice) = tags {
2616 if tag_slice.len() == num_rows {
2617 for (r, t) in tag_slice.iter().enumerate() {
2618 if let ArrayTag::Explicit(nodes) = t {
2619 if !nodes.is_empty() {
2620 let tb = layout_expression(nodes, &text_opts, true);
2621 tag_col_width = tag_col_width.max(tb.width);
2622 row_tag_boxes[r] = Some(tb);
2623 }
2624 }
2625 }
2626 }
2627 }
2628 let tag_gap_em = if tag_col_width > 0.0 {
2629 text_opts.metrics().quad
2630 } else {
2631 0.0
2632 };
2633 let tags_left = false;
2635
2636 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2637
2638 let height = offset;
2639 let depth = total_height - offset;
2640
2641 LayoutBox {
2642 width: total_width,
2643 height,
2644 depth,
2645 content: BoxContent::Array {
2646 cells: cell_boxes,
2647 col_widths: col_widths.clone(),
2648 col_aligns,
2649 row_heights: row_heights.clone(),
2650 row_depths: row_depths.clone(),
2651 col_gap,
2652 offset,
2653 content_x_offset,
2654 col_separators,
2655 hlines_before_row,
2656 rule_thickness,
2657 double_rule_sep,
2658 array_inner_width,
2659 tag_gap_em,
2660 tag_col_width,
2661 row_tags: row_tag_boxes,
2662 tags_left,
2663 },
2664 color: options.color,
2665 }
2666}
2667
2668fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2673 let multiplier = match size {
2675 1 => 0.5,
2676 2 => 0.6,
2677 3 => 0.7,
2678 4 => 0.8,
2679 5 => 0.9,
2680 6 => 1.0,
2681 7 => 1.2,
2682 8 => 1.44,
2683 9 => 1.728,
2684 10 => 2.074,
2685 11 => 2.488,
2686 _ => 1.0,
2687 };
2688
2689 let inner_opts = options.with_style(options.style.text());
2691 let inner = layout_expression(body, &inner_opts, true);
2692 let ratio = multiplier / options.size_multiplier();
2693 if (ratio - 1.0).abs() < 0.001 {
2694 inner
2695 } else {
2696 LayoutBox {
2697 width: inner.width * ratio,
2698 height: inner.height * ratio,
2699 depth: inner.depth * ratio,
2700 content: BoxContent::Scaled {
2701 body: Box::new(inner),
2702 child_scale: ratio,
2703 },
2704 color: options.color,
2705 }
2706 }
2707}
2708
2709#[derive(Default)]
2710struct HtmlStyle {
2711 color: Option<Color>,
2712 font_size_scale: Option<f64>,
2713 bold: bool,
2714 italic: bool,
2715 background_color: Option<Color>,
2716 underline: bool,
2717}
2718
2719fn layout_html(attributes: &HashMap<String, String>, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2720 let style = attributes
2721 .get("style")
2722 .map(|style| parse_html_style(style))
2723 .unwrap_or_default();
2724
2725 let body_options = match style.color {
2726 Some(color) => options.with_color(color),
2727 None => options.clone(),
2728 };
2729 let font_id = match (style.bold, style.italic) {
2730 (true, true) => Some(FontId::MainBoldItalic),
2731 (true, false) => Some(FontId::MainBold),
2732 (false, true) => Some(FontId::MainItalic),
2733 (false, false) => None,
2734 };
2735
2736 let body_node = ParseNode::OrdGroup {
2737 mode: body.first().map(ParseNode::mode).unwrap_or(Mode::Math),
2738 body: body.to_vec(),
2739 semisimple: None,
2740 loc: None,
2741 };
2742 let mut lbox = match font_id {
2743 Some(font_id) => layout_with_font(&body_node, font_id, &body_options),
2744 None => layout_expression(body, &body_options, true),
2745 };
2746
2747 if let Some(scale) = style.font_size_scale {
2748 if (scale - 1.0).abs() >= 0.001 {
2749 lbox = LayoutBox {
2750 width: lbox.width * scale,
2751 height: lbox.height * scale,
2752 depth: lbox.depth * scale,
2753 content: BoxContent::Scaled {
2754 body: Box::new(lbox),
2755 child_scale: scale,
2756 },
2757 color: body_options.color,
2758 };
2759 }
2760 }
2761
2762 if let Some(background_color) = style.background_color {
2763 lbox = LayoutBox {
2764 width: lbox.width,
2765 height: lbox.height,
2766 depth: lbox.depth,
2767 content: BoxContent::Framed {
2768 body: Box::new(lbox),
2769 padding: 0.0,
2770 border_thickness: 0.0,
2771 has_border: false,
2772 bg_color: Some(background_color),
2773 border_color: Color::BLACK,
2774 },
2775 color: body_options.color,
2776 };
2777 }
2778
2779 if style.underline {
2780 lbox = layout_underline_laid_out(lbox, options, body_options.color);
2781 }
2782
2783 lbox
2784}
2785
2786fn parse_html_style(style: &str) -> HtmlStyle {
2787 let mut parsed = HtmlStyle::default();
2788 for declaration in style.split(';') {
2789 let Some((property, value)) = declaration.split_once(':') else {
2790 continue;
2791 };
2792 let property = property.trim().to_ascii_lowercase();
2793 let value = value.trim();
2794 match property.as_str() {
2795 "color" => parsed.color = Color::parse(value),
2796 "font-size" => parsed.font_size_scale = parse_css_font_size(value),
2797 "font-weight" => parsed.bold = is_css_bold(value),
2798 "font-style" => parsed.italic = is_css_italic(value),
2799 "background" | "background-color" => parsed.background_color = Color::parse(value),
2800 "text-decoration" | "text-decoration-line" => {
2801 parsed.underline = value
2802 .split_whitespace()
2803 .any(|part| part.eq_ignore_ascii_case("underline"));
2804 }
2805 _ => {}
2806 }
2807 }
2808 parsed
2809}
2810
2811fn parse_css_font_size(value: &str) -> Option<f64> {
2812 let value = value.trim().to_ascii_lowercase().replace(' ', "");
2813 let parse_number = |s: &str| s.parse::<f64>().ok().filter(|n| n.is_finite() && *n > 0.0);
2814 if let Some(px) = value.strip_suffix("px") {
2815 parse_number(px).map(|n| n / 16.0)
2816 } else if let Some(em) = value.strip_suffix("em").or_else(|| value.strip_suffix("rem")) {
2817 parse_number(em)
2818 } else if let Some(percent) = value.strip_suffix('%') {
2819 parse_number(percent).map(|n| n / 100.0)
2820 } else {
2821 None
2822 }
2823}
2824
2825fn is_css_bold(value: &str) -> bool {
2826 let value = value.trim();
2827 value.eq_ignore_ascii_case("bold")
2828 || value.eq_ignore_ascii_case("bolder")
2829 || value.parse::<u16>().is_ok_and(|weight| weight >= 600)
2830}
2831
2832fn is_css_italic(value: &str) -> bool {
2833 let value = value.trim();
2834 value.eq_ignore_ascii_case("italic") || value.eq_ignore_ascii_case("oblique")
2835}
2836
2837fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2840 let metrics = options.metrics();
2841 let mut children = Vec::new();
2842 for c in body.chars() {
2843 let ch = if star && c == ' ' {
2844 '\u{2423}' } else {
2846 c
2847 };
2848 let code = ch as u32;
2849 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2850 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2851 None => match get_char_metrics(FontId::MainRegular, code) {
2852 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2853 None => (
2854 FontId::TypewriterRegular,
2855 0.5,
2856 metrics.x_height,
2857 0.0,
2858 ),
2859 },
2860 };
2861 children.push(LayoutBox {
2862 width: w,
2863 height: h,
2864 depth: d,
2865 content: BoxContent::Glyph {
2866 font_id,
2867 char_code: code,
2868 },
2869 color: options.color,
2870 });
2871 }
2872 let mut hbox = make_hbox(children);
2873 hbox.color = options.color;
2874 hbox
2875}
2876
2877fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2885 let mut children = Vec::new();
2886 for node in body {
2887 match node {
2888 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2889 children.push(layout_symbol(text, *mode, options));
2890 }
2891 ParseNode::SpacingNode { text, .. } => {
2892 children.push(layout_spacing_command(text, options));
2893 }
2894 _ => {
2895 children.push(layout_node(node, options));
2896 }
2897 }
2898 }
2899 make_hbox(children)
2900}
2901
2902fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2905 let base = layout_expression(body, options, true);
2906 let w = base.width;
2907 let h = base.height;
2908 let d = base.depth;
2909
2910 let shadow = layout_expression(body, options, true);
2912 let shadow_shift_x = 0.02_f64;
2913 let _shadow_shift_y = 0.01_f64;
2914
2915 let kern_back = LayoutBox::new_kern(-w);
2919 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2920
2921 let children = vec![
2928 kern_x,
2929 shadow,
2930 kern_back,
2931 base,
2932 ];
2933 let hbox = make_hbox(children);
2935 LayoutBox {
2937 width: w,
2938 height: h,
2939 depth: d,
2940 content: hbox.content,
2941 color: options.color,
2942 }
2943}
2944
2945fn layout_enclose(
2948 label: &str,
2949 background_color: Option<&str>,
2950 border_color: Option<&str>,
2951 body: &ParseNode,
2952 options: &LayoutOptions,
2953) -> LayoutBox {
2954 use crate::layout_box::BoxContent;
2955 use ratex_types::color::Color;
2956
2957 if label == "\\phase" {
2959 return layout_phase(body, options);
2960 }
2961
2962 if label == "\\angl" {
2964 return layout_angl(body, options);
2965 }
2966
2967 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2969 return layout_cancel(label, body, options);
2970 }
2971
2972 let metrics = options.metrics();
2974 let padding = 3.0 / metrics.pt_per_em;
2975 let border_thickness = 0.4 / metrics.pt_per_em;
2976
2977 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2978
2979 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2980 let border = border_color
2981 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2982 .unwrap_or(Color::BLACK);
2983
2984 let inner = layout_node(body, options);
2985 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2986
2987 let width = inner.width + 2.0 * outer_pad;
2988 let height = inner.height + outer_pad;
2989 let depth = inner.depth + outer_pad;
2990
2991 LayoutBox {
2992 width,
2993 height,
2994 depth,
2995 content: BoxContent::Framed {
2996 body: Box::new(inner),
2997 padding,
2998 border_thickness,
2999 has_border,
3000 bg_color: bg,
3001 border_color: border,
3002 },
3003 color: options.color,
3004 }
3005}
3006
3007fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3009 use crate::layout_box::BoxContent;
3010 let inner = layout_node(body, options);
3011 let height = inner.height + shift;
3013 let depth = (inner.depth - shift).max(0.0);
3014 let width = inner.width;
3015 LayoutBox {
3016 width,
3017 height,
3018 depth,
3019 content: BoxContent::RaiseBox {
3020 body: Box::new(inner),
3021 shift,
3022 },
3023 color: options.color,
3024 }
3025}
3026
3027fn is_single_char_body(node: &ParseNode) -> bool {
3030 use ratex_parser::parse_node::ParseNode as PN;
3031 match node {
3032 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3034 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3035 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
3037 _ => false,
3038 }
3039}
3040
3041fn layout_cancel(
3047 label: &str,
3048 body: &ParseNode,
3049 options: &LayoutOptions,
3050) -> LayoutBox {
3051 use crate::layout_box::BoxContent;
3052 let inner = layout_node(body, options);
3053 let w = inner.width.max(0.01);
3054 let h = inner.height;
3055 let d = inner.depth;
3056
3057 let single = is_single_char_body(body);
3060 let (v_pad, h_pad) = if label == "\\sout" {
3061 (0.0, 0.0)
3062 } else if single {
3063 (0.2, 0.0)
3064 } else {
3065 (0.0, 0.2)
3066 };
3067
3068 let commands: Vec<PathCommand> = match label {
3072 "\\cancel" => vec![
3073 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
3076 "\\bcancel" => vec![
3077 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
3080 "\\xcancel" => vec![
3081 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
3082 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
3083 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
3084 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
3085 ],
3086 "\\sout" => {
3087 let mid_y = -0.5 * options.metrics().x_height;
3089 vec![
3090 PathCommand::MoveTo { x: 0.0, y: mid_y },
3091 PathCommand::LineTo { x: w, y: mid_y },
3092 ]
3093 }
3094 _ => vec![],
3095 };
3096
3097 let line_w = w + 2.0 * h_pad;
3098 let line_h = h + v_pad;
3099 let line_d = d + v_pad;
3100 let line_box = LayoutBox {
3101 width: line_w,
3102 height: line_h,
3103 depth: line_d,
3104 content: BoxContent::SvgPath { commands, fill: false },
3105 color: options.color,
3106 };
3107
3108 let body_kern = -(line_w - h_pad);
3110 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
3111 LayoutBox {
3112 width: w,
3113 height: h,
3114 depth: d,
3115 content: BoxContent::HBox(vec![line_box, body_shifted]),
3116 color: options.color,
3117 }
3118}
3119
3120fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3123 use crate::layout_box::BoxContent;
3124 let metrics = options.metrics();
3125 let inner = layout_node(body, options);
3126 let line_weight = 0.6_f64 / metrics.pt_per_em;
3128 let clearance = 0.35_f64 * metrics.x_height;
3129 let angle_height = inner.height + inner.depth + line_weight + clearance;
3130 let left_pad = angle_height / 2.0 + line_weight;
3131 let width = inner.width + left_pad;
3132
3133 let y_svg = (1000.0 * angle_height).floor().max(80.0);
3135
3136 let sy = angle_height / y_svg;
3138 let sx = sy;
3141 let right_x = (400_000.0_f64 * sx).min(width);
3142
3143 let bottom_y = inner.depth + line_weight + clearance;
3145 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
3146
3147 let x_peak = y_svg / 2.0;
3149 let commands = vec![
3150 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
3151 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
3152 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
3153 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
3154 PathCommand::LineTo {
3155 x: 145.0 * sx,
3156 y: vy(y_svg - 80.0),
3157 },
3158 PathCommand::LineTo {
3159 x: right_x,
3160 y: vy(y_svg - 80.0),
3161 },
3162 PathCommand::Close,
3163 ];
3164
3165 let body_shifted = make_hbox(vec![
3166 LayoutBox::new_kern(left_pad),
3167 inner.clone(),
3168 ]);
3169
3170 let path_height = inner.height;
3171 let path_depth = bottom_y;
3172
3173 LayoutBox {
3174 width,
3175 height: path_height,
3176 depth: path_depth,
3177 content: BoxContent::HBox(vec![
3178 LayoutBox {
3179 width,
3180 height: path_height,
3181 depth: path_depth,
3182 content: BoxContent::SvgPath { commands, fill: true },
3183 color: options.color,
3184 },
3185 LayoutBox::new_kern(-width),
3186 body_shifted,
3187 ]),
3188 color: options.color,
3189 }
3190}
3191
3192fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3195 use crate::layout_box::BoxContent;
3196 let inner = layout_node(body, options);
3197 let w = inner.width.max(0.3);
3198 let clearance = 0.1_f64;
3200 let arc_h = inner.height + clearance;
3201
3202 let path_commands = vec![
3204 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3205 PathCommand::LineTo { x: w, y: -arc_h },
3206 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
3207 ];
3208
3209 let height = arc_h;
3210 LayoutBox {
3211 width: w,
3212 height,
3213 depth: inner.depth,
3214 content: BoxContent::Angl {
3215 path_commands,
3216 body: Box::new(inner),
3217 },
3218 color: options.color,
3219 }
3220}
3221
3222fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3223 let font_id = match font {
3224 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3225 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3226 "mathit" | "\\mathit" | "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3227 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3228 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3229 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3230 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3231 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3232 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3233 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3234 _ => None,
3235 };
3236
3237 if let Some(fid) = font_id {
3238 layout_with_font(body, fid, options)
3239 } else {
3240 layout_node(body, options)
3241 }
3242}
3243
3244fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3245 match node {
3246 ParseNode::OrdGroup { body, .. } => {
3247 let kern = options.inter_glyph_kern_em;
3248 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3249 for (i, n) in body.iter().enumerate() {
3250 if i > 0 && kern > 0.0 {
3251 children.push(LayoutBox::new_kern(kern));
3252 }
3253 children.push(layout_with_font(n, font_id, options));
3254 }
3255 make_hbox(children)
3256 }
3257 ParseNode::SupSub {
3258 base, sup, sub, ..
3259 } => {
3260 if let Some(base_node) = base.as_deref() {
3261 if should_use_op_limits(base_node, options) {
3262 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3263 }
3264 }
3265 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3266 }
3267 ParseNode::MathOrd { text, mode, .. }
3268 | ParseNode::TextOrd { text, mode, .. }
3269 | ParseNode::Atom { text, mode, .. } => {
3270 let ch = resolve_symbol_char(text, *mode);
3271 let char_code = ch as u32;
3272 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3273 .map(|(_, m)| m)
3274 .unwrap_or(char_code);
3275 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3276 LayoutBox {
3277 width: math_glyph_advance_em(&m, *mode),
3279 height: m.height,
3280 depth: m.depth,
3281 content: BoxContent::Glyph { font_id, char_code },
3282 color: options.color,
3283 }
3284 } else {
3285 layout_node(node, options)
3287 }
3288 }
3289 _ => layout_node(node, options),
3290 }
3291}
3292
3293fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3298 let cramped = options.with_style(options.style.cramped());
3299 let body_box = layout_node(body, &cramped);
3300 let metrics = options.metrics();
3301 let rule = metrics.default_rule_thickness;
3302
3303 let height = body_box.height + 3.0 * rule;
3305 LayoutBox {
3306 width: body_box.width,
3307 height,
3308 depth: body_box.depth,
3309 content: BoxContent::Overline {
3310 body: Box::new(body_box),
3311 rule_thickness: rule,
3312 },
3313 color: options.color,
3314 }
3315}
3316
3317fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3318 let body_box = layout_node(body, options);
3319 let metrics = options.metrics();
3320 let rule = metrics.default_rule_thickness;
3321
3322 let depth = body_box.depth + 3.0 * rule;
3324 LayoutBox {
3325 width: body_box.width,
3326 height: body_box.height,
3327 depth,
3328 content: BoxContent::Underline {
3329 body: Box::new(body_box),
3330 rule_thickness: rule,
3331 },
3332 color: options.color,
3333 }
3334}
3335
3336fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3338 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3339 let body_opts = options
3341 .with_color(link_color)
3342 .with_inter_glyph_kern(0.024);
3343 let body_box = layout_expression(body, &body_opts, true);
3344 layout_underline_laid_out(body_box, options, link_color)
3345}
3346
3347fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3349 let metrics = options.metrics();
3350 let rule = metrics.default_rule_thickness;
3351 let depth = body_box.depth + 3.0 * rule;
3352 LayoutBox {
3353 width: body_box.width,
3354 height: body_box.height,
3355 depth,
3356 content: BoxContent::Underline {
3357 body: Box::new(body_box),
3358 rule_thickness: rule,
3359 },
3360 color,
3361 }
3362}
3363
3364fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3369 let metrics = options.metrics();
3370 let mu = metrics.css_em_per_mu();
3371
3372 let width = match text {
3373 "\\," | "\\thinspace" => 3.0 * mu,
3374 "\\:" | "\\medspace" => 4.0 * mu,
3375 "\\;" | "\\thickspace" => 5.0 * mu,
3376 "\\!" | "\\negthinspace" => -3.0 * mu,
3377 "\\negmedspace" => -4.0 * mu,
3378 "\\negthickspace" => -5.0 * mu,
3379 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3380 get_char_metrics(FontId::MainRegular, 160)
3384 .map(|m| m.width)
3385 .unwrap_or(0.25)
3386 }
3387 "\\quad" => metrics.quad,
3388 "\\qquad" => 2.0 * metrics.quad,
3389 "\\enspace" => metrics.quad / 2.0,
3390 _ => 0.0,
3391 };
3392
3393 LayoutBox::new_kern(width)
3394}
3395
3396fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3401 let metrics = options.metrics();
3402 match m.unit.as_str() {
3403 "em" => m.number,
3404 "ex" => m.number * metrics.x_height,
3405 "mu" => m.number * metrics.css_em_per_mu(),
3406 "pt" => m.number / metrics.pt_per_em,
3407 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3408 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3409 "in" => m.number * 72.27 / metrics.pt_per_em,
3410 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3411 "pc" => m.number * 12.0 / metrics.pt_per_em,
3412 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3413 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3414 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3415 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3416 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3417 _ => m.number,
3418 }
3419}
3420
3421fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3427 match node {
3428 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3429 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3430 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3431 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3432 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3434 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3435 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3436 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3437 }
3438 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3439 ParseNode::SupSub { base, .. } => {
3440 base.as_ref().and_then(|b| node_math_class(b))
3441 }
3442 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3443 ParseNode::SpacingNode { .. } => None,
3444 ParseNode::Kern { .. } => None,
3445 ParseNode::HtmlMathMl { html, .. } => {
3446 for child in html {
3448 if let Some(cls) = node_math_class(child) {
3449 return Some(cls);
3450 }
3451 }
3452 None
3453 }
3454 ParseNode::Html { body, .. } => {
3455 for child in body {
3456 if let Some(cls) = node_math_class(child) {
3457 return Some(cls);
3458 }
3459 }
3460 None
3461 }
3462 ParseNode::Lap { .. } => None,
3463 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3464 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3465 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3467 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3469 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3470 ParseNode::Middle { .. } => Some(MathClass::Ord),
3471 _ => Some(MathClass::Ord),
3472 }
3473}
3474
3475fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3476 match mclass {
3477 "mord" => MathClass::Ord,
3478 "mop" => MathClass::Op,
3479 "mbin" => MathClass::Bin,
3480 "mrel" => MathClass::Rel,
3481 "mopen" => MathClass::Open,
3482 "mclose" => MathClass::Close,
3483 "mpunct" => MathClass::Punct,
3484 "minner" => MathClass::Inner,
3485 _ => MathClass::Ord,
3486 }
3487}
3488
3489fn get_base_elem(node: &ParseNode) -> &ParseNode {
3493 match node {
3494 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3495 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3496 ParseNode::Html { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3497 ParseNode::Font { body, .. } => get_base_elem(body),
3498 _ => node,
3499 }
3500}
3501
3502fn is_character_box(node: &ParseNode) -> bool {
3503 matches!(
3504 get_base_elem(node),
3505 ParseNode::MathOrd { .. }
3506 | ParseNode::TextOrd { .. }
3507 | ParseNode::Atom { .. }
3508 | ParseNode::AccentToken { .. }
3509 )
3510}
3511
3512fn family_to_math_class(family: AtomFamily) -> MathClass {
3513 match family {
3514 AtomFamily::Bin => MathClass::Bin,
3515 AtomFamily::Rel => MathClass::Rel,
3516 AtomFamily::Open => MathClass::Open,
3517 AtomFamily::Close => MathClass::Close,
3518 AtomFamily::Punct => MathClass::Punct,
3519 AtomFamily::Inner => MathClass::Inner,
3520 }
3521}
3522
3523fn layout_horiz_brace(
3528 base: &ParseNode,
3529 is_over: bool,
3530 func_label: &str,
3531 options: &LayoutOptions,
3532) -> LayoutBox {
3533 let body_box = layout_node(base, options);
3534 let w = body_box.width.max(0.5);
3535
3536 let is_bracket = func_label
3537 .trim_start_matches('\\')
3538 .ends_with("bracket");
3539
3540 let stretch_key = if is_bracket {
3542 if is_over {
3543 "overbracket"
3544 } else {
3545 "underbracket"
3546 }
3547 } else if is_over {
3548 "overbrace"
3549 } else {
3550 "underbrace"
3551 };
3552
3553 let (raw_commands, brace_h, brace_fill) =
3554 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3555 Some((c, h)) => (c, h, true),
3556 None => {
3557 let h = 0.35_f64;
3558 (horiz_brace_path(w, h, is_over), h, false)
3559 }
3560 };
3561
3562 let y_shift = brace_h / 2.0;
3568 let commands = shift_path_y(raw_commands, y_shift);
3569
3570 let brace_box = LayoutBox {
3571 width: w,
3572 height: 0.0,
3573 depth: brace_h,
3574 content: BoxContent::SvgPath {
3575 commands,
3576 fill: brace_fill,
3577 },
3578 color: options.color,
3579 };
3580
3581 let gap = 0.1;
3582 let (height, depth) = if is_over {
3583 (body_box.height + brace_h + gap, body_box.depth)
3584 } else {
3585 (body_box.height, body_box.depth + brace_h + gap)
3586 };
3587
3588 let clearance = if is_over {
3589 height - brace_h
3590 } else {
3591 body_box.height + body_box.depth + gap
3592 };
3593 let total_w = body_box.width;
3594
3595 LayoutBox {
3596 width: total_w,
3597 height,
3598 depth,
3599 content: BoxContent::Accent {
3600 base: Box::new(body_box),
3601 accent: Box::new(brace_box),
3602 clearance,
3603 skew: 0.0,
3604 is_below: !is_over,
3605 under_gap_em: 0.0,
3606 },
3607 color: options.color,
3608 }
3609}
3610
3611fn layout_xarrow(
3616 label: &str,
3617 body: &ParseNode,
3618 below: Option<&ParseNode>,
3619 options: &LayoutOptions,
3620) -> LayoutBox {
3621 let sup_style = options.style.superscript();
3622 let sub_style = options.style.subscript();
3623 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3624 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3625
3626 let sup_opts = options.with_style(sup_style);
3627 let body_box = layout_node(body, &sup_opts);
3628 let body_w = body_box.width * sup_ratio;
3629
3630 let below_box = below.map(|b| {
3631 let sub_opts = options.with_style(sub_style);
3632 layout_node(b, &sub_opts)
3633 });
3634 let below_w = below_box
3635 .as_ref()
3636 .map(|b| b.width * sub_ratio)
3637 .unwrap_or(0.0);
3638
3639 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3642 let upper_w = body_w + sup_ratio;
3643 let lower_w = if below_box.is_some() {
3644 below_w + sub_ratio
3645 } else {
3646 0.0
3647 };
3648 let arrow_w = upper_w.max(lower_w).max(min_w);
3649 let arrow_h = 0.3;
3650
3651 let (commands, actual_arrow_h, fill_arrow) =
3652 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3653 Some((c, h)) => (c, h, true),
3654 None => (
3655 stretchy_accent_path(label, arrow_w, arrow_h),
3656 arrow_h,
3657 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3658 ),
3659 };
3660 let arrow_box = LayoutBox {
3661 width: arrow_w,
3662 height: actual_arrow_h / 2.0,
3663 depth: actual_arrow_h / 2.0,
3664 content: BoxContent::SvgPath {
3665 commands,
3666 fill: fill_arrow,
3667 },
3668 color: options.color,
3669 };
3670
3671 let metrics = options.metrics();
3674 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3676 let gap = 0.111; let base_shift = -axis;
3680
3681 let sup_kern = gap;
3689 let sub_kern = gap;
3690
3691 let sup_h = body_box.height * sup_ratio;
3692 let sup_d = body_box.depth * sup_ratio;
3693
3694 let height = axis + arrow_half + gap + sup_h + sup_d;
3696 let mut depth = (arrow_half - axis).max(0.0);
3698
3699 if let Some(ref bel) = below_box {
3700 let sub_h = bel.height * sub_ratio;
3701 let sub_d = bel.depth * sub_ratio;
3702 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3704 }
3705
3706 LayoutBox {
3707 width: arrow_w,
3708 height,
3709 depth,
3710 content: BoxContent::OpLimits {
3711 base: Box::new(arrow_box),
3712 sup: Some(Box::new(body_box)),
3713 sub: below_box.map(Box::new),
3714 base_shift,
3715 sup_kern,
3716 sub_kern,
3717 slant: 0.0,
3718 sup_scale: sup_ratio,
3719 sub_scale: sub_ratio,
3720 },
3721 color: options.color,
3722 }
3723}
3724
3725fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3730 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3733 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3734 let diameter = radius * 2.0;
3735
3736 let cx = radius;
3738 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3741
3742 let circle_commands = vec![
3743 PathCommand::MoveTo { x: cx + r, y: cy },
3744 PathCommand::CubicTo {
3745 x1: cx + r, y1: cy - k * r,
3746 x2: cx + k * r, y2: cy - r,
3747 x: cx, y: cy - r,
3748 },
3749 PathCommand::CubicTo {
3750 x1: cx - k * r, y1: cy - r,
3751 x2: cx - r, y2: cy - k * r,
3752 x: cx - r, y: cy,
3753 },
3754 PathCommand::CubicTo {
3755 x1: cx - r, y1: cy + k * r,
3756 x2: cx - k * r, y2: cy + r,
3757 x: cx, y: cy + r,
3758 },
3759 PathCommand::CubicTo {
3760 x1: cx + k * r, y1: cy + r,
3761 x2: cx + r, y2: cy + k * r,
3762 x: cx + r, y: cy,
3763 },
3764 PathCommand::Close,
3765 ];
3766
3767 let circle_box = LayoutBox {
3768 width: diameter,
3769 height: r - cy.min(0.0),
3770 depth: (r + cy).max(0.0),
3771 content: BoxContent::SvgPath {
3772 commands: circle_commands,
3773 fill: false,
3774 },
3775 color: options.color,
3776 };
3777
3778 let content_shift = (diameter - body_box.width) / 2.0;
3780 let children = vec![
3782 circle_box,
3783 LayoutBox::new_kern(-(diameter) + content_shift),
3784 body_box.clone(),
3785 ];
3786
3787 let height = r - cy.min(0.0);
3788 let depth = (r + cy).max(0.0);
3789
3790 LayoutBox {
3791 width: diameter,
3792 height,
3793 depth,
3794 content: BoxContent::HBox(children),
3795 color: options.color,
3796 }
3797}
3798
3799fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3823 let r: f64 = 0.1125;
3825 let cy: f64 = -0.2625;
3829 let k: f64 = 0.5523;
3831 let cx: f64 = r;
3833
3834 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3837
3838 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3847 vec![
3848 PathCommand::MoveTo { x: ox + rad, y: cy },
3849 PathCommand::CubicTo {
3850 x1: ox + rad, y1: cy - k * rad,
3851 x2: ox + k * rad, y2: cy - rad,
3852 x: ox, y: cy - rad,
3853 },
3854 PathCommand::CubicTo {
3855 x1: ox - k * rad, y1: cy - rad,
3856 x2: ox - rad, y2: cy - k * rad,
3857 x: ox - rad, y: cy,
3858 },
3859 PathCommand::CubicTo {
3860 x1: ox - rad, y1: cy + k * rad,
3861 x2: ox - k * rad, y2: cy + rad,
3862 x: ox, y: cy + rad,
3863 },
3864 PathCommand::CubicTo {
3865 x1: ox + k * rad, y1: cy + rad,
3866 x2: ox + rad, y2: cy + k * rad,
3867 x: ox + rad, y: cy,
3868 },
3869 PathCommand::Close,
3870 ]
3871 };
3872
3873 let disk = LayoutBox {
3874 width: 2.0 * r,
3875 height: h,
3876 depth: d,
3877 content: BoxContent::SvgPath {
3878 commands: circle_commands(cx, r),
3879 fill: true,
3880 },
3881 color: options.color,
3882 };
3883
3884 let ring = LayoutBox {
3885 width: 2.0 * r,
3886 height: h,
3887 depth: d,
3888 content: BoxContent::SvgPath {
3889 commands: circle_commands(cx, r_ring),
3890 fill: false,
3891 },
3892 color: options.color,
3893 };
3894
3895 let bar_len: f64 = 0.25;
3899 let bar_th: f64 = 0.04;
3900 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3903
3904 let children = if imageof {
3905 vec![disk, bar, ring]
3906 } else {
3907 vec![ring, bar, disk]
3908 };
3909
3910 let total_width = 4.0 * r + bar_len;
3912 LayoutBox {
3913 width: total_width,
3914 height: h,
3915 depth: d,
3916 content: BoxContent::HBox(children),
3917 color: options.color,
3918 }
3919}
3920
3921fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3925 let cx = width / 2.0;
3926 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3931 PathCommand::MoveTo { x: cx + a, y: cy },
3932 PathCommand::CubicTo {
3933 x1: cx + a,
3934 y1: cy - k * b,
3935 x2: cx + k * a,
3936 y2: cy - b,
3937 x: cx,
3938 y: cy - b,
3939 },
3940 PathCommand::CubicTo {
3941 x1: cx - k * a,
3942 y1: cy - b,
3943 x2: cx - a,
3944 y2: cy - k * b,
3945 x: cx - a,
3946 y: cy,
3947 },
3948 PathCommand::CubicTo {
3949 x1: cx - a,
3950 y1: cy + k * b,
3951 x2: cx - k * a,
3952 y2: cy + b,
3953 x: cx,
3954 y: cy + b,
3955 },
3956 PathCommand::CubicTo {
3957 x1: cx + k * a,
3958 y1: cy + b,
3959 x2: cx + a,
3960 y2: cy + k * b,
3961 x: cx + a,
3962 y: cy,
3963 },
3964 PathCommand::Close,
3965 ]
3966}
3967
3968fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3969 cmds.into_iter().map(|c| match c {
3970 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3971 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3972 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3973 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3974 },
3975 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3976 x1, y1: y1 + dy, x, y: y + dy,
3977 },
3978 PathCommand::Close => PathCommand::Close,
3979 }).collect()
3980}
3981
3982fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3983 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3984 return commands;
3985 }
3986 let ah = height * 0.35; let mid_y = -height / 2.0;
3988
3989 match label {
3990 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3991 vec![
3992 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3993 PathCommand::LineTo { x: 0.0, y: mid_y },
3994 PathCommand::LineTo { x: ah, y: mid_y + ah },
3995 PathCommand::MoveTo { x: 0.0, y: mid_y },
3996 PathCommand::LineTo { x: width, y: mid_y },
3997 ]
3998 }
3999 "\\overleftrightarrow" | "\\underleftrightarrow"
4000 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
4001 vec![
4002 PathCommand::MoveTo { x: ah, y: mid_y - ah },
4003 PathCommand::LineTo { x: 0.0, y: mid_y },
4004 PathCommand::LineTo { x: ah, y: mid_y + ah },
4005 PathCommand::MoveTo { x: 0.0, y: mid_y },
4006 PathCommand::LineTo { x: width, y: mid_y },
4007 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4008 PathCommand::LineTo { x: width, y: mid_y },
4009 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4010 ]
4011 }
4012 "\\xlongequal" => {
4013 let gap = 0.04;
4014 vec![
4015 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4016 PathCommand::LineTo { x: width, y: mid_y - gap },
4017 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
4018 PathCommand::LineTo { x: width, y: mid_y + gap },
4019 ]
4020 }
4021 "\\xhookleftarrow" => {
4022 vec![
4023 PathCommand::MoveTo { x: ah, y: mid_y - ah },
4024 PathCommand::LineTo { x: 0.0, y: mid_y },
4025 PathCommand::LineTo { x: ah, y: mid_y + ah },
4026 PathCommand::MoveTo { x: 0.0, y: mid_y },
4027 PathCommand::LineTo { x: width, y: mid_y },
4028 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
4029 ]
4030 }
4031 "\\xhookrightarrow" => {
4032 vec![
4033 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
4034 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
4035 PathCommand::LineTo { x: width, y: mid_y },
4036 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4037 PathCommand::LineTo { x: width, y: mid_y },
4038 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4039 ]
4040 }
4041 "\\xrightharpoonup" | "\\xleftharpoonup" => {
4042 let right = label.contains("right");
4043 if right {
4044 vec![
4045 PathCommand::MoveTo { x: 0.0, y: mid_y },
4046 PathCommand::LineTo { x: width, y: mid_y },
4047 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4048 PathCommand::LineTo { x: width, y: mid_y },
4049 ]
4050 } else {
4051 vec![
4052 PathCommand::MoveTo { x: ah, y: mid_y - ah },
4053 PathCommand::LineTo { x: 0.0, y: mid_y },
4054 PathCommand::LineTo { x: width, y: mid_y },
4055 ]
4056 }
4057 }
4058 "\\xrightharpoondown" | "\\xleftharpoondown" => {
4059 let right = label.contains("right");
4060 if right {
4061 vec![
4062 PathCommand::MoveTo { x: 0.0, y: mid_y },
4063 PathCommand::LineTo { x: width, y: mid_y },
4064 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
4065 PathCommand::LineTo { x: width, y: mid_y },
4066 ]
4067 } else {
4068 vec![
4069 PathCommand::MoveTo { x: ah, y: mid_y + ah },
4070 PathCommand::LineTo { x: 0.0, y: mid_y },
4071 PathCommand::LineTo { x: width, y: mid_y },
4072 ]
4073 }
4074 }
4075 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
4076 let gap = 0.06;
4077 vec![
4078 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4079 PathCommand::LineTo { x: width, y: mid_y - gap },
4080 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
4081 PathCommand::LineTo { x: width, y: mid_y - gap },
4082 PathCommand::MoveTo { x: width, y: mid_y + gap },
4083 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4084 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
4085 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4086 ]
4087 }
4088 "\\xtofrom" | "\\xrightleftarrows" => {
4089 let gap = 0.06;
4090 vec![
4091 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
4092 PathCommand::LineTo { x: width, y: mid_y - gap },
4093 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
4094 PathCommand::LineTo { x: width, y: mid_y - gap },
4095 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
4096 PathCommand::MoveTo { x: width, y: mid_y + gap },
4097 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4098 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
4099 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
4100 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
4101 ]
4102 }
4103 "\\overlinesegment" | "\\underlinesegment" => {
4104 vec![
4105 PathCommand::MoveTo { x: 0.0, y: mid_y },
4106 PathCommand::LineTo { x: width, y: mid_y },
4107 ]
4108 }
4109 _ => {
4110 vec![
4111 PathCommand::MoveTo { x: 0.0, y: mid_y },
4112 PathCommand::LineTo { x: width, y: mid_y },
4113 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
4114 PathCommand::LineTo { x: width, y: mid_y },
4115 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
4116 ]
4117 }
4118 }
4119}
4120
4121fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
4127 let h = inner.height;
4128 let d = inner.depth;
4129 let w = inner.width + pad_l + pad_r;
4130 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
4131 if pad_l > 0.0 {
4132 children.push(LayoutBox::new_kern(pad_l));
4133 }
4134 children.push(inner);
4135 if pad_r > 0.0 {
4136 children.push(LayoutBox::new_kern(pad_r));
4137 }
4138 LayoutBox {
4139 width: w,
4140 height: h,
4141 depth: d,
4142 content: BoxContent::HBox(children),
4143 color,
4144 }
4145}
4146
4147fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
4158 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
4159 LayoutBox {
4160 width: label.width,
4161 height: box_h,
4162 depth: box_d,
4163 content: BoxContent::RaiseBox {
4164 body: Box::new(label),
4165 shift,
4166 },
4167 color,
4168 }
4169}
4170
4171fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
4175 let sup_style = options.style.superscript();
4176 let sup_opts = options.with_style(sup_style);
4177 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4178 let inner = layout_node(body, &sup_opts);
4179 if (sup_ratio - 1.0).abs() < 1e-6 {
4180 inner
4181 } else {
4182 LayoutBox {
4183 width: inner.width * sup_ratio,
4184 height: inner.height * sup_ratio,
4185 depth: inner.depth * sup_ratio,
4186 content: BoxContent::Scaled {
4187 body: Box::new(inner),
4188 child_scale: sup_ratio,
4189 },
4190 color: options.color,
4191 }
4192 }
4193}
4194
4195fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4201 let axis = options.metrics().axis_height;
4202 let depth = (total_height / 2.0 - axis).max(0.0);
4203 let height = total_height - depth;
4204 if let Some((commands, w)) =
4205 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4206 {
4207 return LayoutBox {
4208 width: w,
4209 height,
4210 depth,
4211 content: BoxContent::SvgPath {
4212 commands,
4213 fill: true,
4214 },
4215 color: options.color,
4216 };
4217 }
4218 if down {
4220 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4221 } else {
4222 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4223 }
4224}
4225
4226fn layout_cd_arrow(
4242 direction: &str,
4243 label_above: Option<&ParseNode>,
4244 label_below: Option<&ParseNode>,
4245 target_size: f64,
4246 target_col_width: f64,
4247 _target_depth: f64,
4248 options: &LayoutOptions,
4249) -> LayoutBox {
4250 let metrics = options.metrics();
4251 let axis = metrics.axis_height;
4252
4253 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4256
4257 match direction {
4258 "right" | "left" | "horiz_eq" => {
4259 let sup_style = options.style.superscript();
4261 let sub_style = options.style.subscript();
4262 let sup_opts = options.with_style(sup_style);
4263 let sub_opts = options.with_style(sub_style);
4264 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4265 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4266
4267 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4268 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4269
4270 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4271 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4272
4273 let path_label = if direction == "right" {
4275 "\\cdrightarrow"
4276 } else if direction == "left" {
4277 "\\cdleftarrow"
4278 } else {
4279 "\\cdlongequal"
4280 };
4281 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4282 const CD_LABEL_PAD_L: f64 = 0.22;
4285 const CD_LABEL_PAD_R: f64 = 0.48;
4286 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4287 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4288 let upper_need = above_box
4289 .as_ref()
4290 .map(|_| above_w + cd_pad_sup)
4291 .unwrap_or(0.0);
4292 let lower_need = below_box
4293 .as_ref()
4294 .map(|_| below_w + cd_pad_sub)
4295 .unwrap_or(0.0);
4296 let natural_w = upper_need.max(lower_need).max(0.0);
4297 let shaft_w = if target_size > 0.0 {
4298 target_size
4299 } else {
4300 natural_w.max(min_shaft_w)
4301 };
4302
4303 let (commands, actual_arrow_h, fill_arrow) =
4304 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4305 Some((c, h)) => (c, h, true),
4306 None => {
4307 let arrow_h = 0.3_f64;
4309 let ah = 0.12_f64;
4310 let cmds = if direction == "horiz_eq" {
4311 let gap = 0.06;
4312 vec![
4313 PathCommand::MoveTo { x: 0.0, y: -gap },
4314 PathCommand::LineTo { x: shaft_w, y: -gap },
4315 PathCommand::MoveTo { x: 0.0, y: gap },
4316 PathCommand::LineTo { x: shaft_w, y: gap },
4317 ]
4318 } else if direction == "right" {
4319 vec![
4320 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4321 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4322 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4323 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4324 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4325 ]
4326 } else {
4327 vec![
4328 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4329 PathCommand::LineTo { x: 0.0, y: 0.0 },
4330 PathCommand::MoveTo { x: ah, y: -ah },
4331 PathCommand::LineTo { x: 0.0, y: 0.0 },
4332 PathCommand::LineTo { x: ah, y: ah },
4333 ]
4334 };
4335 (cmds, arrow_h, false)
4336 }
4337 };
4338
4339 let arrow_half = actual_arrow_h / 2.0;
4341 let arrow_box = LayoutBox {
4342 width: shaft_w,
4343 height: arrow_half,
4344 depth: arrow_half,
4345 content: BoxContent::SvgPath {
4346 commands,
4347 fill: fill_arrow,
4348 },
4349 color: options.color,
4350 };
4351
4352 let gap = 0.111;
4354 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4355 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4356 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4360 sup_d
4361 } else {
4362 0.0
4363 };
4364 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4365 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4366 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4367 let depth = if below_box.is_some() {
4368 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4369 } else {
4370 (arrow_half - axis).max(0.0)
4371 };
4372
4373 let inner = LayoutBox {
4374 width: shaft_w,
4375 height,
4376 depth,
4377 content: BoxContent::OpLimits {
4378 base: Box::new(arrow_box),
4379 sup: above_box.map(Box::new),
4380 sub: below_box.map(Box::new),
4381 base_shift: -axis,
4382 sup_kern: gap,
4383 sub_kern: gap,
4384 slant: 0.0,
4385 sup_scale: sup_ratio,
4386 sub_scale: sub_ratio,
4387 },
4388 color: options.color,
4389 };
4390
4391 if target_col_width > inner.width + 1e-6 {
4395 let extra = target_col_width - inner.width;
4396 let kl = extra / 2.0;
4397 let kr = extra - kl;
4398 cd_wrap_hpad(inner, kl, kr, options.color)
4399 } else {
4400 inner
4401 }
4402 }
4403
4404 "down" | "up" | "vert_eq" => {
4405 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4409 "vert_eq" if target_size > 0.0 => {
4410 make_vert_delim_box(target_size.max(big_total), true, options)
4411 }
4412 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4413 "down" if target_size > 0.0 => {
4414 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4415 }
4416 "up" if target_size > 0.0 => {
4417 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4418 }
4419 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4420 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4421 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4422 };
4423 let box_h = shaft_box.height;
4424 let box_d = shaft_box.depth;
4425 let shaft_w = shaft_box.width;
4426
4427 let left_box = label_above.map(|n| {
4430 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4431 });
4432 let right_box = label_below.map(|n| {
4433 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4434 });
4435
4436 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4437 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4438 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4439 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4440 let inner_w = left_part + shaft_w + right_part;
4441
4442 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4444 let extra = target_col_width - inner_w;
4445 let kl = extra / 2.0;
4446 let kr = extra - kl;
4447 (kl, kr, target_col_width)
4448 } else {
4449 (0.0, 0.0, inner_w)
4450 };
4451
4452 let mut children: Vec<LayoutBox> = Vec::new();
4453 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4454 if let Some(lb) = left_box {
4455 children.push(lb);
4456 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4457 }
4458 children.push(shaft_box);
4459 if let Some(rb) = right_box {
4460 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4461 children.push(rb);
4462 }
4463 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4464
4465 LayoutBox {
4466 width: total_w,
4467 height: box_h,
4468 depth: box_d,
4469 content: BoxContent::HBox(children),
4470 color: options.color,
4471 }
4472 }
4473
4474 _ => LayoutBox::new_empty(),
4476 }
4477}
4478
4479fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4481 let metrics = options.metrics();
4482 let pt = 1.0 / metrics.pt_per_em;
4483 let baselineskip = 3.0 * metrics.x_height;
4485 let arstrut_h = 0.7 * baselineskip;
4486 let arstrut_d = 0.3 * baselineskip;
4487
4488 let num_rows = body.len();
4489 if num_rows == 0 {
4490 return LayoutBox::new_empty();
4491 }
4492 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4493 if num_cols == 0 {
4494 return LayoutBox::new_empty();
4495 }
4496
4497 let jot = 3.0 * pt;
4499
4500 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4502 let mut col_widths = vec![0.0_f64; num_cols];
4503 let mut row_heights = vec![arstrut_h; num_rows];
4504 let mut row_depths = vec![arstrut_d; num_rows];
4505
4506 for (r, row) in body.iter().enumerate() {
4507 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4508
4509 for (c, cell) in row.iter().enumerate() {
4510 let cbox = match cell {
4511 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4512 layout_cd_arrow(
4513 direction,
4514 label_above.as_deref(),
4515 label_below.as_deref(),
4516 0.0, 0.0, 0.0, options,
4520 )
4521 }
4522 ParseNode::OrdGroup { body: cell_body, .. } => {
4526 layout_expression(cell_body, options, false)
4527 }
4528 other => layout_node(other, options),
4529 };
4530
4531 row_heights[r] = row_heights[r].max(cbox.height);
4532 row_depths[r] = row_depths[r].max(cbox.depth);
4533 col_widths[c] = col_widths[c].max(cbox.width);
4534 row_boxes.push(cbox);
4535 }
4536
4537 while row_boxes.len() < num_cols {
4539 row_boxes.push(LayoutBox::new_empty());
4540 }
4541 cell_boxes.push(row_boxes);
4542 }
4543
4544 let col_target_w: Vec<f64> = col_widths.clone();
4548
4549 #[cfg(debug_assertions)]
4550 {
4551 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4552 for (r, row) in cell_boxes.iter().enumerate() {
4553 for (c, b) in row.iter().enumerate() {
4554 if b.width > 0.0 {
4555 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4556 }
4557 }
4558 }
4559 }
4560
4561 for (r, row) in body.iter().enumerate() {
4563 let is_arrow_row = r % 2 == 1;
4564 for (c, cell) in row.iter().enumerate() {
4565 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4566 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4567 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4568 let b = layout_cd_arrow(
4569 direction,
4570 label_above.as_deref(),
4571 label_below.as_deref(),
4572 cell_boxes[r][c].width,
4573 col_target_w[c],
4574 0.0,
4575 options,
4576 );
4577 let w = b.width;
4578 (b, w)
4579 } else if is_arrow_row && c % 2 == 0 {
4580 let v_span = row_heights[r] + row_depths[r];
4584 let b = layout_cd_arrow(
4585 direction,
4586 label_above.as_deref(),
4587 label_below.as_deref(),
4588 v_span,
4589 col_widths[c],
4590 0.0,
4591 options,
4592 );
4593 let w = b.width;
4594 (b, w)
4595 } else {
4596 continue;
4597 };
4598 col_widths[c] = col_widths[c].max(col_w);
4599 cell_boxes[r][c] = new_box;
4600 }
4601 }
4602 }
4603
4604 #[cfg(debug_assertions)]
4605 {
4606 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4607 }
4608
4609 for rd in &mut row_depths {
4612 *rd += jot;
4613 }
4614
4615 let col_gap = 0.5;
4620
4621 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4623
4624 let col_separators = vec![None; num_cols + 1];
4626
4627 let mut total_height = 0.0_f64;
4628 let mut row_positions = Vec::with_capacity(num_rows);
4629 for r in 0..num_rows {
4630 total_height += row_heights[r];
4631 row_positions.push(total_height);
4632 total_height += row_depths[r];
4633 }
4634
4635 let offset = total_height / 2.0 + metrics.axis_height;
4636 let height = offset;
4637 let depth = total_height - offset;
4638
4639 let total_width = col_widths.iter().sum::<f64>()
4641 + col_gap * (num_cols.saturating_sub(1)) as f64;
4642
4643 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4645
4646 LayoutBox {
4647 width: total_width,
4648 height,
4649 depth,
4650 content: BoxContent::Array {
4651 cells: cell_boxes,
4652 col_widths,
4653 col_aligns,
4654 row_heights,
4655 row_depths,
4656 col_gap,
4657 offset,
4658 content_x_offset: 0.0,
4659 col_separators,
4660 hlines_before_row,
4661 rule_thickness: 0.04 * pt,
4662 double_rule_sep: metrics.double_rule_sep,
4663 array_inner_width: total_width,
4664 tag_gap_em: 0.0,
4665 tag_col_width: 0.0,
4666 row_tags: (0..num_rows).map(|_| None).collect(),
4667 tags_left: false,
4668 },
4669 color: options.color,
4670 }
4671}
4672
4673struct ProofTreeLayout {
4674 width: f64,
4675 height: f64,
4676 depth: f64,
4677 root_width: f64,
4678 root_center_x: f64,
4679 children: Vec<PlacedBox>,
4680 rules: Vec<ProofRule>,
4681}
4682
4683fn layout_proof_roman_content(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
4689 if nodes.is_empty() {
4690 return LayoutBox::new_empty();
4691 }
4692 let group = ParseNode::OrdGroup {
4693 mode: Mode::Math,
4694 body: nodes.to_vec(),
4695 semisimple: None,
4696 loc: None,
4697 };
4698 let wrapped = ParseNode::Font {
4699 mode: Mode::Math,
4700 font: "mathrm".to_string(),
4701 body: Box::new(group),
4702 loc: None,
4703 };
4704 layout_node(&wrapped, options)
4705}
4706
4707fn layout_proof_cell_content(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
4708 let mut box_ = layout_proof_roman_content(nodes, options);
4709 box_.depth = box_.depth.max(0.20);
4710 box_
4711}
4712
4713fn layout_proof_tree(tree: &ProofBranch, options: &LayoutOptions) -> LayoutBox {
4714 let laid = layout_proof_branch(tree, options);
4715 LayoutBox {
4716 width: laid.width,
4717 height: laid.height,
4718 depth: laid.depth,
4719 content: BoxContent::ProofTree {
4720 children: laid.children,
4721 rules: laid.rules,
4722 },
4723 color: options.color,
4724 }
4725}
4726
4727fn layout_proof_branch(tree: &ProofBranch, options: &LayoutOptions) -> ProofTreeLayout {
4728 let metrics = options.metrics();
4729 let rule_thickness = (metrics.default_rule_thickness * 2.0).max(0.08);
4730 let premise_gap = 2.0;
4736 let label_gap = 0.08;
4737 let vertical_gap = 0.32;
4738
4739 let conclusion = layout_proof_cell_content(&tree.conclusion, options);
4740 let conclusion_width = conclusion.width;
4741
4742 if tree.premises.is_empty() {
4743 let width = conclusion.width;
4744 let height = conclusion.height;
4745 let depth = conclusion.depth;
4746 return ProofTreeLayout {
4747 width,
4748 height,
4749 depth,
4750 root_width: width,
4751 root_center_x: width / 2.0,
4752 children: vec![PlacedBox {
4753 box_: conclusion,
4754 x: 0.0,
4755 baseline_y: height,
4756 }],
4757 rules: Vec::new(),
4758 };
4759 }
4760
4761 let premise_layouts: Vec<ProofTreeLayout> = tree
4762 .premises
4763 .iter()
4764 .map(|p| layout_proof_branch(p, options))
4765 .collect();
4766
4767 let premise_width = premise_layouts.iter().map(|p| p.width).sum::<f64>()
4768 + premise_gap * premise_layouts.len().saturating_sub(1) as f64;
4769 let premise_height = premise_layouts
4770 .iter()
4771 .map(|p| p.height)
4772 .fold(0.0_f64, f64::max);
4773 let premise_depth = premise_layouts
4774 .iter()
4775 .map(|p| p.depth)
4776 .fold(0.0_f64, f64::max);
4777 let premise_total = premise_height + premise_depth;
4778
4779 let mut premise_root_left = f64::INFINITY;
4780 let mut premise_root_right = f64::NEG_INFINITY;
4781 let mut cursor = 0.0_f64;
4782 for premise in &premise_layouts {
4783 let root_left = cursor + premise.root_center_x - premise.root_width / 2.0;
4784 let root_right = cursor + premise.root_center_x + premise.root_width / 2.0;
4785 premise_root_left = premise_root_left.min(root_left);
4786 premise_root_right = premise_root_right.max(root_right);
4787 cursor += premise.width + premise_gap;
4788 }
4789 let rule_padding = if tree.premises.len() > 1 { 0.65 } else { 0.0 };
4790 let premise_rule_width = premise_root_right - premise_root_left + rule_padding;
4791 let premise_root_center = (premise_root_left + premise_root_right) / 2.0;
4792 let rule_width = premise_rule_width.max(conclusion_width).max(1.12);
4793 let core_width = premise_width.max(rule_width).max(conclusion_width);
4794 let center_x = core_width / 2.0;
4795 let premise_start_x = center_x - premise_width / 2.0;
4796 let root_center_x = premise_start_x + premise_root_center;
4797 let rule_x = root_center_x - rule_width / 2.0;
4798 let rule_y = premise_total + vertical_gap + rule_thickness / 2.0;
4799 let conclusion_x = root_center_x - conclusion_width / 2.0;
4800 let conclusion_baseline_y = rule_y + rule_thickness / 2.0 + vertical_gap + conclusion.height;
4801
4802 let mut children = Vec::new();
4803 let mut rules = Vec::new();
4804 let mut min_x = 0.0_f64;
4805 let mut max_x = core_width;
4806
4807 if tree.root_at_top {
4808 let conclusion_baseline_y = conclusion.height;
4809 let rule_y =
4810 conclusion.height + conclusion.depth + vertical_gap + rule_thickness / 2.0;
4811 let premise_top_y = rule_y + rule_thickness / 2.0 + vertical_gap;
4812 let premise_baseline_y = premise_top_y + premise_height;
4813
4814 min_x = min_x.min(conclusion_x);
4815 max_x = max_x.max(conclusion_x + conclusion_width);
4816 children.push(PlacedBox {
4817 box_: conclusion,
4818 x: conclusion_x,
4819 baseline_y: conclusion_baseline_y,
4820 });
4821
4822 let mut cursor = premise_start_x;
4823 for premise in premise_layouts {
4824 let child_top_y = premise_baseline_y - premise.height;
4825 for child in premise.children {
4826 let x = cursor + child.x;
4827 let y = child_top_y + child.baseline_y;
4828 min_x = min_x.min(x);
4829 max_x = max_x.max(x + child.box_.width);
4830 children.push(PlacedBox {
4831 box_: child.box_,
4832 x,
4833 baseline_y: y,
4834 });
4835 }
4836 for rule in premise.rules {
4837 min_x = min_x.min(cursor + rule.x);
4838 max_x = max_x.max(cursor + rule.x + rule.width);
4839 rules.push(ProofRule {
4840 x: cursor + rule.x,
4841 y: child_top_y + rule.y,
4842 width: rule.width,
4843 thickness: rule.thickness,
4844 dashed: rule.dashed,
4845 });
4846 }
4847 cursor += premise.width + premise_gap;
4848 }
4849
4850 if !matches!(tree.line_style, ProofLineStyle::None) {
4851 rules.push(ProofRule {
4852 x: rule_x,
4853 y: rule_y,
4854 width: rule_width,
4855 thickness: rule_thickness,
4856 dashed: matches!(tree.line_style, ProofLineStyle::Dashed),
4857 });
4858 }
4859
4860 if let Some(label) = tree.left_label.as_ref() {
4861 let label_opts = options.with_style(options.style.text());
4862 let label_box = layout_proof_roman_content(label, &label_opts);
4863 let x = rule_x - label_gap - label_box.width;
4864 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
4865 min_x = min_x.min(x);
4866 max_x = max_x.max(x + label_box.width);
4867 children.push(PlacedBox {
4868 box_: label_box,
4869 x,
4870 baseline_y,
4871 });
4872 }
4873
4874 if let Some(label) = tree.right_label.as_ref() {
4875 let label_opts = options.with_style(options.style.text());
4876 let label_box = layout_proof_roman_content(label, &label_opts);
4877 let x = rule_x + rule_width + label_gap;
4878 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
4879 min_x = min_x.min(x);
4880 max_x = max_x.max(x + label_box.width);
4881 children.push(PlacedBox {
4882 box_: label_box,
4883 x,
4884 baseline_y,
4885 });
4886 }
4887
4888 let mut root_center_x = root_center_x;
4889 if min_x < 0.0 {
4890 let shift_x = -min_x;
4891 for child in &mut children {
4892 child.x += shift_x;
4893 }
4894 for rule in &mut rules {
4895 rule.x += shift_x;
4896 }
4897 root_center_x += shift_x;
4898 }
4899
4900 return ProofTreeLayout {
4901 width: max_x - min_x,
4902 height: conclusion_baseline_y,
4903 depth: children
4904 .iter()
4905 .map(|c| c.baseline_y + c.box_.depth - conclusion_baseline_y)
4906 .fold(0.0_f64, f64::max)
4907 .max(0.0),
4908 root_width: conclusion_width,
4909 root_center_x,
4910 children,
4911 rules,
4912 };
4913 }
4914
4915 let mut cursor = premise_start_x;
4916 for premise in premise_layouts {
4917 let baseline_y = premise_height;
4918 let child_top_y = baseline_y - premise.height;
4919 for child in premise.children {
4920 let x = cursor + child.x;
4921 let y = child_top_y + child.baseline_y;
4922 min_x = min_x.min(x);
4923 max_x = max_x.max(x + child.box_.width);
4924 children.push(PlacedBox {
4925 box_: child.box_,
4926 x,
4927 baseline_y: y,
4928 });
4929 }
4930 for rule in premise.rules {
4931 min_x = min_x.min(cursor + rule.x);
4932 max_x = max_x.max(cursor + rule.x + rule.width);
4933 rules.push(ProofRule {
4934 x: cursor + rule.x,
4935 y: child_top_y + rule.y,
4936 width: rule.width,
4937 thickness: rule.thickness,
4938 dashed: rule.dashed,
4939 });
4940 }
4941 cursor += premise.width + premise_gap;
4942 }
4943
4944 min_x = min_x.min(conclusion_x);
4945 max_x = max_x.max(conclusion_x + conclusion_width);
4946 children.push(PlacedBox {
4947 box_: conclusion,
4948 x: conclusion_x,
4949 baseline_y: conclusion_baseline_y,
4950 });
4951
4952 if !matches!(tree.line_style, ProofLineStyle::None) {
4953 rules.push(ProofRule {
4954 x: rule_x,
4955 y: rule_y,
4956 width: rule_width,
4957 thickness: rule_thickness,
4958 dashed: matches!(tree.line_style, ProofLineStyle::Dashed),
4959 });
4960 }
4961
4962 if let Some(label) = tree.left_label.as_ref() {
4963 let label_opts = options.with_style(options.style.text());
4964 let label_box = layout_proof_roman_content(label, &label_opts);
4965 let x = rule_x - label_gap - label_box.width;
4966 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
4967 min_x = min_x.min(x);
4968 max_x = max_x.max(x + label_box.width);
4969 children.push(PlacedBox {
4970 box_: label_box,
4971 x,
4972 baseline_y,
4973 });
4974 }
4975
4976 if let Some(label) = tree.right_label.as_ref() {
4977 let label_opts = options.with_style(options.style.text());
4978 let label_box = layout_proof_roman_content(label, &label_opts);
4979 let x = rule_x + rule_width + label_gap;
4980 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
4981 min_x = min_x.min(x);
4982 max_x = max_x.max(x + label_box.width);
4983 children.push(PlacedBox {
4984 box_: label_box,
4985 x,
4986 baseline_y,
4987 });
4988 }
4989
4990 let mut root_center_x = root_center_x;
4991 if min_x < 0.0 {
4992 let shift = -min_x;
4993 for child in &mut children {
4994 child.x += shift;
4995 }
4996 for rule in &mut rules {
4997 rule.x += shift;
4998 }
4999 root_center_x += shift;
5000 }
5001
5002 ProofTreeLayout {
5003 width: max_x - min_x,
5004 height: conclusion_baseline_y,
5005 depth: children
5006 .iter()
5007 .map(|c| c.baseline_y + c.box_.depth - conclusion_baseline_y)
5008 .fold(0.0_f64, f64::max),
5009 root_width: conclusion_width,
5010 root_center_x,
5011 children,
5012 rules,
5013 }
5014}
5015
5016fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
5017 let mid = width / 2.0;
5018 let q = height * 0.6;
5019 if is_over {
5020 vec![
5021 PathCommand::MoveTo { x: 0.0, y: 0.0 },
5022 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
5023 PathCommand::LineTo { x: mid - 0.05, y: -q },
5024 PathCommand::LineTo { x: mid, y: -height },
5025 PathCommand::LineTo { x: mid + 0.05, y: -q },
5026 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
5027 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
5028 ]
5029 } else {
5030 vec![
5031 PathCommand::MoveTo { x: 0.0, y: 0.0 },
5032 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
5033 PathCommand::LineTo { x: mid - 0.05, y: q },
5034 PathCommand::LineTo { x: mid, y: height },
5035 PathCommand::LineTo { x: mid + 0.05, y: q },
5036 PathCommand::LineTo { x: width - mid * 0.4, y: q },
5037 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
5038 ]
5039 }
5040}
5041
5042#[cfg(test)]
5043mod missing_glyph_width_em_tests {
5044 use super::{missing_glyph_height_em, missing_glyph_width_em};
5045 use ratex_font::get_global_metrics;
5046
5047 #[test]
5048 fn supplementary_plane_emoji_is_one_em() {
5049 assert_eq!(missing_glyph_width_em('😊'), 1.0);
5050 assert_eq!(missing_glyph_width_em('🚀'), 1.0);
5051 }
5052
5053 #[test]
5054 fn supplementary_plane_emoji_uses_shorter_box_height() {
5055 let m = get_global_metrics(0);
5056 let emoji_h = missing_glyph_height_em('😊', m);
5057 let default_h = (m.quad * 0.92).max(m.x_height);
5058 assert!(
5059 emoji_h < default_h,
5060 "tall placeholder box must not push \\sqrt past KaTeX's small-surd threshold"
5061 );
5062 assert!((emoji_h - 0.74).abs() < 1e-9);
5063 }
5064
5065 #[test]
5066 fn dingbats_block_is_one_em() {
5067 assert_eq!(missing_glyph_width_em('\u{2708}'), 1.0); }
5069
5070 #[test]
5071 fn miscellaneous_symbols_is_one_em() {
5072 assert_eq!(missing_glyph_width_em('\u{2605}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2615}'), 1.0); }
5075
5076 #[test]
5077 fn misc_symbols_and_arrows_is_one_em() {
5078 assert_eq!(missing_glyph_width_em('\u{2B50}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2B1B}'), 1.0); }
5081
5082 #[test]
5083 fn latin_without_metrics_stays_half_em() {
5084 assert_eq!(missing_glyph_width_em('z'), 0.5);
5085 }
5086}
5087
5088#[cfg(test)]
5089mod cjk_font_switching_tests {
5090 use super::super::to_display::to_display_list;
5091 use super::*;
5092 use ratex_parser::parser::parse;
5093 use ratex_types::display_item::DisplayItem;
5094
5095 fn first_glyph_font_name(latex: &str) -> Option<String> {
5096 let ast = parse(latex).ok()?;
5097 let lbox = layout(&ast, &LayoutOptions::default());
5098 let dl = to_display_list(&lbox);
5099 for item in &dl.items {
5100 if let DisplayItem::GlyphPath { font, .. } = item {
5101 return Some(font.clone());
5102 }
5103 }
5104 None
5105 }
5106
5107 #[test]
5108 fn cjk_in_text_uses_cjk_regular() {
5109 assert_eq!(
5110 first_glyph_font_name(r"\text{中}").as_deref(),
5111 Some("CJK-Regular")
5112 );
5113 }
5114
5115 #[test]
5116 fn emoji_in_text_uses_cjk_regular() {
5117 assert_eq!(
5118 first_glyph_font_name(r"\text{😊}").as_deref(),
5119 Some("CJK-Regular")
5120 );
5121 }
5122
5123 #[test]
5124 fn latin_in_text_is_not_cjk() {
5125 assert_ne!(
5126 first_glyph_font_name(r"\text{a}").as_deref(),
5127 Some("CJK-Regular")
5128 );
5129 }
5130
5131 #[test]
5132 fn hiragana_in_text_uses_cjk_regular() {
5133 assert_eq!(
5134 first_glyph_font_name(r"\text{あ}").as_deref(),
5135 Some("CJK-Regular")
5136 );
5137 }
5138}