1use ratex_font::{get_char_metrics, get_global_metrics, FontId};
2use ratex_parser::parse_node::{AtomFamily, Mode, ParseNode};
3use ratex_types::color::Color;
4use ratex_types::math_style::MathStyle;
5use ratex_types::path_command::PathCommand;
6
7use crate::hbox::make_hbox;
8use crate::layout_box::{BoxContent, LayoutBox};
9use crate::layout_options::LayoutOptions;
10use crate::spacing::{atom_spacing, mu_to_em, MathClass};
11use crate::stacked_delim::make_stacked_delim_if_needed;
12
13pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
15 layout_expression(nodes, options, true)
16}
17
18fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
21 let n = raw.len();
22 let mut eff = raw.to_vec();
23 for i in 0..n {
24 if raw[i] != Some(MathClass::Bin) {
25 continue;
26 }
27 let prev = if i == 0 { None } else { raw[i - 1] };
28 let left_cancel = matches!(
29 prev,
30 None
31 | Some(MathClass::Bin)
32 | Some(MathClass::Open)
33 | Some(MathClass::Rel)
34 | Some(MathClass::Op)
35 | Some(MathClass::Punct)
36 );
37 if left_cancel {
38 eff[i] = Some(MathClass::Ord);
39 }
40 }
41 for i in 0..n {
42 if raw[i] != Some(MathClass::Bin) {
43 continue;
44 }
45 let next = if i + 1 < n { raw[i + 1] } else { None };
46 let right_cancel = matches!(
47 next,
48 None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
49 );
50 if right_cancel {
51 eff[i] = Some(MathClass::Ord);
52 }
53 }
54 eff
55}
56
57fn layout_expression(
59 nodes: &[ParseNode],
60 options: &LayoutOptions,
61 is_real_group: bool,
62) -> LayoutBox {
63 if nodes.is_empty() {
64 return LayoutBox::new_empty();
65 }
66
67 let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
69 if has_cr {
70 return layout_multiline(nodes, options, is_real_group);
71 }
72
73 let raw_classes: Vec<Option<MathClass>> =
74 nodes.iter().map(node_math_class).collect();
75 let eff_classes = apply_bin_cancellation(&raw_classes);
76
77 let mut children = Vec::new();
78 let mut prev_class: Option<MathClass> = None;
79
80 for (i, node) in nodes.iter().enumerate() {
81 let lbox = layout_node(node, options);
82 let cur_class = eff_classes.get(i).copied().flatten();
83
84 if is_real_group {
85 if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
86 let mu = atom_spacing(prev, cur, options.style.is_tight());
87 let mu = options
88 .align_relation_spacing
89 .map_or(mu, |cap| mu.min(cap));
90 if mu > 0.0 {
91 let em = mu_to_em(mu, options.metrics().quad);
92 children.push(LayoutBox::new_kern(em));
93 }
94 }
95 }
96
97 if cur_class.is_some() {
98 prev_class = cur_class;
99 }
100
101 children.push(lbox);
102 }
103
104 make_hbox(children)
105}
106
107fn layout_multiline(
109 nodes: &[ParseNode],
110 options: &LayoutOptions,
111 is_real_group: bool,
112) -> LayoutBox {
113 use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
114 let metrics = options.metrics();
115 let pt = 1.0 / metrics.pt_per_em;
116 let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt; let mut rows: Vec<&[ParseNode]> = Vec::new();
121 let mut start = 0;
122 for (i, node) in nodes.iter().enumerate() {
123 if matches!(node, ParseNode::Cr { .. }) {
124 rows.push(&nodes[start..i]);
125 start = i + 1;
126 }
127 }
128 rows.push(&nodes[start..]);
129
130 let row_boxes: Vec<LayoutBox> = rows
131 .iter()
132 .map(|row| layout_expression(row, options, is_real_group))
133 .collect();
134
135 let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
136
137 let mut vchildren: Vec<VBoxChild> = Vec::new();
138 let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
139 let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
140 for (i, row) in row_boxes.iter().enumerate() {
141 if i > 0 {
142 let prev_depth = row_boxes[i - 1].depth;
144 let gap = (baselineskip - prev_depth - row.height).max(lineskip);
145 vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
146 h += gap + row.height + prev_depth;
147 }
148 vchildren.push(VBoxChild { kind: VBoxChildKind::Box(row.clone()), shift: 0.0 });
149 }
150
151 LayoutBox {
152 width: total_width,
153 height: h,
154 depth: d,
155 content: BoxContent::VBox(vchildren),
156 color: options.color,
157 }
158}
159
160
161fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
163 match node {
164 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
165 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
166 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
167 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
168
169 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
170
171 ParseNode::SupSub {
172 base, sup, sub, ..
173 } => {
174 if let Some(base_node) = base.as_deref() {
175 if should_use_op_limits(base_node, options) {
176 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
177 }
178 }
179 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
180 }
181
182 ParseNode::GenFrac {
183 numer,
184 denom,
185 has_bar_line,
186 bar_size,
187 left_delim,
188 right_delim,
189 ..
190 } => {
191 let bar_thickness = if *has_bar_line {
192 bar_size
193 .as_ref()
194 .map(|m| measurement_to_em(m, options))
195 .unwrap_or(options.metrics().default_rule_thickness)
196 } else {
197 0.0
198 };
199 let frac = layout_fraction(numer, denom, bar_thickness, options);
200
201 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
202 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
203
204 if has_left || has_right {
205 let total_h = genfrac_delim_target_height(options);
206 let left_d = left_delim.as_deref().unwrap_or(".");
207 let right_d = right_delim.as_deref().unwrap_or(".");
208 let left_box = make_stretchy_delim(left_d, total_h, options);
209 let right_box = make_stretchy_delim(right_d, total_h, options);
210
211 let width = left_box.width + frac.width + right_box.width;
212 let height = frac.height.max(left_box.height).max(right_box.height);
213 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
214
215 LayoutBox {
216 width,
217 height,
218 depth,
219 content: BoxContent::LeftRight {
220 left: Box::new(left_box),
221 right: Box::new(right_box),
222 inner: Box::new(frac),
223 },
224 color: options.color,
225 }
226 } else {
227 frac
228 }
229 }
230
231 ParseNode::Sqrt { body, index, .. } => {
232 layout_radical(body, index.as_deref(), options)
233 }
234
235 ParseNode::Op {
236 name,
237 symbol,
238 body,
239 limits,
240 suppress_base_shift,
241 ..
242 } => layout_op(
243 name.as_deref(),
244 *symbol,
245 body.as_deref(),
246 *limits,
247 suppress_base_shift.unwrap_or(false),
248 options,
249 ),
250
251 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
252
253 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
254
255 ParseNode::Kern { dimension, .. } => {
256 let em = measurement_to_em(dimension, options);
257 LayoutBox::new_kern(em)
258 }
259
260 ParseNode::Color { color, body, .. } => {
261 let new_color = Color::from_name(color).unwrap_or(options.color);
262 let new_opts = options.with_color(new_color);
263 let mut lbox = layout_expression(body, &new_opts, true);
264 lbox.color = new_color;
265 lbox
266 }
267
268 ParseNode::Styling { style, body, .. } => {
269 let new_style = match style {
270 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
271 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
272 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
273 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
274 };
275 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
276 let new_opts = options.with_style(new_style);
277 let inner = layout_expression(body, &new_opts, true);
278 if (ratio - 1.0).abs() < 0.001 {
279 inner
280 } else {
281 LayoutBox {
282 width: inner.width * ratio,
283 height: inner.height * ratio,
284 depth: inner.depth * ratio,
285 content: BoxContent::Scaled {
286 body: Box::new(inner),
287 child_scale: ratio,
288 },
289 color: options.color,
290 }
291 }
292 }
293
294 ParseNode::Accent {
295 label, base, is_stretchy, is_shifty, ..
296 } => {
297 let is_below = matches!(label.as_str(), "\\c");
299 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
300 }
301
302 ParseNode::AccentUnder {
303 label, base, is_stretchy, ..
304 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
305
306 ParseNode::LeftRight {
307 body, left, right, ..
308 } => layout_left_right(body, left, right, options),
309
310 ParseNode::DelimSizing {
311 size, delim, ..
312 } => layout_delim_sizing(*size, delim, options),
313
314 ParseNode::Array {
315 body,
316 cols,
317 arraystretch,
318 add_jot,
319 row_gaps,
320 hlines_before_row,
321 col_separation_type,
322 hskip_before_and_after,
323 ..
324 } => layout_array(
325 body,
326 cols.as_deref(),
327 *arraystretch,
328 add_jot.unwrap_or(false),
329 row_gaps,
330 hlines_before_row,
331 col_separation_type.as_deref(),
332 hskip_before_and_after.unwrap_or(true),
333 options,
334 ),
335
336 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
337
338 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
339 Some(f) => {
340 let group = ParseNode::OrdGroup {
341 mode: *mode,
342 body: body.clone(),
343 semisimple: None,
344 loc: None,
345 };
346 layout_font(f, &group, options)
347 }
348 None => layout_text(body, options),
349 },
350
351 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
352
353 ParseNode::Href { body, .. } => layout_href(body, options),
354
355 ParseNode::Overline { body, .. } => layout_overline(body, options),
356 ParseNode::Underline { body, .. } => layout_underline(body, options),
357
358 ParseNode::Rule {
359 width: w,
360 height: h,
361 shift,
362 ..
363 } => {
364 let width = measurement_to_em(w, options);
365 let ink_h = measurement_to_em(h, options);
366 let raise = shift
367 .as_ref()
368 .map(|s| measurement_to_em(s, options))
369 .unwrap_or(0.0);
370 let box_height = (raise + ink_h).max(0.0);
371 let box_depth = (-raise).max(0.0);
372 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
373 }
374
375 ParseNode::Phantom { body, .. } => {
376 let inner = layout_expression(body, options, true);
377 LayoutBox {
378 width: inner.width,
379 height: inner.height,
380 depth: inner.depth,
381 content: BoxContent::Empty,
382 color: Color::BLACK,
383 }
384 }
385
386 ParseNode::VPhantom { body, .. } => {
387 let inner = layout_node(body, options);
388 LayoutBox {
389 width: 0.0,
390 height: inner.height,
391 depth: inner.depth,
392 content: BoxContent::Empty,
393 color: Color::BLACK,
394 }
395 }
396
397 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
398 let mut inner = layout_node(body, options);
399 if *smash_height { inner.height = 0.0; }
400 if *smash_depth { inner.depth = 0.0; }
401 inner
402 }
403
404 ParseNode::Middle { delim, .. } => {
405 match options.leftright_delim_height {
406 Some(h) => make_stretchy_delim(delim, h, options),
407 None => {
408 let placeholder = make_stretchy_delim(delim, 1.0, options);
410 LayoutBox {
411 width: placeholder.width,
412 height: 0.0,
413 depth: 0.0,
414 content: BoxContent::Empty,
415 color: options.color,
416 }
417 }
418 }
419 }
420
421 ParseNode::HtmlMathMl { html, .. } => {
422 layout_expression(html, options, true)
423 }
424
425 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
426
427 ParseNode::MathChoice {
428 display, text, script, scriptscript, ..
429 } => {
430 let branch = match options.style {
431 MathStyle::Display | MathStyle::DisplayCramped => display,
432 MathStyle::Text | MathStyle::TextCramped => text,
433 MathStyle::Script | MathStyle::ScriptCramped => script,
434 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
435 };
436 layout_expression(branch, options, true)
437 }
438
439 ParseNode::Lap { alignment, body, .. } => {
440 let inner = layout_node(body, options);
441 let shift = match alignment.as_str() {
442 "llap" => -inner.width,
443 "clap" => -inner.width / 2.0,
444 _ => 0.0, };
446 let mut children = Vec::new();
447 if shift != 0.0 {
448 children.push(LayoutBox::new_kern(shift));
449 }
450 let h = inner.height;
451 let d = inner.depth;
452 children.push(inner);
453 LayoutBox {
454 width: 0.0,
455 height: h,
456 depth: d,
457 content: BoxContent::HBox(children),
458 color: options.color,
459 }
460 }
461
462 ParseNode::HorizBrace {
463 base, is_over, ..
464 } => layout_horiz_brace(base, *is_over, options),
465
466 ParseNode::XArrow {
467 label, body, below, ..
468 } => layout_xarrow(label, body, below.as_deref(), options),
469
470 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
471
472 ParseNode::HBox { body, .. } => layout_text(body, options),
473
474 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
475 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
476 }
477
478 ParseNode::RaiseBox { dy, body, .. } => {
479 let shift = measurement_to_em(dy, options);
480 layout_raisebox(shift, body, options)
481 }
482
483 ParseNode::VCenter { body, .. } => {
484 let inner = layout_node(body, options);
486 let axis = options.metrics().axis_height;
487 let total = inner.height + inner.depth;
488 let height = total / 2.0 + axis;
489 let depth = total - height;
490 LayoutBox {
491 width: inner.width,
492 height,
493 depth,
494 content: inner.content,
495 color: inner.color,
496 }
497 }
498
499 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
500
501 _ => LayoutBox::new_empty(),
503 }
504}
505
506fn missing_glyph_width_em(ch: char) -> f64 {
516 match ch as u32 {
517 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
519 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
521 0xAC00..=0xD7AF => 1.0,
523 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
525 _ => 0.5,
526 }
527}
528
529fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
530 let m = get_global_metrics(options.style.size_index());
531 let w = missing_glyph_width_em(ch);
532 if w >= 0.99 {
533 let h = (m.quad * 0.92).max(m.x_height);
534 (w, h, 0.0)
535 } else {
536 (w, m.x_height, 0.0)
537 }
538}
539
540fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
541 let ch = resolve_symbol_char(text, mode);
542 let mut font_id = select_font(text, ch, mode, options);
543 let char_code = ch as u32;
544
545 let mut metrics = get_char_metrics(font_id, char_code);
546
547 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
548 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
549 font_id = FontId::MathItalic;
550 metrics = Some(m);
551 }
552 }
553
554 let (width, height, depth) = match metrics {
555 Some(m) => (m.width, m.height, m.depth),
556 None => missing_glyph_metrics_fallback(ch, options),
557 };
558
559 LayoutBox {
560 width,
561 height,
562 depth,
563 content: BoxContent::Glyph {
564 font_id,
565 char_code,
566 },
567 color: options.color,
568 }
569}
570
571fn resolve_symbol_char(text: &str, mode: Mode) -> char {
573 let font_mode = match mode {
574 Mode::Math => ratex_font::Mode::Math,
575 Mode::Text => ratex_font::Mode::Text,
576 };
577
578 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
579 if let Some(cp) = info.codepoint {
580 return cp;
581 }
582 }
583
584 text.chars().next().unwrap_or('?')
585}
586
587fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
591 let font_mode = match mode {
592 Mode::Math => ratex_font::Mode::Math,
593 Mode::Text => ratex_font::Mode::Text,
594 };
595
596 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
597 if info.font == ratex_font::SymbolFont::Ams {
598 return FontId::AmsRegular;
599 }
600 }
601
602 match mode {
603 Mode::Math => {
604 if resolved_char.is_ascii_lowercase()
605 || resolved_char.is_ascii_uppercase()
606 || is_greek_letter(resolved_char)
607 {
608 FontId::MathItalic
609 } else {
610 FontId::MainRegular
611 }
612 }
613 Mode::Text => FontId::MainRegular,
614 }
615}
616
617fn is_greek_letter(ch: char) -> bool {
618 matches!(ch,
619 '\u{0391}'..='\u{03C9}' |
620 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
621 '\u{03F1}' | '\u{03F5}'
622 )
623}
624
625fn is_arrow_accent(label: &str) -> bool {
626 matches!(
627 label,
628 "\\overrightarrow"
629 | "\\overleftarrow"
630 | "\\Overrightarrow"
631 | "\\overleftrightarrow"
632 | "\\underrightarrow"
633 | "\\underleftarrow"
634 | "\\underleftrightarrow"
635 | "\\overleftharpoon"
636 | "\\overrightharpoon"
637 | "\\overlinesegment"
638 | "\\underlinesegment"
639 )
640}
641
642fn layout_fraction(
647 numer: &ParseNode,
648 denom: &ParseNode,
649 bar_thickness: f64,
650 options: &LayoutOptions,
651) -> LayoutBox {
652 let numer_s = options.style.numerator();
653 let denom_s = options.style.denominator();
654 let numer_style = options.with_style(numer_s);
655 let denom_style = options.with_style(denom_s);
656
657 let numer_box = layout_node(numer, &numer_style);
658 let denom_box = layout_node(denom, &denom_style);
659
660 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
662 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
663
664 let numer_height = numer_box.height * numer_ratio;
665 let numer_depth = numer_box.depth * numer_ratio;
666 let denom_height = denom_box.height * denom_ratio;
667 let denom_depth = denom_box.depth * denom_ratio;
668 let numer_width = numer_box.width * numer_ratio;
669 let denom_width = denom_box.width * denom_ratio;
670
671 let metrics = options.metrics();
672 let axis = metrics.axis_height;
673 let rule = bar_thickness;
674
675 let (mut num_shift, mut den_shift) = if options.style.is_display() {
677 (metrics.num1, metrics.denom1)
678 } else if bar_thickness > 0.0 {
679 (metrics.num2, metrics.denom2)
680 } else {
681 (metrics.num3, metrics.denom2)
682 };
683
684 if bar_thickness > 0.0 {
685 let min_clearance = if options.style.is_display() {
686 3.0 * rule
687 } else {
688 rule
689 };
690
691 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
692 if num_clearance < min_clearance {
693 num_shift += min_clearance - num_clearance;
694 }
695
696 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
697 if den_clearance < min_clearance {
698 den_shift += min_clearance - den_clearance;
699 }
700 } else {
701 let min_gap = if options.style.is_display() {
702 7.0 * metrics.default_rule_thickness
703 } else {
704 3.0 * metrics.default_rule_thickness
705 };
706
707 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
708 if gap < min_gap {
709 let adjust = (min_gap - gap) / 2.0;
710 num_shift += adjust;
711 den_shift += adjust;
712 }
713 }
714
715 let total_width = numer_width.max(denom_width);
716 let height = numer_height + num_shift;
717 let depth = denom_depth + den_shift;
718
719 LayoutBox {
720 width: total_width,
721 height,
722 depth,
723 content: BoxContent::Fraction {
724 numer: Box::new(numer_box),
725 denom: Box::new(denom_box),
726 numer_shift: num_shift,
727 denom_shift: den_shift,
728 bar_thickness: rule,
729 numer_scale: numer_ratio,
730 denom_scale: denom_ratio,
731 },
732 color: options.color,
733 }
734}
735
736fn layout_supsub(
741 base: Option<&ParseNode>,
742 sup: Option<&ParseNode>,
743 sub: Option<&ParseNode>,
744 options: &LayoutOptions,
745 inherited_font: Option<FontId>,
746) -> LayoutBox {
747 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
748 Some(fid) => layout_with_font(n, fid, opts),
749 None => layout_node(n, opts),
750 };
751
752 let horiz_brace_over = matches!(
753 base,
754 Some(ParseNode::HorizBrace {
755 is_over: true,
756 ..
757 })
758 );
759 let horiz_brace_under = matches!(
760 base,
761 Some(ParseNode::HorizBrace {
762 is_over: false,
763 ..
764 })
765 );
766 let center_scripts = horiz_brace_over || horiz_brace_under;
767
768 let base_box = base
769 .map(|b| layout_child(b, options))
770 .unwrap_or_else(LayoutBox::new_empty);
771
772 let is_char_box = base.is_some_and(is_character_box);
773 let metrics = options.metrics();
774
775 let sup_style = options.style.superscript();
776 let sub_style = options.style.subscript();
777
778 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
779 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
780
781 let sup_box = sup.map(|s| {
782 let sup_opts = options.with_style(sup_style);
783 layout_child(s, &sup_opts)
784 });
785
786 let sub_box = sub.map(|s| {
787 let sub_opts = options.with_style(sub_style);
788 layout_child(s, &sub_opts)
789 });
790
791 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
792 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
793 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
794 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
795
796 let sup_style_metrics = get_global_metrics(sup_style.size_index());
798 let sub_style_metrics = get_global_metrics(sub_style.size_index());
799
800 let mut sup_shift = if !is_char_box && sup_box.is_some() {
803 base_box.height - sup_style_metrics.sup_drop * sup_ratio
804 } else {
805 0.0
806 };
807
808 let mut sub_shift = if !is_char_box && sub_box.is_some() {
809 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
810 } else {
811 0.0
812 };
813
814 let min_sup_shift = if options.style.is_cramped() {
815 metrics.sup3
816 } else if options.style.is_display() {
817 metrics.sup1
818 } else {
819 metrics.sup2
820 };
821
822 if sup_box.is_some() && sub_box.is_some() {
823 sup_shift = sup_shift
825 .max(min_sup_shift)
826 .max(sup_depth_scaled + 0.25 * metrics.x_height);
827 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
830 let max_width = 4.0 * rule_width;
831 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
832 if gap < max_width {
833 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
834 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
835 if psi > 0.0 {
836 sup_shift += psi;
837 sub_shift -= psi;
838 }
839 }
840 } else if sub_box.is_some() {
841 sub_shift = sub_shift
843 .max(metrics.sub1)
844 .max(sub_height_scaled - 0.8 * metrics.x_height);
845 } else if sup_box.is_some() {
846 sup_shift = sup_shift
848 .max(min_sup_shift)
849 .max(sup_depth_scaled + 0.25 * metrics.x_height);
850 }
851
852 if horiz_brace_over && sup_box.is_some() {
856 sup_shift += sup_style_metrics.sup_drop * sup_ratio;
857 sup_shift += metrics.big_op_spacing1;
860 }
861 if horiz_brace_under && sub_box.is_some() {
862 sub_shift += sub_style_metrics.sub_drop * sub_ratio;
863 sub_shift += metrics.big_op_spacing2 + 0.2;
864 }
865
866 let mut height = base_box.height;
868 let mut depth = base_box.depth;
869 let mut total_width = base_box.width;
870
871 if let Some(ref sup_b) = sup_box {
872 height = height.max(sup_shift + sup_height_scaled);
873 if center_scripts {
874 total_width = total_width.max(sup_b.width * sup_ratio);
875 } else {
876 total_width = total_width.max(base_box.width + sup_b.width * sup_ratio);
877 }
878 }
879 if let Some(ref sub_b) = sub_box {
880 depth = depth.max(sub_shift + sub_depth_scaled);
881 if center_scripts {
882 total_width = total_width.max(sub_b.width * sub_ratio);
883 } else {
884 total_width = total_width.max(base_box.width + sub_b.width * sub_ratio);
885 }
886 }
887
888 LayoutBox {
889 width: total_width,
890 height,
891 depth,
892 content: BoxContent::SupSub {
893 base: Box::new(base_box),
894 sup: sup_box.map(Box::new),
895 sub: sub_box.map(Box::new),
896 sup_shift,
897 sub_shift,
898 sup_scale: sup_ratio,
899 sub_scale: sub_ratio,
900 center_scripts,
901 },
902 color: options.color,
903 }
904}
905
906fn layout_radical(
911 body: &ParseNode,
912 index: Option<&ParseNode>,
913 options: &LayoutOptions,
914) -> LayoutBox {
915 let cramped = options.style.cramped();
916 let cramped_opts = options.with_style(cramped);
917 let mut body_box = layout_node(body, &cramped_opts);
918
919 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
921 body_box.height *= body_ratio;
922 body_box.depth *= body_ratio;
923 body_box.width *= body_ratio;
924
925 if body_box.height == 0.0 {
927 body_box.height = options.metrics().x_height;
928 }
929
930 let metrics = options.metrics();
931 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
936 metrics.x_height
937 } else {
938 theta
939 };
940
941 let mut line_clearance = theta + phi / 4.0;
942
943 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
945
946 let tex_height = select_surd_height(min_delim_height);
949 let rule_width = theta;
950 let advance_width = 0.833;
951
952 let delim_depth = tex_height - rule_width;
954 if delim_depth > body_box.height + body_box.depth + line_clearance {
955 line_clearance =
956 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
957 }
958
959 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
960
961 let height = tex_height + rule_width - img_shift;
964 let depth = if img_shift > body_box.depth {
965 img_shift
966 } else {
967 body_box.depth
968 };
969
970 const INDEX_KERN: f64 = 0.05;
972 let (index_box, index_offset) = if let Some(index_node) = index {
973 let script_opts = options.with_style(options.style.superscript());
974 let idx = layout_node(index_node, &script_opts);
975 let script_em = options.style.superscript().size_multiplier();
976 let offset = idx.width * script_em + INDEX_KERN;
977 (Some(Box::new(idx)), offset)
978 } else {
979 (None, 0.0)
980 };
981
982 let width = index_offset + advance_width + body_box.width;
983
984 LayoutBox {
985 width,
986 height,
987 depth,
988 content: BoxContent::Radical {
989 body: Box::new(body_box),
990 index: index_box,
991 index_offset,
992 rule_thickness: rule_width,
993 inner_height: tex_height,
994 },
995 color: options.color,
996 }
997}
998
999fn select_surd_height(min_height: f64) -> f64 {
1002 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1003 for &h in &SURD_HEIGHTS {
1004 if h >= min_height {
1005 return h;
1006 }
1007 }
1008 SURD_HEIGHTS[4].max(min_height)
1010}
1011
1012const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1017
1018fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1020 match base {
1021 ParseNode::Op {
1022 limits,
1023 always_handle_sup_sub,
1024 ..
1025 } => {
1026 *limits
1027 && (options.style.is_display()
1028 || always_handle_sup_sub.unwrap_or(false))
1029 }
1030 ParseNode::OperatorName {
1031 always_handle_sup_sub,
1032 limits,
1033 ..
1034 } => {
1035 *always_handle_sup_sub
1036 && (options.style.is_display() || *limits)
1037 }
1038 _ => false,
1039 }
1040}
1041
1042fn layout_op(
1048 name: Option<&str>,
1049 symbol: bool,
1050 body: Option<&[ParseNode]>,
1051 _limits: bool,
1052 suppress_base_shift: bool,
1053 options: &LayoutOptions,
1054) -> LayoutBox {
1055 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1056
1057 if symbol && !suppress_base_shift {
1059 let axis = options.metrics().axis_height;
1060 let _total = base_box.height + base_box.depth;
1061 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1062 if shift.abs() > 0.001 {
1063 base_box.height -= shift;
1064 base_box.depth += shift;
1065 }
1066 }
1067
1068 base_box
1069}
1070
1071fn build_op_base(
1074 name: Option<&str>,
1075 symbol: bool,
1076 body: Option<&[ParseNode]>,
1077 options: &LayoutOptions,
1078) -> (LayoutBox, f64) {
1079 if symbol {
1080 let large = options.style.is_display()
1081 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1082 let font_id = if large {
1083 FontId::Size2Regular
1084 } else {
1085 FontId::Size1Regular
1086 };
1087
1088 let op_name = name.unwrap_or("");
1089 let ch = resolve_op_char(op_name);
1090 let char_code = ch as u32;
1091
1092 let metrics = get_char_metrics(font_id, char_code);
1093 let (width, height, depth, italic) = match metrics {
1094 Some(m) => (m.width, m.height, m.depth, m.italic),
1095 None => (1.0, 0.75, 0.25, 0.0),
1096 };
1097 let width_with_italic = width + italic;
1100
1101 let base = LayoutBox {
1102 width: width_with_italic,
1103 height,
1104 depth,
1105 content: BoxContent::Glyph {
1106 font_id,
1107 char_code,
1108 },
1109 color: options.color,
1110 };
1111
1112 if op_name == "\\oiint" || op_name == "\\oiiint" {
1115 let w = base.width;
1116 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1117 let overlay_box = LayoutBox {
1118 width: w,
1119 height: base.height,
1120 depth: base.depth,
1121 content: BoxContent::SvgPath {
1122 commands: ellipse_commands,
1123 fill: false,
1124 },
1125 color: options.color,
1126 };
1127 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1128 return (with_overlay, italic);
1129 }
1130
1131 (base, italic)
1132 } else if let Some(body_nodes) = body {
1133 let base = layout_expression(body_nodes, options, true);
1134 (base, 0.0)
1135 } else {
1136 let base = layout_op_text(name.unwrap_or(""), options);
1137 (base, 0.0)
1138 }
1139}
1140
1141fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1143 let text = name.strip_prefix('\\').unwrap_or(name);
1144 let mut children = Vec::new();
1145 for ch in text.chars() {
1146 let char_code = ch as u32;
1147 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1148 let (width, height, depth) = match metrics {
1149 Some(m) => (m.width, m.height, m.depth),
1150 None => (0.5, 0.43, 0.0),
1151 };
1152 children.push(LayoutBox {
1153 width,
1154 height,
1155 depth,
1156 content: BoxContent::Glyph {
1157 font_id: FontId::MainRegular,
1158 char_code,
1159 },
1160 color: options.color,
1161 });
1162 }
1163 make_hbox(children)
1164}
1165
1166fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1168 let metrics = options.metrics();
1169 (base.height - base.depth) / 2.0 - metrics.axis_height
1170}
1171
1172fn resolve_op_char(name: &str) -> char {
1174 match name {
1177 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1180 }
1181 let font_mode = ratex_font::Mode::Math;
1182 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1183 if let Some(cp) = info.codepoint {
1184 return cp;
1185 }
1186 }
1187 name.chars().next().unwrap_or('?')
1188}
1189
1190fn layout_op_with_limits(
1192 base_node: &ParseNode,
1193 sup_node: Option<&ParseNode>,
1194 sub_node: Option<&ParseNode>,
1195 options: &LayoutOptions,
1196) -> LayoutBox {
1197 let (name, symbol, body, suppress_base_shift) = match base_node {
1198 ParseNode::Op {
1199 name,
1200 symbol,
1201 body,
1202 suppress_base_shift,
1203 ..
1204 } => (
1205 name.as_deref(),
1206 *symbol,
1207 body.as_deref(),
1208 suppress_base_shift.unwrap_or(false),
1209 ),
1210 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1211 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1212 };
1213
1214 let (base_box, slant) = build_op_base(name, symbol, body, options);
1215 let base_shift = if symbol && !suppress_base_shift {
1217 compute_op_base_shift(&base_box, options)
1218 } else {
1219 0.0
1220 };
1221
1222 layout_op_limits_inner(&base_box, sup_node, sub_node, slant, base_shift, options)
1223}
1224
1225fn layout_op_limits_inner(
1227 base: &LayoutBox,
1228 sup_node: Option<&ParseNode>,
1229 sub_node: Option<&ParseNode>,
1230 slant: f64,
1231 base_shift: f64,
1232 options: &LayoutOptions,
1233) -> LayoutBox {
1234 let metrics = options.metrics();
1235 let sup_style = options.style.superscript();
1236 let sub_style = options.style.subscript();
1237
1238 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1239 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1240
1241 let extra_clearance = 0.08_f64;
1243
1244 let sup_data = sup_node.map(|s| {
1245 let sup_opts = options.with_style(sup_style);
1246 let elem = layout_node(s, &sup_opts);
1247 let kern = (metrics.big_op_spacing1 + extra_clearance)
1248 .max(metrics.big_op_spacing3 - elem.depth * sup_ratio + extra_clearance);
1249 (elem, kern)
1250 });
1251
1252 let sub_data = sub_node.map(|s| {
1253 let sub_opts = options.with_style(sub_style);
1254 let elem = layout_node(s, &sub_opts);
1255 let kern = (metrics.big_op_spacing2 + extra_clearance)
1256 .max(metrics.big_op_spacing4 - elem.height * sub_ratio + extra_clearance);
1257 (elem, kern)
1258 });
1259
1260 let sp5 = metrics.big_op_spacing5;
1261
1262 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1263 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1264 let sup_h = sup_elem.height * sup_ratio;
1267 let sup_d = sup_elem.depth * sup_ratio;
1268 let sub_h = sub_elem.height * sub_ratio;
1269 let sub_d = sub_elem.depth * sub_ratio;
1270
1271 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1272
1273 let height = bottom
1274 + base.height - base_shift
1275 + sup_kern
1276 + sup_h + sup_d
1277 + sp5
1278 - (base.height + base.depth);
1279
1280 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1281 let total_d = bottom;
1282
1283 let w = base
1284 .width
1285 .max(sup_elem.width * sup_ratio)
1286 .max(sub_elem.width * sub_ratio);
1287 let _ = height; (total_h, total_d, w)
1289 }
1290 (None, Some((sub_elem, sub_kern))) => {
1291 let sub_h = sub_elem.height * sub_ratio;
1294 let sub_d = sub_elem.depth * sub_ratio;
1295
1296 let total_h = base.height - base_shift;
1297 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1298
1299 let w = base.width.max(sub_elem.width * sub_ratio);
1300 (total_h, total_d, w)
1301 }
1302 (Some((sup_elem, sup_kern)), None) => {
1303 let sup_h = sup_elem.height * sup_ratio;
1306 let sup_d = sup_elem.depth * sup_ratio;
1307
1308 let total_h =
1309 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1310 let total_d = base.depth + base_shift;
1311
1312 let w = base.width.max(sup_elem.width * sup_ratio);
1313 (total_h, total_d, w)
1314 }
1315 (None, None) => {
1316 return base.clone();
1317 }
1318 };
1319
1320 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1321 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1322
1323 LayoutBox {
1324 width: total_width,
1325 height: total_height,
1326 depth: total_depth,
1327 content: BoxContent::OpLimits {
1328 base: Box::new(base.clone()),
1329 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1330 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1331 base_shift,
1332 sup_kern: sup_kern_val,
1333 sub_kern: sub_kern_val,
1334 slant,
1335 sup_scale: sup_ratio,
1336 sub_scale: sub_ratio,
1337 },
1338 color: options.color,
1339 }
1340}
1341
1342fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1344 let mut children = Vec::new();
1345 for node in body {
1346 match node {
1347 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1348 let ch = text.chars().next().unwrap_or('?');
1349 let char_code = ch as u32;
1350 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1351 let (width, height, depth) = match metrics {
1352 Some(m) => (m.width, m.height, m.depth),
1353 None => (0.5, 0.43, 0.0),
1354 };
1355 children.push(LayoutBox {
1356 width,
1357 height,
1358 depth,
1359 content: BoxContent::Glyph {
1360 font_id: FontId::MainRegular,
1361 char_code,
1362 },
1363 color: options.color,
1364 });
1365 }
1366 _ => {
1367 children.push(layout_node(node, options));
1368 }
1369 }
1370 }
1371 make_hbox(children)
1372}
1373
1374const VEC_CLEARANCE_PULL_DOWN_EM: f64 = 0.082;
1380const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1381const VEC_CLEARANCE_MIN_FLOOR_EM: f64 = 0.30;
1382
1383fn glyph_skew(lb: &LayoutBox) -> f64 {
1387 match &lb.content {
1388 BoxContent::Glyph { font_id, char_code } => {
1389 get_char_metrics(*font_id, *char_code)
1390 .map(|m| m.skew)
1391 .unwrap_or(0.0)
1392 }
1393 BoxContent::HBox(children) => {
1394 children.last().map(glyph_skew).unwrap_or(0.0)
1395 }
1396 _ => 0.0,
1397 }
1398}
1399
1400fn layout_accent(
1401 label: &str,
1402 base: &ParseNode,
1403 is_stretchy: bool,
1404 is_shifty: bool,
1405 is_below: bool,
1406 options: &LayoutOptions,
1407) -> LayoutBox {
1408 let body_box = layout_node(base, options);
1409 let base_w = body_box.width.max(0.5);
1410
1411 if label == "\\textcircled" {
1413 return layout_textcircled(body_box, options);
1414 }
1415
1416 if let Some((commands, w, h, fill)) =
1418 crate::katex_svg::katex_accent_path(label, base_w)
1419 {
1420 let accent_box = LayoutBox {
1422 width: w,
1423 height: 0.0,
1424 depth: h,
1425 content: BoxContent::SvgPath { commands, fill },
1426 color: options.color,
1427 };
1428 let gap = 0.08;
1432 let clearance = if is_below {
1433 body_box.height + body_box.depth + gap
1434 } else if label == "\\vec" {
1435 (body_box.height.min(options.metrics().x_height) - VEC_CLEARANCE_PULL_DOWN_EM)
1436 .max(VEC_CLEARANCE_MIN_FLOOR_EM)
1437 } else {
1438 body_box.height + gap
1439 };
1440 let (height, depth) = if is_below {
1441 (body_box.height, body_box.depth + h + gap)
1442 } else if label == "\\vec" {
1443 (body_box.height + h, body_box.depth)
1444 } else {
1445 (body_box.height + gap + h, body_box.depth)
1446 };
1447 let vec_skew = if label == "\\vec" {
1448 (if is_shifty {
1449 glyph_skew(&body_box)
1450 } else {
1451 0.0
1452 }) + VEC_SKEW_EXTRA_RIGHT_EM
1453 } else {
1454 0.0
1455 };
1456 return LayoutBox {
1457 width: body_box.width,
1458 height,
1459 depth,
1460 content: BoxContent::Accent {
1461 base: Box::new(body_box),
1462 accent: Box::new(accent_box),
1463 clearance,
1464 skew: vec_skew,
1465 is_below,
1466 },
1467 color: options.color,
1468 };
1469 }
1470
1471 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1473
1474 let accent_box = if use_arrow_path {
1475 let (commands, arrow_h, fill_arrow) =
1476 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1477 Some((c, h)) => (c, h, true),
1478 None => {
1479 let h = 0.3_f64;
1480 let c = stretchy_accent_path(label, base_w, h);
1481 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1482 (c, h, fill)
1483 }
1484 };
1485 LayoutBox {
1486 width: base_w,
1487 height: arrow_h / 2.0,
1488 depth: arrow_h / 2.0,
1489 content: BoxContent::SvgPath {
1490 commands,
1491 fill: fill_arrow,
1492 },
1493 color: options.color,
1494 }
1495 } else {
1496 let accent_char = {
1498 let ch = resolve_symbol_char(label, Mode::Text);
1499 if ch == label.chars().next().unwrap_or('?') {
1500 resolve_symbol_char(label, Mode::Math)
1503 } else {
1504 ch
1505 }
1506 };
1507 let accent_code = accent_char as u32;
1508 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1509 let (accent_w, accent_h, accent_d) = match accent_metrics {
1510 Some(m) => (m.width, m.height, m.depth),
1511 None => (body_box.width, 0.25, 0.0),
1512 };
1513 LayoutBox {
1514 width: accent_w,
1515 height: accent_h,
1516 depth: accent_d,
1517 content: BoxContent::Glyph {
1518 font_id: FontId::MainRegular,
1519 char_code: accent_code,
1520 },
1521 color: options.color,
1522 }
1523 };
1524
1525 let skew = if use_arrow_path {
1526 0.0
1527 } else if is_shifty {
1528 glyph_skew(&body_box)
1531 } else {
1532 0.0
1533 };
1534
1535 let gap = if use_arrow_path {
1544 if label == "\\Overrightarrow" {
1545 0.21
1546 } else {
1547 0.26
1548 }
1549 } else {
1550 0.0
1551 };
1552
1553 let clearance = if is_below {
1554 body_box.height + body_box.depth + accent_box.depth + gap
1555 } else if use_arrow_path {
1556 body_box.height + gap
1557 } else {
1558 let base_clearance = match &body_box.content {
1568 BoxContent::Accent { clearance: inner_cl, is_below, .. } if !is_below => {
1569 inner_cl + 0.3
1570 }
1571 _ => body_box.height,
1572 };
1573 if label == "\\bar" || label == "\\=" {
1575 base_clearance - 0.2
1576 } else {
1577 base_clearance
1578 }
1579 };
1580
1581 let (height, depth) = if is_below {
1582 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1583 } else if use_arrow_path {
1584 (body_box.height + gap + accent_box.height, body_box.depth)
1585 } else {
1586 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1593 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1594 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1595 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1596 } else {
1597 body_box.height.max(accent_visual_top)
1598 };
1599 (h, body_box.depth)
1600 };
1601
1602 LayoutBox {
1603 width: body_box.width,
1604 height,
1605 depth,
1606 content: BoxContent::Accent {
1607 base: Box::new(body_box),
1608 accent: Box::new(accent_box),
1609 clearance,
1610 skew,
1611 is_below,
1612 },
1613 color: options.color,
1614 }
1615}
1616
1617fn node_contains_middle(node: &ParseNode) -> bool {
1623 match node {
1624 ParseNode::Middle { .. } => true,
1625 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1626 body.iter().any(node_contains_middle)
1627 }
1628 ParseNode::SupSub { base, sup, sub, .. } => {
1629 base.as_deref().is_some_and(node_contains_middle)
1630 || sup.as_deref().is_some_and(node_contains_middle)
1631 || sub.as_deref().is_some_and(node_contains_middle)
1632 }
1633 ParseNode::GenFrac { numer, denom, .. } => {
1634 node_contains_middle(numer) || node_contains_middle(denom)
1635 }
1636 ParseNode::Sqrt { body, index, .. } => {
1637 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1638 }
1639 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1640 node_contains_middle(base)
1641 }
1642 ParseNode::Op { body, .. } => body
1643 .as_ref()
1644 .is_some_and(|b| b.iter().any(node_contains_middle)),
1645 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1646 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1647 ParseNode::Font { body, .. } => node_contains_middle(body),
1648 ParseNode::Text { body, .. }
1649 | ParseNode::Color { body, .. }
1650 | ParseNode::Styling { body, .. }
1651 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1652 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1653 node_contains_middle(body)
1654 }
1655 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1656 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1657 node_contains_middle(body)
1658 }
1659 ParseNode::Array { body, .. } => body
1660 .iter()
1661 .any(|row| row.iter().any(node_contains_middle)),
1662 ParseNode::Enclose { body, .. }
1663 | ParseNode::Lap { body, .. }
1664 | ParseNode::RaiseBox { body, .. }
1665 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1666 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1667 ParseNode::XArrow { body, below, .. } => {
1668 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1669 }
1670 ParseNode::MathChoice {
1671 display,
1672 text,
1673 script,
1674 scriptscript,
1675 ..
1676 } => {
1677 display.iter().any(node_contains_middle)
1678 || text.iter().any(node_contains_middle)
1679 || script.iter().any(node_contains_middle)
1680 || scriptscript.iter().any(node_contains_middle)
1681 }
1682 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1683 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1684 _ => false,
1685 }
1686}
1687
1688fn body_contains_middle(nodes: &[ParseNode]) -> bool {
1690 nodes.iter().any(node_contains_middle)
1691}
1692
1693fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
1696 let m = options.metrics();
1697 if options.style.is_display() {
1698 m.delim1
1699 } else if matches!(
1700 options.style,
1701 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
1702 ) {
1703 options
1704 .with_style(MathStyle::Script)
1705 .metrics()
1706 .delim2
1707 } else {
1708 m.delim2
1709 }
1710}
1711
1712fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
1714 let metrics = options.metrics();
1715 let inner_height = inner.height;
1716 let inner_depth = inner.depth;
1717 let axis = metrics.axis_height;
1718 let max_dist = (inner_height - axis).max(inner_depth + axis);
1719 let delim_factor = 901.0;
1720 let delim_extend = 5.0 / metrics.pt_per_em;
1721 (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend)
1722}
1723
1724fn layout_left_right(
1725 body: &[ParseNode],
1726 left_delim: &str,
1727 right_delim: &str,
1728 options: &LayoutOptions,
1729) -> LayoutBox {
1730 let (inner, total_height) = if body_contains_middle(body) {
1731 let opts_first = LayoutOptions {
1733 leftright_delim_height: None,
1734 ..options.clone()
1735 };
1736 let inner_first = layout_expression(body, &opts_first, true);
1737 let total_height = left_right_delim_total_height(&inner_first, options);
1738 let opts_second = LayoutOptions {
1740 leftright_delim_height: Some(total_height),
1741 ..options.clone()
1742 };
1743 let inner_second = layout_expression(body, &opts_second, true);
1744 (inner_second, total_height)
1745 } else {
1746 let inner = layout_expression(body, options, true);
1747 let total_height = left_right_delim_total_height(&inner, options);
1748 (inner, total_height)
1749 };
1750
1751 let inner_height = inner.height;
1752 let inner_depth = inner.depth;
1753
1754 let left_box = make_stretchy_delim(left_delim, total_height, options);
1755 let right_box = make_stretchy_delim(right_delim, total_height, options);
1756
1757 let width = left_box.width + inner.width + right_box.width;
1758 let height = left_box.height.max(right_box.height).max(inner_height);
1759 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
1760
1761 LayoutBox {
1762 width,
1763 height,
1764 depth,
1765 content: BoxContent::LeftRight {
1766 left: Box::new(left_box),
1767 right: Box::new(right_box),
1768 inner: Box::new(inner),
1769 },
1770 color: options.color,
1771 }
1772}
1773
1774const DELIM_FONT_SEQUENCE: [FontId; 5] = [
1775 FontId::MainRegular,
1776 FontId::Size1Regular,
1777 FontId::Size2Regular,
1778 FontId::Size3Regular,
1779 FontId::Size4Regular,
1780];
1781
1782fn normalize_delim(delim: &str) -> &str {
1784 match delim {
1785 "<" | "\\lt" | "\u{27E8}" => "\\langle",
1786 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
1787 _ => delim,
1788 }
1789}
1790
1791fn is_vert_delim(delim: &str) -> bool {
1793 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
1794}
1795
1796fn is_double_vert_delim(delim: &str) -> bool {
1798 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
1799}
1800
1801fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
1805 let axis = options.metrics().axis_height;
1806 let depth = (total_height / 2.0 - axis).max(0.0);
1807 let height = total_height - depth;
1808 let width = if is_double { 0.556 } else { 0.333 };
1809
1810 let commands = if is_double {
1811 double_vert_delim_path(height, depth)
1812 } else {
1813 vert_delim_path(height, depth)
1814 };
1815
1816 LayoutBox {
1817 width,
1818 height,
1819 depth,
1820 content: BoxContent::SvgPath { commands, fill: true },
1821 color: options.color,
1822 }
1823}
1824
1825fn vert_delim_path(height: f64, depth: f64) -> Vec<PathCommand> {
1828 let xl = 0.145_f64;
1830 let xr = 0.188_f64;
1831 vec![
1832 PathCommand::MoveTo { x: xl, y: -height },
1833 PathCommand::LineTo { x: xr, y: -height },
1834 PathCommand::LineTo { x: xr, y: depth },
1835 PathCommand::LineTo { x: xl, y: depth },
1836 PathCommand::Close,
1837 ]
1838}
1839
1840fn double_vert_delim_path(height: f64, depth: f64) -> Vec<PathCommand> {
1842 let (xl1, xr1) = (0.145_f64, 0.188_f64);
1843 let (xl2, xr2) = (0.367_f64, 0.410_f64);
1844 vec![
1845 PathCommand::MoveTo { x: xl1, y: -height },
1846 PathCommand::LineTo { x: xr1, y: -height },
1847 PathCommand::LineTo { x: xr1, y: depth },
1848 PathCommand::LineTo { x: xl1, y: depth },
1849 PathCommand::Close,
1850 PathCommand::MoveTo { x: xl2, y: -height },
1851 PathCommand::LineTo { x: xr2, y: -height },
1852 PathCommand::LineTo { x: xr2, y: depth },
1853 PathCommand::LineTo { x: xl2, y: depth },
1854 PathCommand::Close,
1855 ]
1856}
1857
1858fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
1860 if delim == "." || delim.is_empty() {
1861 return LayoutBox::new_kern(0.0);
1862 }
1863
1864 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
1869 return make_vert_delim_box(total_height, false, options);
1870 }
1871 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
1872 return make_vert_delim_box(total_height, true, options);
1873 }
1874
1875 let delim = normalize_delim(delim);
1877
1878 let ch = resolve_symbol_char(delim, Mode::Math);
1879 let char_code = ch as u32;
1880
1881 let mut best_font = FontId::MainRegular;
1882 let mut best_w = 0.4;
1883 let mut best_h = 0.7;
1884 let mut best_d = 0.2;
1885
1886 for &font_id in &DELIM_FONT_SEQUENCE {
1887 if let Some(m) = get_char_metrics(font_id, char_code) {
1888 best_font = font_id;
1889 best_w = m.width;
1890 best_h = m.height;
1891 best_d = m.depth;
1892 if best_h + best_d >= total_height {
1893 break;
1894 }
1895 }
1896 }
1897
1898 let best_total = best_h + best_d;
1899 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
1900 return stacked;
1901 }
1902
1903 LayoutBox {
1904 width: best_w,
1905 height: best_h,
1906 depth: best_d,
1907 content: BoxContent::Glyph {
1908 font_id: best_font,
1909 char_code,
1910 },
1911 color: options.color,
1912 }
1913}
1914
1915const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
1917
1918fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
1920 if delim == "." || delim.is_empty() {
1921 return LayoutBox::new_kern(0.0);
1922 }
1923
1924 if is_vert_delim(delim) {
1926 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
1927 return make_vert_delim_box(total, false, options);
1928 }
1929 if is_double_vert_delim(delim) {
1930 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
1931 return make_vert_delim_box(total, true, options);
1932 }
1933
1934 let delim = normalize_delim(delim);
1936
1937 let ch = resolve_symbol_char(delim, Mode::Math);
1938 let char_code = ch as u32;
1939
1940 let font_id = match size {
1941 1 => FontId::Size1Regular,
1942 2 => FontId::Size2Regular,
1943 3 => FontId::Size3Regular,
1944 4 => FontId::Size4Regular,
1945 _ => FontId::Size1Regular,
1946 };
1947
1948 let metrics = get_char_metrics(font_id, char_code);
1949 let (width, height, depth, actual_font) = match metrics {
1950 Some(m) => (m.width, m.height, m.depth, font_id),
1951 None => {
1952 let m = get_char_metrics(FontId::MainRegular, char_code);
1953 match m {
1954 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
1955 None => (0.4, 0.7, 0.2, FontId::MainRegular),
1956 }
1957 }
1958 };
1959
1960 LayoutBox {
1961 width,
1962 height,
1963 depth,
1964 content: BoxContent::Glyph {
1965 font_id: actual_font,
1966 char_code,
1967 },
1968 color: options.color,
1969 }
1970}
1971
1972#[allow(clippy::too_many_arguments)]
1977fn layout_array(
1978 body: &[Vec<ParseNode>],
1979 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
1980 arraystretch: f64,
1981 add_jot: bool,
1982 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
1983 _hlines: &[Vec<bool>],
1984 col_sep_type: Option<&str>,
1985 _hskip: bool,
1986 options: &LayoutOptions,
1987) -> LayoutBox {
1988 let metrics = options.metrics();
1989 let pt = 1.0 / metrics.pt_per_em;
1990 let baselineskip = 12.0 * pt;
1991 let jot = 3.0 * pt;
1992 let arrayskip = arraystretch * baselineskip;
1993 let arstrut_h = 0.7 * arrayskip;
1994 let arstrut_d = 0.3 * arrayskip;
1995 const ALIGN_RELATION_MU: f64 = 3.0;
1998 let col_gap = match col_sep_type {
1999 Some("align") | Some("alignat") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2000 _ => 2.0 * 5.0 * pt, };
2002 let cell_options = match col_sep_type {
2003 Some("align") | Some("alignat") => LayoutOptions {
2004 align_relation_spacing: Some(ALIGN_RELATION_MU),
2005 ..options.clone()
2006 },
2007 _ => options.clone(),
2008 };
2009
2010 let num_rows = body.len();
2011 if num_rows == 0 {
2012 return LayoutBox::new_empty();
2013 }
2014
2015 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2016
2017 let col_aligns: Vec<u8> = {
2019 use ratex_parser::parse_node::AlignType;
2020 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2021 .map(|cs| {
2022 cs.iter()
2023 .filter(|s| matches!(s.align_type, AlignType::Align))
2024 .collect()
2025 })
2026 .unwrap_or_default();
2027 (0..num_cols)
2028 .map(|c| {
2029 align_specs
2030 .get(c)
2031 .and_then(|s| s.align.as_deref())
2032 .and_then(|a| a.bytes().next())
2033 .unwrap_or(b'c')
2034 })
2035 .collect()
2036 };
2037
2038 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2040 let mut col_widths = vec![0.0_f64; num_cols];
2041 let mut row_heights = Vec::with_capacity(num_rows);
2042 let mut row_depths = Vec::with_capacity(num_rows);
2043
2044 for row in body {
2045 let mut row_boxes = Vec::with_capacity(num_cols);
2046 let mut rh = arstrut_h;
2047 let mut rd = arstrut_d;
2048
2049 for (c, cell) in row.iter().enumerate() {
2050 let cell_nodes = match cell {
2051 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2052 other => std::slice::from_ref(other),
2053 };
2054 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2055 rh = rh.max(cell_box.height);
2056 rd = rd.max(cell_box.depth);
2057 if c < num_cols {
2058 col_widths[c] = col_widths[c].max(cell_box.width);
2059 }
2060 row_boxes.push(cell_box);
2061 }
2062
2063 while row_boxes.len() < num_cols {
2065 row_boxes.push(LayoutBox::new_empty());
2066 }
2067
2068 if add_jot {
2069 rd += jot;
2070 }
2071
2072 row_heights.push(rh);
2073 row_depths.push(rd);
2074 cell_boxes.push(row_boxes);
2075 }
2076
2077 for (r, gap) in row_gaps.iter().enumerate() {
2079 if r < row_depths.len() {
2080 if let Some(m) = gap {
2081 let gap_em = measurement_to_em(m, options);
2082 if gap_em > 0.0 {
2083 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2084 }
2085 }
2086 }
2087 }
2088
2089 let mut total_height = 0.0;
2091 let mut row_positions = Vec::with_capacity(num_rows);
2092 for r in 0..num_rows {
2093 total_height += row_heights[r];
2094 row_positions.push(total_height);
2095 total_height += row_depths[r];
2096 }
2097
2098 let offset = total_height / 2.0 + metrics.axis_height;
2099
2100 let total_width: f64 = col_widths.iter().sum::<f64>()
2102 + col_gap * (num_cols.saturating_sub(1)) as f64;
2103
2104 let height = offset;
2105 let depth = total_height - offset;
2106
2107 LayoutBox {
2108 width: total_width,
2109 height,
2110 depth,
2111 content: BoxContent::Array {
2112 cells: cell_boxes,
2113 col_widths: col_widths.clone(),
2114 col_aligns,
2115 row_heights: row_heights.clone(),
2116 row_depths: row_depths.clone(),
2117 col_gap,
2118 offset,
2119 },
2120 color: options.color,
2121 }
2122}
2123
2124fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2129 let multiplier = match size {
2131 1 => 0.5,
2132 2 => 0.6,
2133 3 => 0.7,
2134 4 => 0.8,
2135 5 => 0.9,
2136 6 => 1.0,
2137 7 => 1.2,
2138 8 => 1.44,
2139 9 => 1.728,
2140 10 => 2.074,
2141 11 => 2.488,
2142 _ => 1.0,
2143 };
2144
2145 let inner = layout_expression(body, options, true);
2146 let ratio = multiplier / options.size_multiplier();
2147 if (ratio - 1.0).abs() < 0.001 {
2148 inner
2149 } else {
2150 LayoutBox {
2151 width: inner.width * ratio,
2152 height: inner.height * ratio,
2153 depth: inner.depth * ratio,
2154 content: BoxContent::Scaled {
2155 body: Box::new(inner),
2156 child_scale: ratio,
2157 },
2158 color: options.color,
2159 }
2160 }
2161}
2162
2163fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2166 let metrics = options.metrics();
2167 let mut children = Vec::new();
2168 for c in body.chars() {
2169 let ch = if star && c == ' ' {
2170 '\u{2423}' } else {
2172 c
2173 };
2174 let code = ch as u32;
2175 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2176 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2177 None => match get_char_metrics(FontId::MainRegular, code) {
2178 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2179 None => (
2180 FontId::TypewriterRegular,
2181 0.5,
2182 metrics.x_height,
2183 0.0,
2184 ),
2185 },
2186 };
2187 children.push(LayoutBox {
2188 width: w,
2189 height: h,
2190 depth: d,
2191 content: BoxContent::Glyph {
2192 font_id,
2193 char_code: code,
2194 },
2195 color: options.color,
2196 });
2197 }
2198 let mut hbox = make_hbox(children);
2199 hbox.color = options.color;
2200 hbox
2201}
2202
2203fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2204 let mut children = Vec::new();
2205 for node in body {
2206 match node {
2207 ParseNode::TextOrd { text, .. } | ParseNode::MathOrd { text, .. } => {
2208 let ch = resolve_symbol_char(text, Mode::Text);
2209 let char_code = ch as u32;
2210 let m = get_char_metrics(FontId::MainRegular, char_code);
2211 let (w, h, d) = match m {
2212 Some(m) => (m.width, m.height, m.depth),
2213 None => missing_glyph_metrics_fallback(ch, options),
2214 };
2215 children.push(LayoutBox {
2216 width: w,
2217 height: h,
2218 depth: d,
2219 content: BoxContent::Glyph {
2220 font_id: FontId::MainRegular,
2221 char_code,
2222 },
2223 color: options.color,
2224 });
2225 }
2226 ParseNode::SpacingNode { text, .. } => {
2227 children.push(layout_spacing_command(text, options));
2228 }
2229 _ => {
2230 children.push(layout_node(node, options));
2231 }
2232 }
2233 }
2234 make_hbox(children)
2235}
2236
2237fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2240 let base = layout_expression(body, options, true);
2241 let w = base.width;
2242 let h = base.height;
2243 let d = base.depth;
2244
2245 let shadow = layout_expression(body, options, true);
2247 let shadow_shift_x = 0.02_f64;
2248 let _shadow_shift_y = 0.01_f64;
2249
2250 let kern_back = LayoutBox::new_kern(-w);
2254 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2255
2256 let children = vec![
2263 kern_x,
2264 shadow,
2265 kern_back,
2266 base,
2267 ];
2268 let hbox = make_hbox(children);
2270 LayoutBox {
2272 width: w,
2273 height: h,
2274 depth: d,
2275 content: hbox.content,
2276 color: options.color,
2277 }
2278}
2279
2280fn layout_enclose(
2283 label: &str,
2284 background_color: Option<&str>,
2285 border_color: Option<&str>,
2286 body: &ParseNode,
2287 options: &LayoutOptions,
2288) -> LayoutBox {
2289 use crate::layout_box::BoxContent;
2290 use ratex_types::color::Color;
2291
2292 if label == "\\phase" {
2294 return layout_phase(body, options);
2295 }
2296
2297 if label == "\\angl" {
2299 return layout_angl(body, options);
2300 }
2301
2302 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2304 return layout_cancel(label, body, options);
2305 }
2306
2307 let metrics = options.metrics();
2309 let padding = 3.0 / metrics.pt_per_em;
2310 let border_thickness = 0.4 / metrics.pt_per_em;
2311
2312 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2313
2314 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2315 let border = border_color
2316 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2317 .unwrap_or(Color::BLACK);
2318
2319 let inner = layout_node(body, options);
2320 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2321
2322 let width = inner.width + 2.0 * outer_pad;
2323 let height = inner.height + outer_pad;
2324 let depth = inner.depth + outer_pad;
2325
2326 LayoutBox {
2327 width,
2328 height,
2329 depth,
2330 content: BoxContent::Framed {
2331 body: Box::new(inner),
2332 padding,
2333 border_thickness,
2334 has_border,
2335 bg_color: bg,
2336 border_color: border,
2337 },
2338 color: options.color,
2339 }
2340}
2341
2342fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2344 use crate::layout_box::BoxContent;
2345 let inner = layout_node(body, options);
2346 let height = inner.height + shift;
2348 let depth = (inner.depth - shift).max(0.0);
2349 let width = inner.width;
2350 LayoutBox {
2351 width,
2352 height,
2353 depth,
2354 content: BoxContent::RaiseBox {
2355 body: Box::new(inner),
2356 shift,
2357 },
2358 color: options.color,
2359 }
2360}
2361
2362fn is_single_char_body(node: &ParseNode) -> bool {
2365 use ratex_parser::parse_node::ParseNode as PN;
2366 match node {
2367 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2369 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2370 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2372 _ => false,
2373 }
2374}
2375
2376fn layout_cancel(
2382 label: &str,
2383 body: &ParseNode,
2384 options: &LayoutOptions,
2385) -> LayoutBox {
2386 use crate::layout_box::BoxContent;
2387 let inner = layout_node(body, options);
2388 let w = inner.width.max(0.01);
2389 let h = inner.height;
2390 let d = inner.depth;
2391
2392 let single = is_single_char_body(body);
2394 let v_pad = if single { 0.2 } else { 0.0 };
2395 let h_pad = if single { 0.0 } else { 0.2 };
2396
2397 let commands: Vec<PathCommand> = match label {
2401 "\\cancel" => vec![
2402 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2405 "\\bcancel" => vec![
2406 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2409 "\\xcancel" => vec![
2410 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2411 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2412 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2413 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2414 ],
2415 "\\sout" => {
2416 let mid_y = -0.5 * options.metrics().x_height;
2418 vec![
2419 PathCommand::MoveTo { x: 0.0, y: mid_y },
2420 PathCommand::LineTo { x: w, y: mid_y },
2421 ]
2422 }
2423 _ => vec![],
2424 };
2425
2426 let line_w = w + 2.0 * h_pad;
2427 let line_h = h + v_pad;
2428 let line_d = d + v_pad;
2429 let line_box = LayoutBox {
2430 width: line_w,
2431 height: line_h,
2432 depth: line_d,
2433 content: BoxContent::SvgPath { commands, fill: false },
2434 color: options.color,
2435 };
2436
2437 let body_kern = -(line_w - h_pad);
2439 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2440 LayoutBox {
2441 width: w,
2442 height: h,
2443 depth: d,
2444 content: BoxContent::HBox(vec![line_box, body_shifted]),
2445 color: options.color,
2446 }
2447}
2448
2449fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2452 use crate::layout_box::BoxContent;
2453 let metrics = options.metrics();
2454 let inner = layout_node(body, options);
2455 let line_weight = 0.6_f64 / metrics.pt_per_em;
2457 let clearance = 0.35_f64 * metrics.x_height;
2458 let angle_height = inner.height + inner.depth + line_weight + clearance;
2459 let left_pad = angle_height / 2.0 + line_weight;
2460 let width = inner.width + left_pad;
2461
2462 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2464
2465 let sy = angle_height / y_svg;
2467 let sx = sy;
2470 let right_x = (400_000.0_f64 * sx).min(width);
2471
2472 let bottom_y = inner.depth + line_weight + clearance;
2474 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2475
2476 let x_peak = y_svg / 2.0;
2478 let commands = vec![
2479 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2480 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2481 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2482 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2483 PathCommand::LineTo {
2484 x: 145.0 * sx,
2485 y: vy(y_svg - 80.0),
2486 },
2487 PathCommand::LineTo {
2488 x: right_x,
2489 y: vy(y_svg - 80.0),
2490 },
2491 PathCommand::Close,
2492 ];
2493
2494 let body_shifted = make_hbox(vec![
2495 LayoutBox::new_kern(left_pad),
2496 inner.clone(),
2497 ]);
2498
2499 let path_height = inner.height;
2500 let path_depth = bottom_y;
2501
2502 LayoutBox {
2503 width,
2504 height: path_height,
2505 depth: path_depth,
2506 content: BoxContent::HBox(vec![
2507 LayoutBox {
2508 width,
2509 height: path_height,
2510 depth: path_depth,
2511 content: BoxContent::SvgPath { commands, fill: true },
2512 color: options.color,
2513 },
2514 LayoutBox::new_kern(-width),
2515 body_shifted,
2516 ]),
2517 color: options.color,
2518 }
2519}
2520
2521fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2524 use crate::layout_box::BoxContent;
2525 let inner = layout_node(body, options);
2526 let w = inner.width.max(0.3);
2527 let clearance = 0.1_f64;
2529 let arc_h = inner.height + clearance;
2530
2531 let path_commands = vec![
2533 PathCommand::MoveTo { x: 0.0, y: -arc_h },
2534 PathCommand::LineTo { x: w, y: -arc_h },
2535 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
2536 ];
2537
2538 let height = arc_h;
2539 LayoutBox {
2540 width: w,
2541 height,
2542 depth: inner.depth,
2543 content: BoxContent::Angl {
2544 path_commands,
2545 body: Box::new(inner),
2546 },
2547 color: options.color,
2548 }
2549}
2550
2551fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2552 let font_id = match font {
2553 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
2554 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
2555 "mathit" | "\\mathit" | "textit" | "\\textit" => Some(FontId::MainItalic),
2556 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
2557 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
2558 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
2559 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
2560 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
2561 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
2562 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
2563 _ => None,
2564 };
2565
2566 if let Some(fid) = font_id {
2567 layout_with_font(body, fid, options)
2568 } else {
2569 layout_node(body, options)
2570 }
2571}
2572
2573fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
2574 match node {
2575 ParseNode::OrdGroup { body, .. } => {
2576 let children: Vec<LayoutBox> = body.iter().map(|n| layout_with_font(n, font_id, options)).collect();
2577 make_hbox(children)
2578 }
2579 ParseNode::SupSub {
2580 base, sup, sub, ..
2581 } => {
2582 if let Some(base_node) = base.as_deref() {
2583 if should_use_op_limits(base_node, options) {
2584 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
2585 }
2586 }
2587 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
2588 }
2589 ParseNode::MathOrd { text, .. }
2590 | ParseNode::TextOrd { text, .. }
2591 | ParseNode::Atom { text, .. } => {
2592 let ch = resolve_symbol_char(text, Mode::Math);
2593 let char_code = ch as u32;
2594 if let Some(m) = get_char_metrics(font_id, char_code) {
2595 LayoutBox {
2596 width: m.width,
2597 height: m.height,
2598 depth: m.depth,
2599 content: BoxContent::Glyph { font_id, char_code },
2600 color: options.color,
2601 }
2602 } else {
2603 layout_node(node, options)
2605 }
2606 }
2607 _ => layout_node(node, options),
2608 }
2609}
2610
2611fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2616 let cramped = options.with_style(options.style.cramped());
2617 let body_box = layout_node(body, &cramped);
2618 let metrics = options.metrics();
2619 let rule = metrics.default_rule_thickness;
2620
2621 let height = body_box.height + 3.0 * rule;
2623 LayoutBox {
2624 width: body_box.width,
2625 height,
2626 depth: body_box.depth,
2627 content: BoxContent::Overline {
2628 body: Box::new(body_box),
2629 rule_thickness: rule,
2630 },
2631 color: options.color,
2632 }
2633}
2634
2635fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2636 let body_box = layout_node(body, options);
2637 let metrics = options.metrics();
2638 let rule = metrics.default_rule_thickness;
2639
2640 let depth = body_box.depth + 3.0 * rule;
2642 LayoutBox {
2643 width: body_box.width,
2644 height: body_box.height,
2645 depth,
2646 content: BoxContent::Underline {
2647 body: Box::new(body_box),
2648 rule_thickness: rule,
2649 },
2650 color: options.color,
2651 }
2652}
2653
2654fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2656 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
2657 let body_opts = options.with_color(link_color);
2658 let body_box = layout_expression(body, &body_opts, true);
2659 layout_underline_laid_out(body_box, options, link_color)
2660}
2661
2662fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
2664 let metrics = options.metrics();
2665 let rule = metrics.default_rule_thickness;
2666 let depth = body_box.depth + 3.0 * rule;
2667 LayoutBox {
2668 width: body_box.width,
2669 height: body_box.height,
2670 depth,
2671 content: BoxContent::Underline {
2672 body: Box::new(body_box),
2673 rule_thickness: rule,
2674 },
2675 color,
2676 }
2677}
2678
2679fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
2684 let metrics = options.metrics();
2685 let mu = metrics.css_em_per_mu();
2686
2687 let width = match text {
2688 "\\," | "\\thinspace" => 3.0 * mu,
2689 "\\:" | "\\medspace" => 4.0 * mu,
2690 "\\;" | "\\thickspace" => 5.0 * mu,
2691 "\\!" | "\\negthinspace" => -3.0 * mu,
2692 "\\negmedspace" => -4.0 * mu,
2693 "\\negthickspace" => -5.0 * mu,
2694 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
2695 get_char_metrics(FontId::MainRegular, 160)
2699 .map(|m| m.width)
2700 .unwrap_or(0.25)
2701 }
2702 "\\quad" => metrics.quad,
2703 "\\qquad" => 2.0 * metrics.quad,
2704 "\\enspace" => metrics.quad / 2.0,
2705 _ => 0.0,
2706 };
2707
2708 LayoutBox::new_kern(width)
2709}
2710
2711fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
2716 let metrics = options.metrics();
2717 match m.unit.as_str() {
2718 "em" => m.number,
2719 "ex" => m.number * metrics.x_height,
2720 "mu" => m.number * metrics.css_em_per_mu(),
2721 "pt" => m.number / metrics.pt_per_em,
2722 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
2723 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
2724 "in" => m.number * 72.27 / metrics.pt_per_em,
2725 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
2726 "pc" => m.number * 12.0 / metrics.pt_per_em,
2727 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
2728 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
2729 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
2730 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
2731 "sp" => m.number / 65536.0 / metrics.pt_per_em,
2732 _ => m.number,
2733 }
2734}
2735
2736fn node_math_class(node: &ParseNode) -> Option<MathClass> {
2742 match node {
2743 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
2744 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
2745 ParseNode::OpToken { .. } | ParseNode::Op { .. } => Some(MathClass::Op),
2746 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
2747 ParseNode::GenFrac { .. } => Some(MathClass::Inner),
2748 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
2749 ParseNode::SupSub { base, .. } => {
2750 base.as_ref().and_then(|b| node_math_class(b))
2751 }
2752 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
2753 ParseNode::SpacingNode { .. } => None,
2754 ParseNode::Kern { .. } => None,
2755 ParseNode::HtmlMathMl { html, .. } => {
2756 for child in html {
2758 if let Some(cls) = node_math_class(child) {
2759 return Some(cls);
2760 }
2761 }
2762 None
2763 }
2764 ParseNode::Lap { .. } => None,
2765 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
2766 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
2767 ParseNode::XArrow { .. } => Some(MathClass::Rel),
2769 _ => Some(MathClass::Ord),
2770 }
2771}
2772
2773fn mclass_str_to_math_class(mclass: &str) -> MathClass {
2774 match mclass {
2775 "mord" => MathClass::Ord,
2776 "mop" => MathClass::Op,
2777 "mbin" => MathClass::Bin,
2778 "mrel" => MathClass::Rel,
2779 "mopen" => MathClass::Open,
2780 "mclose" => MathClass::Close,
2781 "mpunct" => MathClass::Punct,
2782 "minner" => MathClass::Inner,
2783 _ => MathClass::Ord,
2784 }
2785}
2786
2787fn is_character_box(node: &ParseNode) -> bool {
2789 matches!(
2790 node,
2791 ParseNode::MathOrd { .. }
2792 | ParseNode::TextOrd { .. }
2793 | ParseNode::Atom { .. }
2794 | ParseNode::AccentToken { .. }
2795 )
2796}
2797
2798fn family_to_math_class(family: AtomFamily) -> MathClass {
2799 match family {
2800 AtomFamily::Bin => MathClass::Bin,
2801 AtomFamily::Rel => MathClass::Rel,
2802 AtomFamily::Open => MathClass::Open,
2803 AtomFamily::Close => MathClass::Close,
2804 AtomFamily::Punct => MathClass::Punct,
2805 AtomFamily::Inner => MathClass::Inner,
2806 }
2807}
2808
2809fn layout_horiz_brace(
2814 base: &ParseNode,
2815 is_over: bool,
2816 options: &LayoutOptions,
2817) -> LayoutBox {
2818 let body_box = layout_node(base, options);
2819 let w = body_box.width.max(0.5);
2820
2821 let label = if is_over { "overbrace" } else { "underbrace" };
2822 let (raw_commands, brace_h, brace_fill) =
2825 match crate::katex_svg::katex_stretchy_path(label, w) {
2826 Some((c, h)) => (c, h, true),
2827 None => {
2828 let h = 0.35_f64;
2829 (horiz_brace_path(w, h, is_over), h, false)
2830 }
2831 };
2832
2833 let y_shift = if is_over { -brace_h / 2.0 } else { brace_h / 2.0 };
2837 let commands = shift_path_y(raw_commands, y_shift);
2838
2839 let brace_box = LayoutBox {
2840 width: w,
2841 height: if is_over { brace_h } else { 0.0 },
2842 depth: if is_over { 0.0 } else { brace_h },
2843 content: BoxContent::SvgPath {
2844 commands,
2845 fill: brace_fill,
2846 },
2847 color: options.color,
2848 };
2849
2850 let gap = 0.1;
2851 let (height, depth) = if is_over {
2852 (body_box.height + brace_h + gap, body_box.depth)
2853 } else {
2854 (body_box.height, body_box.depth + brace_h + gap)
2855 };
2856
2857 let clearance = if is_over {
2858 height - brace_h
2859 } else {
2860 body_box.height + body_box.depth + gap
2861 };
2862 let total_w = body_box.width;
2863
2864 LayoutBox {
2865 width: total_w,
2866 height,
2867 depth,
2868 content: BoxContent::Accent {
2869 base: Box::new(body_box),
2870 accent: Box::new(brace_box),
2871 clearance,
2872 skew: 0.0,
2873 is_below: !is_over,
2874 },
2875 color: options.color,
2876 }
2877}
2878
2879fn layout_xarrow(
2884 label: &str,
2885 body: &ParseNode,
2886 below: Option<&ParseNode>,
2887 options: &LayoutOptions,
2888) -> LayoutBox {
2889 let sup_style = options.style.superscript();
2890 let sub_style = options.style.subscript();
2891 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
2892 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
2893
2894 let sup_opts = options.with_style(sup_style);
2895 let body_box = layout_node(body, &sup_opts);
2896 let body_w = body_box.width * sup_ratio;
2897
2898 let below_box = below.map(|b| {
2899 let sub_opts = options.with_style(sub_style);
2900 layout_node(b, &sub_opts)
2901 });
2902 let below_w = below_box
2903 .as_ref()
2904 .map(|b| b.width * sub_ratio)
2905 .unwrap_or(0.0);
2906
2907 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
2910 let upper_w = body_w + sup_ratio;
2911 let lower_w = if below_box.is_some() {
2912 below_w + sub_ratio
2913 } else {
2914 0.0
2915 };
2916 let arrow_w = upper_w.max(lower_w).max(min_w);
2917 let arrow_h = 0.3;
2918
2919 let (commands, actual_arrow_h, fill_arrow) =
2920 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
2921 Some((c, h)) => (c, h, true),
2922 None => (
2923 stretchy_accent_path(label, arrow_w, arrow_h),
2924 arrow_h,
2925 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
2926 ),
2927 };
2928 let arrow_box = LayoutBox {
2929 width: arrow_w,
2930 height: actual_arrow_h / 2.0,
2931 depth: actual_arrow_h / 2.0,
2932 content: BoxContent::SvgPath {
2933 commands,
2934 fill: fill_arrow,
2935 },
2936 color: options.color,
2937 };
2938
2939 let metrics = options.metrics();
2942 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
2944 let gap = 0.111; let base_shift = -axis;
2948
2949 let sup_kern = gap;
2957 let sub_kern = gap;
2958
2959 let sup_h = body_box.height * sup_ratio;
2960 let sup_d = body_box.depth * sup_ratio;
2961
2962 let height = axis + arrow_half + gap + sup_h + sup_d;
2964 let mut depth = (arrow_half - axis).max(0.0);
2966
2967 if let Some(ref bel) = below_box {
2968 let sub_h = bel.height * sub_ratio;
2969 let sub_d = bel.depth * sub_ratio;
2970 depth = (arrow_half - axis) + gap + sub_h + sub_d;
2972 }
2973
2974 LayoutBox {
2975 width: arrow_w,
2976 height,
2977 depth,
2978 content: BoxContent::OpLimits {
2979 base: Box::new(arrow_box),
2980 sup: Some(Box::new(body_box)),
2981 sub: below_box.map(Box::new),
2982 base_shift,
2983 sup_kern,
2984 sub_kern,
2985 slant: 0.0,
2986 sup_scale: sup_ratio,
2987 sub_scale: sub_ratio,
2988 },
2989 color: options.color,
2990 }
2991}
2992
2993fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
2998 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3001 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3002 let diameter = radius * 2.0;
3003
3004 let cx = radius;
3006 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3009
3010 let circle_commands = vec![
3011 PathCommand::MoveTo { x: cx + r, y: cy },
3012 PathCommand::CubicTo {
3013 x1: cx + r, y1: cy - k * r,
3014 x2: cx + k * r, y2: cy - r,
3015 x: cx, y: cy - r,
3016 },
3017 PathCommand::CubicTo {
3018 x1: cx - k * r, y1: cy - r,
3019 x2: cx - r, y2: cy - k * r,
3020 x: cx - r, y: cy,
3021 },
3022 PathCommand::CubicTo {
3023 x1: cx - r, y1: cy + k * r,
3024 x2: cx - k * r, y2: cy + r,
3025 x: cx, y: cy + r,
3026 },
3027 PathCommand::CubicTo {
3028 x1: cx + k * r, y1: cy + r,
3029 x2: cx + r, y2: cy + k * r,
3030 x: cx + r, y: cy,
3031 },
3032 PathCommand::Close,
3033 ];
3034
3035 let circle_box = LayoutBox {
3036 width: diameter,
3037 height: r - cy.min(0.0),
3038 depth: (r + cy).max(0.0),
3039 content: BoxContent::SvgPath { commands: circle_commands, fill: false },
3040 color: options.color,
3041 };
3042
3043 let content_shift = (diameter - body_box.width) / 2.0;
3045 let children = vec![
3047 circle_box,
3048 LayoutBox::new_kern(-(diameter) + content_shift),
3049 body_box.clone(),
3050 ];
3051
3052 let height = r - cy.min(0.0);
3053 let depth = (r + cy).max(0.0);
3054
3055 LayoutBox {
3056 width: diameter,
3057 height,
3058 depth,
3059 content: BoxContent::HBox(children),
3060 color: options.color,
3061 }
3062}
3063
3064fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3072 let cx = width / 2.0;
3073 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3078 PathCommand::MoveTo { x: cx + a, y: cy },
3079 PathCommand::CubicTo {
3080 x1: cx + a,
3081 y1: cy - k * b,
3082 x2: cx + k * a,
3083 y2: cy - b,
3084 x: cx,
3085 y: cy - b,
3086 },
3087 PathCommand::CubicTo {
3088 x1: cx - k * a,
3089 y1: cy - b,
3090 x2: cx - a,
3091 y2: cy - k * b,
3092 x: cx - a,
3093 y: cy,
3094 },
3095 PathCommand::CubicTo {
3096 x1: cx - a,
3097 y1: cy + k * b,
3098 x2: cx - k * a,
3099 y2: cy + b,
3100 x: cx,
3101 y: cy + b,
3102 },
3103 PathCommand::CubicTo {
3104 x1: cx + k * a,
3105 y1: cy + b,
3106 x2: cx + a,
3107 y2: cy + k * b,
3108 x: cx + a,
3109 y: cy,
3110 },
3111 PathCommand::Close,
3112 ]
3113}
3114
3115fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3116 cmds.into_iter().map(|c| match c {
3117 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3118 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3119 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3120 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3121 },
3122 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3123 x1, y1: y1 + dy, x, y: y + dy,
3124 },
3125 PathCommand::Close => PathCommand::Close,
3126 }).collect()
3127}
3128
3129fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3130 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3131 return commands;
3132 }
3133 let ah = height * 0.35; let mid_y = -height / 2.0;
3135
3136 match label {
3137 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3138 vec![
3139 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3140 PathCommand::LineTo { x: 0.0, y: mid_y },
3141 PathCommand::LineTo { x: ah, y: mid_y + ah },
3142 PathCommand::MoveTo { x: 0.0, y: mid_y },
3143 PathCommand::LineTo { x: width, y: mid_y },
3144 ]
3145 }
3146 "\\overleftrightarrow" | "\\underleftrightarrow"
3147 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3148 vec![
3149 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3150 PathCommand::LineTo { x: 0.0, y: mid_y },
3151 PathCommand::LineTo { x: ah, y: mid_y + ah },
3152 PathCommand::MoveTo { x: 0.0, y: mid_y },
3153 PathCommand::LineTo { x: width, y: mid_y },
3154 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3155 PathCommand::LineTo { x: width, y: mid_y },
3156 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3157 ]
3158 }
3159 "\\xlongequal" => {
3160 let gap = 0.04;
3161 vec![
3162 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3163 PathCommand::LineTo { x: width, y: mid_y - gap },
3164 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3165 PathCommand::LineTo { x: width, y: mid_y + gap },
3166 ]
3167 }
3168 "\\xhookleftarrow" => {
3169 vec![
3170 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3171 PathCommand::LineTo { x: 0.0, y: mid_y },
3172 PathCommand::LineTo { x: ah, y: mid_y + ah },
3173 PathCommand::MoveTo { x: 0.0, y: mid_y },
3174 PathCommand::LineTo { x: width, y: mid_y },
3175 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3176 ]
3177 }
3178 "\\xhookrightarrow" => {
3179 vec![
3180 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3181 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3182 PathCommand::LineTo { x: width, y: mid_y },
3183 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3184 PathCommand::LineTo { x: width, y: mid_y },
3185 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3186 ]
3187 }
3188 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3189 let right = label.contains("right");
3190 if right {
3191 vec![
3192 PathCommand::MoveTo { x: 0.0, y: mid_y },
3193 PathCommand::LineTo { x: width, y: mid_y },
3194 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3195 PathCommand::LineTo { x: width, y: mid_y },
3196 ]
3197 } else {
3198 vec![
3199 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3200 PathCommand::LineTo { x: 0.0, y: mid_y },
3201 PathCommand::LineTo { x: width, y: mid_y },
3202 ]
3203 }
3204 }
3205 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3206 let right = label.contains("right");
3207 if right {
3208 vec![
3209 PathCommand::MoveTo { x: 0.0, y: mid_y },
3210 PathCommand::LineTo { x: width, y: mid_y },
3211 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3212 PathCommand::LineTo { x: width, y: mid_y },
3213 ]
3214 } else {
3215 vec![
3216 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3217 PathCommand::LineTo { x: 0.0, y: mid_y },
3218 PathCommand::LineTo { x: width, y: mid_y },
3219 ]
3220 }
3221 }
3222 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3223 let gap = 0.06;
3224 vec![
3225 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3226 PathCommand::LineTo { x: width, y: mid_y - gap },
3227 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3228 PathCommand::LineTo { x: width, y: mid_y - gap },
3229 PathCommand::MoveTo { x: width, y: mid_y + gap },
3230 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3231 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3232 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3233 ]
3234 }
3235 "\\xtofrom" | "\\xrightleftarrows" => {
3236 let gap = 0.06;
3237 vec![
3238 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3239 PathCommand::LineTo { x: width, y: mid_y - gap },
3240 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3241 PathCommand::LineTo { x: width, y: mid_y - gap },
3242 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3243 PathCommand::MoveTo { x: width, y: mid_y + gap },
3244 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3245 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3246 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3247 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3248 ]
3249 }
3250 "\\overlinesegment" | "\\underlinesegment" => {
3251 vec![
3252 PathCommand::MoveTo { x: 0.0, y: mid_y },
3253 PathCommand::LineTo { x: width, y: mid_y },
3254 ]
3255 }
3256 _ => {
3257 vec![
3258 PathCommand::MoveTo { x: 0.0, y: mid_y },
3259 PathCommand::LineTo { x: width, y: mid_y },
3260 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3261 PathCommand::LineTo { x: width, y: mid_y },
3262 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3263 ]
3264 }
3265 }
3266}
3267
3268fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
3269 let mid = width / 2.0;
3270 let q = height * 0.6;
3271 if is_over {
3272 vec![
3273 PathCommand::MoveTo { x: 0.0, y: 0.0 },
3274 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
3275 PathCommand::LineTo { x: mid - 0.05, y: -q },
3276 PathCommand::LineTo { x: mid, y: -height },
3277 PathCommand::LineTo { x: mid + 0.05, y: -q },
3278 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
3279 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
3280 ]
3281 } else {
3282 vec![
3283 PathCommand::MoveTo { x: 0.0, y: 0.0 },
3284 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
3285 PathCommand::LineTo { x: mid - 0.05, y: q },
3286 PathCommand::LineTo { x: mid, y: height },
3287 PathCommand::LineTo { x: mid + 0.05, y: q },
3288 PathCommand::LineTo { x: width - mid * 0.4, y: q },
3289 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
3290 ]
3291 }
3292}