1use ratex_font::{get_char_metrics, get_global_metrics, FontId};
2use ratex_parser::parse_node::{ArrayTag, AtomFamily, Mode, ParseNode};
3use ratex_types::color::Color;
4use ratex_types::math_style::MathStyle;
5use ratex_types::path_command::PathCommand;
6
7use crate::hbox::make_hbox;
8use crate::layout_box::{BoxContent, LayoutBox};
9use crate::layout_options::LayoutOptions;
10
11use crate::katex_svg::parse_svg_path_data;
12use crate::spacing::{atom_spacing, mu_to_em, MathClass};
13use crate::stacked_delim::make_stacked_delim_if_needed;
14
15const NULL_DELIMITER_SPACE: f64 = 0.12;
18
19pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
21 layout_expression(nodes, options, true)
22}
23
24fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
27 let n = raw.len();
28 let mut eff = raw.to_vec();
29 for i in 0..n {
30 if raw[i] != Some(MathClass::Bin) {
31 continue;
32 }
33 let prev = if i == 0 { None } else { raw[i - 1] };
34 let left_cancel = matches!(
35 prev,
36 None
37 | Some(MathClass::Bin)
38 | Some(MathClass::Open)
39 | Some(MathClass::Rel)
40 | Some(MathClass::Op)
41 | Some(MathClass::Punct)
42 );
43 if left_cancel {
44 eff[i] = Some(MathClass::Ord);
45 }
46 }
47 for i in 0..n {
48 if raw[i] != Some(MathClass::Bin) {
49 continue;
50 }
51 let next = if i + 1 < n { raw[i + 1] } else { None };
52 let right_cancel = matches!(
53 next,
54 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
55 );
56 if right_cancel {
57 eff[i] = Some(MathClass::Ord);
58 }
59 }
60 eff
61}
62
63fn node_is_middle_fence(node: &ParseNode) -> bool {
68 matches!(node, ParseNode::Middle { .. })
69}
70
71fn layout_expression(
73 nodes: &[ParseNode],
74 options: &LayoutOptions,
75 is_real_group: bool,
76) -> LayoutBox {
77 if nodes.is_empty() {
78 return LayoutBox::new_empty();
79 }
80
81 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
83 if has_cr {
84 return layout_multiline(nodes, options, is_real_group);
85 }
86
87 let raw_classes: Vec<Option<MathClass>> =
88 nodes.iter().map(node_math_class).collect();
89 let eff_classes = apply_bin_cancellation(&raw_classes);
90
91 let mut children = Vec::new();
92 let mut prev_class: Option<MathClass> = None;
93 let mut prev_class_node_idx: Option<usize> = None;
95
96 for (i, node) in nodes.iter().enumerate() {
97 let lbox = layout_node(node, options);
98 let cur_class = eff_classes.get(i).copied().flatten();
99
100 if is_real_group {
101 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
102 let prev_middle = prev_class_node_idx
103 .is_some_and(|j| node_is_middle_fence(&nodes[j]));
104 let cur_middle = node_is_middle_fence(node);
105 let mu = if prev_middle || cur_middle {
106 0.0
107 } else {
108 atom_spacing(prev, cur, options.style.is_tight())
109 };
110 let mu = if let Some(cap) = options.align_relation_spacing {
111 if prev == MathClass::Rel || cur == MathClass::Rel {
112 mu.min(cap)
113 } else {
114 mu
115 }
116 } else {
117 mu
118 };
119 if mu > 0.0 {
120 let em = mu_to_em(mu, options.metrics().quad);
121 children.push(LayoutBox::new_kern(em));
122 }
123 }
124 }
125
126 if cur_class.is_some() {
127 prev_class = cur_class;
128 prev_class_node_idx = Some(i);
129 }
130
131 children.push(lbox);
132 }
133
134 make_hbox(children)
135}
136
137fn layout_multiline(
139 nodes: &[ParseNode],
140 options: &LayoutOptions,
141 is_real_group: bool,
142) -> LayoutBox {
143 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
144 let metrics = options.metrics();
145 let pt = 1.0 / metrics.pt_per_em;
146 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
151 let mut start = 0;
152 for (i, node) in nodes.iter().enumerate() {
153 if matches!(node, ParseNode::Cr { .. }) {
154 rows.push(&nodes[start..i]);
155 start = i + 1;
156 }
157 }
158 rows.push(&nodes[start..]);
159
160 let row_boxes: Vec<LayoutBox> = rows
161 .iter()
162 .map(|row| layout_expression(row, options, is_real_group))
163 .collect();
164
165 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
166
167 let mut vchildren: Vec<VBoxChild> = Vec::new();
168 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
169 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
170 for (i, row) in row_boxes.iter().enumerate() {
171 if i > 0 {
172 let prev_depth = row_boxes[i - 1].depth;
174 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
175 vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
176 h += gap + row.height + prev_depth;
177 }
178 vchildren.push(VBoxChild {
179 kind: VBoxChildKind::Box(Box::new(row.clone())),
180 shift: 0.0,
181 });
182 }
183
184 LayoutBox {
185 width: total_width,
186 height: h,
187 depth: d,
188 content: BoxContent::VBox(vchildren),
189 color: options.color,
190 }
191}
192
193
194fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
196 match node {
197 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
198 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
199 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
200 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
201
202 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
203
204 ParseNode::SupSub {
205 base, sup, sub, ..
206 } => {
207 if let Some(base_node) = base.as_deref() {
208 if should_use_op_limits(base_node, options) {
209 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
210 }
211 }
212 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
213 }
214
215 ParseNode::GenFrac {
216 numer,
217 denom,
218 has_bar_line,
219 bar_size,
220 left_delim,
221 right_delim,
222 continued,
223 ..
224 } => {
225 let bar_thickness = if *has_bar_line {
226 bar_size
227 .as_ref()
228 .map(|m| measurement_to_em(m, options))
229 .unwrap_or(options.metrics().default_rule_thickness)
230 } else {
231 0.0
232 };
233 let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
234
235 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
236 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
237
238 if has_left || has_right {
239 let total_h = genfrac_delim_target_height(options);
240 let left_d = left_delim.as_deref().unwrap_or(".");
241 let right_d = right_delim.as_deref().unwrap_or(".");
242 let left_box = make_stretchy_delim(left_d, total_h, options);
243 let right_box = make_stretchy_delim(right_d, total_h, options);
244
245 let width = left_box.width + frac.width + right_box.width;
246 let height = frac.height.max(left_box.height).max(right_box.height);
247 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
248
249 LayoutBox {
250 width,
251 height,
252 depth,
253 content: BoxContent::LeftRight {
254 left: Box::new(left_box),
255 right: Box::new(right_box),
256 inner: Box::new(frac),
257 },
258 color: options.color,
259 }
260 } else {
261 let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE };
262 make_hbox(vec![
263 LayoutBox::new_kern(NULL_DELIMITER_SPACE),
264 frac,
265 LayoutBox::new_kern(right_nds),
266 ])
267 }
268 }
269
270 ParseNode::Sqrt { body, index, .. } => {
271 layout_radical(body, index.as_deref(), options)
272 }
273
274 ParseNode::Op {
275 name,
276 symbol,
277 body,
278 limits,
279 suppress_base_shift,
280 ..
281 } => layout_op(
282 name.as_deref(),
283 *symbol,
284 body.as_deref(),
285 *limits,
286 suppress_base_shift.unwrap_or(false),
287 options,
288 ),
289
290 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
291
292 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
293
294 ParseNode::Kern { dimension, .. } => {
295 let em = measurement_to_em(dimension, options);
296 LayoutBox::new_kern(em)
297 }
298
299 ParseNode::Color { color, body, .. } => {
300 let new_color = Color::parse(color).unwrap_or(options.color);
301 let new_opts = options.with_color(new_color);
302 let mut lbox = layout_expression(body, &new_opts, true);
303 lbox.color = new_color;
304 lbox
305 }
306
307 ParseNode::Styling { style, body, .. } => {
308 let new_style = match style {
309 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
310 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
311 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
312 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
313 };
314 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
315 let new_opts = options.with_style(new_style);
316 let inner = layout_expression(body, &new_opts, true);
317 if (ratio - 1.0).abs() < 0.001 {
318 inner
319 } else {
320 LayoutBox {
321 width: inner.width * ratio,
322 height: inner.height * ratio,
323 depth: inner.depth * ratio,
324 content: BoxContent::Scaled {
325 body: Box::new(inner),
326 child_scale: ratio,
327 },
328 color: options.color,
329 }
330 }
331 }
332
333 ParseNode::Accent {
334 label, base, is_stretchy, is_shifty, ..
335 } => {
336 let is_below = matches!(label.as_str(), "\\c");
338 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
339 }
340
341 ParseNode::AccentUnder {
342 label, base, is_stretchy, ..
343 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
344
345 ParseNode::LeftRight {
346 body, left, right, ..
347 } => layout_left_right(body, left, right, options),
348
349 ParseNode::DelimSizing {
350 size, delim, ..
351 } => layout_delim_sizing(*size, delim, options),
352
353 ParseNode::Array {
354 body,
355 cols,
356 arraystretch,
357 add_jot,
358 row_gaps,
359 hlines_before_row,
360 col_separation_type,
361 hskip_before_and_after,
362 is_cd,
363 tags,
364 leqno,
365 ..
366 } => {
367 if is_cd.unwrap_or(false) {
368 layout_cd(body, options)
369 } else {
370 layout_array(
371 body,
372 cols.as_deref(),
373 *arraystretch,
374 add_jot.unwrap_or(false),
375 row_gaps,
376 hlines_before_row,
377 col_separation_type.as_deref(),
378 hskip_before_and_after.unwrap_or(false),
379 tags.as_deref(),
380 leqno.unwrap_or(false),
381 options,
382 )
383 }
384 }
385
386 ParseNode::CdArrow {
387 direction,
388 label_above,
389 label_below,
390 ..
391 } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
392
393 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
394
395 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
396 Some(f) => {
397 let group = ParseNode::OrdGroup {
398 mode: *mode,
399 body: body.clone(),
400 semisimple: None,
401 loc: None,
402 };
403 layout_font(f, &group, options)
404 }
405 None => layout_text(body, options),
406 },
407
408 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
409
410 ParseNode::Href { body, .. } => layout_href(body, options),
411
412 ParseNode::Overline { body, .. } => layout_overline(body, options),
413 ParseNode::Underline { body, .. } => layout_underline(body, options),
414
415 ParseNode::Rule {
416 width: w,
417 height: h,
418 shift,
419 ..
420 } => {
421 let width = measurement_to_em(w, options);
422 let ink_h = measurement_to_em(h, options);
423 let raise = shift
424 .as_ref()
425 .map(|s| measurement_to_em(s, options))
426 .unwrap_or(0.0);
427 let box_height = (raise + ink_h).max(0.0);
428 let box_depth = (-raise).max(0.0);
429 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
430 }
431
432 ParseNode::Phantom { body, .. } => {
433 let inner = layout_expression(body, options, true);
434 LayoutBox {
435 width: inner.width,
436 height: inner.height,
437 depth: inner.depth,
438 content: BoxContent::Empty,
439 color: Color::BLACK,
440 }
441 }
442
443 ParseNode::VPhantom { body, .. } => {
444 let inner = layout_node(body, options);
445 LayoutBox {
446 width: 0.0,
447 height: inner.height,
448 depth: inner.depth,
449 content: BoxContent::Empty,
450 color: Color::BLACK,
451 }
452 }
453
454 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
455 let mut inner = layout_node(body, options);
456 if *smash_height { inner.height = 0.0; }
457 if *smash_depth { inner.depth = 0.0; }
458 inner
459 }
460
461 ParseNode::Middle { delim, .. } => {
462 match options.leftright_delim_height {
463 Some(h) => make_stretchy_delim(delim, h, options),
464 None => {
465 let placeholder = make_stretchy_delim(delim, 1.0, options);
467 LayoutBox {
468 width: placeholder.width,
469 height: 0.0,
470 depth: 0.0,
471 content: BoxContent::Empty,
472 color: options.color,
473 }
474 }
475 }
476 }
477
478 ParseNode::HtmlMathMl { html, .. } => {
479 layout_expression(html, options, true)
480 }
481
482 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
483
484 ParseNode::MathChoice {
485 display, text, script, scriptscript, ..
486 } => {
487 let branch = match options.style {
488 MathStyle::Display | MathStyle::DisplayCramped => display,
489 MathStyle::Text | MathStyle::TextCramped => text,
490 MathStyle::Script | MathStyle::ScriptCramped => script,
491 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
492 };
493 layout_expression(branch, options, true)
494 }
495
496 ParseNode::Lap { alignment, body, .. } => {
497 let inner = layout_node(body, options);
498 let shift = match alignment.as_str() {
499 "llap" => -inner.width,
500 "clap" => -inner.width / 2.0,
501 _ => 0.0, };
503 let mut children = Vec::new();
504 if shift != 0.0 {
505 children.push(LayoutBox::new_kern(shift));
506 }
507 let h = inner.height;
508 let d = inner.depth;
509 children.push(inner);
510 LayoutBox {
511 width: 0.0,
512 height: h,
513 depth: d,
514 content: BoxContent::HBox(children),
515 color: options.color,
516 }
517 }
518
519 ParseNode::HorizBrace {
520 base,
521 is_over,
522 label,
523 ..
524 } => layout_horiz_brace(base, *is_over, label, options),
525
526 ParseNode::XArrow {
527 label, body, below, ..
528 } => layout_xarrow(label, body, below.as_deref(), options),
529
530 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
531
532 ParseNode::HBox { body, .. } => layout_text(body, options),
533
534 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
535 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
536 }
537
538 ParseNode::RaiseBox { dy, body, .. } => {
539 let shift = measurement_to_em(dy, options);
540 layout_raisebox(shift, body, options)
541 }
542
543 ParseNode::VCenter { body, .. } => {
544 let inner = layout_node(body, options);
546 let axis = options.metrics().axis_height;
547 let total = inner.height + inner.depth;
548 let height = total / 2.0 + axis;
549 let depth = total - height;
550 LayoutBox {
551 width: inner.width,
552 height,
553 depth,
554 content: inner.content,
555 color: inner.color,
556 }
557 }
558
559 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
560
561 ParseNode::Tag { tag, .. } => {
562 let text_opts = options.with_style(options.style.text());
563 layout_expression(tag, &text_opts, true)
564 },
565
566 _ => LayoutBox::new_empty(),
568 }
569}
570
571fn missing_glyph_width_em(ch: char) -> f64 {
581 match ch as u32 {
582 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
584 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
586 0xAC00..=0xD7AF => 1.0,
588 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
590 0x1F000..=0x1FAFF => 1.0,
593 0x2700..=0x27BF => 1.0,
595 0x2600..=0x26FF => 1.0,
597 0x2B00..=0x2BFF => 1.0,
599 _ => 0.5,
600 }
601}
602
603fn missing_glyph_height_em(ch: char, m: &ratex_font::MathConstants) -> f64 {
604 let ru = ch as u32;
605 if (0x1F000..=0x1FAFF).contains(&ru) {
606 (m.quad * 0.74).max(m.x_height)
611 } else {
612 (m.quad * 0.92).max(m.x_height)
613 }
614}
615
616fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
617 let m = get_global_metrics(options.style.size_index());
618 let w = missing_glyph_width_em(ch);
619 if w >= 0.99 {
620 let h = missing_glyph_height_em(ch, m);
621 (w, h, 0.0)
622 } else {
623 (w, m.x_height, 0.0)
624 }
625}
626
627#[inline]
629fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
630 if mode == Mode::Math {
631 m.width + m.italic
632 } else {
633 m.width
634 }
635}
636
637fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
638 let ch = resolve_symbol_char(text, mode);
639
640 match ch as u32 {
642 0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
645 }
646
647 let char_code = ch as u32;
648
649 if let Some((font_id, metric_cp)) =
650 ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
651 {
652 let m = get_char_metrics(font_id, metric_cp);
653 let (width, height, depth) = match m {
654 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
655 None => missing_glyph_metrics_fallback(ch, options),
656 };
657 return LayoutBox {
658 width,
659 height,
660 depth,
661 content: BoxContent::Glyph {
662 font_id,
663 char_code,
664 },
665 color: options.color,
666 };
667 }
668
669 let mut font_id = select_font(text, ch, mode, options);
670 let mut metrics = get_char_metrics(font_id, char_code);
671
672 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
673 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
674 font_id = FontId::MathItalic;
675 metrics = Some(m);
676 }
677 }
678
679 let (width, height, depth) = if let Some(m) = metrics {
685 (math_glyph_advance_em(&m, mode), m.height, m.depth)
686 } else if mode == Mode::Math {
687 let size_font = if options.style.is_display() {
688 FontId::Size2Regular
689 } else {
690 FontId::Size1Regular
691 };
692 match get_char_metrics(size_font, char_code)
693 .or_else(|| get_char_metrics(FontId::Size1Regular, char_code))
694 {
695 Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
696 None => missing_glyph_metrics_fallback(ch, options),
697 }
698 } else {
699 missing_glyph_metrics_fallback(ch, options)
700 };
701
702 if metrics.is_none() && missing_glyph_width_em(ch) >= 0.99 {
705 font_id = FontId::CjkRegular;
706 }
707
708 LayoutBox {
709 width,
710 height,
711 depth,
712 content: BoxContent::Glyph {
713 font_id,
714 char_code,
715 },
716 color: options.color,
717 }
718}
719
720fn resolve_symbol_char(text: &str, mode: Mode) -> char {
722 let font_mode = match mode {
723 Mode::Math => ratex_font::Mode::Math,
724 Mode::Text => ratex_font::Mode::Text,
725 };
726
727 if let Some(raw) = text.chars().next() {
728 let ru = raw as u32;
729 if (0x1D400..=0x1D7FF).contains(&ru) {
730 return raw;
731 }
732 }
733
734 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
735 if let Some(cp) = info.codepoint {
736 return cp;
737 }
738 }
739
740 text.chars().next().unwrap_or('?')
741}
742
743fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
747 let font_mode = match mode {
748 Mode::Math => ratex_font::Mode::Math,
749 Mode::Text => ratex_font::Mode::Text,
750 };
751
752 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
753 if info.font == ratex_font::SymbolFont::Ams {
754 return FontId::AmsRegular;
755 }
756 }
757
758 match mode {
759 Mode::Math => {
760 if resolved_char.is_ascii_lowercase()
761 || resolved_char.is_ascii_uppercase()
762 || is_math_italic_greek(resolved_char)
763 {
764 FontId::MathItalic
765 } else {
766 FontId::MainRegular
767 }
768 }
769 Mode::Text => FontId::MainRegular,
770 }
771}
772
773fn is_math_italic_greek(ch: char) -> bool {
776 matches!(ch,
777 '\u{03B1}'..='\u{03C9}' |
778 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
779 '\u{03F1}' | '\u{03F5}'
780 )
781}
782
783fn is_arrow_accent(label: &str) -> bool {
784 matches!(
785 label,
786 "\\overrightarrow"
787 | "\\overleftarrow"
788 | "\\Overrightarrow"
789 | "\\overleftrightarrow"
790 | "\\underrightarrow"
791 | "\\underleftarrow"
792 | "\\underleftrightarrow"
793 | "\\overleftharpoon"
794 | "\\overrightharpoon"
795 | "\\overlinesegment"
796 | "\\underlinesegment"
797 )
798}
799
800fn layout_fraction(
805 numer: &ParseNode,
806 denom: &ParseNode,
807 bar_thickness: f64,
808 continued: bool,
809 options: &LayoutOptions,
810) -> LayoutBox {
811 let numer_s = options.style.numerator();
812 let denom_s = options.style.denominator();
813 let numer_style = options.with_style(numer_s);
814 let denom_style = options.with_style(denom_s);
815
816 let mut numer_box = layout_node(numer, &numer_style);
817 if continued {
819 let pt = options.metrics().pt_per_em;
820 let h_min = 8.5 / pt;
821 let d_min = 3.5 / pt;
822 if numer_box.height < h_min {
823 numer_box.height = h_min;
824 }
825 if numer_box.depth < d_min {
826 numer_box.depth = d_min;
827 }
828 }
829 let denom_box = layout_node(denom, &denom_style);
830
831 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
833 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
834
835 let numer_height = numer_box.height * numer_ratio;
836 let numer_depth = numer_box.depth * numer_ratio;
837 let denom_height = denom_box.height * denom_ratio;
838 let denom_depth = denom_box.depth * denom_ratio;
839 let numer_width = numer_box.width * numer_ratio;
840 let denom_width = denom_box.width * denom_ratio;
841
842 let metrics = options.metrics();
843 let axis = metrics.axis_height;
844 let rule = bar_thickness;
845
846 let (mut num_shift, mut den_shift) = if options.style.is_display() {
848 (metrics.num1, metrics.denom1)
849 } else if bar_thickness > 0.0 {
850 (metrics.num2, metrics.denom2)
851 } else {
852 (metrics.num3, metrics.denom2)
853 };
854
855 if bar_thickness > 0.0 {
856 let min_clearance = if options.style.is_display() {
857 3.0 * rule
858 } else {
859 rule
860 };
861
862 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
863 if num_clearance < min_clearance {
864 num_shift += min_clearance - num_clearance;
865 }
866
867 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
868 if den_clearance < min_clearance {
869 den_shift += min_clearance - den_clearance;
870 }
871 } else {
872 let min_gap = if options.style.is_display() {
873 7.0 * metrics.default_rule_thickness
874 } else {
875 3.0 * metrics.default_rule_thickness
876 };
877
878 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
879 if gap < min_gap {
880 let adjust = (min_gap - gap) / 2.0;
881 num_shift += adjust;
882 den_shift += adjust;
883 }
884 }
885
886 let total_width = numer_width.max(denom_width);
887 let height = numer_height + num_shift;
888 let depth = denom_depth + den_shift;
889
890 LayoutBox {
891 width: total_width,
892 height,
893 depth,
894 content: BoxContent::Fraction {
895 numer: Box::new(numer_box),
896 denom: Box::new(denom_box),
897 numer_shift: num_shift,
898 denom_shift: den_shift,
899 bar_thickness: rule,
900 numer_scale: numer_ratio,
901 denom_scale: denom_ratio,
902 },
903 color: options.color,
904 }
905}
906
907fn layout_supsub(
912 base: Option<&ParseNode>,
913 sup: Option<&ParseNode>,
914 sub: Option<&ParseNode>,
915 options: &LayoutOptions,
916 inherited_font: Option<FontId>,
917) -> LayoutBox {
918 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
919 Some(fid) => layout_with_font(n, fid, opts),
920 None => layout_node(n, opts),
921 };
922
923 let horiz_brace_over = matches!(
924 base,
925 Some(ParseNode::HorizBrace {
926 is_over: true,
927 ..
928 })
929 );
930 let horiz_brace_under = matches!(
931 base,
932 Some(ParseNode::HorizBrace {
933 is_over: false,
934 ..
935 })
936 );
937 let center_scripts = horiz_brace_over || horiz_brace_under;
938
939 let base_box = base
940 .map(|b| layout_child(b, options))
941 .unwrap_or_else(LayoutBox::new_empty);
942
943 let is_char_box = base.is_some_and(is_character_box);
944 let metrics = options.metrics();
945 let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
949
950 let sup_style = options.style.superscript();
951 let sub_style = options.style.subscript();
952
953 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
954 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
955
956 let sup_box = sup.map(|s| {
957 let sup_opts = options.with_style(sup_style);
958 layout_child(s, &sup_opts)
959 });
960
961 let sub_box = sub.map(|s| {
962 let sub_opts = options.with_style(sub_style);
963 layout_child(s, &sub_opts)
964 });
965
966 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
967 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
968 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
969 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
970
971 let sup_style_metrics = get_global_metrics(sup_style.size_index());
973 let sub_style_metrics = get_global_metrics(sub_style.size_index());
974
975 let mut sup_shift = if !is_char_box && sup_box.is_some() {
978 base_box.height - sup_style_metrics.sup_drop * sup_ratio
979 } else {
980 0.0
981 };
982
983 let mut sub_shift = if !is_char_box && sub_box.is_some() {
984 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
985 } else {
986 0.0
987 };
988
989 let min_sup_shift = if options.style.is_cramped() {
990 metrics.sup3
991 } else if options.style.is_display() {
992 metrics.sup1
993 } else {
994 metrics.sup2
995 };
996
997 if sup_box.is_some() && sub_box.is_some() {
998 sup_shift = sup_shift
1000 .max(min_sup_shift)
1001 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1002 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
1005 let max_width = 4.0 * rule_width;
1006 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
1007 if gap < max_width {
1008 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
1009 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
1010 if psi > 0.0 {
1011 sup_shift += psi;
1012 sub_shift -= psi;
1013 }
1014 }
1015 } else if sub_box.is_some() {
1016 sub_shift = sub_shift
1018 .max(metrics.sub1)
1019 .max(sub_height_scaled - 0.8 * metrics.x_height);
1020 } else if sup_box.is_some() {
1021 sup_shift = sup_shift
1023 .max(min_sup_shift)
1024 .max(sup_depth_scaled + 0.25 * metrics.x_height);
1025 }
1026
1027 if horiz_brace_over && sup_box.is_some() {
1031 sup_shift = base_box.height + 0.2 + sup_depth_scaled;
1032 }
1033 if horiz_brace_under && sub_box.is_some() {
1034 sub_shift = base_box.depth + 0.2 + sub_height_scaled;
1035 }
1036
1037 let italic_correction = 0.0;
1040
1041 let sub_h_kern = if sub_box.is_some() && !center_scripts {
1044 -glyph_italic(&base_box)
1045 } else {
1046 0.0
1047 };
1048
1049 let mut height = base_box.height;
1051 let mut depth = base_box.depth;
1052 let mut total_width = base_box.width;
1053
1054 if let Some(ref sup_b) = sup_box {
1055 height = height.max(sup_shift + sup_height_scaled);
1056 if center_scripts {
1057 total_width = total_width.max(sup_b.width * sup_ratio + script_space);
1058 } else {
1059 total_width = total_width.max(
1060 base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
1061 );
1062 }
1063 }
1064 if let Some(ref sub_b) = sub_box {
1065 depth = depth.max(sub_shift + sub_depth_scaled);
1066 if center_scripts {
1067 total_width = total_width.max(sub_b.width * sub_ratio + script_space);
1068 } else {
1069 total_width = total_width.max(
1070 base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
1071 );
1072 }
1073 }
1074
1075 LayoutBox {
1076 width: total_width,
1077 height,
1078 depth,
1079 content: BoxContent::SupSub {
1080 base: Box::new(base_box),
1081 sup: sup_box.map(Box::new),
1082 sub: sub_box.map(Box::new),
1083 sup_shift,
1084 sub_shift,
1085 sup_scale: sup_ratio,
1086 sub_scale: sub_ratio,
1087 center_scripts,
1088 italic_correction,
1089 sub_h_kern,
1090 },
1091 color: options.color,
1092 }
1093}
1094
1095fn layout_radical(
1100 body: &ParseNode,
1101 index: Option<&ParseNode>,
1102 options: &LayoutOptions,
1103) -> LayoutBox {
1104 let cramped = options.style.cramped();
1105 let cramped_opts = options.with_style(cramped);
1106 let mut body_box = layout_node(body, &cramped_opts);
1107
1108 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
1110 body_box.height *= body_ratio;
1111 body_box.depth *= body_ratio;
1112 body_box.width *= body_ratio;
1113
1114 if body_box.height == 0.0 {
1116 body_box.height = options.metrics().x_height;
1117 }
1118
1119 let metrics = options.metrics();
1120 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
1125 metrics.x_height
1126 } else {
1127 theta
1128 };
1129
1130 let mut line_clearance = theta + phi / 4.0;
1131
1132 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
1134
1135 let tex_height = select_surd_height(min_delim_height);
1138 let rule_width = theta;
1139 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
1140 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
1141 .map(|m| m.width)
1142 .unwrap_or(0.833);
1143
1144 let delim_depth = tex_height - rule_width;
1146 if delim_depth > body_box.height + body_box.depth + line_clearance {
1147 line_clearance =
1148 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
1149 }
1150
1151 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
1152
1153 let height = tex_height + rule_width - img_shift;
1156 let depth = if img_shift > body_box.depth {
1157 img_shift
1158 } else {
1159 body_box.depth
1160 };
1161
1162 const INDEX_KERN: f64 = 0.05;
1164 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
1165 let root_style = options.style.superscript().superscript();
1166 let root_opts = options.with_style(root_style);
1167 let idx = layout_node(index_node, &root_opts);
1168 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
1169 let offset = idx.width * index_ratio + INDEX_KERN;
1170 (Some(Box::new(idx)), offset, index_ratio)
1171 } else {
1172 (None, 0.0, 1.0)
1173 };
1174
1175 let width = index_offset + advance_width + body_box.width;
1176
1177 LayoutBox {
1178 width,
1179 height,
1180 depth,
1181 content: BoxContent::Radical {
1182 body: Box::new(body_box),
1183 index: index_box,
1184 index_offset,
1185 index_scale,
1186 rule_thickness: rule_width,
1187 inner_height: tex_height,
1188 },
1189 color: options.color,
1190 }
1191}
1192
1193fn select_surd_height(min_height: f64) -> f64 {
1196 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1197 for &h in &SURD_HEIGHTS {
1198 if h >= min_height {
1199 return h;
1200 }
1201 }
1202 SURD_HEIGHTS[4].max(min_height)
1204}
1205
1206const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1211
1212fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1214 match base {
1215 ParseNode::Op {
1216 limits,
1217 always_handle_sup_sub,
1218 ..
1219 } => {
1220 *limits
1221 && (options.style.is_display()
1222 || always_handle_sup_sub.unwrap_or(false))
1223 }
1224 ParseNode::OperatorName {
1225 always_handle_sup_sub,
1226 limits,
1227 ..
1228 } => {
1229 *always_handle_sup_sub
1230 && (options.style.is_display() || *limits)
1231 }
1232 _ => false,
1233 }
1234}
1235
1236fn layout_op(
1242 name: Option<&str>,
1243 symbol: bool,
1244 body: Option<&[ParseNode]>,
1245 _limits: bool,
1246 suppress_base_shift: bool,
1247 options: &LayoutOptions,
1248) -> LayoutBox {
1249 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1250
1251 if symbol && !suppress_base_shift {
1253 let axis = options.metrics().axis_height;
1254 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1255 if shift.abs() > 0.001 {
1256 base_box.height -= shift;
1257 base_box.depth += shift;
1258 }
1259 }
1260
1261 if !suppress_base_shift && !symbol && body.is_some() {
1266 let axis = options.metrics().axis_height;
1267 let delta = (base_box.height - base_box.depth) / 2.0 - axis;
1268 if delta.abs() > 0.001 {
1269 let w = base_box.width;
1270 let raise = -delta;
1272 base_box = LayoutBox {
1273 width: w,
1274 height: (base_box.height + raise).max(0.0),
1275 depth: (base_box.depth - raise).max(0.0),
1276 content: BoxContent::RaiseBox {
1277 body: Box::new(base_box),
1278 shift: raise,
1279 },
1280 color: options.color,
1281 };
1282 }
1283 }
1284
1285 base_box
1286}
1287
1288fn build_op_base(
1291 name: Option<&str>,
1292 symbol: bool,
1293 body: Option<&[ParseNode]>,
1294 options: &LayoutOptions,
1295) -> (LayoutBox, f64) {
1296 if symbol {
1297 let large = options.style.is_display()
1298 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1299 let font_id = if large {
1300 FontId::Size2Regular
1301 } else {
1302 FontId::Size1Regular
1303 };
1304
1305 let op_name = name.unwrap_or("");
1306 let ch = resolve_op_char(op_name);
1307 let char_code = ch as u32;
1308
1309 let metrics = get_char_metrics(font_id, char_code);
1310 let (width, height, depth, italic) = match metrics {
1311 Some(m) => (m.width, m.height, m.depth, m.italic),
1312 None => (1.0, 0.75, 0.25, 0.0),
1313 };
1314 let width_with_italic = width + italic;
1317
1318 let base = LayoutBox {
1319 width: width_with_italic,
1320 height,
1321 depth,
1322 content: BoxContent::Glyph {
1323 font_id,
1324 char_code,
1325 },
1326 color: options.color,
1327 };
1328
1329 if op_name == "\\oiint" || op_name == "\\oiiint" {
1332 let w = base.width;
1333 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1334 let overlay_box = LayoutBox {
1335 width: w,
1336 height: base.height,
1337 depth: base.depth,
1338 content: BoxContent::SvgPath {
1339 commands: ellipse_commands,
1340 fill: false,
1341 },
1342 color: options.color,
1343 };
1344 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1345 return (with_overlay, italic);
1346 }
1347
1348 (base, italic)
1349 } else if let Some(body_nodes) = body {
1350 let base = layout_expression(body_nodes, options, true);
1351 (base, 0.0)
1352 } else {
1353 let base = layout_op_text(name.unwrap_or(""), options);
1354 (base, 0.0)
1355 }
1356}
1357
1358fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1360 let text = name.strip_prefix('\\').unwrap_or(name);
1361 let mut children = Vec::new();
1362 for ch in text.chars() {
1363 let char_code = ch as u32;
1364 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1365 let (width, height, depth) = match metrics {
1366 Some(m) => (m.width, m.height, m.depth),
1367 None => (0.5, 0.43, 0.0),
1368 };
1369 children.push(LayoutBox {
1370 width,
1371 height,
1372 depth,
1373 content: BoxContent::Glyph {
1374 font_id: FontId::MainRegular,
1375 char_code,
1376 },
1377 color: options.color,
1378 });
1379 }
1380 make_hbox(children)
1381}
1382
1383fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1385 let metrics = options.metrics();
1386 (base.height - base.depth) / 2.0 - metrics.axis_height
1387}
1388
1389fn resolve_op_char(name: &str) -> char {
1391 match name {
1394 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1397 }
1398 let font_mode = ratex_font::Mode::Math;
1399 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1400 if let Some(cp) = info.codepoint {
1401 return cp;
1402 }
1403 }
1404 name.chars().next().unwrap_or('?')
1405}
1406
1407fn layout_op_with_limits(
1409 base_node: &ParseNode,
1410 sup_node: Option<&ParseNode>,
1411 sub_node: Option<&ParseNode>,
1412 options: &LayoutOptions,
1413) -> LayoutBox {
1414 let (name, symbol, body, suppress_base_shift) = match base_node {
1415 ParseNode::Op {
1416 name,
1417 symbol,
1418 body,
1419 suppress_base_shift,
1420 ..
1421 } => (
1422 name.as_deref(),
1423 *symbol,
1424 body.as_deref(),
1425 suppress_base_shift.unwrap_or(false),
1426 ),
1427 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1428 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1429 };
1430
1431 let legacy_limit_kern_padding = !suppress_base_shift;
1433
1434 let (base_box, slant) = build_op_base(name, symbol, body, options);
1435 let base_shift = if symbol && !suppress_base_shift {
1437 compute_op_base_shift(&base_box, options)
1438 } else {
1439 0.0
1440 };
1441
1442 layout_op_limits_inner(
1443 &base_box,
1444 sup_node,
1445 sub_node,
1446 slant,
1447 base_shift,
1448 legacy_limit_kern_padding,
1449 options,
1450 )
1451}
1452
1453fn layout_op_limits_inner(
1458 base: &LayoutBox,
1459 sup_node: Option<&ParseNode>,
1460 sub_node: Option<&ParseNode>,
1461 slant: f64,
1462 base_shift: f64,
1463 legacy_limit_kern_padding: bool,
1464 options: &LayoutOptions,
1465) -> LayoutBox {
1466 let metrics = options.metrics();
1467 let sup_style = options.style.superscript();
1468 let sub_style = options.style.subscript();
1469
1470 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1471 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1472
1473 let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 };
1474
1475 let sup_data = sup_node.map(|s| {
1476 let sup_opts = options.with_style(sup_style);
1477 let elem = layout_node(s, &sup_opts);
1478 let d = if legacy_limit_kern_padding {
1482 elem.depth * sup_ratio
1483 } else {
1484 elem.depth
1485 };
1486 let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern);
1487 (elem, kern)
1488 });
1489
1490 let sub_data = sub_node.map(|s| {
1491 let sub_opts = options.with_style(sub_style);
1492 let elem = layout_node(s, &sub_opts);
1493 let h = if legacy_limit_kern_padding {
1494 elem.height * sub_ratio
1495 } else {
1496 elem.height
1497 };
1498 let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern);
1499 (elem, kern)
1500 });
1501
1502 let sp5 = metrics.big_op_spacing5;
1503
1504 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1505 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1506 let sup_h = sup_elem.height * sup_ratio;
1509 let sup_d = sup_elem.depth * sup_ratio;
1510 let sub_h = sub_elem.height * sub_ratio;
1511 let sub_d = sub_elem.depth * sub_ratio;
1512
1513 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1514
1515 let height = bottom
1516 + base.height - base_shift
1517 + sup_kern
1518 + sup_h + sup_d
1519 + sp5
1520 - (base.height + base.depth);
1521
1522 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1523 let total_d = bottom;
1524
1525 let w = base
1526 .width
1527 .max(sup_elem.width * sup_ratio)
1528 .max(sub_elem.width * sub_ratio);
1529 let _ = height; (total_h, total_d, w)
1531 }
1532 (None, Some((sub_elem, sub_kern))) => {
1533 let sub_h = sub_elem.height * sub_ratio;
1536 let sub_d = sub_elem.depth * sub_ratio;
1537
1538 let total_h = base.height - base_shift;
1539 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1540
1541 let w = base.width.max(sub_elem.width * sub_ratio);
1542 (total_h, total_d, w)
1543 }
1544 (Some((sup_elem, sup_kern)), None) => {
1545 let sup_h = sup_elem.height * sup_ratio;
1548 let sup_d = sup_elem.depth * sup_ratio;
1549
1550 let total_h =
1551 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1552 let total_d = base.depth + base_shift;
1553
1554 let w = base.width.max(sup_elem.width * sup_ratio);
1555 (total_h, total_d, w)
1556 }
1557 (None, None) => {
1558 return base.clone();
1559 }
1560 };
1561
1562 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1563 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1564
1565 LayoutBox {
1566 width: total_width,
1567 height: total_height,
1568 depth: total_depth,
1569 content: BoxContent::OpLimits {
1570 base: Box::new(base.clone()),
1571 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1572 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1573 base_shift,
1574 sup_kern: sup_kern_val,
1575 sub_kern: sub_kern_val,
1576 slant,
1577 sup_scale: sup_ratio,
1578 sub_scale: sub_ratio,
1579 },
1580 color: options.color,
1581 }
1582}
1583
1584fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1586 let mut children = Vec::new();
1587 for node in body {
1588 match node {
1589 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1590 let ch = text.chars().next().unwrap_or('?');
1591 let char_code = ch as u32;
1592 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1593 let (width, height, depth) = match metrics {
1594 Some(m) => (m.width, m.height, m.depth),
1595 None => (0.5, 0.43, 0.0),
1596 };
1597 children.push(LayoutBox {
1598 width,
1599 height,
1600 depth,
1601 content: BoxContent::Glyph {
1602 font_id: FontId::MainRegular,
1603 char_code,
1604 },
1605 color: options.color,
1606 });
1607 }
1608 _ => {
1609 children.push(layout_node(node, options));
1610 }
1611 }
1612 }
1613 make_hbox(children)
1614}
1615
1616const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1622
1623fn glyph_italic(lb: &LayoutBox) -> f64 {
1627 match &lb.content {
1628 BoxContent::Glyph { font_id, char_code } => {
1629 get_char_metrics(*font_id, *char_code)
1630 .map(|m| m.italic)
1631 .unwrap_or(0.0)
1632 }
1633 BoxContent::HBox(children) => {
1634 children.last().map(glyph_italic).unwrap_or(0.0)
1635 }
1636 _ => 0.0,
1637 }
1638}
1639
1640fn accent_ordgroup_len(base: &ParseNode) -> usize {
1645 match base {
1646 ParseNode::OrdGroup { body, .. } => body.len().max(1),
1647 _ => 1,
1648 }
1649}
1650
1651fn glyph_skew(lb: &LayoutBox) -> f64 {
1652 match &lb.content {
1653 BoxContent::Glyph { font_id, char_code } => {
1654 get_char_metrics(*font_id, *char_code)
1655 .map(|m| m.skew)
1656 .unwrap_or(0.0)
1657 }
1658 BoxContent::HBox(children) => {
1659 children.last().map(glyph_skew).unwrap_or(0.0)
1660 }
1661 _ => 0.0,
1662 }
1663}
1664
1665fn layout_accent(
1666 label: &str,
1667 base: &ParseNode,
1668 is_stretchy: bool,
1669 is_shifty: bool,
1670 is_below: bool,
1671 options: &LayoutOptions,
1672) -> LayoutBox {
1673 let body_box = layout_node(base, options);
1674 let base_w = body_box.width.max(0.5);
1675
1676 if label == "\\textcircled" {
1678 return layout_textcircled(body_box, options);
1679 }
1680
1681 if let Some((commands, w, h, fill)) =
1683 crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
1684 {
1685 let accent_box = LayoutBox {
1687 width: w,
1688 height: 0.0,
1689 depth: h,
1690 content: BoxContent::SvgPath { commands, fill },
1691 color: options.color,
1692 };
1693 let gap = 0.065;
1698 let under_gap_em = if is_below && label == "\\utilde" {
1699 0.12
1700 } else {
1701 0.0
1702 };
1703 let clearance = if is_below {
1704 body_box.height + body_box.depth + gap
1705 } else if label == "\\vec" {
1706 (body_box.height - options.metrics().x_height).max(0.0)
1709 } else {
1710 body_box.height + gap
1711 };
1712 let (height, depth) = if is_below {
1713 (body_box.height, body_box.depth + h + gap + under_gap_em)
1714 } else if label == "\\vec" {
1715 (clearance + h, body_box.depth)
1717 } else {
1718 (body_box.height + gap + h, body_box.depth)
1719 };
1720 let vec_skew = if label == "\\vec" {
1721 (if is_shifty {
1722 glyph_skew(&body_box)
1723 } else {
1724 0.0
1725 }) + VEC_SKEW_EXTRA_RIGHT_EM
1726 } else {
1727 0.0
1728 };
1729 return LayoutBox {
1730 width: body_box.width,
1731 height,
1732 depth,
1733 content: BoxContent::Accent {
1734 base: Box::new(body_box),
1735 accent: Box::new(accent_box),
1736 clearance,
1737 skew: vec_skew,
1738 is_below,
1739 under_gap_em,
1740 },
1741 color: options.color,
1742 };
1743 }
1744
1745 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1747
1748 let accent_box = if use_arrow_path {
1749 let (commands, arrow_h, fill_arrow) =
1750 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1751 Some((c, h)) => (c, h, true),
1752 None => {
1753 let h = 0.3_f64;
1754 let c = stretchy_accent_path(label, base_w, h);
1755 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1756 (c, h, fill)
1757 }
1758 };
1759 LayoutBox {
1760 width: base_w,
1761 height: arrow_h / 2.0,
1762 depth: arrow_h / 2.0,
1763 content: BoxContent::SvgPath {
1764 commands,
1765 fill: fill_arrow,
1766 },
1767 color: options.color,
1768 }
1769 } else {
1770 let accent_char = {
1772 let ch = resolve_symbol_char(label, Mode::Text);
1773 if ch == label.chars().next().unwrap_or('?') {
1774 resolve_symbol_char(label, Mode::Math)
1777 } else {
1778 ch
1779 }
1780 };
1781 let accent_code = accent_char as u32;
1782 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1783 let (accent_w, accent_h, accent_d) = match accent_metrics {
1784 Some(m) => (m.width, m.height, m.depth),
1785 None => (body_box.width, 0.25, 0.0),
1786 };
1787 LayoutBox {
1788 width: accent_w,
1789 height: accent_h,
1790 depth: accent_d,
1791 content: BoxContent::Glyph {
1792 font_id: FontId::MainRegular,
1793 char_code: accent_code,
1794 },
1795 color: options.color,
1796 }
1797 };
1798
1799 let skew = if use_arrow_path {
1800 0.0
1801 } else if is_shifty {
1802 glyph_skew(&body_box)
1805 } else {
1806 0.0
1807 };
1808
1809 let gap = if use_arrow_path {
1818 if label == "\\Overrightarrow" {
1819 0.21
1820 } else {
1821 0.26
1822 }
1823 } else {
1824 0.0
1825 };
1826
1827 let clearance = if is_below {
1828 body_box.height + body_box.depth + accent_box.depth + gap
1829 } else if use_arrow_path {
1830 body_box.height + gap
1831 } else {
1832 let base_clearance = match &body_box.content {
1839 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1840 if !is_below =>
1841 {
1842 if inner_accent.height <= 0.001 {
1846 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1852 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1853 katex_pos + correction
1854 } else {
1855 if label == "\\bar" || label == "\\=" {
1861 body_box.height
1862 } else {
1863 let inner_visual_top = inner_cl + 0.35_f64.min(inner_accent.height);
1868 let h_for_kern = if body_box.height > inner_visual_top + 0.002 {
1869 inner_visual_top
1870 } else {
1871 body_box.height
1872 };
1873 let katex_pos = (h_for_kern - options.metrics().x_height).max(0.0);
1874 let correction =
1875 (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1876 katex_pos + correction
1877 }
1878 }
1879 }
1880 _ => {
1881 if label == "\\bar" || label == "\\=" {
1894 body_box.height
1895 } else {
1896 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1897 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1898 katex_pos + correction
1899 }
1900 }
1901 };
1902 let base_clearance = base_clearance + accent_box.depth;
1907 if label == "\\bar" || label == "\\=" {
1908 (base_clearance - 0.12).max(0.0)
1909 } else {
1910 base_clearance
1911 }
1912 };
1913
1914 let (height, depth) = if is_below {
1915 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1916 } else if use_arrow_path {
1917 (body_box.height + gap + accent_box.height, body_box.depth)
1918 } else {
1919 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1926 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1927 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1928 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1929 } else {
1930 body_box.height.max(accent_visual_top)
1931 };
1932 (h, body_box.depth)
1933 };
1934
1935 LayoutBox {
1936 width: body_box.width,
1937 height,
1938 depth,
1939 content: BoxContent::Accent {
1940 base: Box::new(body_box),
1941 accent: Box::new(accent_box),
1942 clearance,
1943 skew,
1944 is_below,
1945 under_gap_em: 0.0,
1946 },
1947 color: options.color,
1948 }
1949}
1950
1951fn node_contains_middle(node: &ParseNode) -> bool {
1957 match node {
1958 ParseNode::Middle { .. } => true,
1959 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1960 body.iter().any(node_contains_middle)
1961 }
1962 ParseNode::SupSub { base, sup, sub, .. } => {
1963 base.as_deref().is_some_and(node_contains_middle)
1964 || sup.as_deref().is_some_and(node_contains_middle)
1965 || sub.as_deref().is_some_and(node_contains_middle)
1966 }
1967 ParseNode::GenFrac { numer, denom, .. } => {
1968 node_contains_middle(numer) || node_contains_middle(denom)
1969 }
1970 ParseNode::Sqrt { body, index, .. } => {
1971 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1972 }
1973 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1974 node_contains_middle(base)
1975 }
1976 ParseNode::Op { body, .. } => body
1977 .as_ref()
1978 .is_some_and(|b| b.iter().any(node_contains_middle)),
1979 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1980 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1981 ParseNode::Font { body, .. } => node_contains_middle(body),
1982 ParseNode::Text { body, .. }
1983 | ParseNode::Color { body, .. }
1984 | ParseNode::Styling { body, .. }
1985 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1986 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1987 node_contains_middle(body)
1988 }
1989 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1990 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1991 node_contains_middle(body)
1992 }
1993 ParseNode::Array { body, .. } => body
1994 .iter()
1995 .any(|row| row.iter().any(node_contains_middle)),
1996 ParseNode::Enclose { body, .. }
1997 | ParseNode::Lap { body, .. }
1998 | ParseNode::RaiseBox { body, .. }
1999 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
2000 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
2001 ParseNode::XArrow { body, below, .. } => {
2002 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
2003 }
2004 ParseNode::CdArrow { label_above, label_below, .. } => {
2005 label_above.as_deref().is_some_and(node_contains_middle)
2006 || label_below.as_deref().is_some_and(node_contains_middle)
2007 }
2008 ParseNode::MathChoice {
2009 display,
2010 text,
2011 script,
2012 scriptscript,
2013 ..
2014 } => {
2015 display.iter().any(node_contains_middle)
2016 || text.iter().any(node_contains_middle)
2017 || script.iter().any(node_contains_middle)
2018 || scriptscript.iter().any(node_contains_middle)
2019 }
2020 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
2021 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
2022 _ => false,
2023 }
2024}
2025
2026fn body_contains_middle(nodes: &[ParseNode]) -> bool {
2028 nodes.iter().any(node_contains_middle)
2029}
2030
2031fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
2034 let m = options.metrics();
2035 if options.style.is_display() {
2036 m.delim1
2037 } else if matches!(
2038 options.style,
2039 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
2040 ) {
2041 options
2042 .with_style(MathStyle::Script)
2043 .metrics()
2044 .delim2
2045 } else {
2046 m.delim2
2047 }
2048}
2049
2050fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
2052 let metrics = options.metrics();
2053 let inner_height = inner.height;
2054 let inner_depth = inner.depth;
2055 let axis = metrics.axis_height;
2056 let max_dist = (inner_height - axis).max(inner_depth + axis);
2057 let delim_factor = 901.0;
2058 let delim_extend = 5.0 / metrics.pt_per_em;
2059 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
2060 from_formula.max(inner_height + inner_depth)
2062}
2063
2064fn layout_left_right(
2065 body: &[ParseNode],
2066 left_delim: &str,
2067 right_delim: &str,
2068 options: &LayoutOptions,
2069) -> LayoutBox {
2070 let (inner, total_height) = if body_contains_middle(body) {
2071 let opts_first = LayoutOptions {
2073 leftright_delim_height: None,
2074 ..options.clone()
2075 };
2076 let inner_first = layout_expression(body, &opts_first, true);
2077 let total_height = left_right_delim_total_height(&inner_first, options);
2078 let opts_second = LayoutOptions {
2080 leftright_delim_height: Some(total_height),
2081 ..options.clone()
2082 };
2083 let inner_second = layout_expression(body, &opts_second, true);
2084 (inner_second, total_height)
2085 } else {
2086 let inner = layout_expression(body, options, true);
2087 let total_height = left_right_delim_total_height(&inner, options);
2088 (inner, total_height)
2089 };
2090
2091 let inner_height = inner.height;
2092 let inner_depth = inner.depth;
2093
2094 let left_box = make_stretchy_delim(left_delim, total_height, options);
2095 let right_box = make_stretchy_delim(right_delim, total_height, options);
2096
2097 let width = left_box.width + inner.width + right_box.width;
2098 let height = left_box.height.max(right_box.height).max(inner_height);
2099 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
2100
2101 LayoutBox {
2102 width,
2103 height,
2104 depth,
2105 content: BoxContent::LeftRight {
2106 left: Box::new(left_box),
2107 right: Box::new(right_box),
2108 inner: Box::new(inner),
2109 },
2110 color: options.color,
2111 }
2112}
2113
2114const DELIM_FONT_SEQUENCE: [FontId; 5] = [
2115 FontId::MainRegular,
2116 FontId::Size1Regular,
2117 FontId::Size2Regular,
2118 FontId::Size3Regular,
2119 FontId::Size4Regular,
2120];
2121
2122fn normalize_delim(delim: &str) -> &str {
2124 match delim {
2125 "<" | "\\lt" | "\u{27E8}" => "\\langle",
2126 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
2127 _ => delim,
2128 }
2129}
2130
2131fn is_vert_delim(delim: &str) -> bool {
2133 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
2134}
2135
2136fn is_double_vert_delim(delim: &str) -> bool {
2138 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
2139}
2140
2141fn vert_repeat_piece_height(is_double: bool) -> f64 {
2143 let code = if is_double { 8741_u32 } else { 8739 };
2144 get_char_metrics(FontId::Size1Regular, code)
2145 .map(|m| m.height + m.depth)
2146 .unwrap_or(0.5)
2147}
2148
2149fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
2151 let piece = vert_repeat_piece_height(is_double);
2152 let min_h = 2.0 * piece;
2153 let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
2154 let mut h = min_h + repeat_count * piece;
2155 if (requested_total - 3.0).abs() < 0.01 && !is_double {
2159 h *= 1.135;
2160 }
2161 h
2162}
2163
2164fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
2166 let neg = -mid_th;
2167 if !is_double {
2168 format!(
2169 "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"
2170 )
2171 } else {
2172 format!(
2173 "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"
2174 )
2175 }
2176}
2177
2178fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
2179 let s = 0.001_f64;
2180 cmds.iter()
2181 .map(|c| match *c {
2182 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2183 x: x * s,
2184 y: y * s,
2185 },
2186 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2187 x: x * s,
2188 y: y * s,
2189 },
2190 PathCommand::CubicTo {
2191 x1,
2192 y1,
2193 x2,
2194 y2,
2195 x,
2196 y,
2197 } => PathCommand::CubicTo {
2198 x1: x1 * s,
2199 y1: y1 * s,
2200 x2: x2 * s,
2201 y2: y2 * s,
2202 x: x * s,
2203 y: y * s,
2204 },
2205 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2206 x1: x1 * s,
2207 y1: y1 * s,
2208 x: x * s,
2209 y: y * s,
2210 },
2211 PathCommand::Close => PathCommand::Close,
2212 })
2213 .collect()
2214}
2215
2216fn map_vert_path_y_to_baseline(
2218 cmds: Vec<PathCommand>,
2219 height: f64,
2220 depth: f64,
2221 view_box_height: i64,
2222) -> Vec<PathCommand> {
2223 let span_em = view_box_height as f64 / 1000.0;
2224 let total = height + depth;
2225 let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
2226 cmds.into_iter()
2227 .map(|c| match c {
2228 PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
2229 x,
2230 y: -height + y * scale_y,
2231 },
2232 PathCommand::LineTo { x, y } => PathCommand::LineTo {
2233 x,
2234 y: -height + y * scale_y,
2235 },
2236 PathCommand::CubicTo {
2237 x1,
2238 y1,
2239 x2,
2240 y2,
2241 x,
2242 y,
2243 } => PathCommand::CubicTo {
2244 x1,
2245 y1: -height + y1 * scale_y,
2246 x2,
2247 y2: -height + y2 * scale_y,
2248 x,
2249 y: -height + y * scale_y,
2250 },
2251 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
2252 x1,
2253 y1: -height + y1 * scale_y,
2254 x,
2255 y: -height + y * scale_y,
2256 },
2257 PathCommand::Close => PathCommand::Close,
2258 })
2259 .collect()
2260}
2261
2262fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
2265 let real_h = katex_vert_real_height(total_height, is_double);
2266 let axis = options.metrics().axis_height;
2267 let depth = (real_h / 2.0 - axis).max(0.0);
2268 let height = real_h - depth;
2269 let width = if is_double { 0.556 } else { 0.333 };
2270
2271 let piece = vert_repeat_piece_height(is_double);
2272 let mid_em = (real_h - 2.0 * piece).max(0.0);
2273 let mid_th = (mid_em * 1000.0).round() as i64;
2274 let view_box_height = (real_h * 1000.0).round() as i64;
2275
2276 let d = tall_vert_svg_path_data(mid_th, is_double);
2277 let raw = parse_svg_path_data(&d);
2278 let scaled = scale_svg_path_to_em(&raw);
2279 let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
2280
2281 LayoutBox {
2282 width,
2283 height,
2284 depth,
2285 content: BoxContent::SvgPath { commands, fill: true },
2286 color: options.color,
2287 }
2288}
2289
2290fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
2292 if delim == "." || delim.is_empty() {
2293 return LayoutBox::new_kern(0.0);
2294 }
2295
2296 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2301 return make_vert_delim_box(total_height, false, options);
2302 }
2303 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
2304 return make_vert_delim_box(total_height, true, options);
2305 }
2306
2307 let delim = normalize_delim(delim);
2309
2310 let ch = resolve_symbol_char(delim, Mode::Math);
2311 let char_code = ch as u32;
2312
2313 let mut best_font = FontId::MainRegular;
2314 let mut best_w = 0.4;
2315 let mut best_h = 0.7;
2316 let mut best_d = 0.2;
2317
2318 for &font_id in &DELIM_FONT_SEQUENCE {
2319 if let Some(m) = get_char_metrics(font_id, char_code) {
2320 best_font = font_id;
2321 best_w = m.width;
2322 best_h = m.height;
2323 best_d = m.depth;
2324 if best_h + best_d >= total_height {
2325 break;
2326 }
2327 }
2328 }
2329
2330 let best_total = best_h + best_d;
2331 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
2332 return stacked;
2333 }
2334
2335 LayoutBox {
2336 width: best_w,
2337 height: best_h,
2338 depth: best_d,
2339 content: BoxContent::Glyph {
2340 font_id: best_font,
2341 char_code,
2342 },
2343 color: options.color,
2344 }
2345}
2346
2347const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
2349
2350fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
2352 if delim == "." || delim.is_empty() {
2353 return LayoutBox::new_kern(0.0);
2354 }
2355
2356 if is_vert_delim(delim) {
2358 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2359 return make_vert_delim_box(total, false, options);
2360 }
2361 if is_double_vert_delim(delim) {
2362 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
2363 return make_vert_delim_box(total, true, options);
2364 }
2365
2366 let delim = normalize_delim(delim);
2368
2369 let ch = resolve_symbol_char(delim, Mode::Math);
2370 let char_code = ch as u32;
2371
2372 let font_id = match size {
2373 1 => FontId::Size1Regular,
2374 2 => FontId::Size2Regular,
2375 3 => FontId::Size3Regular,
2376 4 => FontId::Size4Regular,
2377 _ => FontId::Size1Regular,
2378 };
2379
2380 let metrics = get_char_metrics(font_id, char_code);
2381 let (width, height, depth, actual_font) = match metrics {
2382 Some(m) => (m.width, m.height, m.depth, font_id),
2383 None => {
2384 let m = get_char_metrics(FontId::MainRegular, char_code);
2385 match m {
2386 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2387 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2388 }
2389 }
2390 };
2391
2392 LayoutBox {
2393 width,
2394 height,
2395 depth,
2396 content: BoxContent::Glyph {
2397 font_id: actual_font,
2398 char_code,
2399 },
2400 color: options.color,
2401 }
2402}
2403
2404#[allow(clippy::too_many_arguments)]
2409fn layout_array(
2410 body: &[Vec<ParseNode>],
2411 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2412 arraystretch: f64,
2413 add_jot: bool,
2414 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2415 hlines: &[Vec<bool>],
2416 col_sep_type: Option<&str>,
2417 hskip: bool,
2418 tags: Option<&[ArrayTag]>,
2419 _leqno: bool,
2420 options: &LayoutOptions,
2421) -> LayoutBox {
2422 let metrics = options.metrics();
2423 let pt = 1.0 / metrics.pt_per_em;
2424 let baselineskip = 12.0 * pt;
2425 let jot = 3.0 * pt;
2426 let arrayskip = arraystretch * baselineskip;
2427 let arstrut_h = 0.7 * arrayskip;
2428 let arstrut_d = 0.3 * arrayskip;
2429 const ALIGN_RELATION_MU: f64 = 3.0;
2432 let col_gap = match col_sep_type {
2433 Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2434 Some("alignat") => 0.0,
2435 Some("small") => {
2436 2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
2439 / options.size_multiplier()
2440 }
2441 _ => 2.0 * 5.0 * pt, };
2443 let cell_options = match col_sep_type {
2444 Some("align") | Some("alignat") => LayoutOptions {
2445 align_relation_spacing: Some(ALIGN_RELATION_MU),
2446 ..options.clone()
2447 },
2448 _ => options.clone(),
2449 };
2450
2451 let num_rows = body.len();
2452 if num_rows == 0 {
2453 return LayoutBox::new_empty();
2454 }
2455
2456 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2457
2458 use ratex_parser::parse_node::AlignType;
2460 let col_aligns: Vec<u8> = {
2461 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2462 .map(|cs| {
2463 cs.iter()
2464 .filter(|s| matches!(s.align_type, AlignType::Align))
2465 .collect()
2466 })
2467 .unwrap_or_default();
2468 (0..num_cols)
2469 .map(|c| {
2470 align_specs
2471 .get(c)
2472 .and_then(|s| s.align.as_deref())
2473 .and_then(|a| a.bytes().next())
2474 .unwrap_or(b'c')
2475 })
2476 .collect()
2477 };
2478
2479 let col_separators: Vec<Option<bool>> = {
2482 let mut seps = vec![None; num_cols + 1];
2483 let mut align_count = 0usize;
2484 if let Some(cs) = cols {
2485 for spec in cs {
2486 match spec.align_type {
2487 AlignType::Align => align_count += 1,
2488 AlignType::Separator
2489 if spec.align.as_deref() == Some("|") && align_count <= num_cols =>
2490 {
2491 seps[align_count] = Some(false);
2492 }
2493 AlignType::Separator
2494 if spec.align.as_deref() == Some(":") && align_count <= num_cols =>
2495 {
2496 seps[align_count] = Some(true);
2497 }
2498 _ => {}
2499 }
2500 }
2501 }
2502 seps
2503 };
2504
2505 let rule_thickness = 0.4 * pt;
2506 let double_rule_sep = metrics.double_rule_sep;
2507
2508 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2510 let mut col_widths = vec![0.0_f64; num_cols];
2511 let mut row_heights = Vec::with_capacity(num_rows);
2512 let mut row_depths = Vec::with_capacity(num_rows);
2513
2514 for row in body {
2515 let mut row_boxes = Vec::with_capacity(num_cols);
2516 let mut rh = arstrut_h;
2517 let mut rd = arstrut_d;
2518
2519 for (c, cell) in row.iter().enumerate() {
2520 let cell_nodes = match cell {
2521 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2522 other => std::slice::from_ref(other),
2523 };
2524 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2525 rh = rh.max(cell_box.height);
2526 rd = rd.max(cell_box.depth);
2527 if c < num_cols {
2528 col_widths[c] = col_widths[c].max(cell_box.width);
2529 }
2530 row_boxes.push(cell_box);
2531 }
2532
2533 while row_boxes.len() < num_cols {
2535 row_boxes.push(LayoutBox::new_empty());
2536 }
2537
2538 if add_jot {
2539 rd += jot;
2540 }
2541
2542 row_heights.push(rh);
2543 row_depths.push(rd);
2544 cell_boxes.push(row_boxes);
2545 }
2546
2547 for (r, gap) in row_gaps.iter().enumerate() {
2549 if r < row_depths.len() {
2550 if let Some(m) = gap {
2551 let gap_em = measurement_to_em(m, options);
2552 if gap_em > 0.0 {
2553 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2554 }
2555 }
2556 }
2557 }
2558
2559 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2561 while hlines_before_row.len() < num_rows + 1 {
2562 hlines_before_row.push(vec![]);
2563 }
2564
2565 for r in 0..=num_rows {
2571 let n = hlines_before_row[r].len();
2572 if n > 1 {
2573 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2574 if r == 0 {
2575 if num_rows > 0 {
2576 row_heights[0] += extra;
2577 }
2578 } else {
2579 row_depths[r - 1] += extra;
2580 }
2581 }
2582 }
2583
2584 let mut total_height = 0.0;
2586 let mut row_positions = Vec::with_capacity(num_rows);
2587 for r in 0..num_rows {
2588 total_height += row_heights[r];
2589 row_positions.push(total_height);
2590 total_height += row_depths[r];
2591 }
2592
2593 let offset = total_height / 2.0 + metrics.axis_height;
2594
2595 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2597
2598 let array_inner_width: f64 = col_widths.iter().sum::<f64>()
2600 + col_gap * (num_cols.saturating_sub(1)) as f64
2601 + 2.0 * content_x_offset;
2602
2603 let mut row_tag_boxes: Vec<Option<LayoutBox>> = (0..num_rows).map(|_| None).collect();
2604 let mut tag_col_width = 0.0_f64;
2605 let text_opts = options.with_style(options.style.text());
2606 if let Some(tag_slice) = tags {
2607 if tag_slice.len() == num_rows {
2608 for (r, t) in tag_slice.iter().enumerate() {
2609 if let ArrayTag::Explicit(nodes) = t {
2610 if !nodes.is_empty() {
2611 let tb = layout_expression(nodes, &text_opts, true);
2612 tag_col_width = tag_col_width.max(tb.width);
2613 row_tag_boxes[r] = Some(tb);
2614 }
2615 }
2616 }
2617 }
2618 }
2619 let tag_gap_em = if tag_col_width > 0.0 {
2620 text_opts.metrics().quad
2621 } else {
2622 0.0
2623 };
2624 let tags_left = false;
2626
2627 let total_width = array_inner_width + tag_gap_em + tag_col_width;
2628
2629 let height = offset;
2630 let depth = total_height - offset;
2631
2632 LayoutBox {
2633 width: total_width,
2634 height,
2635 depth,
2636 content: BoxContent::Array {
2637 cells: cell_boxes,
2638 col_widths: col_widths.clone(),
2639 col_aligns,
2640 row_heights: row_heights.clone(),
2641 row_depths: row_depths.clone(),
2642 col_gap,
2643 offset,
2644 content_x_offset,
2645 col_separators,
2646 hlines_before_row,
2647 rule_thickness,
2648 double_rule_sep,
2649 array_inner_width,
2650 tag_gap_em,
2651 tag_col_width,
2652 row_tags: row_tag_boxes,
2653 tags_left,
2654 },
2655 color: options.color,
2656 }
2657}
2658
2659fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2664 let multiplier = match size {
2666 1 => 0.5,
2667 2 => 0.6,
2668 3 => 0.7,
2669 4 => 0.8,
2670 5 => 0.9,
2671 6 => 1.0,
2672 7 => 1.2,
2673 8 => 1.44,
2674 9 => 1.728,
2675 10 => 2.074,
2676 11 => 2.488,
2677 _ => 1.0,
2678 };
2679
2680 let inner_opts = options.with_style(options.style.text());
2682 let inner = layout_expression(body, &inner_opts, true);
2683 let ratio = multiplier / options.size_multiplier();
2684 if (ratio - 1.0).abs() < 0.001 {
2685 inner
2686 } else {
2687 LayoutBox {
2688 width: inner.width * ratio,
2689 height: inner.height * ratio,
2690 depth: inner.depth * ratio,
2691 content: BoxContent::Scaled {
2692 body: Box::new(inner),
2693 child_scale: ratio,
2694 },
2695 color: options.color,
2696 }
2697 }
2698}
2699
2700fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2703 let metrics = options.metrics();
2704 let mut children = Vec::new();
2705 for c in body.chars() {
2706 let ch = if star && c == ' ' {
2707 '\u{2423}' } else {
2709 c
2710 };
2711 let code = ch as u32;
2712 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2713 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2714 None => match get_char_metrics(FontId::MainRegular, code) {
2715 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2716 None => (
2717 FontId::TypewriterRegular,
2718 0.5,
2719 metrics.x_height,
2720 0.0,
2721 ),
2722 },
2723 };
2724 children.push(LayoutBox {
2725 width: w,
2726 height: h,
2727 depth: d,
2728 content: BoxContent::Glyph {
2729 font_id,
2730 char_code: code,
2731 },
2732 color: options.color,
2733 });
2734 }
2735 let mut hbox = make_hbox(children);
2736 hbox.color = options.color;
2737 hbox
2738}
2739
2740fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2748 let mut children = Vec::new();
2749 for node in body {
2750 match node {
2751 ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
2752 children.push(layout_symbol(text, *mode, options));
2753 }
2754 ParseNode::SpacingNode { text, .. } => {
2755 children.push(layout_spacing_command(text, options));
2756 }
2757 _ => {
2758 children.push(layout_node(node, options));
2759 }
2760 }
2761 }
2762 make_hbox(children)
2763}
2764
2765fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2768 let base = layout_expression(body, options, true);
2769 let w = base.width;
2770 let h = base.height;
2771 let d = base.depth;
2772
2773 let shadow = layout_expression(body, options, true);
2775 let shadow_shift_x = 0.02_f64;
2776 let _shadow_shift_y = 0.01_f64;
2777
2778 let kern_back = LayoutBox::new_kern(-w);
2782 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2783
2784 let children = vec![
2791 kern_x,
2792 shadow,
2793 kern_back,
2794 base,
2795 ];
2796 let hbox = make_hbox(children);
2798 LayoutBox {
2800 width: w,
2801 height: h,
2802 depth: d,
2803 content: hbox.content,
2804 color: options.color,
2805 }
2806}
2807
2808fn layout_enclose(
2811 label: &str,
2812 background_color: Option<&str>,
2813 border_color: Option<&str>,
2814 body: &ParseNode,
2815 options: &LayoutOptions,
2816) -> LayoutBox {
2817 use crate::layout_box::BoxContent;
2818 use ratex_types::color::Color;
2819
2820 if label == "\\phase" {
2822 return layout_phase(body, options);
2823 }
2824
2825 if label == "\\angl" {
2827 return layout_angl(body, options);
2828 }
2829
2830 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2832 return layout_cancel(label, body, options);
2833 }
2834
2835 let metrics = options.metrics();
2837 let padding = 3.0 / metrics.pt_per_em;
2838 let border_thickness = 0.4 / metrics.pt_per_em;
2839
2840 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2841
2842 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2843 let border = border_color
2844 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2845 .unwrap_or(Color::BLACK);
2846
2847 let inner = layout_node(body, options);
2848 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2849
2850 let width = inner.width + 2.0 * outer_pad;
2851 let height = inner.height + outer_pad;
2852 let depth = inner.depth + outer_pad;
2853
2854 LayoutBox {
2855 width,
2856 height,
2857 depth,
2858 content: BoxContent::Framed {
2859 body: Box::new(inner),
2860 padding,
2861 border_thickness,
2862 has_border,
2863 bg_color: bg,
2864 border_color: border,
2865 },
2866 color: options.color,
2867 }
2868}
2869
2870fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2872 use crate::layout_box::BoxContent;
2873 let inner = layout_node(body, options);
2874 let height = inner.height + shift;
2876 let depth = (inner.depth - shift).max(0.0);
2877 let width = inner.width;
2878 LayoutBox {
2879 width,
2880 height,
2881 depth,
2882 content: BoxContent::RaiseBox {
2883 body: Box::new(inner),
2884 shift,
2885 },
2886 color: options.color,
2887 }
2888}
2889
2890fn is_single_char_body(node: &ParseNode) -> bool {
2893 use ratex_parser::parse_node::ParseNode as PN;
2894 match node {
2895 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2897 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2898 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2900 _ => false,
2901 }
2902}
2903
2904fn layout_cancel(
2910 label: &str,
2911 body: &ParseNode,
2912 options: &LayoutOptions,
2913) -> LayoutBox {
2914 use crate::layout_box::BoxContent;
2915 let inner = layout_node(body, options);
2916 let w = inner.width.max(0.01);
2917 let h = inner.height;
2918 let d = inner.depth;
2919
2920 let single = is_single_char_body(body);
2923 let (v_pad, h_pad) = if label == "\\sout" {
2924 (0.0, 0.0)
2925 } else if single {
2926 (0.2, 0.0)
2927 } else {
2928 (0.0, 0.2)
2929 };
2930
2931 let commands: Vec<PathCommand> = match label {
2935 "\\cancel" => vec![
2936 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2939 "\\bcancel" => vec![
2940 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2943 "\\xcancel" => vec![
2944 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2945 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2946 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2947 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2948 ],
2949 "\\sout" => {
2950 let mid_y = -0.5 * options.metrics().x_height;
2952 vec![
2953 PathCommand::MoveTo { x: 0.0, y: mid_y },
2954 PathCommand::LineTo { x: w, y: mid_y },
2955 ]
2956 }
2957 _ => vec![],
2958 };
2959
2960 let line_w = w + 2.0 * h_pad;
2961 let line_h = h + v_pad;
2962 let line_d = d + v_pad;
2963 let line_box = LayoutBox {
2964 width: line_w,
2965 height: line_h,
2966 depth: line_d,
2967 content: BoxContent::SvgPath { commands, fill: false },
2968 color: options.color,
2969 };
2970
2971 let body_kern = -(line_w - h_pad);
2973 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2974 LayoutBox {
2975 width: w,
2976 height: h,
2977 depth: d,
2978 content: BoxContent::HBox(vec![line_box, body_shifted]),
2979 color: options.color,
2980 }
2981}
2982
2983fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2986 use crate::layout_box::BoxContent;
2987 let metrics = options.metrics();
2988 let inner = layout_node(body, options);
2989 let line_weight = 0.6_f64 / metrics.pt_per_em;
2991 let clearance = 0.35_f64 * metrics.x_height;
2992 let angle_height = inner.height + inner.depth + line_weight + clearance;
2993 let left_pad = angle_height / 2.0 + line_weight;
2994 let width = inner.width + left_pad;
2995
2996 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2998
2999 let sy = angle_height / y_svg;
3001 let sx = sy;
3004 let right_x = (400_000.0_f64 * sx).min(width);
3005
3006 let bottom_y = inner.depth + line_weight + clearance;
3008 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
3009
3010 let x_peak = y_svg / 2.0;
3012 let commands = vec![
3013 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
3014 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
3015 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
3016 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
3017 PathCommand::LineTo {
3018 x: 145.0 * sx,
3019 y: vy(y_svg - 80.0),
3020 },
3021 PathCommand::LineTo {
3022 x: right_x,
3023 y: vy(y_svg - 80.0),
3024 },
3025 PathCommand::Close,
3026 ];
3027
3028 let body_shifted = make_hbox(vec![
3029 LayoutBox::new_kern(left_pad),
3030 inner.clone(),
3031 ]);
3032
3033 let path_height = inner.height;
3034 let path_depth = bottom_y;
3035
3036 LayoutBox {
3037 width,
3038 height: path_height,
3039 depth: path_depth,
3040 content: BoxContent::HBox(vec![
3041 LayoutBox {
3042 width,
3043 height: path_height,
3044 depth: path_depth,
3045 content: BoxContent::SvgPath { commands, fill: true },
3046 color: options.color,
3047 },
3048 LayoutBox::new_kern(-width),
3049 body_shifted,
3050 ]),
3051 color: options.color,
3052 }
3053}
3054
3055fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3058 use crate::layout_box::BoxContent;
3059 let inner = layout_node(body, options);
3060 let w = inner.width.max(0.3);
3061 let clearance = 0.1_f64;
3063 let arc_h = inner.height + clearance;
3064
3065 let path_commands = vec![
3067 PathCommand::MoveTo { x: 0.0, y: -arc_h },
3068 PathCommand::LineTo { x: w, y: -arc_h },
3069 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
3070 ];
3071
3072 let height = arc_h;
3073 LayoutBox {
3074 width: w,
3075 height,
3076 depth: inner.depth,
3077 content: BoxContent::Angl {
3078 path_commands,
3079 body: Box::new(inner),
3080 },
3081 color: options.color,
3082 }
3083}
3084
3085fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3086 let font_id = match font {
3087 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
3088 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
3089 "mathit" | "\\mathit" | "textit" | "\\textit" | "\\emph" => Some(FontId::MainItalic),
3090 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
3091 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
3092 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
3093 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
3094 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
3095 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
3096 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
3097 _ => None,
3098 };
3099
3100 if let Some(fid) = font_id {
3101 layout_with_font(body, fid, options)
3102 } else {
3103 layout_node(body, options)
3104 }
3105}
3106
3107fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
3108 match node {
3109 ParseNode::OrdGroup { body, .. } => {
3110 let kern = options.inter_glyph_kern_em;
3111 let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
3112 for (i, n) in body.iter().enumerate() {
3113 if i > 0 && kern > 0.0 {
3114 children.push(LayoutBox::new_kern(kern));
3115 }
3116 children.push(layout_with_font(n, font_id, options));
3117 }
3118 make_hbox(children)
3119 }
3120 ParseNode::SupSub {
3121 base, sup, sub, ..
3122 } => {
3123 if let Some(base_node) = base.as_deref() {
3124 if should_use_op_limits(base_node, options) {
3125 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
3126 }
3127 }
3128 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
3129 }
3130 ParseNode::MathOrd { text, mode, .. }
3131 | ParseNode::TextOrd { text, mode, .. }
3132 | ParseNode::Atom { text, mode, .. } => {
3133 let ch = resolve_symbol_char(text, *mode);
3134 let char_code = ch as u32;
3135 let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
3136 .map(|(_, m)| m)
3137 .unwrap_or(char_code);
3138 if let Some(m) = get_char_metrics(font_id, metric_cp) {
3139 LayoutBox {
3140 width: math_glyph_advance_em(&m, *mode),
3142 height: m.height,
3143 depth: m.depth,
3144 content: BoxContent::Glyph { font_id, char_code },
3145 color: options.color,
3146 }
3147 } else {
3148 layout_node(node, options)
3150 }
3151 }
3152 _ => layout_node(node, options),
3153 }
3154}
3155
3156fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3161 let cramped = options.with_style(options.style.cramped());
3162 let body_box = layout_node(body, &cramped);
3163 let metrics = options.metrics();
3164 let rule = metrics.default_rule_thickness;
3165
3166 let height = body_box.height + 3.0 * rule;
3168 LayoutBox {
3169 width: body_box.width,
3170 height,
3171 depth: body_box.depth,
3172 content: BoxContent::Overline {
3173 body: Box::new(body_box),
3174 rule_thickness: rule,
3175 },
3176 color: options.color,
3177 }
3178}
3179
3180fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
3181 let body_box = layout_node(body, options);
3182 let metrics = options.metrics();
3183 let rule = metrics.default_rule_thickness;
3184
3185 let depth = body_box.depth + 3.0 * rule;
3187 LayoutBox {
3188 width: body_box.width,
3189 height: body_box.height,
3190 depth,
3191 content: BoxContent::Underline {
3192 body: Box::new(body_box),
3193 rule_thickness: rule,
3194 },
3195 color: options.color,
3196 }
3197}
3198
3199fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
3201 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
3202 let body_opts = options
3204 .with_color(link_color)
3205 .with_inter_glyph_kern(0.024);
3206 let body_box = layout_expression(body, &body_opts, true);
3207 layout_underline_laid_out(body_box, options, link_color)
3208}
3209
3210fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
3212 let metrics = options.metrics();
3213 let rule = metrics.default_rule_thickness;
3214 let depth = body_box.depth + 3.0 * rule;
3215 LayoutBox {
3216 width: body_box.width,
3217 height: body_box.height,
3218 depth,
3219 content: BoxContent::Underline {
3220 body: Box::new(body_box),
3221 rule_thickness: rule,
3222 },
3223 color,
3224 }
3225}
3226
3227fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
3232 let metrics = options.metrics();
3233 let mu = metrics.css_em_per_mu();
3234
3235 let width = match text {
3236 "\\," | "\\thinspace" => 3.0 * mu,
3237 "\\:" | "\\medspace" => 4.0 * mu,
3238 "\\;" | "\\thickspace" => 5.0 * mu,
3239 "\\!" | "\\negthinspace" => -3.0 * mu,
3240 "\\negmedspace" => -4.0 * mu,
3241 "\\negthickspace" => -5.0 * mu,
3242 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
3243 get_char_metrics(FontId::MainRegular, 160)
3247 .map(|m| m.width)
3248 .unwrap_or(0.25)
3249 }
3250 "\\quad" => metrics.quad,
3251 "\\qquad" => 2.0 * metrics.quad,
3252 "\\enspace" => metrics.quad / 2.0,
3253 _ => 0.0,
3254 };
3255
3256 LayoutBox::new_kern(width)
3257}
3258
3259fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
3264 let metrics = options.metrics();
3265 match m.unit.as_str() {
3266 "em" => m.number,
3267 "ex" => m.number * metrics.x_height,
3268 "mu" => m.number * metrics.css_em_per_mu(),
3269 "pt" => m.number / metrics.pt_per_em,
3270 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
3271 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
3272 "in" => m.number * 72.27 / metrics.pt_per_em,
3273 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
3274 "pc" => m.number * 12.0 / metrics.pt_per_em,
3275 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
3276 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
3277 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
3278 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
3279 "sp" => m.number / 65536.0 / metrics.pt_per_em,
3280 _ => m.number,
3281 }
3282}
3283
3284fn node_math_class(node: &ParseNode) -> Option<MathClass> {
3290 match node {
3291 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
3292 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
3293 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
3294 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
3295 ParseNode::GenFrac { left_delim, right_delim, .. } => {
3297 let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
3298 || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
3299 if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
3300 }
3301 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
3302 ParseNode::SupSub { base, .. } => {
3303 base.as_ref().and_then(|b| node_math_class(b))
3304 }
3305 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3306 ParseNode::SpacingNode { .. } => None,
3307 ParseNode::Kern { .. } => None,
3308 ParseNode::HtmlMathMl { html, .. } => {
3309 for child in html {
3311 if let Some(cls) = node_math_class(child) {
3312 return Some(cls);
3313 }
3314 }
3315 None
3316 }
3317 ParseNode::Lap { .. } => None,
3318 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
3319 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
3320 ParseNode::XArrow { .. } => Some(MathClass::Rel),
3322 ParseNode::CdArrow { .. } => Some(MathClass::Rel),
3324 ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
3325 ParseNode::Middle { .. } => Some(MathClass::Ord),
3326 _ => Some(MathClass::Ord),
3327 }
3328}
3329
3330fn mclass_str_to_math_class(mclass: &str) -> MathClass {
3331 match mclass {
3332 "mord" => MathClass::Ord,
3333 "mop" => MathClass::Op,
3334 "mbin" => MathClass::Bin,
3335 "mrel" => MathClass::Rel,
3336 "mopen" => MathClass::Open,
3337 "mclose" => MathClass::Close,
3338 "mpunct" => MathClass::Punct,
3339 "minner" => MathClass::Inner,
3340 _ => MathClass::Ord,
3341 }
3342}
3343
3344fn get_base_elem(node: &ParseNode) -> &ParseNode {
3348 match node {
3349 ParseNode::OrdGroup { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3350 ParseNode::Color { body, .. } if body.len() == 1 => get_base_elem(&body[0]),
3351 ParseNode::Font { body, .. } => get_base_elem(body),
3352 _ => node,
3353 }
3354}
3355
3356fn is_character_box(node: &ParseNode) -> bool {
3357 matches!(
3358 get_base_elem(node),
3359 ParseNode::MathOrd { .. }
3360 | ParseNode::TextOrd { .. }
3361 | ParseNode::Atom { .. }
3362 | ParseNode::AccentToken { .. }
3363 )
3364}
3365
3366fn family_to_math_class(family: AtomFamily) -> MathClass {
3367 match family {
3368 AtomFamily::Bin => MathClass::Bin,
3369 AtomFamily::Rel => MathClass::Rel,
3370 AtomFamily::Open => MathClass::Open,
3371 AtomFamily::Close => MathClass::Close,
3372 AtomFamily::Punct => MathClass::Punct,
3373 AtomFamily::Inner => MathClass::Inner,
3374 }
3375}
3376
3377fn layout_horiz_brace(
3382 base: &ParseNode,
3383 is_over: bool,
3384 func_label: &str,
3385 options: &LayoutOptions,
3386) -> LayoutBox {
3387 let body_box = layout_node(base, options);
3388 let w = body_box.width.max(0.5);
3389
3390 let is_bracket = func_label
3391 .trim_start_matches('\\')
3392 .ends_with("bracket");
3393
3394 let stretch_key = if is_bracket {
3396 if is_over {
3397 "overbracket"
3398 } else {
3399 "underbracket"
3400 }
3401 } else if is_over {
3402 "overbrace"
3403 } else {
3404 "underbrace"
3405 };
3406
3407 let (raw_commands, brace_h, brace_fill) =
3408 match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
3409 Some((c, h)) => (c, h, true),
3410 None => {
3411 let h = 0.35_f64;
3412 (horiz_brace_path(w, h, is_over), h, false)
3413 }
3414 };
3415
3416 let y_shift = brace_h / 2.0;
3422 let commands = shift_path_y(raw_commands, y_shift);
3423
3424 let brace_box = LayoutBox {
3425 width: w,
3426 height: 0.0,
3427 depth: brace_h,
3428 content: BoxContent::SvgPath {
3429 commands,
3430 fill: brace_fill,
3431 },
3432 color: options.color,
3433 };
3434
3435 let gap = 0.1;
3436 let (height, depth) = if is_over {
3437 (body_box.height + brace_h + gap, body_box.depth)
3438 } else {
3439 (body_box.height, body_box.depth + brace_h + gap)
3440 };
3441
3442 let clearance = if is_over {
3443 height - brace_h
3444 } else {
3445 body_box.height + body_box.depth + gap
3446 };
3447 let total_w = body_box.width;
3448
3449 LayoutBox {
3450 width: total_w,
3451 height,
3452 depth,
3453 content: BoxContent::Accent {
3454 base: Box::new(body_box),
3455 accent: Box::new(brace_box),
3456 clearance,
3457 skew: 0.0,
3458 is_below: !is_over,
3459 under_gap_em: 0.0,
3460 },
3461 color: options.color,
3462 }
3463}
3464
3465fn layout_xarrow(
3470 label: &str,
3471 body: &ParseNode,
3472 below: Option<&ParseNode>,
3473 options: &LayoutOptions,
3474) -> LayoutBox {
3475 let sup_style = options.style.superscript();
3476 let sub_style = options.style.subscript();
3477 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3478 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3479
3480 let sup_opts = options.with_style(sup_style);
3481 let body_box = layout_node(body, &sup_opts);
3482 let body_w = body_box.width * sup_ratio;
3483
3484 let below_box = below.map(|b| {
3485 let sub_opts = options.with_style(sub_style);
3486 layout_node(b, &sub_opts)
3487 });
3488 let below_w = below_box
3489 .as_ref()
3490 .map(|b| b.width * sub_ratio)
3491 .unwrap_or(0.0);
3492
3493 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3496 let upper_w = body_w + sup_ratio;
3497 let lower_w = if below_box.is_some() {
3498 below_w + sub_ratio
3499 } else {
3500 0.0
3501 };
3502 let arrow_w = upper_w.max(lower_w).max(min_w);
3503 let arrow_h = 0.3;
3504
3505 let (commands, actual_arrow_h, fill_arrow) =
3506 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3507 Some((c, h)) => (c, h, true),
3508 None => (
3509 stretchy_accent_path(label, arrow_w, arrow_h),
3510 arrow_h,
3511 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3512 ),
3513 };
3514 let arrow_box = LayoutBox {
3515 width: arrow_w,
3516 height: actual_arrow_h / 2.0,
3517 depth: actual_arrow_h / 2.0,
3518 content: BoxContent::SvgPath {
3519 commands,
3520 fill: fill_arrow,
3521 },
3522 color: options.color,
3523 };
3524
3525 let metrics = options.metrics();
3528 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3530 let gap = 0.111; let base_shift = -axis;
3534
3535 let sup_kern = gap;
3543 let sub_kern = gap;
3544
3545 let sup_h = body_box.height * sup_ratio;
3546 let sup_d = body_box.depth * sup_ratio;
3547
3548 let height = axis + arrow_half + gap + sup_h + sup_d;
3550 let mut depth = (arrow_half - axis).max(0.0);
3552
3553 if let Some(ref bel) = below_box {
3554 let sub_h = bel.height * sub_ratio;
3555 let sub_d = bel.depth * sub_ratio;
3556 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3558 }
3559
3560 LayoutBox {
3561 width: arrow_w,
3562 height,
3563 depth,
3564 content: BoxContent::OpLimits {
3565 base: Box::new(arrow_box),
3566 sup: Some(Box::new(body_box)),
3567 sub: below_box.map(Box::new),
3568 base_shift,
3569 sup_kern,
3570 sub_kern,
3571 slant: 0.0,
3572 sup_scale: sup_ratio,
3573 sub_scale: sub_ratio,
3574 },
3575 color: options.color,
3576 }
3577}
3578
3579fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3584 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3587 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3588 let diameter = radius * 2.0;
3589
3590 let cx = radius;
3592 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3595
3596 let circle_commands = vec![
3597 PathCommand::MoveTo { x: cx + r, y: cy },
3598 PathCommand::CubicTo {
3599 x1: cx + r, y1: cy - k * r,
3600 x2: cx + k * r, y2: cy - r,
3601 x: cx, y: cy - r,
3602 },
3603 PathCommand::CubicTo {
3604 x1: cx - k * r, y1: cy - r,
3605 x2: cx - r, y2: cy - k * r,
3606 x: cx - r, y: cy,
3607 },
3608 PathCommand::CubicTo {
3609 x1: cx - r, y1: cy + k * r,
3610 x2: cx - k * r, y2: cy + r,
3611 x: cx, y: cy + r,
3612 },
3613 PathCommand::CubicTo {
3614 x1: cx + k * r, y1: cy + r,
3615 x2: cx + r, y2: cy + k * r,
3616 x: cx + r, y: cy,
3617 },
3618 PathCommand::Close,
3619 ];
3620
3621 let circle_box = LayoutBox {
3622 width: diameter,
3623 height: r - cy.min(0.0),
3624 depth: (r + cy).max(0.0),
3625 content: BoxContent::SvgPath {
3626 commands: circle_commands,
3627 fill: false,
3628 },
3629 color: options.color,
3630 };
3631
3632 let content_shift = (diameter - body_box.width) / 2.0;
3634 let children = vec![
3636 circle_box,
3637 LayoutBox::new_kern(-(diameter) + content_shift),
3638 body_box.clone(),
3639 ];
3640
3641 let height = r - cy.min(0.0);
3642 let depth = (r + cy).max(0.0);
3643
3644 LayoutBox {
3645 width: diameter,
3646 height,
3647 depth,
3648 content: BoxContent::HBox(children),
3649 color: options.color,
3650 }
3651}
3652
3653fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
3677 let r: f64 = 0.1125;
3679 let cy: f64 = -0.2625;
3683 let k: f64 = 0.5523;
3685 let cx: f64 = r;
3687
3688 let h: f64 = r + cy.abs(); let d: f64 = 0.0;
3691
3692 let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half; let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
3701 vec![
3702 PathCommand::MoveTo { x: ox + rad, y: cy },
3703 PathCommand::CubicTo {
3704 x1: ox + rad, y1: cy - k * rad,
3705 x2: ox + k * rad, y2: cy - rad,
3706 x: ox, y: cy - rad,
3707 },
3708 PathCommand::CubicTo {
3709 x1: ox - k * rad, y1: cy - rad,
3710 x2: ox - rad, y2: cy - k * rad,
3711 x: ox - rad, y: cy,
3712 },
3713 PathCommand::CubicTo {
3714 x1: ox - rad, y1: cy + k * rad,
3715 x2: ox - k * rad, y2: cy + rad,
3716 x: ox, y: cy + rad,
3717 },
3718 PathCommand::CubicTo {
3719 x1: ox + k * rad, y1: cy + rad,
3720 x2: ox + rad, y2: cy + k * rad,
3721 x: ox + rad, y: cy,
3722 },
3723 PathCommand::Close,
3724 ]
3725 };
3726
3727 let disk = LayoutBox {
3728 width: 2.0 * r,
3729 height: h,
3730 depth: d,
3731 content: BoxContent::SvgPath {
3732 commands: circle_commands(cx, r),
3733 fill: true,
3734 },
3735 color: options.color,
3736 };
3737
3738 let ring = LayoutBox {
3739 width: 2.0 * r,
3740 height: h,
3741 depth: d,
3742 content: BoxContent::SvgPath {
3743 commands: circle_commands(cx, r_ring),
3744 fill: false,
3745 },
3746 color: options.color,
3747 };
3748
3749 let bar_len: f64 = 0.25;
3753 let bar_th: f64 = 0.04;
3754 let bar_raise: f64 = cy.abs() - bar_th / 2.0; let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
3757
3758 let children = if imageof {
3759 vec![disk, bar, ring]
3760 } else {
3761 vec![ring, bar, disk]
3762 };
3763
3764 let total_width = 4.0 * r + bar_len;
3766 LayoutBox {
3767 width: total_width,
3768 height: h,
3769 depth: d,
3770 content: BoxContent::HBox(children),
3771 color: options.color,
3772 }
3773}
3774
3775fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3779 let cx = width / 2.0;
3780 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3785 PathCommand::MoveTo { x: cx + a, y: cy },
3786 PathCommand::CubicTo {
3787 x1: cx + a,
3788 y1: cy - k * b,
3789 x2: cx + k * a,
3790 y2: cy - b,
3791 x: cx,
3792 y: cy - b,
3793 },
3794 PathCommand::CubicTo {
3795 x1: cx - k * a,
3796 y1: cy - b,
3797 x2: cx - a,
3798 y2: cy - k * b,
3799 x: cx - a,
3800 y: cy,
3801 },
3802 PathCommand::CubicTo {
3803 x1: cx - a,
3804 y1: cy + k * b,
3805 x2: cx - k * a,
3806 y2: cy + b,
3807 x: cx,
3808 y: cy + b,
3809 },
3810 PathCommand::CubicTo {
3811 x1: cx + k * a,
3812 y1: cy + b,
3813 x2: cx + a,
3814 y2: cy + k * b,
3815 x: cx + a,
3816 y: cy,
3817 },
3818 PathCommand::Close,
3819 ]
3820}
3821
3822fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3823 cmds.into_iter().map(|c| match c {
3824 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3825 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3826 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3827 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3828 },
3829 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3830 x1, y1: y1 + dy, x, y: y + dy,
3831 },
3832 PathCommand::Close => PathCommand::Close,
3833 }).collect()
3834}
3835
3836fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3837 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3838 return commands;
3839 }
3840 let ah = height * 0.35; let mid_y = -height / 2.0;
3842
3843 match label {
3844 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3845 vec![
3846 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3847 PathCommand::LineTo { x: 0.0, y: mid_y },
3848 PathCommand::LineTo { x: ah, y: mid_y + ah },
3849 PathCommand::MoveTo { x: 0.0, y: mid_y },
3850 PathCommand::LineTo { x: width, y: mid_y },
3851 ]
3852 }
3853 "\\overleftrightarrow" | "\\underleftrightarrow"
3854 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3855 vec![
3856 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3857 PathCommand::LineTo { x: 0.0, y: mid_y },
3858 PathCommand::LineTo { x: ah, y: mid_y + ah },
3859 PathCommand::MoveTo { x: 0.0, y: mid_y },
3860 PathCommand::LineTo { x: width, y: mid_y },
3861 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3862 PathCommand::LineTo { x: width, y: mid_y },
3863 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3864 ]
3865 }
3866 "\\xlongequal" => {
3867 let gap = 0.04;
3868 vec![
3869 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3870 PathCommand::LineTo { x: width, y: mid_y - gap },
3871 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3872 PathCommand::LineTo { x: width, y: mid_y + gap },
3873 ]
3874 }
3875 "\\xhookleftarrow" => {
3876 vec![
3877 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3878 PathCommand::LineTo { x: 0.0, y: mid_y },
3879 PathCommand::LineTo { x: ah, y: mid_y + ah },
3880 PathCommand::MoveTo { x: 0.0, y: mid_y },
3881 PathCommand::LineTo { x: width, y: mid_y },
3882 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3883 ]
3884 }
3885 "\\xhookrightarrow" => {
3886 vec![
3887 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3888 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3889 PathCommand::LineTo { x: width, y: mid_y },
3890 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3891 PathCommand::LineTo { x: width, y: mid_y },
3892 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3893 ]
3894 }
3895 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3896 let right = label.contains("right");
3897 if right {
3898 vec![
3899 PathCommand::MoveTo { x: 0.0, y: mid_y },
3900 PathCommand::LineTo { x: width, y: mid_y },
3901 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3902 PathCommand::LineTo { x: width, y: mid_y },
3903 ]
3904 } else {
3905 vec![
3906 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3907 PathCommand::LineTo { x: 0.0, y: mid_y },
3908 PathCommand::LineTo { x: width, y: mid_y },
3909 ]
3910 }
3911 }
3912 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3913 let right = label.contains("right");
3914 if right {
3915 vec![
3916 PathCommand::MoveTo { x: 0.0, y: mid_y },
3917 PathCommand::LineTo { x: width, y: mid_y },
3918 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3919 PathCommand::LineTo { x: width, y: mid_y },
3920 ]
3921 } else {
3922 vec![
3923 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3924 PathCommand::LineTo { x: 0.0, y: mid_y },
3925 PathCommand::LineTo { x: width, y: mid_y },
3926 ]
3927 }
3928 }
3929 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3930 let gap = 0.06;
3931 vec![
3932 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3933 PathCommand::LineTo { x: width, y: mid_y - gap },
3934 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3935 PathCommand::LineTo { x: width, y: mid_y - gap },
3936 PathCommand::MoveTo { x: width, y: mid_y + gap },
3937 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3938 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3939 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3940 ]
3941 }
3942 "\\xtofrom" | "\\xrightleftarrows" => {
3943 let gap = 0.06;
3944 vec![
3945 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3946 PathCommand::LineTo { x: width, y: mid_y - gap },
3947 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3948 PathCommand::LineTo { x: width, y: mid_y - gap },
3949 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3950 PathCommand::MoveTo { x: width, y: mid_y + gap },
3951 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3952 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3953 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3954 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3955 ]
3956 }
3957 "\\overlinesegment" | "\\underlinesegment" => {
3958 vec![
3959 PathCommand::MoveTo { x: 0.0, y: mid_y },
3960 PathCommand::LineTo { x: width, y: mid_y },
3961 ]
3962 }
3963 _ => {
3964 vec![
3965 PathCommand::MoveTo { x: 0.0, y: mid_y },
3966 PathCommand::LineTo { x: width, y: mid_y },
3967 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3968 PathCommand::LineTo { x: width, y: mid_y },
3969 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3970 ]
3971 }
3972 }
3973}
3974
3975fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
3981 let h = inner.height;
3982 let d = inner.depth;
3983 let w = inner.width + pad_l + pad_r;
3984 let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
3985 if pad_l > 0.0 {
3986 children.push(LayoutBox::new_kern(pad_l));
3987 }
3988 children.push(inner);
3989 if pad_r > 0.0 {
3990 children.push(LayoutBox::new_kern(pad_r));
3991 }
3992 LayoutBox {
3993 width: w,
3994 height: h,
3995 depth: d,
3996 content: BoxContent::HBox(children),
3997 color,
3998 }
3999}
4000
4001fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
4012 let shift = (box_h - box_d + label.depth - label.height) / 2.0;
4013 LayoutBox {
4014 width: label.width,
4015 height: box_h,
4016 depth: box_d,
4017 content: BoxContent::RaiseBox {
4018 body: Box::new(label),
4019 shift,
4020 },
4021 color,
4022 }
4023}
4024
4025fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
4029 let sup_style = options.style.superscript();
4030 let sup_opts = options.with_style(sup_style);
4031 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4032 let inner = layout_node(body, &sup_opts);
4033 if (sup_ratio - 1.0).abs() < 1e-6 {
4034 inner
4035 } else {
4036 LayoutBox {
4037 width: inner.width * sup_ratio,
4038 height: inner.height * sup_ratio,
4039 depth: inner.depth * sup_ratio,
4040 content: BoxContent::Scaled {
4041 body: Box::new(inner),
4042 child_scale: sup_ratio,
4043 },
4044 color: options.color,
4045 }
4046 }
4047}
4048
4049fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
4055 let axis = options.metrics().axis_height;
4056 let depth = (total_height / 2.0 - axis).max(0.0);
4057 let height = total_height - depth;
4058 if let Some((commands, w)) =
4059 crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
4060 {
4061 return LayoutBox {
4062 width: w,
4063 height,
4064 depth,
4065 content: BoxContent::SvgPath {
4066 commands,
4067 fill: true,
4068 },
4069 color: options.color,
4070 };
4071 }
4072 if down {
4074 make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
4075 } else {
4076 make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
4077 }
4078}
4079
4080fn layout_cd_arrow(
4096 direction: &str,
4097 label_above: Option<&ParseNode>,
4098 label_below: Option<&ParseNode>,
4099 target_size: f64,
4100 target_col_width: f64,
4101 _target_depth: f64,
4102 options: &LayoutOptions,
4103) -> LayoutBox {
4104 let metrics = options.metrics();
4105 let axis = metrics.axis_height;
4106
4107 const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
4110
4111 match direction {
4112 "right" | "left" | "horiz_eq" => {
4113 let sup_style = options.style.superscript();
4115 let sub_style = options.style.subscript();
4116 let sup_opts = options.with_style(sup_style);
4117 let sub_opts = options.with_style(sub_style);
4118 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
4119 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
4120
4121 let above_box = label_above.map(|n| layout_node(n, &sup_opts));
4122 let below_box = label_below.map(|n| layout_node(n, &sub_opts));
4123
4124 let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
4125 let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
4126
4127 let path_label = if direction == "right" {
4129 "\\cdrightarrow"
4130 } else if direction == "left" {
4131 "\\cdleftarrow"
4132 } else {
4133 "\\cdlongequal"
4134 };
4135 let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
4136 const CD_LABEL_PAD_L: f64 = 0.22;
4139 const CD_LABEL_PAD_R: f64 = 0.48;
4140 let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
4141 let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
4142 let upper_need = above_box
4143 .as_ref()
4144 .map(|_| above_w + cd_pad_sup)
4145 .unwrap_or(0.0);
4146 let lower_need = below_box
4147 .as_ref()
4148 .map(|_| below_w + cd_pad_sub)
4149 .unwrap_or(0.0);
4150 let natural_w = upper_need.max(lower_need).max(0.0);
4151 let shaft_w = if target_size > 0.0 {
4152 target_size
4153 } else {
4154 natural_w.max(min_shaft_w)
4155 };
4156
4157 let (commands, actual_arrow_h, fill_arrow) =
4158 match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
4159 Some((c, h)) => (c, h, true),
4160 None => {
4161 let arrow_h = 0.3_f64;
4163 let ah = 0.12_f64;
4164 let cmds = if direction == "horiz_eq" {
4165 let gap = 0.06;
4166 vec![
4167 PathCommand::MoveTo { x: 0.0, y: -gap },
4168 PathCommand::LineTo { x: shaft_w, y: -gap },
4169 PathCommand::MoveTo { x: 0.0, y: gap },
4170 PathCommand::LineTo { x: shaft_w, y: gap },
4171 ]
4172 } else if direction == "right" {
4173 vec![
4174 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4175 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4176 PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
4177 PathCommand::LineTo { x: shaft_w, y: 0.0 },
4178 PathCommand::LineTo { x: shaft_w - ah, y: ah },
4179 ]
4180 } else {
4181 vec![
4182 PathCommand::MoveTo { x: shaft_w, y: 0.0 },
4183 PathCommand::LineTo { x: 0.0, y: 0.0 },
4184 PathCommand::MoveTo { x: ah, y: -ah },
4185 PathCommand::LineTo { x: 0.0, y: 0.0 },
4186 PathCommand::LineTo { x: ah, y: ah },
4187 ]
4188 };
4189 (cmds, arrow_h, false)
4190 }
4191 };
4192
4193 let arrow_half = actual_arrow_h / 2.0;
4195 let arrow_box = LayoutBox {
4196 width: shaft_w,
4197 height: arrow_half,
4198 depth: arrow_half,
4199 content: BoxContent::SvgPath {
4200 commands,
4201 fill: fill_arrow,
4202 },
4203 color: options.color,
4204 };
4205
4206 let gap = 0.111;
4208 let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
4209 let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
4210 let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
4214 sup_d
4215 } else {
4216 0.0
4217 };
4218 let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
4219 let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
4220 let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
4221 let depth = if below_box.is_some() {
4222 (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
4223 } else {
4224 (arrow_half - axis).max(0.0)
4225 };
4226
4227 let inner = LayoutBox {
4228 width: shaft_w,
4229 height,
4230 depth,
4231 content: BoxContent::OpLimits {
4232 base: Box::new(arrow_box),
4233 sup: above_box.map(Box::new),
4234 sub: below_box.map(Box::new),
4235 base_shift: -axis,
4236 sup_kern: gap,
4237 sub_kern: gap,
4238 slant: 0.0,
4239 sup_scale: sup_ratio,
4240 sub_scale: sub_ratio,
4241 },
4242 color: options.color,
4243 };
4244
4245 if target_col_width > inner.width + 1e-6 {
4249 let extra = target_col_width - inner.width;
4250 let kl = extra / 2.0;
4251 let kr = extra - kl;
4252 cd_wrap_hpad(inner, kl, kr, options.color)
4253 } else {
4254 inner
4255 }
4256 }
4257
4258 "down" | "up" | "vert_eq" => {
4259 let big_total = SIZE_TO_MAX_HEIGHT[2]; let shaft_box = match direction {
4263 "vert_eq" if target_size > 0.0 => {
4264 make_vert_delim_box(target_size.max(big_total), true, options)
4265 }
4266 "vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
4267 "down" if target_size > 0.0 => {
4268 cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
4269 }
4270 "up" if target_size > 0.0 => {
4271 cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
4272 }
4273 "down" => cd_stretch_vert_arrow_box(big_total, true, options),
4274 "up" => cd_stretch_vert_arrow_box(big_total, false, options),
4275 _ => cd_stretch_vert_arrow_box(big_total, true, options),
4276 };
4277 let box_h = shaft_box.height;
4278 let box_d = shaft_box.depth;
4279 let shaft_w = shaft_box.width;
4280
4281 let left_box = label_above.map(|n| {
4284 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4285 });
4286 let right_box = label_below.map(|n| {
4287 cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
4288 });
4289
4290 let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4291 let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
4292 let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
4293 let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
4294 let inner_w = left_part + shaft_w + right_part;
4295
4296 let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
4298 let extra = target_col_width - inner_w;
4299 let kl = extra / 2.0;
4300 let kr = extra - kl;
4301 (kl, kr, target_col_width)
4302 } else {
4303 (0.0, 0.0, inner_w)
4304 };
4305
4306 let mut children: Vec<LayoutBox> = Vec::new();
4307 if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
4308 if let Some(lb) = left_box {
4309 children.push(lb);
4310 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4311 }
4312 children.push(shaft_box);
4313 if let Some(rb) = right_box {
4314 children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
4315 children.push(rb);
4316 }
4317 if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
4318
4319 LayoutBox {
4320 width: total_w,
4321 height: box_h,
4322 depth: box_d,
4323 content: BoxContent::HBox(children),
4324 color: options.color,
4325 }
4326 }
4327
4328 _ => LayoutBox::new_empty(),
4330 }
4331}
4332
4333fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
4335 let metrics = options.metrics();
4336 let pt = 1.0 / metrics.pt_per_em;
4337 let baselineskip = 3.0 * metrics.x_height;
4339 let arstrut_h = 0.7 * baselineskip;
4340 let arstrut_d = 0.3 * baselineskip;
4341
4342 let num_rows = body.len();
4343 if num_rows == 0 {
4344 return LayoutBox::new_empty();
4345 }
4346 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
4347 if num_cols == 0 {
4348 return LayoutBox::new_empty();
4349 }
4350
4351 let jot = 3.0 * pt;
4353
4354 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
4356 let mut col_widths = vec![0.0_f64; num_cols];
4357 let mut row_heights = vec![arstrut_h; num_rows];
4358 let mut row_depths = vec![arstrut_d; num_rows];
4359
4360 for (r, row) in body.iter().enumerate() {
4361 let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
4362
4363 for (c, cell) in row.iter().enumerate() {
4364 let cbox = match cell {
4365 ParseNode::CdArrow { direction, label_above, label_below, .. } => {
4366 layout_cd_arrow(
4367 direction,
4368 label_above.as_deref(),
4369 label_below.as_deref(),
4370 0.0, 0.0, 0.0, options,
4374 )
4375 }
4376 ParseNode::OrdGroup { body: cell_body, .. } => {
4380 layout_expression(cell_body, options, false)
4381 }
4382 other => layout_node(other, options),
4383 };
4384
4385 row_heights[r] = row_heights[r].max(cbox.height);
4386 row_depths[r] = row_depths[r].max(cbox.depth);
4387 col_widths[c] = col_widths[c].max(cbox.width);
4388 row_boxes.push(cbox);
4389 }
4390
4391 while row_boxes.len() < num_cols {
4393 row_boxes.push(LayoutBox::new_empty());
4394 }
4395 cell_boxes.push(row_boxes);
4396 }
4397
4398 let col_target_w: Vec<f64> = col_widths.clone();
4402
4403 #[cfg(debug_assertions)]
4404 {
4405 eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4406 for (r, row) in cell_boxes.iter().enumerate() {
4407 for (c, b) in row.iter().enumerate() {
4408 if b.width > 0.0 {
4409 eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
4410 }
4411 }
4412 }
4413 }
4414
4415 for (r, row) in body.iter().enumerate() {
4417 let is_arrow_row = r % 2 == 1;
4418 for (c, cell) in row.iter().enumerate() {
4419 if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
4420 let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
4421 let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
4422 let b = layout_cd_arrow(
4423 direction,
4424 label_above.as_deref(),
4425 label_below.as_deref(),
4426 cell_boxes[r][c].width,
4427 col_target_w[c],
4428 0.0,
4429 options,
4430 );
4431 let w = b.width;
4432 (b, w)
4433 } else if is_arrow_row && c % 2 == 0 {
4434 let v_span = row_heights[r] + row_depths[r];
4438 let b = layout_cd_arrow(
4439 direction,
4440 label_above.as_deref(),
4441 label_below.as_deref(),
4442 v_span,
4443 col_widths[c],
4444 0.0,
4445 options,
4446 );
4447 let w = b.width;
4448 (b, w)
4449 } else {
4450 continue;
4451 };
4452 col_widths[c] = col_widths[c].max(col_w);
4453 cell_boxes[r][c] = new_box;
4454 }
4455 }
4456 }
4457
4458 #[cfg(debug_assertions)]
4459 {
4460 eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
4461 }
4462
4463 for rd in &mut row_depths {
4466 *rd += jot;
4467 }
4468
4469 let col_gap = 0.5;
4474
4475 let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
4477
4478 let col_separators = vec![None; num_cols + 1];
4480
4481 let mut total_height = 0.0_f64;
4482 let mut row_positions = Vec::with_capacity(num_rows);
4483 for r in 0..num_rows {
4484 total_height += row_heights[r];
4485 row_positions.push(total_height);
4486 total_height += row_depths[r];
4487 }
4488
4489 let offset = total_height / 2.0 + metrics.axis_height;
4490 let height = offset;
4491 let depth = total_height - offset;
4492
4493 let total_width = col_widths.iter().sum::<f64>()
4495 + col_gap * (num_cols.saturating_sub(1)) as f64;
4496
4497 let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
4499
4500 LayoutBox {
4501 width: total_width,
4502 height,
4503 depth,
4504 content: BoxContent::Array {
4505 cells: cell_boxes,
4506 col_widths,
4507 col_aligns,
4508 row_heights,
4509 row_depths,
4510 col_gap,
4511 offset,
4512 content_x_offset: 0.0,
4513 col_separators,
4514 hlines_before_row,
4515 rule_thickness: 0.04 * pt,
4516 double_rule_sep: metrics.double_rule_sep,
4517 array_inner_width: total_width,
4518 tag_gap_em: 0.0,
4519 tag_col_width: 0.0,
4520 row_tags: (0..num_rows).map(|_| None).collect(),
4521 tags_left: false,
4522 },
4523 color: options.color,
4524 }
4525}
4526
4527fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
4528 let mid = width / 2.0;
4529 let q = height * 0.6;
4530 if is_over {
4531 vec![
4532 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4533 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
4534 PathCommand::LineTo { x: mid - 0.05, y: -q },
4535 PathCommand::LineTo { x: mid, y: -height },
4536 PathCommand::LineTo { x: mid + 0.05, y: -q },
4537 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
4538 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
4539 ]
4540 } else {
4541 vec![
4542 PathCommand::MoveTo { x: 0.0, y: 0.0 },
4543 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
4544 PathCommand::LineTo { x: mid - 0.05, y: q },
4545 PathCommand::LineTo { x: mid, y: height },
4546 PathCommand::LineTo { x: mid + 0.05, y: q },
4547 PathCommand::LineTo { x: width - mid * 0.4, y: q },
4548 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
4549 ]
4550 }
4551}
4552
4553#[cfg(test)]
4554mod missing_glyph_width_em_tests {
4555 use super::{missing_glyph_height_em, missing_glyph_width_em};
4556 use ratex_font::get_global_metrics;
4557
4558 #[test]
4559 fn supplementary_plane_emoji_is_one_em() {
4560 assert_eq!(missing_glyph_width_em('😊'), 1.0);
4561 assert_eq!(missing_glyph_width_em('🚀'), 1.0);
4562 }
4563
4564 #[test]
4565 fn supplementary_plane_emoji_uses_shorter_box_height() {
4566 let m = get_global_metrics(0);
4567 let emoji_h = missing_glyph_height_em('😊', m);
4568 let default_h = (m.quad * 0.92).max(m.x_height);
4569 assert!(
4570 emoji_h < default_h,
4571 "tall placeholder box must not push \\sqrt past KaTeX's small-surd threshold"
4572 );
4573 assert!((emoji_h - 0.74).abs() < 1e-9);
4574 }
4575
4576 #[test]
4577 fn dingbats_block_is_one_em() {
4578 assert_eq!(missing_glyph_width_em('\u{2708}'), 1.0); }
4580
4581 #[test]
4582 fn miscellaneous_symbols_is_one_em() {
4583 assert_eq!(missing_glyph_width_em('\u{2605}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2615}'), 1.0); }
4586
4587 #[test]
4588 fn misc_symbols_and_arrows_is_one_em() {
4589 assert_eq!(missing_glyph_width_em('\u{2B50}'), 1.0); assert_eq!(missing_glyph_width_em('\u{2B1B}'), 1.0); }
4592
4593 #[test]
4594 fn latin_without_metrics_stays_half_em() {
4595 assert_eq!(missing_glyph_width_em('z'), 0.5);
4596 }
4597}
4598
4599#[cfg(test)]
4600mod cjk_font_switching_tests {
4601 use super::super::to_display::to_display_list;
4602 use super::*;
4603 use ratex_parser::parser::parse;
4604 use ratex_types::display_item::DisplayItem;
4605
4606 fn first_glyph_font_name(latex: &str) -> Option<String> {
4607 let ast = parse(latex).ok()?;
4608 let lbox = layout(&ast, &LayoutOptions::default());
4609 let dl = to_display_list(&lbox);
4610 for item in &dl.items {
4611 if let DisplayItem::GlyphPath { font, .. } = item {
4612 return Some(font.clone());
4613 }
4614 }
4615 None
4616 }
4617
4618 #[test]
4619 fn cjk_in_text_uses_cjk_regular() {
4620 assert_eq!(
4621 first_glyph_font_name(r"\text{中}").as_deref(),
4622 Some("CJK-Regular")
4623 );
4624 }
4625
4626 #[test]
4627 fn emoji_in_text_uses_cjk_regular() {
4628 assert_eq!(
4629 first_glyph_font_name(r"\text{😊}").as_deref(),
4630 Some("CJK-Regular")
4631 );
4632 }
4633
4634 #[test]
4635 fn latin_in_text_is_not_cjk() {
4636 assert_ne!(
4637 first_glyph_font_name(r"\text{a}").as_deref(),
4638 Some("CJK-Regular")
4639 );
4640 }
4641
4642 #[test]
4643 fn hiragana_in_text_uses_cjk_regular() {
4644 assert_eq!(
4645 first_glyph_font_name(r"\text{あ}").as_deref(),
4646 Some("CJK-Regular")
4647 );
4648 }
4649}