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, StyleStr,
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
23fn style_str_to_math_style(style: &StyleStr) -> MathStyle {
24 match style {
25 StyleStr::Display => MathStyle::Display,
26 StyleStr::Text => MathStyle::Text,
27 StyleStr::Script => MathStyle::Script,
28 StyleStr::Scriptscript => MathStyle::ScriptScript,
29 }
30}
31
32pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
34 layout_expression(nodes, options, true)
35}
36
37fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
40 let n = raw.len();
41 let mut eff = raw.to_vec();
42 for i in 0..n {
43 if raw[i] != Some(MathClass::Bin) {
44 continue;
45 }
46 let prev = if i == 0 { None } else { raw[i - 1] };
47 let left_cancel = matches!(
48 prev,
49 None | Some(MathClass::Bin)
50 | Some(MathClass::Open)
51 | Some(MathClass::Rel)
52 | Some(MathClass::Op)
53 | Some(MathClass::Punct)
54 );
55 if left_cancel {
56 eff[i] = Some(MathClass::Ord);
57 }
58 }
59 for i in 0..n {
60 if raw[i] != Some(MathClass::Bin) {
61 continue;
62 }
63 let next = if i + 1 < n { raw[i + 1] } else { None };
64 let right_cancel = matches!(
65 next,
66 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
67 );
68 if right_cancel {
69 eff[i] = Some(MathClass::Ord);
70 }
71 }
72 eff
73}
74
75fn node_is_middle_fence(node: &ParseNode) -> bool {
80 matches!(node, ParseNode::Middle { .. })
81}
82
83fn layout_expression(
85 nodes: &[ParseNode],
86 options: &LayoutOptions,
87 is_real_group: bool,
88) -> LayoutBox {
89 if nodes.is_empty() {
90 return LayoutBox::new_empty();
91 }
92
93 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
95 if has_cr {
96 return layout_multiline(nodes, options, is_real_group);
97 }
98
99 let raw_classes: Vec<Option<MathClass>> = nodes.iter().map(node_math_class).collect();
100 let eff_classes = apply_bin_cancellation(&raw_classes);
101
102 let mut children = Vec::new();
103 let mut prev_class: Option<MathClass> = None;
104 let mut prev_class_node_idx: Option<usize> = None;
106
107 for (i, node) in nodes.iter().enumerate() {
108 let lbox = layout_node(node, options);
109 let cur_class = eff_classes.get(i).copied().flatten();
110
111 if is_real_group {
112 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
113 let prev_middle =
114 prev_class_node_idx.is_some_and(|j| node_is_middle_fence(&nodes[j]));
115 let cur_middle = node_is_middle_fence(node);
116 let mu = if prev_middle || cur_middle {
117 0.0
118 } else {
119 atom_spacing(prev, cur, options.style.is_tight())
120 };
121 let mu = if let Some(cap) = options.align_relation_spacing {
122 if prev == MathClass::Rel || cur == MathClass::Rel {
123 mu.min(cap)
124 } else {
125 mu
126 }
127 } else {
128 mu
129 };
130 if mu > 0.0 {
131 let em = mu_to_em(mu, options.metrics().quad);
132 children.push(LayoutBox::new_kern(em));
133 }
134 }
135 }
136
137 if cur_class.is_some() {
138 prev_class = cur_class;
139 prev_class_node_idx = Some(i);
140 }
141
142 children.push(lbox);
143 }
144
145 make_hbox(children)
146}
147
148fn layout_multiline(
150 nodes: &[ParseNode],
151 options: &LayoutOptions,
152 is_real_group: bool,
153) -> LayoutBox {
154 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
155 let metrics = options.metrics();
156 let pt = 1.0 / metrics.pt_per_em;
157 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
162 let mut start = 0;
163 for (i, node) in nodes.iter().enumerate() {
164 if matches!(node, ParseNode::Cr { .. }) {
165 rows.push(&nodes[start..i]);
166 start = i + 1;
167 }
168 }
169 rows.push(&nodes[start..]);
170
171 let row_boxes: Vec<LayoutBox> = rows
172 .iter()
173 .map(|row| layout_expression(row, options, is_real_group))
174 .collect();
175
176 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
177
178 let mut vchildren: Vec<VBoxChild> = Vec::new();
179 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
180 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
181 for (i, row) in row_boxes.iter().enumerate() {
182 if i > 0 {
183 let prev_depth = row_boxes[i - 1].depth;
185 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
186 vchildren.push(VBoxChild {
187 kind: VBoxChildKind::Kern(gap),
188 shift: 0.0,
189 });
190 h += gap + row.height + prev_depth;
191 }
192 vchildren.push(VBoxChild {
193 kind: VBoxChildKind::Box(Box::new(row.clone())),
194 shift: 0.0,
195 });
196 }
197
198 LayoutBox {
199 width: total_width,
200 height: h,
201 depth: d,
202 content: BoxContent::VBox(vchildren),
203 color: options.color,
204 }
205}
206
207fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
209 match node {
210 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
211 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
212 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
213 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
214
215 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
216
217 ParseNode::SupSub { base, sup, sub, .. } => {
218 if let Some(base_node) = base.as_deref() {
219 if should_use_op_limits(base_node, options) {
220 return layout_op_with_limits(
221 base_node,
222 sup.as_deref(),
223 sub.as_deref(),
224 options,
225 );
226 }
227 }
228 layout_supsub(
229 base.as_deref(),
230 sup.as_deref(),
231 sub.as_deref(),
232 options,
233 None,
234 )
235 }
236
237 ParseNode::GenFrac {
238 numer,
239 denom,
240 has_bar_line,
241 bar_size,
242 left_delim,
243 right_delim,
244 continued,
245 ..
246 } => {
247 let bar_thickness = if *has_bar_line {
248 bar_size
249 .as_ref()
250 .map(|m| measurement_to_em(m, options))
251 .unwrap_or(options.metrics().default_rule_thickness)
252 } else {
253 0.0
254 };
255 let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
256
257 let has_left = left_delim
258 .as_ref()
259 .is_some_and(|d| !d.is_empty() && d != ".");
260 let has_right = right_delim
261 .as_ref()
262 .is_some_and(|d| !d.is_empty() && d != ".");
263
264 if has_left || has_right {
265 let total_h = genfrac_delim_target_height(options);
266 let left_d = left_delim.as_deref().unwrap_or(".");
267 let right_d = right_delim.as_deref().unwrap_or(".");
268 let left_box = make_stretchy_delim(left_d, total_h, options);
269 let right_box = make_stretchy_delim(right_d, total_h, options);
270
271 let width = left_box.width + frac.width + right_box.width;
272 let height = frac.height.max(left_box.height).max(right_box.height);
273 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
274
275 LayoutBox {
276 width,
277 height,
278 depth,
279 content: BoxContent::LeftRight {
280 left: Box::new(left_box),
281 right: Box::new(right_box),
282 inner: Box::new(frac),
283 },
284 color: options.color,
285 }
286 } else {
287 let right_nds = if *continued {
288 0.0
289 } else {
290 NULL_DELIMITER_SPACE
291 };
292 make_hbox(vec![
293 LayoutBox::new_kern(NULL_DELIMITER_SPACE),
294 frac,
295 LayoutBox::new_kern(right_nds),
296 ])
297 }
298 }
299
300 ParseNode::Sqrt { body, index, .. } => layout_radical(body, index.as_deref(), options),
301
302 ParseNode::Op {
303 name,
304 symbol,
305 body,
306 limits,
307 suppress_base_shift,
308 ..
309 } => layout_op(
310 name.as_deref(),
311 *symbol,
312 body.as_deref(),
313 *limits,
314 suppress_base_shift.unwrap_or(false),
315 options,
316 ),
317
318 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
319
320 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
321
322 ParseNode::Kern { dimension, .. } => {
323 let em = measurement_to_em(dimension, options);
324 LayoutBox::new_kern(em)
325 }
326
327 ParseNode::Color { color, body, .. } => {
328 let new_color = Color::parse(color).unwrap_or(options.color);
329 let new_opts = options.with_color(new_color);
330 let mut lbox = layout_expression(body, &new_opts, true);
331 lbox.color = new_color;
332 lbox
333 }
334
335 ParseNode::Styling { style, body, .. } => {
336 let new_style = style_str_to_math_style(style);
337 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
338 let new_opts = options.with_style(new_style);
339 let inner = layout_expression(body, &new_opts, true);
340 if (ratio - 1.0).abs() < 0.001 {
341 inner
342 } else {
343 LayoutBox {
344 width: inner.width * ratio,
345 height: inner.height * ratio,
346 depth: inner.depth * ratio,
347 content: BoxContent::Scaled {
348 body: Box::new(inner),
349 child_scale: ratio,
350 },
351 color: options.color,
352 }
353 }
354 }
355
356 ParseNode::Accent {
357 label,
358 base,
359 is_stretchy,
360 is_shifty,
361 ..
362 } => {
363 let is_below = matches!(label.as_str(), "\\c");
365 layout_accent(
366 label,
367 base,
368 is_stretchy.unwrap_or(false),
369 is_shifty.unwrap_or(false),
370 is_below,
371 options,
372 )
373 }
374
375 ParseNode::AccentUnder {
376 label,
377 base,
378 is_stretchy,
379 ..
380 } => layout_accent(
381 label,
382 base,
383 is_stretchy.unwrap_or(false),
384 false,
385 true,
386 options,
387 ),
388
389 ParseNode::LeftRight {
390 body, left, right, ..
391 } => layout_left_right(body, left, right, options),
392
393 ParseNode::DelimSizing { size, delim, .. } => layout_delim_sizing(*size, delim, options),
394
395 ParseNode::Array {
396 body,
397 cols,
398 arraystretch,
399 add_jot,
400 row_gaps,
401 hlines_before_row,
402 col_separation_type,
403 hskip_before_and_after,
404 is_cd,
405 tags,
406 leqno,
407 ..
408 } => {
409 if is_cd.unwrap_or(false) {
410 layout_cd(body, options)
411 } else {
412 layout_array(
413 body,
414 cols.as_deref(),
415 *arraystretch,
416 add_jot.unwrap_or(false),
417 row_gaps,
418 hlines_before_row,
419 col_separation_type.as_deref(),
420 hskip_before_and_after.unwrap_or(false),
421 tags.as_deref(),
422 leqno.unwrap_or(false),
423 options,
424 )
425 }
426 }
427
428 ParseNode::CdArrow {
429 direction,
430 label_above,
431 label_below,
432 ..
433 } => layout_cd_arrow(
434 direction,
435 label_above.as_deref(),
436 label_below.as_deref(),
437 0.0,
438 0.0,
439 0.0,
440 options,
441 ),
442
443 ParseNode::ProofTree { tree, .. } => layout_proof_tree(tree, options),
444
445 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
446
447 ParseNode::Text {
448 body, font, mode, ..
449 } => match font.as_deref() {
450 Some(f) => {
451 let group = ParseNode::OrdGroup {
452 mode: *mode,
453 body: body.clone(),
454 semisimple: None,
455 loc: None,
456 };
457 layout_font(f, &group, options)
458 }
459 None => layout_text(body, options),
460 },
461
462 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
463
464 ParseNode::Href { body, .. } => layout_href(body, options),
465
466 ParseNode::Overline { body, .. } => layout_overline(body, options),
467 ParseNode::Underline { body, .. } => layout_underline(body, options),
468
469 ParseNode::Rule {
470 width: w,
471 height: h,
472 shift,
473 ..
474 } => {
475 let width = measurement_to_em(w, options);
476 let ink_h = measurement_to_em(h, options);
477 let raise = shift
478 .as_ref()
479 .map(|s| measurement_to_em(s, options))
480 .unwrap_or(0.0);
481 let box_height = (raise + ink_h).max(0.0);
482 let box_depth = (-raise).max(0.0);
483 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
484 }
485
486 ParseNode::Phantom { body, .. } => {
487 let inner = layout_expression(body, options, true);
488 LayoutBox {
489 width: inner.width,
490 height: inner.height,
491 depth: inner.depth,
492 content: BoxContent::Empty,
493 color: Color::BLACK,
494 }
495 }
496
497 ParseNode::VPhantom { body, .. } => {
498 let inner = layout_node(body, options);
499 LayoutBox {
500 width: 0.0,
501 height: inner.height,
502 depth: inner.depth,
503 content: BoxContent::Empty,
504 color: Color::BLACK,
505 }
506 }
507
508 ParseNode::Smash {
509 body,
510 smash_height,
511 smash_depth,
512 ..
513 } => {
514 let mut inner = layout_node(body, options);
515 if *smash_height {
516 inner.height = 0.0;
517 }
518 if *smash_depth {
519 inner.depth = 0.0;
520 }
521 inner
522 }
523
524 ParseNode::Middle { delim, .. } => {
525 match options.leftright_delim_height {
526 Some(h) => make_stretchy_delim(delim, h, options),
527 None => {
528 let placeholder = make_stretchy_delim(delim, 1.0, options);
530 LayoutBox {
531 width: placeholder.width,
532 height: 0.0,
533 depth: 0.0,
534 content: BoxContent::Empty,
535 color: options.color,
536 }
537 }
538 }
539 }
540
541 ParseNode::HtmlMathMl { html, .. } => layout_expression(html, options, true),
542
543 ParseNode::Html {
544 attributes, body, ..
545 } => layout_html(attributes, body, options),
546
547 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
548
549 ParseNode::MathChoice {
550 display,
551 text,
552 script,
553 scriptscript,
554 ..
555 } => {
556 let branch = match options.style {
557 MathStyle::Display | MathStyle::DisplayCramped => display,
558 MathStyle::Text | MathStyle::TextCramped => text,
559 MathStyle::Script | MathStyle::ScriptCramped => script,
560 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
561 };
562 layout_expression(branch, options, true)
563 }
564
565 ParseNode::Lap {
566 alignment, body, ..
567 } => {
568 let inner = layout_node(body, options);
569 let shift = match alignment.as_str() {
570 "llap" => -inner.width,
571 "clap" => -inner.width / 2.0,
572 _ => 0.0, };
574 let mut children = Vec::new();
575 if shift != 0.0 {
576 children.push(LayoutBox::new_kern(shift));
577 }
578 let h = inner.height;
579 let d = inner.depth;
580 children.push(inner);
581 LayoutBox {
582 width: 0.0,
583 height: h,
584 depth: d,
585 content: BoxContent::HBox(children),
586 color: options.color,
587 }
588 }
589
590 ParseNode::HorizBrace {
591 base,
592 is_over,
593 label,
594 ..
595 } => layout_horiz_brace(base, *is_over, label, options),
596
597 ParseNode::XArrow {
598 label, body, below, ..
599 } => layout_xarrow(label, body, below.as_deref(), options),
600
601 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
602
603 ParseNode::HBox { body, .. } => layout_text(body, options),
604
605 ParseNode::Enclose {
606 label,
607 background_color,
608 border_color,
609 body,
610 ..
611 } => layout_enclose(
612 label,
613 background_color.as_deref(),
614 border_color.as_deref(),
615 body,
616 options,
617 ),
618
619 ParseNode::RaiseBox { dy, body, .. } => {
620 let shift = measurement_to_em(dy, options);
621 layout_raisebox(shift, body, options)
622 }
623
624 ParseNode::VCenter { body, .. } => {
625 let inner = layout_node(body, options);
627 let axis = options.metrics().axis_height;
628 let total = inner.height + inner.depth;
629 let height = total / 2.0 + axis;
630 let depth = total - height;
631 LayoutBox {
632 width: inner.width,
633 height,
634 depth,
635 content: inner.content,
636 color: inner.color,
637 }
638 }
639
640 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
641
642 ParseNode::Tag { tag, .. } => {
643 let text_opts = options.with_style(options.style.text());
644 layout_expression(tag, &text_opts, true)
645 }
646
647 _ => LayoutBox::new_empty(),
649 }
650}
651
652fn missing_glyph_width_em(ch: char) -> f64 {
662 match ch as u32 {
663 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
665 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
667 0xAC00..=0xD7AF => 1.0,
669 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
671 0x1F000..=0x1FAFF => 1.0,
674 0x2700..=0x27BF => 1.0,
676 0x2600..=0x26FF => 1.0,
678 0x2B00..=0x2BFF => 1.0,
680 _ => 0.5,
681 }
682}
683
684fn missing_glyph_height_em(ch: char, m: &ratex_font::MathConstants) -> f64 {
685 let ru = ch as u32;
686 if (0x1F000..=0x1FAFF).contains(&ru) {
687 (m.quad * 0.74).max(m.x_height)
692 } else {
693 (m.quad * 0.92).max(m.x_height)
694 }
695}
696
697fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
698 let m = get_global_metrics(options.style.size_index());
699 let w = missing_glyph_width_em(ch);
700 if w >= 0.99 {
701 let h = missing_glyph_height_em(ch, m);
702 (w, h, 0.0)
703 } else {
704 (w, m.x_height, 0.0)
705 }
706}
707
708#[inline]
710fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
711 if mode == Mode::Math {
712 m.width + m.italic
713 } else {
714 m.width
715 }
716}
717
718fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
719 let ch = resolve_symbol_char(text, mode);
720
721 if text == "\\shortmid" {
722 return make_shortmid_box(options);
723 }
724 if text == "\\textunderscore" {
725 return make_textunderscore_box(options);
726 }
727
728 match ch as u32 {
730 0x2258 | 0xE258 => return make_corresponds_symbol(options),
731 0x225E | 0xE25E => return make_measured_by_symbol(options),
732 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
735 }
736
737 let char_code = ch as u32;
738
739 if let Some((font_id, metric_cp)) =
740 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
741 {
742 let m = get_char_metrics(font_id, metric_cp);
743 let (width, height, depth) = match m {
744 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
745 None => missing_glyph_metrics_fallback(ch, options),
746 };
747 return LayoutBox {
748 width,
749 height,
750 depth,
751 content: BoxContent::Glyph { font_id, char_code },
752 color: options.color,
753 };
754 }
755
756 let mut font_id = select_font(text, ch, mode, options);
757 let mut metrics = get_char_metrics(font_id, char_code);
758
759 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
760 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
761 font_id = FontId::MathItalic;
762 metrics = Some(m);
763 }
764 }
765
766 let (width, height, depth) = if let Some(m) = metrics {
772 (math_glyph_advance_em(&m, mode), m.height, m.depth)
773 } else if mode == Mode::Math {
774 let size_font = if options.style.is_display() {
775 FontId::Size2Regular
776 } else {
777 FontId::Size1Regular
778 };
779 match get_char_metrics(size_font, char_code)
780 .or_else(|| get_char_metrics(FontId::Size1Regular, char_code))
781 {
782 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
783 None => missing_glyph_metrics_fallback(ch, options),
784 }
785 } else {
786 missing_glyph_metrics_fallback(ch, options)
787 };
788
789 if metrics.is_none() && missing_glyph_width_em(ch) >= 0.99 {
792 font_id = FontId::CjkRegular;
793 }
794
795 LayoutBox {
796 width,
797 height,
798 depth,
799 content: BoxContent::Glyph { font_id, char_code },
800 color: options.color,
801 }
802}
803
804fn resolve_symbol_char(text: &str, mode: Mode) -> char {
806 let font_mode = match mode {
807 Mode::Math => ratex_font::Mode::Math,
808 Mode::Text => ratex_font::Mode::Text,
809 };
810
811 if let Some(raw) = text.chars().next() {
812 let ru = raw as u32;
813 if (0x1D400..=0x1D7FF).contains(&ru) {
814 return raw;
815 }
816 }
817
818 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
819 if let Some(cp) = info.codepoint {
820 return cp;
821 }
822 }
823
824 text.chars().next().unwrap_or('?')
825}
826
827fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
831 let font_mode = match mode {
832 Mode::Math => ratex_font::Mode::Math,
833 Mode::Text => ratex_font::Mode::Text,
834 };
835
836 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
837 if info.font == ratex_font::SymbolFont::Ams {
838 return FontId::AmsRegular;
839 }
840 }
841
842 match mode {
843 Mode::Math => {
844 if resolved_char.is_ascii_lowercase()
845 || resolved_char.is_ascii_uppercase()
846 || is_math_italic_greek(resolved_char)
847 {
848 FontId::MathItalic
849 } else {
850 FontId::MainRegular
851 }
852 }
853 Mode::Text => FontId::MainRegular,
854 }
855}
856
857fn is_math_italic_greek(ch: char) -> bool {
860 matches!(
861 ch,
862 '\u{03B1}'..='\u{03C9}' | '\u{03D1}' | '\u{03D5}' | '\u{03D6}' | '\u{03F1}' | '\u{03F5}'
863 )
864}
865
866fn is_arrow_accent(label: &str) -> bool {
867 matches!(
868 label,
869 "\\overrightarrow"
870 | "\\overleftarrow"
871 | "\\Overrightarrow"
872 | "\\overleftrightarrow"
873 | "\\underrightarrow"
874 | "\\underleftarrow"
875 | "\\underleftrightarrow"
876 | "\\overleftharpoon"
877 | "\\overrightharpoon"
878 | "\\overlinesegment"
879 | "\\underlinesegment"
880 )
881}
882
883fn layout_fraction(
888 numer: &ParseNode,
889 denom: &ParseNode,
890 bar_thickness: f64,
891 continued: bool,
892 options: &LayoutOptions,
893) -> LayoutBox {
894 let numer_s = options.style.numerator();
895 let denom_s = options.style.denominator();
896 let numer_style = options.with_style(numer_s);
897 let denom_style = options.with_style(denom_s);
898
899 let mut numer_box = layout_node(numer, &numer_style);
900 if continued {
902 let pt = options.metrics().pt_per_em;
903 let h_min = 8.5 / pt;
904 let d_min = 3.5 / pt;
905 if numer_box.height < h_min {
906 numer_box.height = h_min;
907 }
908 if numer_box.depth < d_min {
909 numer_box.depth = d_min;
910 }
911 }
912 let denom_box = layout_node(denom, &denom_style);
913
914 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
916 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
917
918 let numer_height = numer_box.height * numer_ratio;
919 let numer_depth = numer_box.depth * numer_ratio;
920 let denom_height = denom_box.height * denom_ratio;
921 let denom_depth = denom_box.depth * denom_ratio;
922 let numer_width = numer_box.width * numer_ratio;
923 let denom_width = denom_box.width * denom_ratio;
924
925 let metrics = options.metrics();
926 let axis = metrics.axis_height;
927 let rule = bar_thickness;
928
929 let (mut num_shift, mut den_shift) = if options.style.is_display() {
931 (metrics.num1, metrics.denom1)
932 } else if bar_thickness > 0.0 {
933 (metrics.num2, metrics.denom2)
934 } else {
935 (metrics.num3, metrics.denom2)
936 };
937
938 if bar_thickness > 0.0 {
939 let min_clearance = if options.style.is_display() {
940 3.0 * rule
941 } else {
942 rule
943 };
944
945 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
946 if num_clearance < min_clearance {
947 num_shift += min_clearance - num_clearance;
948 }
949
950 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
951 if den_clearance < min_clearance {
952 den_shift += min_clearance - den_clearance;
953 }
954 } else {
955 let min_gap = if options.style.is_display() {
956 7.0 * metrics.default_rule_thickness
957 } else {
958 3.0 * metrics.default_rule_thickness
959 };
960
961 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
962 if gap < min_gap {
963 let adjust = (min_gap - gap) / 2.0;
964 num_shift += adjust;
965 den_shift += adjust;
966 }
967 }
968
969 let total_width = numer_width.max(denom_width);
970 let height = numer_height + num_shift;
971 let depth = denom_depth + den_shift;
972
973 LayoutBox {
974 width: total_width,
975 height,
976 depth,
977 content: BoxContent::Fraction {
978 numer: Box::new(numer_box),
979 denom: Box::new(denom_box),
980 numer_shift: num_shift,
981 denom_shift: den_shift,
982 bar_thickness: rule,
983 numer_scale: numer_ratio,
984 denom_scale: denom_ratio,
985 },
986 color: options.color,
987 }
988}
989
990fn layout_supsub(
995 base: Option<&ParseNode>,
996 sup: Option<&ParseNode>,
997 sub: Option<&ParseNode>,
998 options: &LayoutOptions,
999 inherited_font: Option<FontId>,
1000) -> LayoutBox {
1001 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
1002 Some(fid) => layout_with_font(n, fid, opts),
1003 None => layout_node(n, opts),
1004 };
1005
1006 let horiz_brace_over = matches!(base, Some(ParseNode::HorizBrace { is_over: true, .. }));
1007 let horiz_brace_under = matches!(base, Some(ParseNode::HorizBrace { is_over: false, .. }));
1008 let center_scripts = horiz_brace_over || horiz_brace_under;
1009
1010 let base_box = base
1011 .map(|b| layout_child(b, options))
1012 .unwrap_or_else(LayoutBox::new_empty);
1013
1014 let is_char_box = base.is_some_and(is_character_box);
1015 let metrics = options.metrics();
1016 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
1020
1021 let sup_style = options.style.superscript();
1022 let sub_style = options.style.subscript();
1023
1024 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1025 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1026
1027 let sup_box = sup.map(|s| {
1028 let sup_opts = options.with_style(sup_style);
1029 layout_child(s, &sup_opts)
1030 });
1031
1032 let sub_box = sub.map(|s| {
1033 let sub_opts = options.with_style(sub_style);
1034 layout_child(s, &sub_opts)
1035 });
1036
1037 let sup_height_scaled = sup_box
1038 .as_ref()
1039 .map(|b| b.height * sup_ratio)
1040 .unwrap_or(0.0);
1041 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
1042 let sub_height_scaled = sub_box
1043 .as_ref()
1044 .map(|b| b.height * sub_ratio)
1045 .unwrap_or(0.0);
1046 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
1047
1048 let sup_style_metrics = get_global_metrics(sup_style.size_index());
1050 let sub_style_metrics = get_global_metrics(sub_style.size_index());
1051
1052 let mut sup_shift = if !is_char_box && sup_box.is_some() {
1055 base_box.height - sup_style_metrics.sup_drop * sup_ratio
1056 } else {
1057 0.0
1058 };
1059
1060 let mut sub_shift = if !is_char_box && sub_box.is_some() {
1061 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
1062 } else {
1063 0.0
1064 };
1065
1066 let min_sup_shift = if options.style.is_cramped() {
1067 metrics.sup3
1068 } else if options.style.is_display() {
1069 metrics.sup1
1070 } else {
1071 metrics.sup2
1072 };
1073
1074 if sup_box.is_some() && sub_box.is_some() {
1075 sup_shift = sup_shift
1077 .max(min_sup_shift)
1078 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1079 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
1082 let max_width = 4.0 * rule_width;
1083 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
1084 if gap < max_width {
1085 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
1086 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
1087 if psi > 0.0 {
1088 sup_shift += psi;
1089 sub_shift -= psi;
1090 }
1091 }
1092 } else if sub_box.is_some() {
1093 sub_shift = sub_shift
1095 .max(metrics.sub1)
1096 .max(sub_height_scaled - 0.8 * metrics.x_height);
1097 } else if sup_box.is_some() {
1098 sup_shift = sup_shift
1100 .max(min_sup_shift)
1101 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1102 }
1103
1104 if horiz_brace_over && sup_box.is_some() {
1108 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
1109 }
1110 if horiz_brace_under && sub_box.is_some() {
1111 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
1112 }
1113
1114 let italic_correction = 0.0;
1117
1118 let sub_h_kern = if sub_box.is_some() && !center_scripts {
1121 -glyph_italic(&base_box)
1122 } else {
1123 0.0
1124 };
1125
1126 let mut height = base_box.height;
1128 let mut depth = base_box.depth;
1129 let mut total_width = base_box.width;
1130
1131 if let Some(ref sup_b) = sup_box {
1132 height = height.max(sup_shift + sup_height_scaled);
1133 if center_scripts {
1134 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1135 } else {
1136 total_width = total_width
1137 .max(base_box.width + italic_correction + sup_b.width * sup_ratio + script_space);
1138 }
1139 }
1140 if let Some(ref sub_b) = sub_box {
1141 depth = depth.max(sub_shift + sub_depth_scaled);
1142 if center_scripts {
1143 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1144 } else {
1145 total_width = total_width
1146 .max(base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space);
1147 }
1148 }
1149
1150 LayoutBox {
1151 width: total_width,
1152 height,
1153 depth,
1154 content: BoxContent::SupSub {
1155 base: Box::new(base_box),
1156 sup: sup_box.map(Box::new),
1157 sub: sub_box.map(Box::new),
1158 sup_shift,
1159 sub_shift,
1160 sup_scale: sup_ratio,
1161 sub_scale: sub_ratio,
1162 center_scripts,
1163 italic_correction,
1164 sub_h_kern,
1165 },
1166 color: options.color,
1167 }
1168}
1169
1170fn layout_radical(
1175 body: &ParseNode,
1176 index: Option<&ParseNode>,
1177 options: &LayoutOptions,
1178) -> LayoutBox {
1179 let cramped = options.style.cramped();
1180 let cramped_opts = options.with_style(cramped);
1181 let mut body_box = layout_node(body, &cramped_opts);
1182
1183 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1185 body_box.height *= body_ratio;
1186 body_box.depth *= body_ratio;
1187 body_box.width *= body_ratio;
1188
1189 if body_box.height == 0.0 {
1191 body_box.height = options.metrics().x_height;
1192 }
1193
1194 let metrics = options.metrics();
1195 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1200 metrics.x_height
1201 } else {
1202 theta
1203 };
1204
1205 let mut line_clearance = theta + phi / 4.0;
1206
1207 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1209
1210 let tex_height = select_surd_height(min_delim_height);
1213 let rule_width = theta;
1214 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1215 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1216 .map(|m| m.width)
1217 .unwrap_or(0.833);
1218
1219 let delim_depth = tex_height - rule_width;
1221 if delim_depth > body_box.height + body_box.depth + line_clearance {
1222 line_clearance = (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1223 }
1224
1225 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1226
1227 let height = tex_height + rule_width - img_shift;
1230 let depth = if img_shift > body_box.depth {
1231 img_shift
1232 } else {
1233 body_box.depth
1234 };
1235
1236 const INDEX_KERN: f64 = 0.05;
1238 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1239 let root_style = options.style.superscript().superscript();
1240 let root_opts = options.with_style(root_style);
1241 let idx = layout_node(index_node, &root_opts);
1242 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1243 let offset = idx.width * index_ratio + INDEX_KERN;
1244 (Some(Box::new(idx)), offset, index_ratio)
1245 } else {
1246 (None, 0.0, 1.0)
1247 };
1248
1249 let width = index_offset + advance_width + body_box.width;
1250
1251 LayoutBox {
1252 width,
1253 height,
1254 depth,
1255 content: BoxContent::Radical {
1256 body: Box::new(body_box),
1257 index: index_box,
1258 index_offset,
1259 index_scale,
1260 rule_thickness: rule_width,
1261 inner_height: tex_height,
1262 },
1263 color: options.color,
1264 }
1265}
1266
1267fn select_surd_height(min_height: f64) -> f64 {
1270 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1271 for &h in &SURD_HEIGHTS {
1272 if h >= min_height {
1273 return h;
1274 }
1275 }
1276 SURD_HEIGHTS[4].max(min_height)
1278}
1279
1280const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1285
1286fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1288 match base {
1289 ParseNode::Op {
1290 limits,
1291 always_handle_sup_sub,
1292 ..
1293 } => *limits && (options.style.is_display() || always_handle_sup_sub.unwrap_or(false)),
1294 ParseNode::OperatorName {
1295 always_handle_sup_sub,
1296 limits,
1297 ..
1298 } => *always_handle_sup_sub && (options.style.is_display() || *limits),
1299 _ => false,
1300 }
1301}
1302
1303fn layout_op(
1309 name: Option<&str>,
1310 symbol: bool,
1311 body: Option<&[ParseNode]>,
1312 _limits: bool,
1313 suppress_base_shift: bool,
1314 options: &LayoutOptions,
1315) -> LayoutBox {
1316 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1317
1318 if symbol && !suppress_base_shift {
1320 let axis = options.metrics().axis_height;
1321 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1322 if shift.abs() > 0.001 {
1323 base_box.height -= shift;
1324 base_box.depth += shift;
1325 }
1326 }
1327
1328 if !suppress_base_shift && !symbol && body.is_some() {
1333 let axis = options.metrics().axis_height;
1334 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1335 if delta.abs() > 0.001 {
1336 let w = base_box.width;
1337 let raise = -delta;
1339 base_box = LayoutBox {
1340 width: w,
1341 height: (base_box.height + raise).max(0.0),
1342 depth: (base_box.depth - raise).max(0.0),
1343 content: BoxContent::RaiseBox {
1344 body: Box::new(base_box),
1345 shift: raise,
1346 },
1347 color: options.color,
1348 };
1349 }
1350 }
1351
1352 base_box
1353}
1354
1355fn build_op_base(
1358 name: Option<&str>,
1359 symbol: bool,
1360 body: Option<&[ParseNode]>,
1361 options: &LayoutOptions,
1362) -> (LayoutBox, f64) {
1363 if symbol {
1364 let large = options.style.is_display() && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1365 let font_id = if large {
1366 FontId::Size2Regular
1367 } else {
1368 FontId::Size1Regular
1369 };
1370
1371 let op_name = name.unwrap_or("");
1372 let ch = resolve_op_char(op_name);
1373 let char_code = ch as u32;
1374
1375 let metrics = get_char_metrics(font_id, char_code);
1376 let (width, height, depth, italic) = match metrics {
1377 Some(m) => (m.width, m.height, m.depth, m.italic),
1378 None => (1.0, 0.75, 0.25, 0.0),
1379 };
1380 let width_with_italic = width + italic;
1383
1384 let base = LayoutBox {
1385 width: width_with_italic,
1386 height,
1387 depth,
1388 content: BoxContent::Glyph { font_id, char_code },
1389 color: options.color,
1390 };
1391
1392 if op_name == "\\oiint" || op_name == "\\oiiint" {
1395 let w = base.width;
1396 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1397 let overlay_box = LayoutBox {
1398 width: w,
1399 height: base.height,
1400 depth: base.depth,
1401 content: BoxContent::SvgPath {
1402 commands: ellipse_commands,
1403 fill: false,
1404 },
1405 color: options.color,
1406 };
1407 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1408 return (with_overlay, italic);
1409 }
1410
1411 (base, italic)
1412 } else if let Some(body_nodes) = body {
1413 let base = layout_expression(body_nodes, options, true);
1414 (base, 0.0)
1415 } else {
1416 let base = layout_op_text(name.unwrap_or(""), options);
1417 (base, 0.0)
1418 }
1419}
1420
1421fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1423 let text = name.strip_prefix('\\').unwrap_or(name);
1424 let mut children = Vec::new();
1425 for ch in text.chars() {
1426 let char_code = ch as u32;
1427 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1428 let (width, height, depth) = match metrics {
1429 Some(m) => (m.width, m.height, m.depth),
1430 None => (0.5, 0.43, 0.0),
1431 };
1432 children.push(LayoutBox {
1433 width,
1434 height,
1435 depth,
1436 content: BoxContent::Glyph {
1437 font_id: FontId::MainRegular,
1438 char_code,
1439 },
1440 color: options.color,
1441 });
1442 }
1443 make_hbox(children)
1444}
1445
1446fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1448 let metrics = options.metrics();
1449 (base.height - base.depth) / 2.0 - metrics.axis_height
1450}
1451
1452fn resolve_op_char(name: &str) -> char {
1454 match name {
1457 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1460 }
1461 let font_mode = ratex_font::Mode::Math;
1462 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1463 if let Some(cp) = info.codepoint {
1464 return cp;
1465 }
1466 }
1467 name.chars().next().unwrap_or('?')
1468}
1469
1470fn layout_op_with_limits(
1472 base_node: &ParseNode,
1473 sup_node: Option<&ParseNode>,
1474 sub_node: Option<&ParseNode>,
1475 options: &LayoutOptions,
1476) -> LayoutBox {
1477 let (name, symbol, body, suppress_base_shift) = match base_node {
1478 ParseNode::Op {
1479 name,
1480 symbol,
1481 body,
1482 suppress_base_shift,
1483 ..
1484 } => (
1485 name.as_deref(),
1486 *symbol,
1487 body.as_deref(),
1488 suppress_base_shift.unwrap_or(false),
1489 ),
1490 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1491 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1492 };
1493
1494 let legacy_limit_kern_padding = !suppress_base_shift;
1496
1497 let (base_box, slant) = build_op_base(name, symbol, body, options);
1498 let base_shift = if symbol && !suppress_base_shift {
1500 compute_op_base_shift(&base_box, options)
1501 } else {
1502 0.0
1503 };
1504
1505 layout_op_limits_inner(
1506 &base_box,
1507 sup_node,
1508 sub_node,
1509 slant,
1510 base_shift,
1511 legacy_limit_kern_padding,
1512 options,
1513 )
1514}
1515
1516fn layout_op_limits_inner(
1521 base: &LayoutBox,
1522 sup_node: Option<&ParseNode>,
1523 sub_node: Option<&ParseNode>,
1524 slant: f64,
1525 base_shift: f64,
1526 legacy_limit_kern_padding: bool,
1527 options: &LayoutOptions,
1528) -> LayoutBox {
1529 let metrics = options.metrics();
1530 let sup_style = options.style.superscript();
1531 let sub_style = options.style.subscript();
1532
1533 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1534 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1535
1536 let extra_kern = if legacy_limit_kern_padding {
1537 0.08_f64
1538 } else {
1539 0.0_f64
1540 };
1541
1542 let sup_data = sup_node.map(|s| {
1543 let sup_opts = options.with_style(sup_style);
1544 let elem = layout_node(s, &sup_opts);
1545 let d = if legacy_limit_kern_padding {
1549 elem.depth * sup_ratio
1550 } else {
1551 elem.depth
1552 };
1553 let kern =
1554 (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1555 (elem, kern)
1556 });
1557
1558 let sub_data = sub_node.map(|s| {
1559 let sub_opts = options.with_style(sub_style);
1560 let elem = layout_node(s, &sub_opts);
1561 let h = if legacy_limit_kern_padding {
1562 elem.height * sub_ratio
1563 } else {
1564 elem.height
1565 };
1566 let kern =
1567 (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1568 (elem, kern)
1569 });
1570
1571 let sp5 = metrics.big_op_spacing5;
1572
1573 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1574 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1575 let sup_h = sup_elem.height * sup_ratio;
1578 let sup_d = sup_elem.depth * sup_ratio;
1579 let sub_h = sub_elem.height * sub_ratio;
1580 let sub_d = sub_elem.depth * sub_ratio;
1581
1582 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1583
1584 let height = bottom + base.height - base_shift + sup_kern + sup_h + sup_d + sp5
1585 - (base.height + base.depth);
1586
1587 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1588 let total_d = bottom;
1589
1590 let w = base
1591 .width
1592 .max(sup_elem.width * sup_ratio)
1593 .max(sub_elem.width * sub_ratio);
1594 let _ = height; (total_h, total_d, w)
1596 }
1597 (None, Some((sub_elem, sub_kern))) => {
1598 let sub_h = sub_elem.height * sub_ratio;
1601 let sub_d = sub_elem.depth * sub_ratio;
1602
1603 let total_h = base.height - base_shift;
1604 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1605
1606 let w = base.width.max(sub_elem.width * sub_ratio);
1607 (total_h, total_d, w)
1608 }
1609 (Some((sup_elem, sup_kern)), None) => {
1610 let sup_h = sup_elem.height * sup_ratio;
1613 let sup_d = sup_elem.depth * sup_ratio;
1614
1615 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1616 let total_d = base.depth + base_shift;
1617
1618 let w = base.width.max(sup_elem.width * sup_ratio);
1619 (total_h, total_d, w)
1620 }
1621 (None, None) => {
1622 return base.clone();
1623 }
1624 };
1625
1626 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1627 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1628
1629 LayoutBox {
1630 width: total_width,
1631 height: total_height,
1632 depth: total_depth,
1633 content: BoxContent::OpLimits {
1634 base: Box::new(base.clone()),
1635 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1636 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1637 base_shift,
1638 sup_kern: sup_kern_val,
1639 sub_kern: sub_kern_val,
1640 slant,
1641 sup_scale: sup_ratio,
1642 sub_scale: sub_ratio,
1643 },
1644 color: options.color,
1645 }
1646}
1647
1648fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1650 let mut children = Vec::new();
1651 for node in body {
1652 match node {
1653 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1654 let ch = text.chars().next().unwrap_or('?');
1655 let char_code = ch as u32;
1656 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1657 let (width, height, depth) = match metrics {
1658 Some(m) => (m.width, m.height, m.depth),
1659 None => (0.5, 0.43, 0.0),
1660 };
1661 children.push(LayoutBox {
1662 width,
1663 height,
1664 depth,
1665 content: BoxContent::Glyph {
1666 font_id: FontId::MainRegular,
1667 char_code,
1668 },
1669 color: options.color,
1670 });
1671 }
1672 _ => {
1673 children.push(layout_node(node, options));
1674 }
1675 }
1676 }
1677 make_hbox(children)
1678}
1679
1680const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1686
1687fn glyph_italic(lb: &LayoutBox) -> f64 {
1691 match &lb.content {
1692 BoxContent::Glyph { font_id, char_code } => get_char_metrics(*font_id, *char_code)
1693 .map(|m| m.italic)
1694 .unwrap_or(0.0),
1695 BoxContent::HBox(children) => children.last().map(glyph_italic).unwrap_or(0.0),
1696 _ => 0.0,
1697 }
1698}
1699
1700fn accent_ordgroup_len(base: &ParseNode) -> usize {
1705 match base {
1706 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1707 _ => 1,
1708 }
1709}
1710
1711fn glyph_skew(lb: &LayoutBox) -> f64 {
1712 match &lb.content {
1713 BoxContent::Glyph { font_id, char_code } => get_char_metrics(*font_id, *char_code)
1714 .map(|m| m.skew)
1715 .unwrap_or(0.0),
1716 BoxContent::HBox(children) => children.last().map(glyph_skew).unwrap_or(0.0),
1717 _ => 0.0,
1718 }
1719}
1720
1721fn layout_accent(
1722 label: &str,
1723 base: &ParseNode,
1724 is_stretchy: bool,
1725 is_shifty: bool,
1726 is_below: bool,
1727 options: &LayoutOptions,
1728) -> LayoutBox {
1729 let body_box = layout_node(base, options);
1730 let base_w = body_box.width.max(0.5);
1731
1732 if label == "\\textcircled" {
1734 return layout_textcircled(body_box, options);
1735 }
1736
1737 let accent_target_w = base_w;
1741 if let Some((commands, w, h, fill)) =
1742 crate::katex_svg::katex_accent_path(label, accent_target_w, accent_ordgroup_len(base))
1743 {
1744 let accent_box = LayoutBox {
1746 width: w,
1747 height: 0.0,
1748 depth: h,
1749 content: BoxContent::SvgPath { commands, fill },
1750 color: options.color,
1751 };
1752 let gap = 0.065;
1757 let under_gap_em = if is_below && label == "\\utilde" {
1758 0.12
1759 } else {
1760 0.0
1761 };
1762 let clearance = if is_below {
1763 body_box.height + body_box.depth + gap
1764 } else if label == "\\vec" {
1765 (body_box.height - options.metrics().x_height).max(0.0)
1768 } else {
1769 body_box.height + gap
1770 };
1771 let (height, depth) = if is_below {
1772 (body_box.height, body_box.depth + h + gap + under_gap_em)
1773 } else if label == "\\vec" {
1774 (clearance + h, body_box.depth)
1776 } else {
1777 (body_box.height + gap + h, body_box.depth)
1778 };
1779 let vec_skew = if label == "\\vec" {
1780 (if is_shifty {
1781 glyph_skew(&body_box)
1782 } else {
1783 0.0
1784 }) + VEC_SKEW_EXTRA_RIGHT_EM
1785 } else {
1786 0.0
1787 };
1788 return LayoutBox {
1789 width: body_box.width,
1790 height,
1791 depth,
1792 content: BoxContent::Accent {
1793 base: Box::new(body_box),
1794 accent: Box::new(accent_box),
1795 clearance,
1796 skew: vec_skew,
1797 is_below,
1798 under_gap_em,
1799 },
1800 color: options.color,
1801 };
1802 }
1803
1804 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1806
1807 let accent_box = if use_arrow_path {
1808 let (commands, arrow_h, fill_arrow) =
1809 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1810 Some((c, h)) => (c, h, true),
1811 None => {
1812 let h = 0.3_f64;
1813 let c = stretchy_accent_path(label, base_w, h);
1814 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1815 (c, h, fill)
1816 }
1817 };
1818 LayoutBox {
1819 width: base_w,
1820 height: arrow_h / 2.0,
1821 depth: arrow_h / 2.0,
1822 content: BoxContent::SvgPath {
1823 commands,
1824 fill: fill_arrow,
1825 },
1826 color: options.color,
1827 }
1828 } else {
1829 let accent_char = {
1831 let ch = resolve_symbol_char(label, Mode::Text);
1832 if ch == label.chars().next().unwrap_or('?') {
1833 resolve_symbol_char(label, Mode::Math)
1836 } else {
1837 ch
1838 }
1839 };
1840 let accent_code = accent_char as u32;
1841 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1842 let (accent_w, accent_h, accent_d) = match accent_metrics {
1843 Some(m) => (m.width, m.height, m.depth),
1844 None => (body_box.width, 0.25, 0.0),
1845 };
1846 LayoutBox {
1847 width: accent_w,
1848 height: accent_h,
1849 depth: accent_d,
1850 content: BoxContent::Glyph {
1851 font_id: FontId::MainRegular,
1852 char_code: accent_code,
1853 },
1854 color: options.color,
1855 }
1856 };
1857
1858 let skew = if use_arrow_path {
1859 0.0
1860 } else if is_shifty {
1861 glyph_skew(&body_box)
1864 } else {
1865 0.0
1866 };
1867
1868 let gap = if use_arrow_path {
1877 if label == "\\Overrightarrow" {
1878 0.21
1879 } else {
1880 0.26
1881 }
1882 } else {
1883 0.0
1884 };
1885
1886 let clearance = if is_below {
1887 body_box.height + body_box.depth + accent_box.depth + gap
1888 } else if use_arrow_path {
1889 body_box.height + gap
1890 } else {
1891 let base_clearance = match &body_box.content {
1898 BoxContent::Accent {
1899 clearance: inner_cl,
1900 is_below,
1901 accent: inner_accent,
1902 ..
1903 } if !is_below => {
1904 if inner_accent.height <= 0.001 {
1908 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1914 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1915 katex_pos + correction
1916 } else {
1917 if label == "\\bar" || label == "\\=" {
1923 body_box.height
1924 } else {
1925 let inner_visual_top = inner_cl + 0.35_f64.min(inner_accent.height);
1930 let h_for_kern = if body_box.height > inner_visual_top + 0.002 {
1931 inner_visual_top
1932 } else {
1933 body_box.height
1934 };
1935 let katex_pos = (h_for_kern - options.metrics().x_height).max(0.0);
1936 let correction =
1937 (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1938 katex_pos + correction
1939 }
1940 }
1941 }
1942 _ => {
1943 if label == "\\bar" || label == "\\=" {
1956 body_box.height
1957 } else {
1958 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1959 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1960 katex_pos + correction
1961 }
1962 }
1963 };
1964 let base_clearance = base_clearance + accent_box.depth;
1969 if label == "\\bar" || label == "\\=" {
1970 (base_clearance - 0.12).max(0.0)
1971 } else {
1972 base_clearance
1973 }
1974 };
1975
1976 let (height, depth) = if is_below {
1977 (
1978 body_box.height,
1979 body_box.depth + accent_box.height + accent_box.depth + gap,
1980 )
1981 } else if use_arrow_path {
1982 (body_box.height + gap + accent_box.height, body_box.depth)
1983 } else {
1984 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1991 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1992 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1993 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1994 } else {
1995 body_box.height.max(accent_visual_top)
1996 };
1997 (h, body_box.depth)
1998 };
1999
2000 LayoutBox {
2001 width: body_box.width,
2002 height,
2003 depth,
2004 content: BoxContent::Accent {
2005 base: Box::new(body_box),
2006 accent: Box::new(accent_box),
2007 clearance,
2008 skew,
2009 is_below,
2010 under_gap_em: 0.0,
2011 },
2012 color: options.color,
2013 }
2014}
2015
2016fn node_contains_middle(node: &ParseNode, style: MathStyle) -> bool {
2024 match node {
2025 ParseNode::Middle { .. } => true,
2026 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
2027 body.iter().any(|n| node_contains_middle(n, style))
2028 }
2029 ParseNode::SupSub { base, sup, sub, .. } => {
2030 base.as_deref()
2031 .is_some_and(|n| node_contains_middle(n, style))
2032 || sup
2033 .as_deref()
2034 .is_some_and(|n| node_contains_middle(n, style.superscript()))
2035 || sub
2036 .as_deref()
2037 .is_some_and(|n| node_contains_middle(n, style.subscript()))
2038 }
2039 ParseNode::GenFrac { numer, denom, .. } => {
2040 node_contains_middle(numer, style.numerator())
2041 || node_contains_middle(denom, style.denominator())
2042 }
2043 ParseNode::Sqrt { body, index, .. } => {
2044 node_contains_middle(body, style.cramped())
2045 || index
2046 .as_deref()
2047 .is_some_and(|n| node_contains_middle(n, style.superscript().superscript()))
2048 }
2049 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
2050 node_contains_middle(base, style)
2051 }
2052 ParseNode::Op { body, .. } => body
2053 .as_ref()
2054 .is_some_and(|b| b.iter().any(|n| node_contains_middle(n, style))),
2055 ParseNode::LeftRight { .. } => false,
2056 ParseNode::OperatorName { body, .. } => body.iter().any(|n| node_contains_middle(n, style)),
2057 ParseNode::Font { body, .. } => node_contains_middle(body, style),
2058 ParseNode::Text { body, .. } | ParseNode::Sizing { body, .. } => {
2059 body.iter().any(|n| node_contains_middle(n, style.text()))
2060 }
2061 ParseNode::Color { body, .. } => body.iter().any(|n| node_contains_middle(n, style)),
2062 ParseNode::Styling {
2063 style: node_style,
2064 body,
2065 ..
2066 } => {
2067 let new_style = style_str_to_math_style(node_style);
2068 body.iter().any(|n| node_contains_middle(n, new_style))
2069 }
2070 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
2071 node_contains_middle(body, style)
2072 }
2073 ParseNode::Phantom { body, .. } => body.iter().any(|n| node_contains_middle(n, style)),
2074 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
2075 node_contains_middle(body, style)
2076 }
2077 ParseNode::Array { body, .. } => body
2078 .iter()
2079 .any(|row| row.iter().any(|n| node_contains_middle(n, style))),
2080 ParseNode::Enclose { body, .. }
2081 | ParseNode::Lap { body, .. }
2082 | ParseNode::RaiseBox { body, .. }
2083 | ParseNode::VCenter { body, .. } => node_contains_middle(body, style),
2084 ParseNode::Pmb { body, .. } => body.iter().any(|n| node_contains_middle(n, style)),
2085 ParseNode::XArrow { body, below, .. } => {
2086 node_contains_middle(body, style.superscript())
2087 || below
2088 .as_deref()
2089 .is_some_and(|n| node_contains_middle(n, style.subscript()))
2090 }
2091 ParseNode::CdArrow {
2092 label_above,
2093 label_below,
2094 ..
2095 } => {
2096 label_above
2097 .as_deref()
2098 .is_some_and(|n| node_contains_middle(n, style.superscript()))
2099 || label_below
2100 .as_deref()
2101 .is_some_and(|n| node_contains_middle(n, style.subscript()))
2102 }
2103 ParseNode::MathChoice {
2104 display,
2105 text,
2106 script,
2107 scriptscript,
2108 ..
2109 } => {
2110 let branch = match style {
2111 MathStyle::Display | MathStyle::DisplayCramped => display,
2112 MathStyle::Text | MathStyle::TextCramped => text,
2113 MathStyle::Script | MathStyle::ScriptCramped => script,
2114 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
2115 };
2116 branch.iter().any(|n| node_contains_middle(n, style))
2117 }
2118 ParseNode::HorizBrace { base, .. } => node_contains_middle(base, style),
2119 ParseNode::Href { body, .. } | ParseNode::Html { body, .. } => {
2120 body.iter().any(|n| node_contains_middle(n, style))
2121 }
2122 ParseNode::HtmlMathMl { html, .. } => html.iter().any(|n| node_contains_middle(n, style)),
2123 _ => false,
2124 }
2125}
2126
2127fn body_contains_middle(nodes: &[ParseNode], style: MathStyle) -> bool {
2130 nodes.iter().any(|n| node_contains_middle(n, style))
2131}
2132
2133fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
2136 let m = options.metrics();
2137 if options.style.is_display() {
2138 m.delim1
2139 } else if matches!(
2140 options.style,
2141 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
2142 ) {
2143 options.with_style(MathStyle::Script).metrics().delim2
2144 } else {
2145 m.delim2
2146 }
2147}
2148
2149fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2151 let metrics = options.metrics();
2152 let inner_height = inner.height;
2153 let inner_depth = inner.depth;
2154 let axis = metrics.axis_height;
2155 let max_dist = (inner_height - axis).max(inner_depth + axis);
2156 let delim_factor = 901.0;
2157 let delim_extend = 5.0 / metrics.pt_per_em;
2158 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2159 from_formula.max(inner_height + inner_depth)
2161}
2162
2163fn layout_left_right(
2164 body: &[ParseNode],
2165 left_delim: &str,
2166 right_delim: &str,
2167 options: &LayoutOptions,
2168) -> LayoutBox {
2169 let (inner, total_height) = if body_contains_middle(body, options.style) {
2170 let opts_first = LayoutOptions {
2172 leftright_delim_height: None,
2173 ..options.clone()
2174 };
2175 let inner_first = layout_expression(body, &opts_first, true);
2176 let total_height = left_right_delim_total_height(&inner_first, options);
2177 let opts_second = LayoutOptions {
2179 leftright_delim_height: Some(total_height),
2180 ..options.clone()
2181 };
2182 let inner_second = layout_expression(body, &opts_second, true);
2183 (inner_second, total_height)
2184 } else {
2185 let inner = layout_expression(body, options, true);
2186 let total_height = left_right_delim_total_height(&inner, options);
2187 (inner, total_height)
2188 };
2189
2190 let inner_height = inner.height;
2191 let inner_depth = inner.depth;
2192
2193 let left_box = make_stretchy_delim(left_delim, total_height, options);
2194 let right_box = make_stretchy_delim(right_delim, total_height, options);
2195
2196 let width = left_box.width + inner.width + right_box.width;
2197 let height = left_box.height.max(right_box.height).max(inner_height);
2198 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2199
2200 LayoutBox {
2201 width,
2202 height,
2203 depth,
2204 content: BoxContent::LeftRight {
2205 left: Box::new(left_box),
2206 right: Box::new(right_box),
2207 inner: Box::new(inner),
2208 },
2209 color: options.color,
2210 }
2211}
2212
2213const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2214 FontId::MainRegular,
2215 FontId::Size1Regular,
2216 FontId::Size2Regular,
2217 FontId::Size3Regular,
2218 FontId::Size4Regular,
2219];
2220
2221fn normalize_delim(delim: &str) -> &str {
2223 match delim {
2224 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2225 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2226 _ => delim,
2227 }
2228}
2229
2230fn is_vert_delim(delim: &str) -> bool {
2232 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2233}
2234
2235fn is_double_vert_delim(delim: &str) -> bool {
2237 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2238}
2239
2240fn vert_repeat_piece_height(is_double: bool) -> f64 {
2242 let code = if is_double { 8741_u32 } else { 8739 };
2243 get_char_metrics(FontId::Size1Regular, code)
2244 .map(|m| m.height + m.depth)
2245 .unwrap_or(0.5)
2246}
2247
2248fn katex_vert_real_height(requested_total: f64, is_double: bool, is_sized_delim: bool) -> f64 {
2250 let piece = vert_repeat_piece_height(is_double);
2251 let min_h = 2.0 * piece;
2252 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2253 let mut h = min_h + repeat_count * piece;
2254 if is_sized_delim && !is_double {
2259 h *= if requested_total < 3.0 { 1.2 } else { 1.135 };
2260 }
2261 h
2262}
2263
2264fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2266 let neg = -mid_th;
2267 if !is_double {
2268 format!(
2269 "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"
2270 )
2271 } else {
2272 format!(
2273 "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"
2274 )
2275 }
2276}
2277
2278fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2279 let s = 0.001_f64;
2280 cmds.iter()
2281 .map(|c| match *c {
2282 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * s, y: y * s },
2283 PathCommand::LineTo { x, y } => PathCommand::LineTo { x: x * s, y: y * s },
2284 PathCommand::CubicTo {
2285 x1,
2286 y1,
2287 x2,
2288 y2,
2289 x,
2290 y,
2291 } => PathCommand::CubicTo {
2292 x1: x1 * s,
2293 y1: y1 * s,
2294 x2: x2 * s,
2295 y2: y2 * s,
2296 x: x * s,
2297 y: y * s,
2298 },
2299 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2300 x1: x1 * s,
2301 y1: y1 * s,
2302 x: x * s,
2303 y: y * s,
2304 },
2305 PathCommand::Close => PathCommand::Close,
2306 })
2307 .collect()
2308}
2309
2310fn map_vert_path_y_to_baseline(
2312 cmds: Vec<PathCommand>,
2313 height: f64,
2314 depth: f64,
2315 view_box_height: i64,
2316) -> Vec<PathCommand> {
2317 let span_em = view_box_height as f64 / 1000.0;
2318 let total = height + depth;
2319 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2320 cmds.into_iter()
2321 .map(|c| match c {
2322 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2323 x,
2324 y: -height + y * scale_y,
2325 },
2326 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2327 x,
2328 y: -height + y * scale_y,
2329 },
2330 PathCommand::CubicTo {
2331 x1,
2332 y1,
2333 x2,
2334 y2,
2335 x,
2336 y,
2337 } => PathCommand::CubicTo {
2338 x1,
2339 y1: -height + y1 * scale_y,
2340 x2,
2341 y2: -height + y2 * scale_y,
2342 x,
2343 y: -height + y * scale_y,
2344 },
2345 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2346 x1,
2347 y1: -height + y1 * scale_y,
2348 x,
2349 y: -height + y * scale_y,
2350 },
2351 PathCommand::Close => PathCommand::Close,
2352 })
2353 .collect()
2354}
2355
2356fn make_shortmid_box(options: &LayoutOptions) -> LayoutBox {
2357 let total = 0.675_f64;
2358 let axis = options.metrics().axis_height;
2359 let depth = (total / 2.0 - axis).max(0.0);
2360 let height = total - depth;
2361 let raw = parse_svg_path_data(&tall_vert_svg_path_data(0, false));
2362 let scaled = scale_svg_path_to_em(&raw);
2363 let commands = map_vert_path_y_to_baseline(scaled, height, depth, 1170);
2364 LayoutBox {
2365 width: 0.22222,
2366 height,
2367 depth,
2368 content: BoxContent::SvgPath {
2369 commands,
2370 fill: true,
2371 },
2372 color: options.color,
2373 }
2374}
2375
2376fn make_textunderscore_box(options: &LayoutOptions) -> LayoutBox {
2377 LayoutBox {
2378 width: 0.65,
2379 height: 0.0,
2380 depth: 0.31,
2381 content: BoxContent::Rule {
2382 thickness: 0.08,
2383 raise: -0.31,
2384 },
2385 color: options.color,
2386 }
2387}
2388
2389fn make_corresponds_symbol(options: &LayoutOptions) -> LayoutBox {
2390 let width = 1.16;
2391 let height = 0.55;
2392 let depth = 0.24;
2393 let rule_thickness = 0.055;
2394 let arc = LayoutBox {
2395 width,
2396 height: 0.36,
2397 depth: 0.0,
2398 content: BoxContent::SvgPath {
2399 commands: vec![
2400 PathCommand::MoveTo { x: 0.02, y: 0.0 },
2401 PathCommand::CubicTo {
2402 x1: 0.18,
2403 y1: -0.34,
2404 x2: 0.64,
2405 y2: -0.34,
2406 x: 0.80,
2407 y: 0.0,
2408 },
2409 ],
2410 fill: false,
2411 },
2412 color: options.color,
2413 };
2414
2415 LayoutBox {
2416 width,
2417 height,
2418 depth,
2419 content: BoxContent::ProofTree {
2420 children: vec![PlacedBox {
2421 box_: arc,
2422 x: 0.0,
2423 baseline_y: 0.36,
2424 }],
2425 rules: vec![
2426 ProofRule {
2427 x: 0.28,
2428 y: 0.48,
2429 width: 0.86,
2430 thickness: rule_thickness,
2431 dashed: false,
2432 },
2433 ProofRule {
2434 x: 0.28,
2435 y: 0.68,
2436 width: 0.86,
2437 thickness: rule_thickness,
2438 dashed: false,
2439 },
2440 ],
2441 },
2442 color: options.color,
2443 }
2444}
2445
2446fn make_measured_by_symbol(options: &LayoutOptions) -> LayoutBox {
2447 let width = 0.98;
2448 let height = 0.56;
2449 let depth = 0.30;
2450 let rule_thickness = 0.055;
2451 let m_metrics = get_char_metrics(FontId::MainRegular, 'm' as u32);
2452 let (m_width, m_height, m_depth) = m_metrics
2453 .map(|m| (m.width, m.height, m.depth))
2454 .unwrap_or((0.78, 0.43, 0.0));
2455 let m_scale = 0.61;
2456 let m_glyph = LayoutBox {
2457 width: m_width,
2458 height: m_height,
2459 depth: m_depth,
2460 content: BoxContent::Glyph {
2461 font_id: FontId::MainRegular,
2462 char_code: 'm' as u32,
2463 },
2464 color: options.color,
2465 };
2466 let m_box = LayoutBox {
2467 width: m_width * m_scale,
2468 height: m_height * m_scale,
2469 depth: m_depth * m_scale,
2470 content: BoxContent::Scaled {
2471 body: Box::new(m_glyph),
2472 child_scale: m_scale,
2473 },
2474 color: options.color,
2475 };
2476
2477 LayoutBox {
2478 width,
2479 height,
2480 depth,
2481 content: BoxContent::ProofTree {
2482 children: vec![PlacedBox {
2483 box_: m_box,
2484 x: (width - m_width * m_scale) / 2.0,
2485 baseline_y: 0.02 + m_height * m_scale,
2486 }],
2487 rules: vec![
2488 ProofRule {
2489 x: 0.06,
2490 y: 0.55,
2491 width: 0.86,
2492 thickness: rule_thickness,
2493 dashed: false,
2494 },
2495 ProofRule {
2496 x: 0.06,
2497 y: 0.78,
2498 width: 0.86,
2499 thickness: rule_thickness,
2500 dashed: false,
2501 },
2502 ],
2503 },
2504 color: options.color,
2505 }
2506}
2507
2508fn make_vert_delim_box(
2511 total_height: f64,
2512 is_double: bool,
2513 is_sized_delim: bool,
2514 options: &LayoutOptions,
2515) -> LayoutBox {
2516 let real_h = katex_vert_real_height(total_height, is_double, is_sized_delim);
2517 let axis = options.metrics().axis_height;
2518 let depth = (real_h / 2.0 - axis).max(0.0);
2519 let height = real_h - depth;
2520 let width = if is_double { 0.556 } else { 0.333 };
2521
2522 let piece = vert_repeat_piece_height(is_double);
2523 let mid_em = (real_h - 2.0 * piece).max(0.0);
2524 let mid_th = (mid_em * 1000.0).round() as i64;
2525 let view_box_height = (real_h * 1000.0).round() as i64;
2526
2527 let d = tall_vert_svg_path_data(mid_th, is_double);
2528 let raw = parse_svg_path_data(&d);
2529 let scaled = scale_svg_path_to_em(&raw);
2530 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2531
2532 LayoutBox {
2533 width,
2534 height,
2535 depth,
2536 content: BoxContent::SvgPath {
2537 commands,
2538 fill: true,
2539 },
2540 color: options.color,
2541 }
2542}
2543
2544fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2546 if delim == "." || delim.is_empty() {
2547 return LayoutBox::new_kern(0.0);
2548 }
2549
2550 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2555 return make_vert_delim_box(total_height, false, false, options);
2556 }
2557 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2558 return make_vert_delim_box(total_height, true, false, options);
2559 }
2560
2561 let delim = normalize_delim(delim);
2563
2564 let ch = resolve_symbol_char(delim, Mode::Math);
2565 let char_code = ch as u32;
2566
2567 let mut best_font = FontId::MainRegular;
2568 let mut best_w = 0.4;
2569 let mut best_h = 0.7;
2570 let mut best_d = 0.2;
2571
2572 for &font_id in &DELIM_FONT_SEQUENCE {
2573 if let Some(m) = get_char_metrics(font_id, char_code) {
2574 best_font = font_id;
2575 best_w = m.width;
2576 best_h = m.height;
2577 best_d = m.depth;
2578 if best_h + best_d >= total_height {
2579 break;
2580 }
2581 }
2582 }
2583
2584 let best_total = best_h + best_d;
2585 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2586 return stacked;
2587 }
2588
2589 LayoutBox {
2590 width: best_w,
2591 height: best_h,
2592 depth: best_d,
2593 content: BoxContent::Glyph {
2594 font_id: best_font,
2595 char_code,
2596 },
2597 color: options.color,
2598 }
2599}
2600
2601const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2603
2604fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2606 if delim == "." || delim.is_empty() {
2607 return LayoutBox::new_kern(0.0);
2608 }
2609
2610 if is_vert_delim(delim) {
2612 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2613 return make_vert_delim_box(total, false, true, options);
2614 }
2615 if is_double_vert_delim(delim) {
2616 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2617 return make_vert_delim_box(total, true, true, options);
2618 }
2619
2620 let delim = normalize_delim(delim);
2622
2623 let ch = resolve_symbol_char(delim, Mode::Math);
2624 let char_code = ch as u32;
2625
2626 let font_id = match size {
2627 1 => FontId::Size1Regular,
2628 2 => FontId::Size2Regular,
2629 3 => FontId::Size3Regular,
2630 4 => FontId::Size4Regular,
2631 _ => FontId::Size1Regular,
2632 };
2633
2634 let metrics = get_char_metrics(font_id, char_code);
2635 let (width, height, depth, actual_font) = match metrics {
2636 Some(m) => (m.width, m.height, m.depth, font_id),
2637 None => {
2638 let m = get_char_metrics(FontId::MainRegular, char_code);
2639 match m {
2640 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2641 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2642 }
2643 }
2644 };
2645
2646 LayoutBox {
2647 width,
2648 height,
2649 depth,
2650 content: BoxContent::Glyph {
2651 font_id: actual_font,
2652 char_code,
2653 },
2654 color: options.color,
2655 }
2656}
2657
2658#[allow(clippy::too_many_arguments)]
2663fn layout_array(
2664 body: &[Vec<ParseNode>],
2665 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2666 arraystretch: f64,
2667 add_jot: bool,
2668 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2669 hlines: &[Vec<bool>],
2670 col_sep_type: Option<&str>,
2671 hskip: bool,
2672 tags: Option<&[ArrayTag]>,
2673 _leqno: bool,
2674 options: &LayoutOptions,
2675) -> LayoutBox {
2676 let metrics = options.metrics();
2677 let pt = 1.0 / metrics.pt_per_em;
2678 let baselineskip = 12.0 * pt;
2679 let jot = 3.0 * pt;
2680 let arrayskip = arraystretch * baselineskip;
2681 let arstrut_h = 0.7 * arrayskip;
2682 let arstrut_d = 0.3 * arrayskip;
2683 const ALIGN_RELATION_MU: f64 = 3.0;
2686 let col_gap = match col_sep_type {
2687 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2688 Some("alignat") => 0.0,
2689 Some("small") => {
2690 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2693 / options.size_multiplier()
2694 }
2695 _ => 2.0 * 5.0 * pt, };
2697 let cell_options = match col_sep_type {
2698 Some("align") | Some("alignat") => LayoutOptions {
2699 align_relation_spacing: Some(ALIGN_RELATION_MU),
2700 ..options.clone()
2701 },
2702 _ => options.clone(),
2703 };
2704
2705 let num_rows = body.len();
2706 if num_rows == 0 {
2707 return LayoutBox::new_empty();
2708 }
2709
2710 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2711
2712 use ratex_parser::parse_node::AlignType;
2714 let col_aligns: Vec<u8> = {
2715 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2716 .map(|cs| {
2717 cs.iter()
2718 .filter(|s| matches!(s.align_type, AlignType::Align))
2719 .collect()
2720 })
2721 .unwrap_or_default();
2722 (0..num_cols)
2723 .map(|c| {
2724 align_specs
2725 .get(c)
2726 .and_then(|s| s.align.as_deref())
2727 .and_then(|a| a.bytes().next())
2728 .unwrap_or(b'c')
2729 })
2730 .collect()
2731 };
2732
2733 let col_separators: Vec<Option<bool>> = {
2736 let mut seps = vec![None; num_cols + 1];
2737 let mut align_count = 0usize;
2738 if let Some(cs) = cols {
2739 for spec in cs {
2740 match spec.align_type {
2741 AlignType::Align => align_count += 1,
2742 AlignType::Separator
2743 if spec.align.as_deref() == Some("|") && align_count <= num_cols =>
2744 {
2745 seps[align_count] = Some(false);
2746 }
2747 AlignType::Separator
2748 if spec.align.as_deref() == Some(":") && align_count <= num_cols =>
2749 {
2750 seps[align_count] = Some(true);
2751 }
2752 _ => {}
2753 }
2754 }
2755 }
2756 seps
2757 };
2758
2759 let rule_thickness = 0.4 * pt;
2760 let double_rule_sep = metrics.double_rule_sep;
2761
2762 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2764 let mut col_widths = vec![0.0_f64; num_cols];
2765 let mut row_heights = Vec::with_capacity(num_rows);
2766 let mut row_depths = Vec::with_capacity(num_rows);
2767
2768 for row in body {
2769 let mut row_boxes = Vec::with_capacity(num_cols);
2770 let mut rh = arstrut_h;
2771 let mut rd = arstrut_d;
2772
2773 for (c, cell) in row.iter().enumerate() {
2774 let cell_nodes = match cell {
2775 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2776 other => std::slice::from_ref(other),
2777 };
2778 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2779 rh = rh.max(cell_box.height);
2780 rd = rd.max(cell_box.depth);
2781 if c < num_cols {
2782 col_widths[c] = col_widths[c].max(cell_box.width);
2783 }
2784 row_boxes.push(cell_box);
2785 }
2786
2787 while row_boxes.len() < num_cols {
2789 row_boxes.push(LayoutBox::new_empty());
2790 }
2791
2792 if add_jot {
2793 rd += jot;
2794 }
2795
2796 row_heights.push(rh);
2797 row_depths.push(rd);
2798 cell_boxes.push(row_boxes);
2799 }
2800
2801 for (r, gap) in row_gaps.iter().enumerate() {
2803 if r < row_depths.len() {
2804 if let Some(m) = gap {
2805 let gap_em = measurement_to_em(m, options);
2806 if gap_em > 0.0 {
2807 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2808 }
2809 }
2810 }
2811 }
2812
2813 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2815 while hlines_before_row.len() < num_rows + 1 {
2816 hlines_before_row.push(vec![]);
2817 }
2818
2819 for r in 0..=num_rows {
2825 let n = hlines_before_row[r].len();
2826 if n > 1 {
2827 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2828 if r == 0 {
2829 if num_rows > 0 {
2830 row_heights[0] += extra;
2831 }
2832 } else {
2833 row_depths[r - 1] += extra;
2834 }
2835 }
2836 }
2837
2838 let mut total_height = 0.0;
2840 let mut row_positions = Vec::with_capacity(num_rows);
2841 for r in 0..num_rows {
2842 total_height += row_heights[r];
2843 row_positions.push(total_height);
2844 total_height += row_depths[r];
2845 }
2846
2847 let offset = total_height / 2.0 + metrics.axis_height;
2848
2849 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2851
2852 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2854 + col_gap * (num_cols.saturating_sub(1)) as f64
2855 + 2.0 * content_x_offset;
2856
2857 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2858 let mut tag_col_width = 0.0_f64;
2859 let text_opts = options.with_style(options.style.text());
2860 if let Some(tag_slice) = tags {
2861 if tag_slice.len() == num_rows {
2862 for (r, t) in tag_slice.iter().enumerate() {
2863 if let ArrayTag::Explicit(nodes) = t {
2864 if !nodes.is_empty() {
2865 let tb = layout_expression(nodes, &text_opts, true);
2866 tag_col_width = tag_col_width.max(tb.width);
2867 row_tag_boxes[r] = Some(tb);
2868 }
2869 }
2870 }
2871 }
2872 }
2873 let tag_gap_em = if tag_col_width > 0.0 {
2874 text_opts.metrics().quad
2875 } else {
2876 0.0
2877 };
2878 let tags_left = false;
2880
2881 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2882
2883 let height = offset;
2884 let depth = total_height - offset;
2885
2886 LayoutBox {
2887 width: total_width,
2888 height,
2889 depth,
2890 content: BoxContent::Array {
2891 cells: cell_boxes,
2892 col_widths: col_widths.clone(),
2893 col_aligns,
2894 row_heights: row_heights.clone(),
2895 row_depths: row_depths.clone(),
2896 col_gap,
2897 offset,
2898 content_x_offset,
2899 col_separators,
2900 hlines_before_row,
2901 rule_thickness,
2902 double_rule_sep,
2903 array_inner_width,
2904 tag_gap_em,
2905 tag_col_width,
2906 row_tags: row_tag_boxes,
2907 tags_left,
2908 },
2909 color: options.color,
2910 }
2911}
2912
2913fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2918 let multiplier = match size {
2920 1 => 0.5,
2921 2 => 0.6,
2922 3 => 0.7,
2923 4 => 0.8,
2924 5 => 0.9,
2925 6 => 1.0,
2926 7 => 1.2,
2927 8 => 1.44,
2928 9 => 1.728,
2929 10 => 2.074,
2930 11 => 2.488,
2931 _ => 1.0,
2932 };
2933
2934 let inner_opts = options.with_style(options.style.text());
2936 let inner = layout_expression(body, &inner_opts, true);
2937 let ratio = multiplier / options.size_multiplier();
2938 if (ratio - 1.0).abs() < 0.001 {
2939 inner
2940 } else {
2941 LayoutBox {
2942 width: inner.width * ratio,
2943 height: inner.height * ratio,
2944 depth: inner.depth * ratio,
2945 content: BoxContent::Scaled {
2946 body: Box::new(inner),
2947 child_scale: ratio,
2948 },
2949 color: options.color,
2950 }
2951 }
2952}
2953
2954#[derive(Default)]
2955struct HtmlStyle {
2956 color: Option<Color>,
2957 font_size_scale: Option<f64>,
2958 bold: bool,
2959 italic: bool,
2960 background_color: Option<Color>,
2961 underline: bool,
2962}
2963
2964fn layout_html(
2965 attributes: &HashMap<String, String>,
2966 body: &[ParseNode],
2967 options: &LayoutOptions,
2968) -> LayoutBox {
2969 let style = attributes
2970 .get("style")
2971 .map(|style| parse_html_style(style))
2972 .unwrap_or_default();
2973
2974 let body_options = match style.color {
2975 Some(color) => options.with_color(color),
2976 None => options.clone(),
2977 };
2978 let font_id = match (style.bold, style.italic) {
2979 (true, true) => Some(FontId::MainBoldItalic),
2980 (true, false) => Some(FontId::MainBold),
2981 (false, true) => Some(FontId::MainItalic),
2982 (false, false) => None,
2983 };
2984
2985 let body_node = ParseNode::OrdGroup {
2986 mode: body.first().map(ParseNode::mode).unwrap_or(Mode::Math),
2987 body: body.to_vec(),
2988 semisimple: None,
2989 loc: None,
2990 };
2991 let mut lbox = match font_id {
2992 Some(font_id) => layout_with_font(&body_node, font_id, &body_options),
2993 None => layout_expression(body, &body_options, true),
2994 };
2995
2996 if let Some(scale) = style.font_size_scale {
2997 if (scale - 1.0).abs() >= 0.001 {
2998 lbox = LayoutBox {
2999 width: lbox.width * scale,
3000 height: lbox.height * scale,
3001 depth: lbox.depth * scale,
3002 content: BoxContent::Scaled {
3003 body: Box::new(lbox),
3004 child_scale: scale,
3005 },
3006 color: body_options.color,
3007 };
3008 }
3009 }
3010
3011 if let Some(background_color) = style.background_color {
3012 lbox = LayoutBox {
3013 width: lbox.width,
3014 height: lbox.height,
3015 depth: lbox.depth,
3016 content: BoxContent::Framed {
3017 body: Box::new(lbox),
3018 padding: 0.0,
3019 border_thickness: 0.0,
3020 has_border: false,
3021 bg_color: Some(background_color),
3022 border_color: Color::BLACK,
3023 },
3024 color: body_options.color,
3025 };
3026 }
3027
3028 if style.underline {
3029 lbox = layout_underline_laid_out(lbox, options, body_options.color);
3030 }
3031
3032 if options.leftright_delim_height.is_none() && body_contains_middle(body, body_options.style) {
3033 let total = SIZE_TO_MAX_HEIGHT[1];
3034 let axis = body_options.metrics().axis_height;
3035 let height = total / 2.0 + axis;
3036 let depth = total - height;
3037 lbox.height = lbox.height.max(height);
3038 lbox.depth = lbox.depth.max(depth);
3039 }
3040
3041 lbox
3042}
3043
3044fn parse_html_style(style: &str) -> HtmlStyle {
3045 let mut parsed = HtmlStyle::default();
3046 for declaration in style.split(';') {
3047 let Some((property, value)) = declaration.split_once(':') else {
3048 continue;
3049 };
3050 let property = property.trim().to_ascii_lowercase();
3051 let value = value.trim();
3052 match property.as_str() {
3053 "color" => parsed.color = Color::parse(value),
3054 "font-size" => parsed.font_size_scale = parse_css_font_size(value),
3055 "font-weight" => parsed.bold = is_css_bold(value),
3056 "font-style" => parsed.italic = is_css_italic(value),
3057 "background" | "background-color" => parsed.background_color = Color::parse(value),
3058 "text-decoration" | "text-decoration-line" => {
3059 parsed.underline = value
3060 .split_whitespace()
3061 .any(|part| part.eq_ignore_ascii_case("underline"));
3062 }
3063 _ => {}
3064 }
3065 }
3066 parsed
3067}
3068
3069fn parse_css_font_size(value: &str) -> Option<f64> {
3070 let value = value.trim().to_ascii_lowercase().replace(' ', "");
3071 let parse_number = |s: &str| s.parse::<f64>().ok().filter(|n| n.is_finite() && *n > 0.0);
3072 if let Some(px) = value.strip_suffix("px") {
3073 parse_number(px).map(|n| n / 16.0)
3074 } else if let Some(em) = value
3075 .strip_suffix("em")
3076 .or_else(|| value.strip_suffix("rem"))
3077 {
3078 parse_number(em)
3079 } else if let Some(percent) = value.strip_suffix('%') {
3080 parse_number(percent).map(|n| n / 100.0)
3081 } else {
3082 None
3083 }
3084}
3085
3086fn is_css_bold(value: &str) -> bool {
3087 let value = value.trim();
3088 value.eq_ignore_ascii_case("bold")
3089 || value.eq_ignore_ascii_case("bolder")
3090 || value.parse::<u16>().is_ok_and(|weight| weight >= 600)
3091}
3092
3093fn is_css_italic(value: &str) -> bool {
3094 let value = value.trim();
3095 value.eq_ignore_ascii_case("italic") || value.eq_ignore_ascii_case("oblique")
3096}
3097
3098fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
3101 let metrics = options.metrics();
3102 let mut children = Vec::new();
3103 for c in body.chars() {
3104 let ch = if star && c == ' ' {
3105 '\u{2423}' } else {
3107 c
3108 };
3109 let code = ch as u32;
3110 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
3111 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
3112 None => match get_char_metrics(FontId::MainRegular, code) {
3113 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
3114 None => (FontId::TypewriterRegular, 0.5, metrics.x_height, 0.0),
3115 },
3116 };
3117 children.push(LayoutBox {
3118 width: w,
3119 height: h,
3120 depth: d,
3121 content: BoxContent::Glyph {
3122 font_id,
3123 char_code: code,
3124 },
3125 color: options.color,
3126 });
3127 }
3128 let mut hbox = make_hbox(children);
3129 hbox.color = options.color;
3130 hbox
3131}
3132
3133fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3141 let mut children = Vec::new();
3142 for node in body {
3143 match node {
3144 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
3145 children.push(layout_symbol(text, *mode, options));
3146 }
3147 ParseNode::SpacingNode { text, .. } => {
3148 children.push(layout_spacing_command(text, options));
3149 }
3150 _ => {
3151 children.push(layout_node(node, options));
3152 }
3153 }
3154 }
3155 make_hbox(children)
3156}
3157
3158fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3161 let base = layout_expression(body, options, true);
3162 let w = base.width;
3163 let h = base.height;
3164 let d = base.depth;
3165
3166 let shadow = layout_expression(body, options, true);
3168 let shadow_shift_x = 0.02_f64;
3169 let _shadow_shift_y = 0.01_f64;
3170
3171 let kern_back = LayoutBox::new_kern(-w);
3175 let kern_x = LayoutBox::new_kern(shadow_shift_x);
3176
3177 let children = vec![kern_x, shadow, kern_back, base];
3184 let hbox = make_hbox(children);
3186 LayoutBox {
3188 width: w,
3189 height: h,
3190 depth: d,
3191 content: hbox.content,
3192 color: options.color,
3193 }
3194}
3195
3196fn layout_enclose(
3199 label: &str,
3200 background_color: Option<&str>,
3201 border_color: Option<&str>,
3202 body: &ParseNode,
3203 options: &LayoutOptions,
3204) -> LayoutBox {
3205 use crate::layout_box::BoxContent;
3206 use ratex_types::color::Color;
3207
3208 if label == "\\phase" {
3210 return layout_phase(body, options);
3211 }
3212
3213 if label == "\\angl" {
3215 return layout_angl(body, options);
3216 }
3217
3218 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
3220 return layout_cancel(label, body, options);
3221 }
3222
3223 let metrics = options.metrics();
3225 let padding = 3.0 / metrics.pt_per_em;
3226 let border_thickness = 0.4 / metrics.pt_per_em;
3227
3228 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
3229
3230 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
3231 let border = border_color
3232 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
3233 .unwrap_or(Color::BLACK);
3234
3235 let inner = layout_node(body, options);
3236 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
3237
3238 let width = inner.width + 2.0 * outer_pad;
3239 let height = inner.height + outer_pad;
3240 let depth = inner.depth + outer_pad;
3241
3242 LayoutBox {
3243 width,
3244 height,
3245 depth,
3246 content: BoxContent::Framed {
3247 body: Box::new(inner),
3248 padding,
3249 border_thickness,
3250 has_border,
3251 bg_color: bg,
3252 border_color: border,
3253 },
3254 color: options.color,
3255 }
3256}
3257
3258fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3260 use crate::layout_box::BoxContent;
3261 let inner = layout_node(body, options);
3262 let height = inner.height + shift;
3264 let depth = (inner.depth - shift).max(0.0);
3265 let width = inner.width;
3266 LayoutBox {
3267 width,
3268 height,
3269 depth,
3270 content: BoxContent::RaiseBox {
3271 body: Box::new(inner),
3272 shift,
3273 },
3274 color: options.color,
3275 }
3276}
3277
3278fn is_single_char_body(node: &ParseNode) -> bool {
3281 use ratex_parser::parse_node::ParseNode as PN;
3282 match node {
3283 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3285 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
3286 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
3288 _ => false,
3289 }
3290}
3291
3292fn layout_cancel(label: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3298 use crate::layout_box::BoxContent;
3299 let inner = layout_node(body, options);
3300 let w = inner.width.max(0.01);
3301 let h = inner.height;
3302 let d = inner.depth;
3303
3304 let single = is_single_char_body(body);
3307 let (v_pad, h_pad) = if label == "\\sout" {
3308 (0.0, 0.0)
3309 } else if single {
3310 (0.2, 0.0)
3311 } else {
3312 (0.0, 0.2)
3313 };
3314
3315 let commands: Vec<PathCommand> = match label {
3319 "\\cancel" => vec![
3320 PathCommand::MoveTo {
3321 x: -h_pad,
3322 y: d + v_pad,
3323 }, PathCommand::LineTo {
3325 x: w + h_pad,
3326 y: -h - v_pad,
3327 }, ],
3329 "\\bcancel" => vec![
3330 PathCommand::MoveTo {
3331 x: -h_pad,
3332 y: -h - v_pad,
3333 }, PathCommand::LineTo {
3335 x: w + h_pad,
3336 y: d + v_pad,
3337 }, ],
3339 "\\xcancel" => vec![
3340 PathCommand::MoveTo {
3341 x: -h_pad,
3342 y: d + v_pad,
3343 },
3344 PathCommand::LineTo {
3345 x: w + h_pad,
3346 y: -h - v_pad,
3347 },
3348 PathCommand::MoveTo {
3349 x: -h_pad,
3350 y: -h - v_pad,
3351 },
3352 PathCommand::LineTo {
3353 x: w + h_pad,
3354 y: d + v_pad,
3355 },
3356 ],
3357 "\\sout" => {
3358 let mid_y = -0.5 * options.metrics().x_height;
3360 vec![
3361 PathCommand::MoveTo { x: 0.0, y: mid_y },
3362 PathCommand::LineTo { x: w, y: mid_y },
3363 ]
3364 }
3365 _ => vec![],
3366 };
3367
3368 let line_w = w + 2.0 * h_pad;
3369 let line_h = h + v_pad;
3370 let line_d = d + v_pad;
3371 let line_box = LayoutBox {
3372 width: line_w,
3373 height: line_h,
3374 depth: line_d,
3375 content: BoxContent::SvgPath {
3376 commands,
3377 fill: false,
3378 },
3379 color: options.color,
3380 };
3381
3382 let body_kern = -(line_w - h_pad);
3384 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
3385 LayoutBox {
3386 width: w,
3387 height: h,
3388 depth: d,
3389 content: BoxContent::HBox(vec![line_box, body_shifted]),
3390 color: options.color,
3391 }
3392}
3393
3394fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3397 use crate::layout_box::BoxContent;
3398 let metrics = options.metrics();
3399 let inner = layout_node(body, options);
3400 let line_weight = 0.6_f64 / metrics.pt_per_em;
3402 let clearance = 0.35_f64 * metrics.x_height;
3403 let angle_height = inner.height + inner.depth + line_weight + clearance;
3404 let left_pad = angle_height / 2.0 + line_weight;
3405 let width = inner.width + left_pad;
3406
3407 let y_svg = (1000.0 * angle_height).floor().max(80.0);
3409
3410 let sy = angle_height / y_svg;
3412 let sx = sy;
3415 let right_x = (400_000.0_f64 * sx).min(width);
3416
3417 let bottom_y = inner.depth + line_weight + clearance;
3419 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
3420
3421 let x_peak = y_svg / 2.0;
3423 let commands = vec![
3424 PathCommand::MoveTo {
3425 x: right_x,
3426 y: vy(y_svg),
3427 },
3428 PathCommand::LineTo {
3429 x: 0.0,
3430 y: vy(y_svg),
3431 },
3432 PathCommand::LineTo {
3433 x: x_peak * sx,
3434 y: vy(0.0),
3435 },
3436 PathCommand::LineTo {
3437 x: (x_peak + 65.0) * sx,
3438 y: vy(45.0),
3439 },
3440 PathCommand::LineTo {
3441 x: 145.0 * sx,
3442 y: vy(y_svg - 80.0),
3443 },
3444 PathCommand::LineTo {
3445 x: right_x,
3446 y: vy(y_svg - 80.0),
3447 },
3448 PathCommand::Close,
3449 ];
3450
3451 let body_shifted = make_hbox(vec![LayoutBox::new_kern(left_pad), inner.clone()]);
3452
3453 let path_height = inner.height;
3454 let path_depth = bottom_y;
3455
3456 LayoutBox {
3457 width,
3458 height: path_height,
3459 depth: path_depth,
3460 content: BoxContent::HBox(vec![
3461 LayoutBox {
3462 width,
3463 height: path_height,
3464 depth: path_depth,
3465 content: BoxContent::SvgPath {
3466 commands,
3467 fill: true,
3468 },
3469 color: options.color,
3470 },
3471 LayoutBox::new_kern(-width),
3472 body_shifted,
3473 ]),
3474 color: options.color,
3475 }
3476}
3477
3478fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3481 use crate::layout_box::BoxContent;
3482 let inner = layout_node(body, options);
3483 let w = inner.width.max(0.3);
3484 let clearance = 0.1_f64;
3486 let arc_h = inner.height + clearance;
3487
3488 let path_commands = vec![
3490 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3491 PathCommand::LineTo { x: w, y: -arc_h },
3492 PathCommand::LineTo {
3493 x: w,
3494 y: inner.depth + 0.3_f64,
3495 },
3496 ];
3497
3498 let height = arc_h;
3499 LayoutBox {
3500 width: w,
3501 height,
3502 depth: inner.depth,
3503 content: BoxContent::Angl {
3504 path_commands,
3505 body: Box::new(inner),
3506 },
3507 color: options.color,
3508 }
3509}
3510
3511fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3512 let font_id = match font {
3513 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3514 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3515 "mathit" | "\\mathit" | "it" | "\\it" => Some(FontId::MathItalic),
3516 "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3517 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3518 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3519 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3520 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3521 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3522 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3523 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3524 _ => None,
3525 };
3526
3527 if let Some(fid) = font_id {
3528 layout_with_font(body, fid, options)
3529 } else {
3530 layout_node(body, options)
3531 }
3532}
3533
3534fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3535 match node {
3536 ParseNode::OrdGroup { body, .. } => {
3537 let kern = options.inter_glyph_kern_em;
3538 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3539 for (i, n) in body.iter().enumerate() {
3540 if i > 0 && kern > 0.0 {
3541 children.push(LayoutBox::new_kern(kern));
3542 }
3543 children.push(layout_with_font(n, font_id, options));
3544 }
3545 make_hbox(children)
3546 }
3547 ParseNode::SupSub { base, sup, sub, .. } => {
3548 if let Some(base_node) = base.as_deref() {
3549 if should_use_op_limits(base_node, options) {
3550 return layout_op_with_limits(
3551 base_node,
3552 sup.as_deref(),
3553 sub.as_deref(),
3554 options,
3555 );
3556 }
3557 }
3558 layout_supsub(
3559 base.as_deref(),
3560 sup.as_deref(),
3561 sub.as_deref(),
3562 options,
3563 Some(font_id),
3564 )
3565 }
3566 ParseNode::MathOrd { text, mode, .. }
3567 | ParseNode::TextOrd { text, mode, .. }
3568 | ParseNode::Atom { text, mode, .. } => {
3569 let ch = resolve_symbol_char(text, *mode);
3570 let char_code = ch as u32;
3571 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3572 .map(|(_, m)| m)
3573 .unwrap_or(char_code);
3574 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3575 LayoutBox {
3576 width: math_glyph_advance_em(&m, *mode),
3578 height: m.height,
3579 depth: m.depth,
3580 content: BoxContent::Glyph { font_id, char_code },
3581 color: options.color,
3582 }
3583 } else {
3584 layout_node(node, options)
3586 }
3587 }
3588 _ => layout_node(node, options),
3589 }
3590}
3591
3592fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3597 let cramped = options.with_style(options.style.cramped());
3598 let body_box = layout_node(body, &cramped);
3599 let metrics = options.metrics();
3600 let rule = metrics.default_rule_thickness;
3601
3602 let height = body_box.height + 3.0 * rule;
3604 LayoutBox {
3605 width: body_box.width,
3606 height,
3607 depth: body_box.depth,
3608 content: BoxContent::Overline {
3609 body: Box::new(body_box),
3610 rule_thickness: rule,
3611 },
3612 color: options.color,
3613 }
3614}
3615
3616fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3617 let body_box = layout_node(body, options);
3618 let metrics = options.metrics();
3619 let rule = metrics.default_rule_thickness;
3620 let offset = 4.0 * rule;
3621
3622 let depth = body_box.depth + offset + 0.5 * rule;
3623 LayoutBox {
3624 width: body_box.width,
3625 height: body_box.height,
3626 depth,
3627 content: BoxContent::Underline {
3628 body: Box::new(body_box),
3629 rule_thickness: rule,
3630 offset,
3631 },
3632 color: options.color,
3633 }
3634}
3635
3636fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3638 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3639 let body_opts = options.with_color(link_color).with_inter_glyph_kern(0.05);
3641 let body_box = layout_expression(body, &body_opts, true);
3642 if box_contains_font(&body_box, FontId::TypewriterRegular) {
3643 layout_link_underline_laid_out(body_box, options, link_color)
3644 } else {
3645 layout_underline_laid_out(body_box, options, link_color)
3646 }
3647}
3648
3649fn layout_underline_laid_out(
3651 body_box: LayoutBox,
3652 options: &LayoutOptions,
3653 color: Color,
3654) -> LayoutBox {
3655 let metrics = options.metrics();
3656 let rule = metrics.default_rule_thickness;
3657 layout_underline_laid_out_with_rule(body_box, rule, 2.5 * rule, color)
3658}
3659
3660fn layout_underline_laid_out_with_rule(
3661 body_box: LayoutBox,
3662 rule: f64,
3663 offset: f64,
3664 color: Color,
3665) -> LayoutBox {
3666 let depth = body_box.depth + offset + 0.5 * rule;
3667 LayoutBox {
3668 width: body_box.width,
3669 height: body_box.height,
3670 depth,
3671 content: BoxContent::Underline {
3672 body: Box::new(body_box),
3673 rule_thickness: rule,
3674 offset,
3675 },
3676 color,
3677 }
3678}
3679
3680fn box_contains_font(lbox: &LayoutBox, font_id: FontId) -> bool {
3681 match &lbox.content {
3682 BoxContent::Glyph { font_id: fid, .. } => *fid == font_id,
3683 BoxContent::HBox(children) => children
3684 .iter()
3685 .any(|child| box_contains_font(child, font_id)),
3686 BoxContent::Scaled { body, .. } | BoxContent::RaiseBox { body, .. } => {
3687 box_contains_font(body, font_id)
3688 }
3689 _ => false,
3690 }
3691}
3692
3693fn link_underline_skip_cuts(lbox: &LayoutBox, x: f64, scale: f64, cuts: &mut Vec<(f64, f64)>) {
3694 match &lbox.content {
3695 BoxContent::HBox(children) => {
3696 let mut cur_x = x;
3697 for child in children {
3698 link_underline_skip_cuts(child, cur_x, scale, cuts);
3699 cur_x += child.width * scale;
3700 }
3701 }
3702 BoxContent::Glyph { char_code, .. } => {
3703 if let Some(ch) = char::from_u32(*char_code) {
3704 if matches!(ch, 'g' | 'j' | 'p' | 'q' | 'y') {
3705 let pad = (0.04 * scale).min(lbox.width * scale / 3.0);
3706 cuts.push((x + pad, x + lbox.width * scale - pad));
3707 }
3708 }
3709 }
3710 BoxContent::Scaled { body, child_scale } => {
3711 link_underline_skip_cuts(body, x, scale * child_scale, cuts);
3712 }
3713 BoxContent::RaiseBox { body, .. } => {
3714 link_underline_skip_cuts(body, x, scale, cuts);
3715 }
3716 _ => {}
3717 }
3718}
3719
3720fn link_underline_segments(body_box: &LayoutBox) -> Vec<(f64, f64)> {
3721 let mut cuts = Vec::new();
3722 link_underline_skip_cuts(body_box, 0.0, 1.0, &mut cuts);
3723 if cuts.is_empty() {
3724 return vec![(0.0, body_box.width)];
3725 }
3726 cuts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
3727
3728 let mut segments = Vec::new();
3729 let mut x = 0.0;
3730 for (cut_l, cut_r) in cuts {
3731 let l = cut_l.clamp(0.0, body_box.width);
3732 let r = cut_r.clamp(0.0, body_box.width);
3733 if l > x + 0.02 {
3734 segments.push((x, l - x));
3735 }
3736 x = x.max(r);
3737 }
3738 if body_box.width > x + 0.02 {
3739 segments.push((x, body_box.width - x));
3740 }
3741 if segments.is_empty() {
3742 vec![(0.0, body_box.width)]
3743 } else {
3744 segments
3745 }
3746}
3747
3748fn layout_link_underline_laid_out(
3749 body_box: LayoutBox,
3750 options: &LayoutOptions,
3751 color: Color,
3752) -> LayoutBox {
3753 let metrics = options.metrics();
3754 let rule = metrics.default_rule_thickness;
3755 let offset = 2.5 * rule;
3756 let segments = link_underline_segments(&body_box);
3757 if segments.len() <= 1 {
3758 return layout_underline_laid_out_with_rule(body_box, rule, offset, color);
3759 }
3760
3761 let height = body_box.height;
3762 let depth = body_box.depth + offset + 0.5 * rule;
3763 let rule_y = height + body_box.depth + offset;
3764 let width = body_box.width;
3765 LayoutBox {
3766 width,
3767 height,
3768 depth,
3769 content: BoxContent::ProofTree {
3770 children: vec![PlacedBox {
3771 box_: body_box,
3772 x: 0.0,
3773 baseline_y: height,
3774 }],
3775 rules: segments
3776 .into_iter()
3777 .map(|(x, width)| ProofRule {
3778 x,
3779 y: rule_y,
3780 width,
3781 thickness: rule,
3782 dashed: false,
3783 })
3784 .collect(),
3785 },
3786 color,
3787 }
3788}
3789
3790fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3795 let metrics = options.metrics();
3796 let mu = metrics.css_em_per_mu();
3797
3798 let width = match text {
3799 "\\," | "\\thinspace" => 3.0 * mu,
3800 "\\:" | "\\medspace" => 4.0 * mu,
3801 "\\;" | "\\thickspace" => 5.0 * mu,
3802 "\\!" | "\\negthinspace" => -3.0 * mu,
3803 "\\negmedspace" => -4.0 * mu,
3804 "\\negthickspace" => -5.0 * mu,
3805 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3806 get_char_metrics(FontId::MainRegular, 160)
3810 .map(|m| m.width)
3811 .unwrap_or(0.25)
3812 }
3813 "\\quad" => metrics.quad,
3814 "\\qquad" => 2.0 * metrics.quad,
3815 "\\enspace" => metrics.quad / 2.0,
3816 _ => 0.0,
3817 };
3818
3819 LayoutBox::new_kern(width)
3820}
3821
3822fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3827 let metrics = options.metrics();
3828 match m.unit.as_str() {
3829 "em" => m.number,
3830 "ex" => m.number * metrics.x_height,
3831 "mu" => m.number * metrics.css_em_per_mu(),
3832 "pt" => m.number / metrics.pt_per_em,
3833 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3834 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3835 "in" => m.number * 72.27 / metrics.pt_per_em,
3836 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3837 "pc" => m.number * 12.0 / metrics.pt_per_em,
3838 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3839 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3840 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3841 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3842 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3843 _ => m.number,
3844 }
3845}
3846
3847fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3853 match node {
3854 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3855 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3856 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => {
3857 Some(MathClass::Op)
3858 }
3859 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3860 ParseNode::GenFrac {
3862 left_delim,
3863 right_delim,
3864 ..
3865 } => {
3866 let has_delim = left_delim
3867 .as_ref()
3868 .is_some_and(|d| !d.is_empty() && d != ".")
3869 || right_delim
3870 .as_ref()
3871 .is_some_and(|d| !d.is_empty() && d != ".");
3872 if has_delim {
3873 Some(MathClass::Ord)
3874 } else {
3875 Some(MathClass::Inner)
3876 }
3877 }
3878 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3879 ParseNode::SupSub { base, .. } => base.as_ref().and_then(|b| node_math_class(b)),
3880 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3881 ParseNode::SpacingNode { .. } => None,
3882 ParseNode::Kern { .. } => None,
3883 ParseNode::HtmlMathMl { html, .. } => {
3884 for child in html {
3886 if let Some(cls) = node_math_class(child) {
3887 return Some(cls);
3888 }
3889 }
3890 None
3891 }
3892 ParseNode::Html { body, .. } => {
3893 for child in body {
3894 if let Some(cls) = node_math_class(child) {
3895 return Some(cls);
3896 }
3897 }
3898 None
3899 }
3900 ParseNode::Lap { .. } => None,
3901 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3902 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3903 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3905 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3907 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3908 ParseNode::Middle { .. } => Some(MathClass::Ord),
3909 _ => Some(MathClass::Ord),
3910 }
3911}
3912
3913fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3914 match mclass {
3915 "mord" => MathClass::Ord,
3916 "mop" => MathClass::Op,
3917 "mbin" => MathClass::Bin,
3918 "mrel" => MathClass::Rel,
3919 "mopen" => MathClass::Open,
3920 "mclose" => MathClass::Close,
3921 "mpunct" => MathClass::Punct,
3922 "minner" => MathClass::Inner,
3923 _ => MathClass::Ord,
3924 }
3925}
3926
3927fn get_base_elem(node: &ParseNode) -> &ParseNode {
3931 match node {
3932 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3933 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3934 ParseNode::Html { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3935 ParseNode::Font { body, .. } => get_base_elem(body),
3936 _ => node,
3937 }
3938}
3939
3940fn is_character_box(node: &ParseNode) -> bool {
3941 matches!(
3942 get_base_elem(node),
3943 ParseNode::MathOrd { .. }
3944 | ParseNode::TextOrd { .. }
3945 | ParseNode::Atom { .. }
3946 | ParseNode::AccentToken { .. }
3947 )
3948}
3949
3950fn family_to_math_class(family: AtomFamily) -> MathClass {
3951 match family {
3952 AtomFamily::Bin => MathClass::Bin,
3953 AtomFamily::Rel => MathClass::Rel,
3954 AtomFamily::Open => MathClass::Open,
3955 AtomFamily::Close => MathClass::Close,
3956 AtomFamily::Punct => MathClass::Punct,
3957 AtomFamily::Inner => MathClass::Inner,
3958 }
3959}
3960
3961fn layout_horiz_brace(
3966 base: &ParseNode,
3967 is_over: bool,
3968 func_label: &str,
3969 options: &LayoutOptions,
3970) -> LayoutBox {
3971 let body_box = layout_node(base, options);
3972 let w = body_box.width.max(0.5);
3973
3974 let is_bracket = func_label.trim_start_matches('\\').ends_with("bracket");
3975
3976 let stretch_key = if is_bracket {
3978 if is_over {
3979 "overbracket"
3980 } else {
3981 "underbracket"
3982 }
3983 } else if is_over {
3984 "overbrace"
3985 } else {
3986 "underbrace"
3987 };
3988
3989 let (raw_commands, brace_h, brace_fill) =
3990 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3991 Some((c, h)) => (c, h, true),
3992 None => {
3993 let h = 0.35_f64;
3994 (horiz_brace_path(w, h, is_over), h, false)
3995 }
3996 };
3997
3998 let y_shift = brace_h / 2.0;
4004 let commands = shift_path_y(raw_commands, y_shift);
4005
4006 let brace_box = LayoutBox {
4007 width: w,
4008 height: 0.0,
4009 depth: brace_h,
4010 content: BoxContent::SvgPath {
4011 commands,
4012 fill: brace_fill,
4013 },
4014 color: options.color,
4015 };
4016
4017 let gap = 0.1;
4018 let (height, depth) = if is_over {
4019 (body_box.height + brace_h + gap, body_box.depth)
4020 } else {
4021 (body_box.height, body_box.depth + brace_h + gap)
4022 };
4023
4024 let clearance = if is_over {
4025 height - brace_h
4026 } else {
4027 body_box.height + body_box.depth + gap
4028 };
4029 let total_w = body_box.width;
4030
4031 LayoutBox {
4032 width: total_w,
4033 height,
4034 depth,
4035 content: BoxContent::Accent {
4036 base: Box::new(body_box),
4037 accent: Box::new(brace_box),
4038 clearance,
4039 skew: 0.0,
4040 is_below: !is_over,
4041 under_gap_em: 0.0,
4042 },
4043 color: options.color,
4044 }
4045}
4046
4047fn layout_xarrow(
4052 label: &str,
4053 body: &ParseNode,
4054 below: Option<&ParseNode>,
4055 options: &LayoutOptions,
4056) -> LayoutBox {
4057 let sup_style = options.style.superscript();
4058 let sub_style = options.style.subscript();
4059 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4060 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4061
4062 let sup_opts = options.with_style(sup_style);
4063 let body_box = layout_node(body, &sup_opts);
4064 let body_w = body_box.width * sup_ratio;
4065
4066 let below_box = below.map(|b| {
4067 let sub_opts = options.with_style(sub_style);
4068 layout_node(b, &sub_opts)
4069 });
4070 let below_w = below_box
4071 .as_ref()
4072 .map(|b| b.width * sub_ratio)
4073 .unwrap_or(0.0);
4074
4075 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
4078 let upper_w = body_w + sup_ratio;
4079 let lower_w = if below_box.is_some() {
4080 below_w + sub_ratio
4081 } else {
4082 0.0
4083 };
4084 let arrow_w = upper_w.max(lower_w).max(min_w);
4085 let arrow_h = 0.3;
4086
4087 let (commands, actual_arrow_h, fill_arrow) =
4088 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
4089 Some((c, h)) => (c, h, true),
4090 None => (
4091 stretchy_accent_path(label, arrow_w, arrow_h),
4092 arrow_h,
4093 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
4094 ),
4095 };
4096 let arrow_box = LayoutBox {
4097 width: arrow_w,
4098 height: actual_arrow_h / 2.0,
4099 depth: actual_arrow_h / 2.0,
4100 content: BoxContent::SvgPath {
4101 commands,
4102 fill: fill_arrow,
4103 },
4104 color: options.color,
4105 };
4106
4107 let metrics = options.metrics();
4110 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
4112 let gap = 0.111; let base_shift = -axis;
4116
4117 let sup_kern = gap;
4125 let sub_kern = gap;
4126
4127 let sup_h = body_box.height * sup_ratio;
4128 let sup_d = body_box.depth * sup_ratio;
4129
4130 let height = axis + arrow_half + gap + sup_h + sup_d;
4132 let mut depth = (arrow_half - axis).max(0.0);
4134
4135 if let Some(ref bel) = below_box {
4136 let sub_h = bel.height * sub_ratio;
4137 let sub_d = bel.depth * sub_ratio;
4138 depth = (arrow_half - axis) + gap + sub_h + sub_d;
4140 }
4141
4142 LayoutBox {
4143 width: arrow_w,
4144 height,
4145 depth,
4146 content: BoxContent::OpLimits {
4147 base: Box::new(arrow_box),
4148 sup: Some(Box::new(body_box)),
4149 sub: below_box.map(Box::new),
4150 base_shift,
4151 sup_kern,
4152 sub_kern,
4153 slant: 0.0,
4154 sup_scale: sup_ratio,
4155 sub_scale: sub_ratio,
4156 },
4157 color: options.color,
4158 }
4159}
4160
4161fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
4166 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
4169 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
4170 let diameter = radius * 2.0;
4171
4172 let cx = radius;
4174 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
4177
4178 let circle_commands = vec![
4179 PathCommand::MoveTo { x: cx + r, y: cy },
4180 PathCommand::CubicTo {
4181 x1: cx + r,
4182 y1: cy - k * r,
4183 x2: cx + k * r,
4184 y2: cy - r,
4185 x: cx,
4186 y: cy - r,
4187 },
4188 PathCommand::CubicTo {
4189 x1: cx - k * r,
4190 y1: cy - r,
4191 x2: cx - r,
4192 y2: cy - k * r,
4193 x: cx - r,
4194 y: cy,
4195 },
4196 PathCommand::CubicTo {
4197 x1: cx - r,
4198 y1: cy + k * r,
4199 x2: cx - k * r,
4200 y2: cy + r,
4201 x: cx,
4202 y: cy + r,
4203 },
4204 PathCommand::CubicTo {
4205 x1: cx + k * r,
4206 y1: cy + r,
4207 x2: cx + r,
4208 y2: cy + k * r,
4209 x: cx + r,
4210 y: cy,
4211 },
4212 PathCommand::Close,
4213 ];
4214
4215 let circle_box = LayoutBox {
4216 width: diameter,
4217 height: r - cy.min(0.0),
4218 depth: (r + cy).max(0.0),
4219 content: BoxContent::SvgPath {
4220 commands: circle_commands,
4221 fill: false,
4222 },
4223 color: options.color,
4224 };
4225
4226 let content_shift = (diameter - body_box.width) / 2.0;
4228 let children = vec![
4230 circle_box,
4231 LayoutBox::new_kern(-(diameter) + content_shift),
4232 body_box.clone(),
4233 ];
4234
4235 let height = r - cy.min(0.0);
4236 let depth = (r + cy).max(0.0);
4237
4238 LayoutBox {
4239 width: diameter,
4240 height,
4241 depth,
4242 content: BoxContent::HBox(children),
4243 color: options.color,
4244 }
4245}
4246
4247fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
4271 let r: f64 = 0.1125;
4273 let cy: f64 = -0.2625;
4277 let k: f64 = 0.5523;
4279 let cx: f64 = r;
4281
4282 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
4285
4286 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
4295 vec![
4296 PathCommand::MoveTo { x: ox + rad, y: cy },
4297 PathCommand::CubicTo {
4298 x1: ox + rad,
4299 y1: cy - k * rad,
4300 x2: ox + k * rad,
4301 y2: cy - rad,
4302 x: ox,
4303 y: cy - rad,
4304 },
4305 PathCommand::CubicTo {
4306 x1: ox - k * rad,
4307 y1: cy - rad,
4308 x2: ox - rad,
4309 y2: cy - k * rad,
4310 x: ox - rad,
4311 y: cy,
4312 },
4313 PathCommand::CubicTo {
4314 x1: ox - rad,
4315 y1: cy + k * rad,
4316 x2: ox - k * rad,
4317 y2: cy + rad,
4318 x: ox,
4319 y: cy + rad,
4320 },
4321 PathCommand::CubicTo {
4322 x1: ox + k * rad,
4323 y1: cy + rad,
4324 x2: ox + rad,
4325 y2: cy + k * rad,
4326 x: ox + rad,
4327 y: cy,
4328 },
4329 PathCommand::Close,
4330 ]
4331 };
4332
4333 let disk = LayoutBox {
4334 width: 2.0 * r,
4335 height: h,
4336 depth: d,
4337 content: BoxContent::SvgPath {
4338 commands: circle_commands(cx, r),
4339 fill: true,
4340 },
4341 color: options.color,
4342 };
4343
4344 let ring = LayoutBox {
4345 width: 2.0 * r,
4346 height: h,
4347 depth: d,
4348 content: BoxContent::SvgPath {
4349 commands: circle_commands(cx, r_ring),
4350 fill: false,
4351 },
4352 color: options.color,
4353 };
4354
4355 let bar_len: f64 = 0.25;
4359 let bar_th: f64 = 0.04;
4360 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
4363
4364 let children = if imageof {
4365 vec![disk, bar, ring]
4366 } else {
4367 vec![ring, bar, disk]
4368 };
4369
4370 let total_width = 4.0 * r + bar_len;
4372 LayoutBox {
4373 width: total_width,
4374 height: h,
4375 depth: d,
4376 content: BoxContent::HBox(children),
4377 color: options.color,
4378 }
4379}
4380
4381fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
4385 let cx = width / 2.0;
4386 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
4391 PathCommand::MoveTo { x: cx + a, y: cy },
4392 PathCommand::CubicTo {
4393 x1: cx + a,
4394 y1: cy - k * b,
4395 x2: cx + k * a,
4396 y2: cy - b,
4397 x: cx,
4398 y: cy - b,
4399 },
4400 PathCommand::CubicTo {
4401 x1: cx - k * a,
4402 y1: cy - b,
4403 x2: cx - a,
4404 y2: cy - k * b,
4405 x: cx - a,
4406 y: cy,
4407 },
4408 PathCommand::CubicTo {
4409 x1: cx - a,
4410 y1: cy + k * b,
4411 x2: cx - k * a,
4412 y2: cy + b,
4413 x: cx,
4414 y: cy + b,
4415 },
4416 PathCommand::CubicTo {
4417 x1: cx + k * a,
4418 y1: cy + b,
4419 x2: cx + a,
4420 y2: cy + k * b,
4421 x: cx + a,
4422 y: cy,
4423 },
4424 PathCommand::Close,
4425 ]
4426}
4427
4428fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
4429 cmds.into_iter()
4430 .map(|c| match c {
4431 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
4432 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
4433 PathCommand::CubicTo {
4434 x1,
4435 y1,
4436 x2,
4437 y2,
4438 x,
4439 y,
4440 } => PathCommand::CubicTo {
4441 x1,
4442 y1: y1 + dy,
4443 x2,
4444 y2: y2 + dy,
4445 x,
4446 y: y + dy,
4447 },
4448 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
4449 x1,
4450 y1: y1 + dy,
4451 x,
4452 y: y + dy,
4453 },
4454 PathCommand::Close => PathCommand::Close,
4455 })
4456 .collect()
4457}
4458
4459fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
4460 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
4461 return commands;
4462 }
4463 let ah = height * 0.35; let mid_y = -height / 2.0;
4465
4466 match label {
4467 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
4468 vec![
4469 PathCommand::MoveTo {
4470 x: ah,
4471 y: mid_y - ah,
4472 },
4473 PathCommand::LineTo { x: 0.0, y: mid_y },
4474 PathCommand::LineTo {
4475 x: ah,
4476 y: mid_y + ah,
4477 },
4478 PathCommand::MoveTo { x: 0.0, y: mid_y },
4479 PathCommand::LineTo { x: width, y: mid_y },
4480 ]
4481 }
4482 "\\overleftrightarrow"
4483 | "\\underleftrightarrow"
4484 | "\\xleftrightarrow"
4485 | "\\xLeftrightarrow" => {
4486 vec![
4487 PathCommand::MoveTo {
4488 x: ah,
4489 y: mid_y - ah,
4490 },
4491 PathCommand::LineTo { x: 0.0, y: mid_y },
4492 PathCommand::LineTo {
4493 x: ah,
4494 y: mid_y + ah,
4495 },
4496 PathCommand::MoveTo { x: 0.0, y: mid_y },
4497 PathCommand::LineTo { x: width, y: mid_y },
4498 PathCommand::MoveTo {
4499 x: width - ah,
4500 y: mid_y - ah,
4501 },
4502 PathCommand::LineTo { x: width, y: mid_y },
4503 PathCommand::LineTo {
4504 x: width - ah,
4505 y: mid_y + ah,
4506 },
4507 ]
4508 }
4509 "\\xlongequal" => {
4510 let gap = 0.04;
4511 vec![
4512 PathCommand::MoveTo {
4513 x: 0.0,
4514 y: mid_y - gap,
4515 },
4516 PathCommand::LineTo {
4517 x: width,
4518 y: mid_y - gap,
4519 },
4520 PathCommand::MoveTo {
4521 x: 0.0,
4522 y: mid_y + gap,
4523 },
4524 PathCommand::LineTo {
4525 x: width,
4526 y: mid_y + gap,
4527 },
4528 ]
4529 }
4530 "\\xhookleftarrow" => {
4531 vec![
4532 PathCommand::MoveTo {
4533 x: ah,
4534 y: mid_y - ah,
4535 },
4536 PathCommand::LineTo { x: 0.0, y: mid_y },
4537 PathCommand::LineTo {
4538 x: ah,
4539 y: mid_y + ah,
4540 },
4541 PathCommand::MoveTo { x: 0.0, y: mid_y },
4542 PathCommand::LineTo { x: width, y: mid_y },
4543 PathCommand::QuadTo {
4544 x1: width + ah,
4545 y1: mid_y,
4546 x: width + ah,
4547 y: mid_y + ah,
4548 },
4549 ]
4550 }
4551 "\\xhookrightarrow" => {
4552 vec![
4553 PathCommand::MoveTo {
4554 x: 0.0 - ah,
4555 y: mid_y - ah,
4556 },
4557 PathCommand::QuadTo {
4558 x1: 0.0 - ah,
4559 y1: mid_y,
4560 x: 0.0,
4561 y: mid_y,
4562 },
4563 PathCommand::LineTo { x: width, y: mid_y },
4564 PathCommand::MoveTo {
4565 x: width - ah,
4566 y: mid_y - ah,
4567 },
4568 PathCommand::LineTo { x: width, y: mid_y },
4569 PathCommand::LineTo {
4570 x: width - ah,
4571 y: mid_y + ah,
4572 },
4573 ]
4574 }
4575 "\\xrightharpoonup" | "\\xleftharpoonup" => {
4576 let right = label.contains("right");
4577 if right {
4578 vec![
4579 PathCommand::MoveTo { x: 0.0, y: mid_y },
4580 PathCommand::LineTo { x: width, y: mid_y },
4581 PathCommand::MoveTo {
4582 x: width - ah,
4583 y: mid_y - ah,
4584 },
4585 PathCommand::LineTo { x: width, y: mid_y },
4586 ]
4587 } else {
4588 vec![
4589 PathCommand::MoveTo {
4590 x: ah,
4591 y: mid_y - ah,
4592 },
4593 PathCommand::LineTo { x: 0.0, y: mid_y },
4594 PathCommand::LineTo { x: width, y: mid_y },
4595 ]
4596 }
4597 }
4598 "\\xrightharpoondown" | "\\xleftharpoondown" => {
4599 let right = label.contains("right");
4600 if right {
4601 vec![
4602 PathCommand::MoveTo { x: 0.0, y: mid_y },
4603 PathCommand::LineTo { x: width, y: mid_y },
4604 PathCommand::MoveTo {
4605 x: width - ah,
4606 y: mid_y + ah,
4607 },
4608 PathCommand::LineTo { x: width, y: mid_y },
4609 ]
4610 } else {
4611 vec![
4612 PathCommand::MoveTo {
4613 x: ah,
4614 y: mid_y + ah,
4615 },
4616 PathCommand::LineTo { x: 0.0, y: mid_y },
4617 PathCommand::LineTo { x: width, y: mid_y },
4618 ]
4619 }
4620 }
4621 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
4622 let gap = 0.06;
4623 vec![
4624 PathCommand::MoveTo {
4625 x: 0.0,
4626 y: mid_y - gap,
4627 },
4628 PathCommand::LineTo {
4629 x: width,
4630 y: mid_y - gap,
4631 },
4632 PathCommand::MoveTo {
4633 x: width - ah,
4634 y: mid_y - gap - ah,
4635 },
4636 PathCommand::LineTo {
4637 x: width,
4638 y: mid_y - gap,
4639 },
4640 PathCommand::MoveTo {
4641 x: width,
4642 y: mid_y + gap,
4643 },
4644 PathCommand::LineTo {
4645 x: 0.0,
4646 y: mid_y + gap,
4647 },
4648 PathCommand::MoveTo {
4649 x: ah,
4650 y: mid_y + gap + ah,
4651 },
4652 PathCommand::LineTo {
4653 x: 0.0,
4654 y: mid_y + gap,
4655 },
4656 ]
4657 }
4658 "\\xtofrom" | "\\xrightleftarrows" => {
4659 let gap = 0.06;
4660 vec![
4661 PathCommand::MoveTo {
4662 x: 0.0,
4663 y: mid_y - gap,
4664 },
4665 PathCommand::LineTo {
4666 x: width,
4667 y: mid_y - gap,
4668 },
4669 PathCommand::MoveTo {
4670 x: width - ah,
4671 y: mid_y - gap - ah,
4672 },
4673 PathCommand::LineTo {
4674 x: width,
4675 y: mid_y - gap,
4676 },
4677 PathCommand::LineTo {
4678 x: width - ah,
4679 y: mid_y - gap + ah,
4680 },
4681 PathCommand::MoveTo {
4682 x: width,
4683 y: mid_y + gap,
4684 },
4685 PathCommand::LineTo {
4686 x: 0.0,
4687 y: mid_y + gap,
4688 },
4689 PathCommand::MoveTo {
4690 x: ah,
4691 y: mid_y + gap - ah,
4692 },
4693 PathCommand::LineTo {
4694 x: 0.0,
4695 y: mid_y + gap,
4696 },
4697 PathCommand::LineTo {
4698 x: ah,
4699 y: mid_y + gap + ah,
4700 },
4701 ]
4702 }
4703 "\\overlinesegment" | "\\underlinesegment" => {
4704 vec![
4705 PathCommand::MoveTo { x: 0.0, y: mid_y },
4706 PathCommand::LineTo { x: width, y: mid_y },
4707 ]
4708 }
4709 _ => {
4710 vec![
4711 PathCommand::MoveTo { x: 0.0, y: mid_y },
4712 PathCommand::LineTo { x: width, y: mid_y },
4713 PathCommand::MoveTo {
4714 x: width - ah,
4715 y: mid_y - ah,
4716 },
4717 PathCommand::LineTo { x: width, y: mid_y },
4718 PathCommand::LineTo {
4719 x: width - ah,
4720 y: mid_y + ah,
4721 },
4722 ]
4723 }
4724 }
4725}
4726
4727fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
4733 let h = inner.height;
4734 let d = inner.depth;
4735 let w = inner.width + pad_l + pad_r;
4736 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
4737 if pad_l > 0.0 {
4738 children.push(LayoutBox::new_kern(pad_l));
4739 }
4740 children.push(inner);
4741 if pad_r > 0.0 {
4742 children.push(LayoutBox::new_kern(pad_r));
4743 }
4744 LayoutBox {
4745 width: w,
4746 height: h,
4747 depth: d,
4748 content: BoxContent::HBox(children),
4749 color,
4750 }
4751}
4752
4753fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
4764 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
4765 LayoutBox {
4766 width: label.width,
4767 height: box_h,
4768 depth: box_d,
4769 content: BoxContent::RaiseBox {
4770 body: Box::new(label),
4771 shift,
4772 },
4773 color,
4774 }
4775}
4776
4777fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
4781 let sup_style = options.style.superscript();
4782 let sup_opts = options.with_style(sup_style);
4783 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4784 let inner = layout_node(body, &sup_opts);
4785 if (sup_ratio - 1.0).abs() < 1e-6 {
4786 inner
4787 } else {
4788 LayoutBox {
4789 width: inner.width * sup_ratio,
4790 height: inner.height * sup_ratio,
4791 depth: inner.depth * sup_ratio,
4792 content: BoxContent::Scaled {
4793 body: Box::new(inner),
4794 child_scale: sup_ratio,
4795 },
4796 color: options.color,
4797 }
4798 }
4799}
4800
4801fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4807 let axis = options.metrics().axis_height;
4808 let depth = (total_height / 2.0 - axis).max(0.0);
4809 let height = total_height - depth;
4810 if let Some((commands, w)) =
4811 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4812 {
4813 return LayoutBox {
4814 width: w,
4815 height,
4816 depth,
4817 content: BoxContent::SvgPath {
4818 commands,
4819 fill: true,
4820 },
4821 color: options.color,
4822 };
4823 }
4824 if down {
4826 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4827 } else {
4828 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4829 }
4830}
4831
4832fn layout_cd_arrow(
4848 direction: &str,
4849 label_above: Option<&ParseNode>,
4850 label_below: Option<&ParseNode>,
4851 target_size: f64,
4852 target_col_width: f64,
4853 _target_depth: f64,
4854 options: &LayoutOptions,
4855) -> LayoutBox {
4856 let metrics = options.metrics();
4857 let axis = metrics.axis_height;
4858
4859 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4862
4863 match direction {
4864 "right" | "left" | "horiz_eq" => {
4865 let sup_style = options.style.superscript();
4867 let sub_style = options.style.subscript();
4868 let sup_opts = options.with_style(sup_style);
4869 let sub_opts = options.with_style(sub_style);
4870 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4871 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4872
4873 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4874 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4875
4876 let above_w = above_box
4877 .as_ref()
4878 .map(|b| b.width * sup_ratio)
4879 .unwrap_or(0.0);
4880 let below_w = below_box
4881 .as_ref()
4882 .map(|b| b.width * sub_ratio)
4883 .unwrap_or(0.0);
4884
4885 let path_label = if direction == "right" {
4887 "\\cdrightarrow"
4888 } else if direction == "left" {
4889 "\\cdleftarrow"
4890 } else {
4891 "\\cdlongequal"
4892 };
4893 let min_shaft_w =
4894 crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4895 const CD_LABEL_PAD_L: f64 = 0.22;
4898 const CD_LABEL_PAD_R: f64 = 0.48;
4899 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4900 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4901 let upper_need = above_box
4902 .as_ref()
4903 .map(|_| above_w + cd_pad_sup)
4904 .unwrap_or(0.0);
4905 let lower_need = below_box
4906 .as_ref()
4907 .map(|_| below_w + cd_pad_sub)
4908 .unwrap_or(0.0);
4909 let natural_w = upper_need.max(lower_need).max(0.0);
4910 let shaft_w = if target_size > 0.0 {
4911 target_size
4912 } else {
4913 natural_w.max(min_shaft_w)
4914 };
4915
4916 let (commands, actual_arrow_h, fill_arrow) =
4917 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4918 Some((c, h)) => (c, h, true),
4919 None => {
4920 let arrow_h = 0.3_f64;
4922 let ah = 0.12_f64;
4923 let cmds = if direction == "horiz_eq" {
4924 let gap = 0.06;
4925 vec![
4926 PathCommand::MoveTo { x: 0.0, y: -gap },
4927 PathCommand::LineTo {
4928 x: shaft_w,
4929 y: -gap,
4930 },
4931 PathCommand::MoveTo { x: 0.0, y: gap },
4932 PathCommand::LineTo { x: shaft_w, y: gap },
4933 ]
4934 } else if direction == "right" {
4935 vec![
4936 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4937 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4938 PathCommand::MoveTo {
4939 x: shaft_w - ah,
4940 y: -ah,
4941 },
4942 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4943 PathCommand::LineTo {
4944 x: shaft_w - ah,
4945 y: ah,
4946 },
4947 ]
4948 } else {
4949 vec![
4950 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4951 PathCommand::LineTo { x: 0.0, y: 0.0 },
4952 PathCommand::MoveTo { x: ah, y: -ah },
4953 PathCommand::LineTo { x: 0.0, y: 0.0 },
4954 PathCommand::LineTo { x: ah, y: ah },
4955 ]
4956 };
4957 (cmds, arrow_h, false)
4958 }
4959 };
4960
4961 let arrow_half = actual_arrow_h / 2.0;
4963 let arrow_box = LayoutBox {
4964 width: shaft_w,
4965 height: arrow_half,
4966 depth: arrow_half,
4967 content: BoxContent::SvgPath {
4968 commands,
4969 fill: fill_arrow,
4970 },
4971 color: options.color,
4972 };
4973
4974 let gap = 0.111;
4976 let sup_h = above_box
4977 .as_ref()
4978 .map(|b| b.height * sup_ratio)
4979 .unwrap_or(0.0);
4980 let sup_d = above_box
4981 .as_ref()
4982 .map(|b| b.depth * sup_ratio)
4983 .unwrap_or(0.0);
4984 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4988 sup_d
4989 } else {
4990 0.0
4991 };
4992 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4993 let sub_h_raw = below_box
4994 .as_ref()
4995 .map(|b| b.height * sub_ratio)
4996 .unwrap_or(0.0);
4997 let sub_d_raw = below_box
4998 .as_ref()
4999 .map(|b| b.depth * sub_ratio)
5000 .unwrap_or(0.0);
5001 let depth = if below_box.is_some() {
5002 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
5003 } else {
5004 (arrow_half - axis).max(0.0)
5005 };
5006
5007 let inner = LayoutBox {
5008 width: shaft_w,
5009 height,
5010 depth,
5011 content: BoxContent::OpLimits {
5012 base: Box::new(arrow_box),
5013 sup: above_box.map(Box::new),
5014 sub: below_box.map(Box::new),
5015 base_shift: -axis,
5016 sup_kern: gap,
5017 sub_kern: gap,
5018 slant: 0.0,
5019 sup_scale: sup_ratio,
5020 sub_scale: sub_ratio,
5021 },
5022 color: options.color,
5023 };
5024
5025 if target_col_width > inner.width + 1e-6 {
5029 let extra = target_col_width - inner.width;
5030 let kl = extra / 2.0;
5031 let kr = extra - kl;
5032 cd_wrap_hpad(inner, kl, kr, options.color)
5033 } else {
5034 inner
5035 }
5036 }
5037
5038 "down" | "up" | "vert_eq" => {
5039 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
5043 "vert_eq" if target_size > 0.0 => {
5044 make_vert_delim_box(target_size.max(big_total), true, false, options)
5045 }
5046 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
5047 "down" if target_size > 0.0 => {
5048 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
5049 }
5050 "up" if target_size > 0.0 => {
5051 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
5052 }
5053 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
5054 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
5055 _ => cd_stretch_vert_arrow_box(big_total, true, options),
5056 };
5057 let box_h = shaft_box.height;
5058 let box_d = shaft_box.depth;
5059 let shaft_w = shaft_box.width;
5060
5061 let left_box = label_above.map(|n| {
5064 cd_vcenter_side_label(
5065 cd_side_label_scaled(n, options),
5066 box_h,
5067 box_d,
5068 options.color,
5069 )
5070 });
5071 let right_box = label_below.map(|n| {
5072 cd_vcenter_side_label(
5073 cd_side_label_scaled(n, options),
5074 box_h,
5075 box_d,
5076 options.color,
5077 )
5078 });
5079
5080 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
5081 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
5082 let left_part = left_w
5083 + if left_w > 0.0 {
5084 CD_VERT_SIDE_KERN_EM
5085 } else {
5086 0.0
5087 };
5088 let right_part = (if right_w > 0.0 {
5089 CD_VERT_SIDE_KERN_EM
5090 } else {
5091 0.0
5092 }) + right_w;
5093 let inner_w = left_part + shaft_w + right_part;
5094
5095 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
5097 let extra = target_col_width - inner_w;
5098 let kl = extra / 2.0;
5099 let kr = extra - kl;
5100 (kl, kr, target_col_width)
5101 } else {
5102 (0.0, 0.0, inner_w)
5103 };
5104
5105 let mut children: Vec<LayoutBox> = Vec::new();
5106 if kern_left > 0.0 {
5107 children.push(LayoutBox::new_kern(kern_left));
5108 }
5109 if let Some(lb) = left_box {
5110 children.push(lb);
5111 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
5112 }
5113 children.push(shaft_box);
5114 if let Some(rb) = right_box {
5115 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
5116 children.push(rb);
5117 }
5118 if kern_right > 0.0 {
5119 children.push(LayoutBox::new_kern(kern_right));
5120 }
5121
5122 LayoutBox {
5123 width: total_w,
5124 height: box_h,
5125 depth: box_d,
5126 content: BoxContent::HBox(children),
5127 color: options.color,
5128 }
5129 }
5130
5131 _ => LayoutBox::new_empty(),
5133 }
5134}
5135
5136fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
5138 let metrics = options.metrics();
5139 let pt = 1.0 / metrics.pt_per_em;
5140 let baselineskip = 3.0 * metrics.x_height;
5142 let arstrut_h = 0.7 * baselineskip;
5143 let arstrut_d = 0.3 * baselineskip;
5144
5145 let num_rows = body.len();
5146 if num_rows == 0 {
5147 return LayoutBox::new_empty();
5148 }
5149 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
5150 if num_cols == 0 {
5151 return LayoutBox::new_empty();
5152 }
5153
5154 let jot = 3.0 * pt;
5156
5157 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
5159 let mut col_widths = vec![0.0_f64; num_cols];
5160 let mut row_heights = vec![arstrut_h; num_rows];
5161 let mut row_depths = vec![arstrut_d; num_rows];
5162
5163 for (r, row) in body.iter().enumerate() {
5164 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
5165
5166 for (c, cell) in row.iter().enumerate() {
5167 let cbox = match cell {
5168 ParseNode::CdArrow {
5169 direction,
5170 label_above,
5171 label_below,
5172 ..
5173 } => {
5174 layout_cd_arrow(
5175 direction,
5176 label_above.as_deref(),
5177 label_below.as_deref(),
5178 0.0, 0.0, 0.0, options,
5182 )
5183 }
5184 ParseNode::OrdGroup {
5188 body: cell_body, ..
5189 } => layout_expression(cell_body, options, false),
5190 other => layout_node(other, options),
5191 };
5192
5193 row_heights[r] = row_heights[r].max(cbox.height);
5194 row_depths[r] = row_depths[r].max(cbox.depth);
5195 col_widths[c] = col_widths[c].max(cbox.width);
5196 row_boxes.push(cbox);
5197 }
5198
5199 while row_boxes.len() < num_cols {
5201 row_boxes.push(LayoutBox::new_empty());
5202 }
5203 cell_boxes.push(row_boxes);
5204 }
5205
5206 let col_target_w: Vec<f64> = col_widths.clone();
5210
5211 #[cfg(debug_assertions)]
5212 {
5213 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
5214 for (r, row) in cell_boxes.iter().enumerate() {
5215 for (c, b) in row.iter().enumerate() {
5216 if b.width > 0.0 {
5217 eprintln!(
5218 "[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}",
5219 b.width, b.height, b.depth
5220 );
5221 }
5222 }
5223 }
5224 }
5225
5226 for (r, row) in body.iter().enumerate() {
5228 let is_arrow_row = r % 2 == 1;
5229 for (c, cell) in row.iter().enumerate() {
5230 if let ParseNode::CdArrow {
5231 direction,
5232 label_above,
5233 label_below,
5234 ..
5235 } = cell
5236 {
5237 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
5238 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
5239 let b = layout_cd_arrow(
5240 direction,
5241 label_above.as_deref(),
5242 label_below.as_deref(),
5243 cell_boxes[r][c].width,
5244 col_target_w[c],
5245 0.0,
5246 options,
5247 );
5248 let w = b.width;
5249 (b, w)
5250 } else if is_arrow_row && c % 2 == 0 {
5251 let v_span = row_heights[r] + row_depths[r];
5255 let b = layout_cd_arrow(
5256 direction,
5257 label_above.as_deref(),
5258 label_below.as_deref(),
5259 v_span,
5260 col_widths[c],
5261 0.0,
5262 options,
5263 );
5264 let w = b.width;
5265 (b, w)
5266 } else {
5267 continue;
5268 };
5269 col_widths[c] = col_widths[c].max(col_w);
5270 cell_boxes[r][c] = new_box;
5271 }
5272 }
5273 }
5274
5275 #[cfg(debug_assertions)]
5276 {
5277 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
5278 }
5279
5280 for rd in &mut row_depths {
5283 *rd += jot;
5284 }
5285
5286 let col_gap = 0.5;
5291
5292 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
5294
5295 let col_separators = vec![None; num_cols + 1];
5297
5298 let mut total_height = 0.0_f64;
5299 let mut row_positions = Vec::with_capacity(num_rows);
5300 for r in 0..num_rows {
5301 total_height += row_heights[r];
5302 row_positions.push(total_height);
5303 total_height += row_depths[r];
5304 }
5305
5306 let offset = total_height / 2.0 + metrics.axis_height;
5307 let height = offset;
5308 let depth = total_height - offset;
5309
5310 let total_width =
5312 col_widths.iter().sum::<f64>() + col_gap * (num_cols.saturating_sub(1)) as f64;
5313
5314 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
5316
5317 LayoutBox {
5318 width: total_width,
5319 height,
5320 depth,
5321 content: BoxContent::Array {
5322 cells: cell_boxes,
5323 col_widths,
5324 col_aligns,
5325 row_heights,
5326 row_depths,
5327 col_gap,
5328 offset,
5329 content_x_offset: 0.0,
5330 col_separators,
5331 hlines_before_row,
5332 rule_thickness: 0.04 * pt,
5333 double_rule_sep: metrics.double_rule_sep,
5334 array_inner_width: total_width,
5335 tag_gap_em: 0.0,
5336 tag_col_width: 0.0,
5337 row_tags: (0..num_rows).map(|_| None).collect(),
5338 tags_left: false,
5339 },
5340 color: options.color,
5341 }
5342}
5343
5344struct ProofTreeLayout {
5345 width: f64,
5346 height: f64,
5347 depth: f64,
5348 root_width: f64,
5349 root_center_x: f64,
5350 children: Vec<PlacedBox>,
5351 rules: Vec<ProofRule>,
5352}
5353
5354fn layout_proof_roman_content(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
5360 if nodes.is_empty() {
5361 return LayoutBox::new_empty();
5362 }
5363 let group = ParseNode::OrdGroup {
5364 mode: Mode::Math,
5365 body: nodes.to_vec(),
5366 semisimple: None,
5367 loc: None,
5368 };
5369 let wrapped = ParseNode::Font {
5370 mode: Mode::Math,
5371 font: "mathrm".to_string(),
5372 body: Box::new(group),
5373 loc: None,
5374 };
5375 layout_node(&wrapped, options)
5376}
5377
5378fn layout_proof_cell_content(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
5379 let mut box_ = layout_proof_roman_content(nodes, options);
5380 box_.depth = box_.depth.max(0.20);
5381 box_
5382}
5383
5384fn layout_proof_tree(tree: &ProofBranch, options: &LayoutOptions) -> LayoutBox {
5385 let laid = layout_proof_branch(tree, options);
5386 LayoutBox {
5387 width: laid.width,
5388 height: laid.height,
5389 depth: laid.depth,
5390 content: BoxContent::ProofTree {
5391 children: laid.children,
5392 rules: laid.rules,
5393 },
5394 color: options.color,
5395 }
5396}
5397
5398fn layout_proof_branch(tree: &ProofBranch, options: &LayoutOptions) -> ProofTreeLayout {
5399 let metrics = options.metrics();
5400 let rule_thickness = (metrics.default_rule_thickness * 2.0).max(0.08);
5401 let premise_gap = 2.0;
5407 let label_gap = 0.08;
5408 let vertical_gap = 0.32;
5409
5410 let conclusion = layout_proof_cell_content(&tree.conclusion, options);
5411 let conclusion_width = conclusion.width;
5412
5413 if tree.premises.is_empty() {
5414 let width = conclusion.width;
5415 let height = conclusion.height;
5416 let depth = conclusion.depth;
5417 return ProofTreeLayout {
5418 width,
5419 height,
5420 depth,
5421 root_width: width,
5422 root_center_x: width / 2.0,
5423 children: vec![PlacedBox {
5424 box_: conclusion,
5425 x: 0.0,
5426 baseline_y: height,
5427 }],
5428 rules: Vec::new(),
5429 };
5430 }
5431
5432 let premise_layouts: Vec<ProofTreeLayout> = tree
5433 .premises
5434 .iter()
5435 .map(|p| layout_proof_branch(p, options))
5436 .collect();
5437
5438 let premise_width = premise_layouts.iter().map(|p| p.width).sum::<f64>()
5439 + premise_gap * premise_layouts.len().saturating_sub(1) as f64;
5440 let premise_height = premise_layouts
5441 .iter()
5442 .map(|p| p.height)
5443 .fold(0.0_f64, f64::max);
5444 let premise_depth = premise_layouts
5445 .iter()
5446 .map(|p| p.depth)
5447 .fold(0.0_f64, f64::max);
5448 let premise_total = premise_height + premise_depth;
5449
5450 let mut premise_root_left = f64::INFINITY;
5451 let mut premise_root_right = f64::NEG_INFINITY;
5452 let mut cursor = 0.0_f64;
5453 for premise in &premise_layouts {
5454 let root_left = cursor + premise.root_center_x - premise.root_width / 2.0;
5455 let root_right = cursor + premise.root_center_x + premise.root_width / 2.0;
5456 premise_root_left = premise_root_left.min(root_left);
5457 premise_root_right = premise_root_right.max(root_right);
5458 cursor += premise.width + premise_gap;
5459 }
5460 let rule_padding = if tree.premises.len() > 1 { 0.65 } else { 0.0 };
5461 let premise_rule_width = premise_root_right - premise_root_left + rule_padding;
5462 let premise_root_center = (premise_root_left + premise_root_right) / 2.0;
5463 let rule_width = premise_rule_width.max(conclusion_width).max(1.12);
5464 let core_width = premise_width.max(rule_width).max(conclusion_width);
5465 let center_x = core_width / 2.0;
5466 let premise_start_x = center_x - premise_width / 2.0;
5467 let root_center_x = premise_start_x + premise_root_center;
5468 let rule_x = root_center_x - rule_width / 2.0;
5469 let rule_y = premise_total + vertical_gap + rule_thickness / 2.0;
5470 let conclusion_x = root_center_x - conclusion_width / 2.0;
5471 let conclusion_baseline_y = rule_y + rule_thickness / 2.0 + vertical_gap + conclusion.height;
5472
5473 let mut children = Vec::new();
5474 let mut rules = Vec::new();
5475 let mut min_x = 0.0_f64;
5476 let mut max_x = core_width;
5477
5478 if tree.root_at_top {
5479 let conclusion_baseline_y = conclusion.height;
5480 let rule_y = conclusion.height + conclusion.depth + vertical_gap + rule_thickness / 2.0;
5481 let premise_top_y = rule_y + rule_thickness / 2.0 + vertical_gap;
5482 let premise_baseline_y = premise_top_y + premise_height;
5483
5484 min_x = min_x.min(conclusion_x);
5485 max_x = max_x.max(conclusion_x + conclusion_width);
5486 children.push(PlacedBox {
5487 box_: conclusion,
5488 x: conclusion_x,
5489 baseline_y: conclusion_baseline_y,
5490 });
5491
5492 let mut cursor = premise_start_x;
5493 for premise in premise_layouts {
5494 let child_top_y = premise_baseline_y - premise.height;
5495 for child in premise.children {
5496 let x = cursor + child.x;
5497 let y = child_top_y + child.baseline_y;
5498 min_x = min_x.min(x);
5499 max_x = max_x.max(x + child.box_.width);
5500 children.push(PlacedBox {
5501 box_: child.box_,
5502 x,
5503 baseline_y: y,
5504 });
5505 }
5506 for rule in premise.rules {
5507 min_x = min_x.min(cursor + rule.x);
5508 max_x = max_x.max(cursor + rule.x + rule.width);
5509 rules.push(ProofRule {
5510 x: cursor + rule.x,
5511 y: child_top_y + rule.y,
5512 width: rule.width,
5513 thickness: rule.thickness,
5514 dashed: rule.dashed,
5515 });
5516 }
5517 cursor += premise.width + premise_gap;
5518 }
5519
5520 if !matches!(tree.line_style, ProofLineStyle::None) {
5521 rules.push(ProofRule {
5522 x: rule_x,
5523 y: rule_y,
5524 width: rule_width,
5525 thickness: rule_thickness,
5526 dashed: matches!(tree.line_style, ProofLineStyle::Dashed),
5527 });
5528 }
5529
5530 if let Some(label) = tree.left_label.as_ref() {
5531 let label_opts = options.with_style(options.style.text());
5532 let label_box = layout_proof_roman_content(label, &label_opts);
5533 let x = rule_x - label_gap - label_box.width;
5534 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
5535 min_x = min_x.min(x);
5536 max_x = max_x.max(x + label_box.width);
5537 children.push(PlacedBox {
5538 box_: label_box,
5539 x,
5540 baseline_y,
5541 });
5542 }
5543
5544 if let Some(label) = tree.right_label.as_ref() {
5545 let label_opts = options.with_style(options.style.text());
5546 let label_box = layout_proof_roman_content(label, &label_opts);
5547 let x = rule_x + rule_width + label_gap;
5548 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
5549 min_x = min_x.min(x);
5550 max_x = max_x.max(x + label_box.width);
5551 children.push(PlacedBox {
5552 box_: label_box,
5553 x,
5554 baseline_y,
5555 });
5556 }
5557
5558 let mut root_center_x = root_center_x;
5559 if min_x < 0.0 {
5560 let shift_x = -min_x;
5561 for child in &mut children {
5562 child.x += shift_x;
5563 }
5564 for rule in &mut rules {
5565 rule.x += shift_x;
5566 }
5567 root_center_x += shift_x;
5568 }
5569
5570 return ProofTreeLayout {
5571 width: max_x - min_x,
5572 height: conclusion_baseline_y,
5573 depth: children
5574 .iter()
5575 .map(|c| c.baseline_y + c.box_.depth - conclusion_baseline_y)
5576 .fold(0.0_f64, f64::max)
5577 .max(0.0),
5578 root_width: conclusion_width,
5579 root_center_x,
5580 children,
5581 rules,
5582 };
5583 }
5584
5585 let mut cursor = premise_start_x;
5586 for premise in premise_layouts {
5587 let baseline_y = premise_height;
5588 let child_top_y = baseline_y - premise.height;
5589 for child in premise.children {
5590 let x = cursor + child.x;
5591 let y = child_top_y + child.baseline_y;
5592 min_x = min_x.min(x);
5593 max_x = max_x.max(x + child.box_.width);
5594 children.push(PlacedBox {
5595 box_: child.box_,
5596 x,
5597 baseline_y: y,
5598 });
5599 }
5600 for rule in premise.rules {
5601 min_x = min_x.min(cursor + rule.x);
5602 max_x = max_x.max(cursor + rule.x + rule.width);
5603 rules.push(ProofRule {
5604 x: cursor + rule.x,
5605 y: child_top_y + rule.y,
5606 width: rule.width,
5607 thickness: rule.thickness,
5608 dashed: rule.dashed,
5609 });
5610 }
5611 cursor += premise.width + premise_gap;
5612 }
5613
5614 min_x = min_x.min(conclusion_x);
5615 max_x = max_x.max(conclusion_x + conclusion_width);
5616 children.push(PlacedBox {
5617 box_: conclusion,
5618 x: conclusion_x,
5619 baseline_y: conclusion_baseline_y,
5620 });
5621
5622 if !matches!(tree.line_style, ProofLineStyle::None) {
5623 rules.push(ProofRule {
5624 x: rule_x,
5625 y: rule_y,
5626 width: rule_width,
5627 thickness: rule_thickness,
5628 dashed: matches!(tree.line_style, ProofLineStyle::Dashed),
5629 });
5630 }
5631
5632 if let Some(label) = tree.left_label.as_ref() {
5633 let label_opts = options.with_style(options.style.text());
5634 let label_box = layout_proof_roman_content(label, &label_opts);
5635 let x = rule_x - label_gap - label_box.width;
5636 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
5637 min_x = min_x.min(x);
5638 max_x = max_x.max(x + label_box.width);
5639 children.push(PlacedBox {
5640 box_: label_box,
5641 x,
5642 baseline_y,
5643 });
5644 }
5645
5646 if let Some(label) = tree.right_label.as_ref() {
5647 let label_opts = options.with_style(options.style.text());
5648 let label_box = layout_proof_roman_content(label, &label_opts);
5649 let x = rule_x + rule_width + label_gap;
5650 let baseline_y = rule_y + (label_box.height - label_box.depth) / 2.0;
5651 min_x = min_x.min(x);
5652 max_x = max_x.max(x + label_box.width);
5653 children.push(PlacedBox {
5654 box_: label_box,
5655 x,
5656 baseline_y,
5657 });
5658 }
5659
5660 let mut root_center_x = root_center_x;
5661 if min_x < 0.0 {
5662 let shift = -min_x;
5663 for child in &mut children {
5664 child.x += shift;
5665 }
5666 for rule in &mut rules {
5667 rule.x += shift;
5668 }
5669 root_center_x += shift;
5670 }
5671
5672 ProofTreeLayout {
5673 width: max_x - min_x,
5674 height: conclusion_baseline_y,
5675 depth: children
5676 .iter()
5677 .map(|c| c.baseline_y + c.box_.depth - conclusion_baseline_y)
5678 .fold(0.0_f64, f64::max),
5679 root_width: conclusion_width,
5680 root_center_x,
5681 children,
5682 rules,
5683 }
5684}
5685
5686fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
5687 let mid = width / 2.0;
5688 let q = height * 0.6;
5689 if is_over {
5690 vec![
5691 PathCommand::MoveTo { x: 0.0, y: 0.0 },
5692 PathCommand::QuadTo {
5693 x1: 0.0,
5694 y1: -q,
5695 x: mid * 0.4,
5696 y: -q,
5697 },
5698 PathCommand::LineTo {
5699 x: mid - 0.05,
5700 y: -q,
5701 },
5702 PathCommand::LineTo { x: mid, y: -height },
5703 PathCommand::LineTo {
5704 x: mid + 0.05,
5705 y: -q,
5706 },
5707 PathCommand::LineTo {
5708 x: width - mid * 0.4,
5709 y: -q,
5710 },
5711 PathCommand::QuadTo {
5712 x1: width,
5713 y1: -q,
5714 x: width,
5715 y: 0.0,
5716 },
5717 ]
5718 } else {
5719 vec![
5720 PathCommand::MoveTo { x: 0.0, y: 0.0 },
5721 PathCommand::QuadTo {
5722 x1: 0.0,
5723 y1: q,
5724 x: mid * 0.4,
5725 y: q,
5726 },
5727 PathCommand::LineTo {
5728 x: mid - 0.05,
5729 y: q,
5730 },
5731 PathCommand::LineTo { x: mid, y: height },
5732 PathCommand::LineTo {
5733 x: mid + 0.05,
5734 y: q,
5735 },
5736 PathCommand::LineTo {
5737 x: width - mid * 0.4,
5738 y: q,
5739 },
5740 PathCommand::QuadTo {
5741 x1: width,
5742 y1: q,
5743 x: width,
5744 y: 0.0,
5745 },
5746 ]
5747 }
5748}
5749
5750#[cfg(test)]
5751mod missing_glyph_width_em_tests {
5752 use super::{missing_glyph_height_em, missing_glyph_width_em};
5753 use ratex_font::get_global_metrics;
5754
5755 #[test]
5756 fn supplementary_plane_emoji_is_one_em() {
5757 assert_eq!(missing_glyph_width_em('😊'), 1.0);
5758 assert_eq!(missing_glyph_width_em('🚀'), 1.0);
5759 }
5760
5761 #[test]
5762 fn supplementary_plane_emoji_uses_shorter_box_height() {
5763 let m = get_global_metrics(0);
5764 let emoji_h = missing_glyph_height_em('😊', m);
5765 let default_h = (m.quad * 0.92).max(m.x_height);
5766 assert!(
5767 emoji_h < default_h,
5768 "tall placeholder box must not push \\sqrt past KaTeX's small-surd threshold"
5769 );
5770 assert!((emoji_h - 0.74).abs() < 1e-9);
5771 }
5772
5773 #[test]
5774 fn dingbats_block_is_one_em() {
5775 assert_eq!(missing_glyph_width_em('\u{2708}'), 1.0); }
5777
5778 #[test]
5779 fn miscellaneous_symbols_is_one_em() {
5780 assert_eq!(missing_glyph_width_em('\u{2605}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2615}'), 1.0); }
5783
5784 #[test]
5785 fn misc_symbols_and_arrows_is_one_em() {
5786 assert_eq!(missing_glyph_width_em('\u{2B50}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2B1B}'), 1.0); }
5789
5790 #[test]
5791 fn latin_without_metrics_stays_half_em() {
5792 assert_eq!(missing_glyph_width_em('z'), 0.5);
5793 }
5794}
5795
5796#[cfg(test)]
5797mod cjk_font_switching_tests {
5798 use super::super::to_display::to_display_list;
5799 use super::*;
5800 use ratex_parser::parser::parse;
5801 use ratex_types::display_item::DisplayItem;
5802
5803 fn first_glyph_font_name(latex: &str) -> Option<String> {
5804 let ast = parse(latex).ok()?;
5805 let lbox = layout(&ast, &LayoutOptions::default());
5806 let dl = to_display_list(&lbox);
5807 for item in &dl.items {
5808 if let DisplayItem::GlyphPath { font, .. } = item {
5809 return Some(font.clone());
5810 }
5811 }
5812 None
5813 }
5814
5815 #[test]
5816 fn cjk_in_text_uses_cjk_regular() {
5817 assert_eq!(
5818 first_glyph_font_name(r"\text{中}").as_deref(),
5819 Some("CJK-Regular")
5820 );
5821 }
5822
5823 #[test]
5824 fn emoji_in_text_uses_cjk_regular() {
5825 assert_eq!(
5826 first_glyph_font_name(r"\text{😊}").as_deref(),
5827 Some("CJK-Regular")
5828 );
5829 }
5830
5831 #[test]
5832 fn latin_in_text_is_not_cjk() {
5833 assert_ne!(
5834 first_glyph_font_name(r"\text{a}").as_deref(),
5835 Some("CJK-Regular")
5836 );
5837 }
5838
5839 #[test]
5840 fn hiragana_in_text_uses_cjk_regular() {
5841 assert_eq!(
5842 first_glyph_font_name(r"\text{あ}").as_deref(),
5843 Some("CJK-Regular")
5844 );
5845 }
5846}