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 {
149 kind: VBoxChildKind::Box(Box::new(row.clone())),
150 shift: 0.0,
151 });
152 }
153
154 LayoutBox {
155 width: total_width,
156 height: h,
157 depth: d,
158 content: BoxContent::VBox(vchildren),
159 color: options.color,
160 }
161}
162
163
164fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
166 match node {
167 ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
168 ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
169 ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
170 ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
171
172 ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
173
174 ParseNode::SupSub {
175 base, sup, sub, ..
176 } => {
177 if let Some(base_node) = base.as_deref() {
178 if should_use_op_limits(base_node, options) {
179 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
180 }
181 }
182 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
183 }
184
185 ParseNode::GenFrac {
186 numer,
187 denom,
188 has_bar_line,
189 bar_size,
190 left_delim,
191 right_delim,
192 ..
193 } => {
194 let bar_thickness = if *has_bar_line {
195 bar_size
196 .as_ref()
197 .map(|m| measurement_to_em(m, options))
198 .unwrap_or(options.metrics().default_rule_thickness)
199 } else {
200 0.0
201 };
202 let frac = layout_fraction(numer, denom, bar_thickness, options);
203
204 let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
205 let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
206
207 if has_left || has_right {
208 let total_h = genfrac_delim_target_height(options);
209 let left_d = left_delim.as_deref().unwrap_or(".");
210 let right_d = right_delim.as_deref().unwrap_or(".");
211 let left_box = make_stretchy_delim(left_d, total_h, options);
212 let right_box = make_stretchy_delim(right_d, total_h, options);
213
214 let width = left_box.width + frac.width + right_box.width;
215 let height = frac.height.max(left_box.height).max(right_box.height);
216 let depth = frac.depth.max(left_box.depth).max(right_box.depth);
217
218 LayoutBox {
219 width,
220 height,
221 depth,
222 content: BoxContent::LeftRight {
223 left: Box::new(left_box),
224 right: Box::new(right_box),
225 inner: Box::new(frac),
226 },
227 color: options.color,
228 }
229 } else {
230 frac
231 }
232 }
233
234 ParseNode::Sqrt { body, index, .. } => {
235 layout_radical(body, index.as_deref(), options)
236 }
237
238 ParseNode::Op {
239 name,
240 symbol,
241 body,
242 limits,
243 suppress_base_shift,
244 ..
245 } => layout_op(
246 name.as_deref(),
247 *symbol,
248 body.as_deref(),
249 *limits,
250 suppress_base_shift.unwrap_or(false),
251 options,
252 ),
253
254 ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
255
256 ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
257
258 ParseNode::Kern { dimension, .. } => {
259 let em = measurement_to_em(dimension, options);
260 LayoutBox::new_kern(em)
261 }
262
263 ParseNode::Color { color, body, .. } => {
264 let new_color = Color::parse(color).unwrap_or(options.color);
265 let new_opts = options.with_color(new_color);
266 let mut lbox = layout_expression(body, &new_opts, true);
267 lbox.color = new_color;
268 lbox
269 }
270
271 ParseNode::Styling { style, body, .. } => {
272 let new_style = match style {
273 ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
274 ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
275 ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
276 ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
277 };
278 let ratio = new_style.size_multiplier() / options.style.size_multiplier();
279 let new_opts = options.with_style(new_style);
280 let inner = layout_expression(body, &new_opts, true);
281 if (ratio - 1.0).abs() < 0.001 {
282 inner
283 } else {
284 LayoutBox {
285 width: inner.width * ratio,
286 height: inner.height * ratio,
287 depth: inner.depth * ratio,
288 content: BoxContent::Scaled {
289 body: Box::new(inner),
290 child_scale: ratio,
291 },
292 color: options.color,
293 }
294 }
295 }
296
297 ParseNode::Accent {
298 label, base, is_stretchy, is_shifty, ..
299 } => {
300 let is_below = matches!(label.as_str(), "\\c");
302 layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
303 }
304
305 ParseNode::AccentUnder {
306 label, base, is_stretchy, ..
307 } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
308
309 ParseNode::LeftRight {
310 body, left, right, ..
311 } => layout_left_right(body, left, right, options),
312
313 ParseNode::DelimSizing {
314 size, delim, ..
315 } => layout_delim_sizing(*size, delim, options),
316
317 ParseNode::Array {
318 body,
319 cols,
320 arraystretch,
321 add_jot,
322 row_gaps,
323 hlines_before_row,
324 col_separation_type,
325 hskip_before_and_after,
326 ..
327 } => layout_array(
328 body,
329 cols.as_deref(),
330 *arraystretch,
331 add_jot.unwrap_or(false),
332 row_gaps,
333 hlines_before_row,
334 col_separation_type.as_deref(),
335 hskip_before_and_after.unwrap_or(false),
336 options,
337 ),
338
339 ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
340
341 ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
342 Some(f) => {
343 let group = ParseNode::OrdGroup {
344 mode: *mode,
345 body: body.clone(),
346 semisimple: None,
347 loc: None,
348 };
349 layout_font(f, &group, options)
350 }
351 None => layout_text(body, options),
352 },
353
354 ParseNode::Font { font, body, .. } => layout_font(font, body, options),
355
356 ParseNode::Href { body, .. } => layout_href(body, options),
357
358 ParseNode::Overline { body, .. } => layout_overline(body, options),
359 ParseNode::Underline { body, .. } => layout_underline(body, options),
360
361 ParseNode::Rule {
362 width: w,
363 height: h,
364 shift,
365 ..
366 } => {
367 let width = measurement_to_em(w, options);
368 let ink_h = measurement_to_em(h, options);
369 let raise = shift
370 .as_ref()
371 .map(|s| measurement_to_em(s, options))
372 .unwrap_or(0.0);
373 let box_height = (raise + ink_h).max(0.0);
374 let box_depth = (-raise).max(0.0);
375 LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
376 }
377
378 ParseNode::Phantom { body, .. } => {
379 let inner = layout_expression(body, options, true);
380 LayoutBox {
381 width: inner.width,
382 height: inner.height,
383 depth: inner.depth,
384 content: BoxContent::Empty,
385 color: Color::BLACK,
386 }
387 }
388
389 ParseNode::VPhantom { body, .. } => {
390 let inner = layout_node(body, options);
391 LayoutBox {
392 width: 0.0,
393 height: inner.height,
394 depth: inner.depth,
395 content: BoxContent::Empty,
396 color: Color::BLACK,
397 }
398 }
399
400 ParseNode::Smash { body, smash_height, smash_depth, .. } => {
401 let mut inner = layout_node(body, options);
402 if *smash_height { inner.height = 0.0; }
403 if *smash_depth { inner.depth = 0.0; }
404 inner
405 }
406
407 ParseNode::Middle { delim, .. } => {
408 match options.leftright_delim_height {
409 Some(h) => make_stretchy_delim(delim, h, options),
410 None => {
411 let placeholder = make_stretchy_delim(delim, 1.0, options);
413 LayoutBox {
414 width: placeholder.width,
415 height: 0.0,
416 depth: 0.0,
417 content: BoxContent::Empty,
418 color: options.color,
419 }
420 }
421 }
422 }
423
424 ParseNode::HtmlMathMl { html, .. } => {
425 layout_expression(html, options, true)
426 }
427
428 ParseNode::MClass { body, .. } => layout_expression(body, options, true),
429
430 ParseNode::MathChoice {
431 display, text, script, scriptscript, ..
432 } => {
433 let branch = match options.style {
434 MathStyle::Display | MathStyle::DisplayCramped => display,
435 MathStyle::Text | MathStyle::TextCramped => text,
436 MathStyle::Script | MathStyle::ScriptCramped => script,
437 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
438 };
439 layout_expression(branch, options, true)
440 }
441
442 ParseNode::Lap { alignment, body, .. } => {
443 let inner = layout_node(body, options);
444 let shift = match alignment.as_str() {
445 "llap" => -inner.width,
446 "clap" => -inner.width / 2.0,
447 _ => 0.0, };
449 let mut children = Vec::new();
450 if shift != 0.0 {
451 children.push(LayoutBox::new_kern(shift));
452 }
453 let h = inner.height;
454 let d = inner.depth;
455 children.push(inner);
456 LayoutBox {
457 width: 0.0,
458 height: h,
459 depth: d,
460 content: BoxContent::HBox(children),
461 color: options.color,
462 }
463 }
464
465 ParseNode::HorizBrace {
466 base, is_over, ..
467 } => layout_horiz_brace(base, *is_over, options),
468
469 ParseNode::XArrow {
470 label, body, below, ..
471 } => layout_xarrow(label, body, below.as_deref(), options),
472
473 ParseNode::Pmb { body, .. } => layout_pmb(body, options),
474
475 ParseNode::HBox { body, .. } => layout_text(body, options),
476
477 ParseNode::Enclose { label, background_color, border_color, body, .. } => {
478 layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
479 }
480
481 ParseNode::RaiseBox { dy, body, .. } => {
482 let shift = measurement_to_em(dy, options);
483 layout_raisebox(shift, body, options)
484 }
485
486 ParseNode::VCenter { body, .. } => {
487 let inner = layout_node(body, options);
489 let axis = options.metrics().axis_height;
490 let total = inner.height + inner.depth;
491 let height = total / 2.0 + axis;
492 let depth = total - height;
493 LayoutBox {
494 width: inner.width,
495 height,
496 depth,
497 content: inner.content,
498 color: inner.color,
499 }
500 }
501
502 ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
503
504 _ => LayoutBox::new_empty(),
506 }
507}
508
509fn missing_glyph_width_em(ch: char) -> f64 {
519 match ch as u32 {
520 0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
522 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
524 0xAC00..=0xD7AF => 1.0,
526 0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
528 _ => 0.5,
529 }
530}
531
532fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
533 let m = get_global_metrics(options.style.size_index());
534 let w = missing_glyph_width_em(ch);
535 if w >= 0.99 {
536 let h = (m.quad * 0.92).max(m.x_height);
537 (w, h, 0.0)
538 } else {
539 (w, m.x_height, 0.0)
540 }
541}
542
543fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
544 let ch = resolve_symbol_char(text, mode);
545 let mut font_id = select_font(text, ch, mode, options);
546 let char_code = ch as u32;
547
548 let mut metrics = get_char_metrics(font_id, char_code);
549
550 if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
551 if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
552 font_id = FontId::MathItalic;
553 metrics = Some(m);
554 }
555 }
556
557 let (width, height, depth) = match metrics {
558 Some(m) => (m.width, m.height, m.depth),
559 None => missing_glyph_metrics_fallback(ch, options),
560 };
561
562 LayoutBox {
563 width,
564 height,
565 depth,
566 content: BoxContent::Glyph {
567 font_id,
568 char_code,
569 },
570 color: options.color,
571 }
572}
573
574fn resolve_symbol_char(text: &str, mode: Mode) -> char {
576 let font_mode = match mode {
577 Mode::Math => ratex_font::Mode::Math,
578 Mode::Text => ratex_font::Mode::Text,
579 };
580
581 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
582 if let Some(cp) = info.codepoint {
583 return cp;
584 }
585 }
586
587 text.chars().next().unwrap_or('?')
588}
589
590fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
594 let font_mode = match mode {
595 Mode::Math => ratex_font::Mode::Math,
596 Mode::Text => ratex_font::Mode::Text,
597 };
598
599 if let Some(info) = ratex_font::get_symbol(text, font_mode) {
600 if info.font == ratex_font::SymbolFont::Ams {
601 return FontId::AmsRegular;
602 }
603 }
604
605 match mode {
606 Mode::Math => {
607 if resolved_char.is_ascii_lowercase()
608 || resolved_char.is_ascii_uppercase()
609 || is_greek_letter(resolved_char)
610 {
611 FontId::MathItalic
612 } else {
613 FontId::MainRegular
614 }
615 }
616 Mode::Text => FontId::MainRegular,
617 }
618}
619
620fn is_greek_letter(ch: char) -> bool {
621 matches!(ch,
622 '\u{0391}'..='\u{03C9}' |
623 '\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
624 '\u{03F1}' | '\u{03F5}'
625 )
626}
627
628fn is_arrow_accent(label: &str) -> bool {
629 matches!(
630 label,
631 "\\overrightarrow"
632 | "\\overleftarrow"
633 | "\\Overrightarrow"
634 | "\\overleftrightarrow"
635 | "\\underrightarrow"
636 | "\\underleftarrow"
637 | "\\underleftrightarrow"
638 | "\\overleftharpoon"
639 | "\\overrightharpoon"
640 | "\\overlinesegment"
641 | "\\underlinesegment"
642 )
643}
644
645fn layout_fraction(
650 numer: &ParseNode,
651 denom: &ParseNode,
652 bar_thickness: f64,
653 options: &LayoutOptions,
654) -> LayoutBox {
655 let numer_s = options.style.numerator();
656 let denom_s = options.style.denominator();
657 let numer_style = options.with_style(numer_s);
658 let denom_style = options.with_style(denom_s);
659
660 let numer_box = layout_node(numer, &numer_style);
661 let denom_box = layout_node(denom, &denom_style);
662
663 let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
665 let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
666
667 let numer_height = numer_box.height * numer_ratio;
668 let numer_depth = numer_box.depth * numer_ratio;
669 let denom_height = denom_box.height * denom_ratio;
670 let denom_depth = denom_box.depth * denom_ratio;
671 let numer_width = numer_box.width * numer_ratio;
672 let denom_width = denom_box.width * denom_ratio;
673
674 let metrics = options.metrics();
675 let axis = metrics.axis_height;
676 let rule = bar_thickness;
677
678 let (mut num_shift, mut den_shift) = if options.style.is_display() {
680 (metrics.num1, metrics.denom1)
681 } else if bar_thickness > 0.0 {
682 (metrics.num2, metrics.denom2)
683 } else {
684 (metrics.num3, metrics.denom2)
685 };
686
687 if bar_thickness > 0.0 {
688 let min_clearance = if options.style.is_display() {
689 3.0 * rule
690 } else {
691 rule
692 };
693
694 let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
695 if num_clearance < min_clearance {
696 num_shift += min_clearance - num_clearance;
697 }
698
699 let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
700 if den_clearance < min_clearance {
701 den_shift += min_clearance - den_clearance;
702 }
703 } else {
704 let min_gap = if options.style.is_display() {
705 7.0 * metrics.default_rule_thickness
706 } else {
707 3.0 * metrics.default_rule_thickness
708 };
709
710 let gap = (num_shift - numer_depth) - (denom_height - den_shift);
711 if gap < min_gap {
712 let adjust = (min_gap - gap) / 2.0;
713 num_shift += adjust;
714 den_shift += adjust;
715 }
716 }
717
718 let total_width = numer_width.max(denom_width);
719 let height = numer_height + num_shift;
720 let depth = denom_depth + den_shift;
721
722 LayoutBox {
723 width: total_width,
724 height,
725 depth,
726 content: BoxContent::Fraction {
727 numer: Box::new(numer_box),
728 denom: Box::new(denom_box),
729 numer_shift: num_shift,
730 denom_shift: den_shift,
731 bar_thickness: rule,
732 numer_scale: numer_ratio,
733 denom_scale: denom_ratio,
734 },
735 color: options.color,
736 }
737}
738
739fn layout_supsub(
744 base: Option<&ParseNode>,
745 sup: Option<&ParseNode>,
746 sub: Option<&ParseNode>,
747 options: &LayoutOptions,
748 inherited_font: Option<FontId>,
749) -> LayoutBox {
750 let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
751 Some(fid) => layout_with_font(n, fid, opts),
752 None => layout_node(n, opts),
753 };
754
755 let horiz_brace_over = matches!(
756 base,
757 Some(ParseNode::HorizBrace {
758 is_over: true,
759 ..
760 })
761 );
762 let horiz_brace_under = matches!(
763 base,
764 Some(ParseNode::HorizBrace {
765 is_over: false,
766 ..
767 })
768 );
769 let center_scripts = horiz_brace_over || horiz_brace_under;
770
771 let base_box = base
772 .map(|b| layout_child(b, options))
773 .unwrap_or_else(LayoutBox::new_empty);
774
775 let is_char_box = base.is_some_and(is_character_box);
776 let metrics = options.metrics();
777
778 let sup_style = options.style.superscript();
779 let sub_style = options.style.subscript();
780
781 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
782 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
783
784 let sup_box = sup.map(|s| {
785 let sup_opts = options.with_style(sup_style);
786 layout_child(s, &sup_opts)
787 });
788
789 let sub_box = sub.map(|s| {
790 let sub_opts = options.with_style(sub_style);
791 layout_child(s, &sub_opts)
792 });
793
794 let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
795 let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
796 let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
797 let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
798
799 let sup_style_metrics = get_global_metrics(sup_style.size_index());
801 let sub_style_metrics = get_global_metrics(sub_style.size_index());
802
803 let mut sup_shift = if !is_char_box && sup_box.is_some() {
806 base_box.height - sup_style_metrics.sup_drop * sup_ratio
807 } else {
808 0.0
809 };
810
811 let mut sub_shift = if !is_char_box && sub_box.is_some() {
812 base_box.depth + sub_style_metrics.sub_drop * sub_ratio
813 } else {
814 0.0
815 };
816
817 let min_sup_shift = if options.style.is_cramped() {
818 metrics.sup3
819 } else if options.style.is_display() {
820 metrics.sup1
821 } else {
822 metrics.sup2
823 };
824
825 if sup_box.is_some() && sub_box.is_some() {
826 sup_shift = sup_shift
828 .max(min_sup_shift)
829 .max(sup_depth_scaled + 0.25 * metrics.x_height);
830 sub_shift = sub_shift.max(metrics.sub2); let rule_width = metrics.default_rule_thickness;
833 let max_width = 4.0 * rule_width;
834 let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
835 if gap < max_width {
836 sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
837 let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
838 if psi > 0.0 {
839 sup_shift += psi;
840 sub_shift -= psi;
841 }
842 }
843 } else if sub_box.is_some() {
844 sub_shift = sub_shift
846 .max(metrics.sub1)
847 .max(sub_height_scaled - 0.8 * metrics.x_height);
848 } else if sup_box.is_some() {
849 sup_shift = sup_shift
851 .max(min_sup_shift)
852 .max(sup_depth_scaled + 0.25 * metrics.x_height);
853 }
854
855 if horiz_brace_over && sup_box.is_some() {
859 sup_shift += sup_style_metrics.sup_drop * sup_ratio;
860 sup_shift += metrics.big_op_spacing1;
863 }
864 if horiz_brace_under && sub_box.is_some() {
865 sub_shift += sub_style_metrics.sub_drop * sub_ratio;
866 sub_shift += metrics.big_op_spacing2 + 0.2;
867 }
868
869 let italic_correction = if is_char_box && !center_scripts {
873 glyph_italic(&base_box)
874 } else {
875 0.0
876 };
877
878 let mut height = base_box.height;
880 let mut depth = base_box.depth;
881 let mut total_width = base_box.width;
882
883 if let Some(ref sup_b) = sup_box {
884 height = height.max(sup_shift + sup_height_scaled);
885 if center_scripts {
886 total_width = total_width.max(sup_b.width * sup_ratio);
887 } else {
888 total_width = total_width.max(base_box.width + italic_correction + sup_b.width * sup_ratio);
889 }
890 }
891 if let Some(ref sub_b) = sub_box {
892 depth = depth.max(sub_shift + sub_depth_scaled);
893 if center_scripts {
894 total_width = total_width.max(sub_b.width * sub_ratio);
895 } else {
896 total_width = total_width.max(base_box.width + sub_b.width * sub_ratio);
897 }
898 }
899
900 LayoutBox {
901 width: total_width,
902 height,
903 depth,
904 content: BoxContent::SupSub {
905 base: Box::new(base_box),
906 sup: sup_box.map(Box::new),
907 sub: sub_box.map(Box::new),
908 sup_shift,
909 sub_shift,
910 sup_scale: sup_ratio,
911 sub_scale: sub_ratio,
912 center_scripts,
913 italic_correction,
914 },
915 color: options.color,
916 }
917}
918
919fn layout_radical(
924 body: &ParseNode,
925 index: Option<&ParseNode>,
926 options: &LayoutOptions,
927) -> LayoutBox {
928 let cramped = options.style.cramped();
929 let cramped_opts = options.with_style(cramped);
930 let mut body_box = layout_node(body, &cramped_opts);
931
932 let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
934 body_box.height *= body_ratio;
935 body_box.depth *= body_ratio;
936 body_box.width *= body_ratio;
937
938 if body_box.height == 0.0 {
940 body_box.height = options.metrics().x_height;
941 }
942
943 let metrics = options.metrics();
944 let theta = metrics.default_rule_thickness; let phi = if options.style.is_display() {
949 metrics.x_height
950 } else {
951 theta
952 };
953
954 let mut line_clearance = theta + phi / 4.0;
955
956 let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
958
959 let tex_height = select_surd_height(min_delim_height);
962 let rule_width = theta;
963 let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
964 let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
965 .map(|m| m.width)
966 .unwrap_or(0.833);
967
968 let delim_depth = tex_height - rule_width;
970 if delim_depth > body_box.height + body_box.depth + line_clearance {
971 line_clearance =
972 (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
973 }
974
975 let img_shift = tex_height - body_box.height - line_clearance - rule_width;
976
977 let height = tex_height + rule_width - img_shift;
980 let depth = if img_shift > body_box.depth {
981 img_shift
982 } else {
983 body_box.depth
984 };
985
986 const INDEX_KERN: f64 = 0.05;
988 let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
989 let root_style = options.style.superscript().superscript();
990 let root_opts = options.with_style(root_style);
991 let idx = layout_node(index_node, &root_opts);
992 let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
993 let offset = idx.width * index_ratio + INDEX_KERN;
994 (Some(Box::new(idx)), offset, index_ratio)
995 } else {
996 (None, 0.0, 1.0)
997 };
998
999 let width = index_offset + advance_width + body_box.width;
1000
1001 LayoutBox {
1002 width,
1003 height,
1004 depth,
1005 content: BoxContent::Radical {
1006 body: Box::new(body_box),
1007 index: index_box,
1008 index_offset,
1009 index_scale,
1010 rule_thickness: rule_width,
1011 inner_height: tex_height,
1012 },
1013 color: options.color,
1014 }
1015}
1016
1017fn select_surd_height(min_height: f64) -> f64 {
1020 const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
1021 for &h in &SURD_HEIGHTS {
1022 if h >= min_height {
1023 return h;
1024 }
1025 }
1026 SURD_HEIGHTS[4].max(min_height)
1028}
1029
1030const NO_SUCCESSOR: &[&str] = &["\\smallint"];
1035
1036fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
1038 match base {
1039 ParseNode::Op {
1040 limits,
1041 always_handle_sup_sub,
1042 ..
1043 } => {
1044 *limits
1045 && (options.style.is_display()
1046 || always_handle_sup_sub.unwrap_or(false))
1047 }
1048 ParseNode::OperatorName {
1049 always_handle_sup_sub,
1050 limits,
1051 ..
1052 } => {
1053 *always_handle_sup_sub
1054 && (options.style.is_display() || *limits)
1055 }
1056 _ => false,
1057 }
1058}
1059
1060fn layout_op(
1066 name: Option<&str>,
1067 symbol: bool,
1068 body: Option<&[ParseNode]>,
1069 _limits: bool,
1070 suppress_base_shift: bool,
1071 options: &LayoutOptions,
1072) -> LayoutBox {
1073 let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
1074
1075 if symbol && !suppress_base_shift {
1077 let axis = options.metrics().axis_height;
1078 let _total = base_box.height + base_box.depth;
1079 let shift = (base_box.height - base_box.depth) / 2.0 - axis;
1080 if shift.abs() > 0.001 {
1081 base_box.height -= shift;
1082 base_box.depth += shift;
1083 }
1084 }
1085
1086 base_box
1087}
1088
1089fn build_op_base(
1092 name: Option<&str>,
1093 symbol: bool,
1094 body: Option<&[ParseNode]>,
1095 options: &LayoutOptions,
1096) -> (LayoutBox, f64) {
1097 if symbol {
1098 let large = options.style.is_display()
1099 && !NO_SUCCESSOR.contains(&name.unwrap_or(""));
1100 let font_id = if large {
1101 FontId::Size2Regular
1102 } else {
1103 FontId::Size1Regular
1104 };
1105
1106 let op_name = name.unwrap_or("");
1107 let ch = resolve_op_char(op_name);
1108 let char_code = ch as u32;
1109
1110 let metrics = get_char_metrics(font_id, char_code);
1111 let (width, height, depth, italic) = match metrics {
1112 Some(m) => (m.width, m.height, m.depth, m.italic),
1113 None => (1.0, 0.75, 0.25, 0.0),
1114 };
1115 let width_with_italic = width + italic;
1118
1119 let base = LayoutBox {
1120 width: width_with_italic,
1121 height,
1122 depth,
1123 content: BoxContent::Glyph {
1124 font_id,
1125 char_code,
1126 },
1127 color: options.color,
1128 };
1129
1130 if op_name == "\\oiint" || op_name == "\\oiiint" {
1133 let w = base.width;
1134 let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
1135 let overlay_box = LayoutBox {
1136 width: w,
1137 height: base.height,
1138 depth: base.depth,
1139 content: BoxContent::SvgPath {
1140 commands: ellipse_commands,
1141 fill: false,
1142 },
1143 color: options.color,
1144 };
1145 let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
1146 return (with_overlay, italic);
1147 }
1148
1149 (base, italic)
1150 } else if let Some(body_nodes) = body {
1151 let base = layout_expression(body_nodes, options, true);
1152 (base, 0.0)
1153 } else {
1154 let base = layout_op_text(name.unwrap_or(""), options);
1155 (base, 0.0)
1156 }
1157}
1158
1159fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
1161 let text = name.strip_prefix('\\').unwrap_or(name);
1162 let mut children = Vec::new();
1163 for ch in text.chars() {
1164 let char_code = ch as u32;
1165 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1166 let (width, height, depth) = match metrics {
1167 Some(m) => (m.width, m.height, m.depth),
1168 None => (0.5, 0.43, 0.0),
1169 };
1170 children.push(LayoutBox {
1171 width,
1172 height,
1173 depth,
1174 content: BoxContent::Glyph {
1175 font_id: FontId::MainRegular,
1176 char_code,
1177 },
1178 color: options.color,
1179 });
1180 }
1181 make_hbox(children)
1182}
1183
1184fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
1186 let metrics = options.metrics();
1187 (base.height - base.depth) / 2.0 - metrics.axis_height
1188}
1189
1190fn resolve_op_char(name: &str) -> char {
1192 match name {
1195 "\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
1198 }
1199 let font_mode = ratex_font::Mode::Math;
1200 if let Some(info) = ratex_font::get_symbol(name, font_mode) {
1201 if let Some(cp) = info.codepoint {
1202 return cp;
1203 }
1204 }
1205 name.chars().next().unwrap_or('?')
1206}
1207
1208fn layout_op_with_limits(
1210 base_node: &ParseNode,
1211 sup_node: Option<&ParseNode>,
1212 sub_node: Option<&ParseNode>,
1213 options: &LayoutOptions,
1214) -> LayoutBox {
1215 let (name, symbol, body, suppress_base_shift) = match base_node {
1216 ParseNode::Op {
1217 name,
1218 symbol,
1219 body,
1220 suppress_base_shift,
1221 ..
1222 } => (
1223 name.as_deref(),
1224 *symbol,
1225 body.as_deref(),
1226 suppress_base_shift.unwrap_or(false),
1227 ),
1228 ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
1229 _ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
1230 };
1231
1232 let (base_box, slant) = build_op_base(name, symbol, body, options);
1233 let base_shift = if symbol && !suppress_base_shift {
1235 compute_op_base_shift(&base_box, options)
1236 } else {
1237 0.0
1238 };
1239
1240 layout_op_limits_inner(&base_box, sup_node, sub_node, slant, base_shift, options)
1241}
1242
1243fn layout_op_limits_inner(
1245 base: &LayoutBox,
1246 sup_node: Option<&ParseNode>,
1247 sub_node: Option<&ParseNode>,
1248 slant: f64,
1249 base_shift: f64,
1250 options: &LayoutOptions,
1251) -> LayoutBox {
1252 let metrics = options.metrics();
1253 let sup_style = options.style.superscript();
1254 let sub_style = options.style.subscript();
1255
1256 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
1257 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
1258
1259 let extra_clearance = 0.08_f64;
1261
1262 let sup_data = sup_node.map(|s| {
1263 let sup_opts = options.with_style(sup_style);
1264 let elem = layout_node(s, &sup_opts);
1265 let kern = (metrics.big_op_spacing1 + extra_clearance)
1266 .max(metrics.big_op_spacing3 - elem.depth * sup_ratio + extra_clearance);
1267 (elem, kern)
1268 });
1269
1270 let sub_data = sub_node.map(|s| {
1271 let sub_opts = options.with_style(sub_style);
1272 let elem = layout_node(s, &sub_opts);
1273 let kern = (metrics.big_op_spacing2 + extra_clearance)
1274 .max(metrics.big_op_spacing4 - elem.height * sub_ratio + extra_clearance);
1275 (elem, kern)
1276 });
1277
1278 let sp5 = metrics.big_op_spacing5;
1279
1280 let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
1281 (Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
1282 let sup_h = sup_elem.height * sup_ratio;
1285 let sup_d = sup_elem.depth * sup_ratio;
1286 let sub_h = sub_elem.height * sub_ratio;
1287 let sub_d = sub_elem.depth * sub_ratio;
1288
1289 let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
1290
1291 let height = bottom
1292 + base.height - base_shift
1293 + sup_kern
1294 + sup_h + sup_d
1295 + sp5
1296 - (base.height + base.depth);
1297
1298 let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1299 let total_d = bottom;
1300
1301 let w = base
1302 .width
1303 .max(sup_elem.width * sup_ratio)
1304 .max(sub_elem.width * sub_ratio);
1305 let _ = height; (total_h, total_d, w)
1307 }
1308 (None, Some((sub_elem, sub_kern))) => {
1309 let sub_h = sub_elem.height * sub_ratio;
1312 let sub_d = sub_elem.depth * sub_ratio;
1313
1314 let total_h = base.height - base_shift;
1315 let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
1316
1317 let w = base.width.max(sub_elem.width * sub_ratio);
1318 (total_h, total_d, w)
1319 }
1320 (Some((sup_elem, sup_kern)), None) => {
1321 let sup_h = sup_elem.height * sup_ratio;
1324 let sup_d = sup_elem.depth * sup_ratio;
1325
1326 let total_h =
1327 base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
1328 let total_d = base.depth + base_shift;
1329
1330 let w = base.width.max(sup_elem.width * sup_ratio);
1331 (total_h, total_d, w)
1332 }
1333 (None, None) => {
1334 return base.clone();
1335 }
1336 };
1337
1338 let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1339 let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
1340
1341 LayoutBox {
1342 width: total_width,
1343 height: total_height,
1344 depth: total_depth,
1345 content: BoxContent::OpLimits {
1346 base: Box::new(base.clone()),
1347 sup: sup_data.map(|(elem, _)| Box::new(elem)),
1348 sub: sub_data.map(|(elem, _)| Box::new(elem)),
1349 base_shift,
1350 sup_kern: sup_kern_val,
1351 sub_kern: sub_kern_val,
1352 slant,
1353 sup_scale: sup_ratio,
1354 sub_scale: sub_ratio,
1355 },
1356 color: options.color,
1357 }
1358}
1359
1360fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
1362 let mut children = Vec::new();
1363 for node in body {
1364 match node {
1365 ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
1366 let ch = text.chars().next().unwrap_or('?');
1367 let char_code = ch as u32;
1368 let metrics = get_char_metrics(FontId::MainRegular, char_code);
1369 let (width, height, depth) = match metrics {
1370 Some(m) => (m.width, m.height, m.depth),
1371 None => (0.5, 0.43, 0.0),
1372 };
1373 children.push(LayoutBox {
1374 width,
1375 height,
1376 depth,
1377 content: BoxContent::Glyph {
1378 font_id: FontId::MainRegular,
1379 char_code,
1380 },
1381 color: options.color,
1382 });
1383 }
1384 _ => {
1385 children.push(layout_node(node, options));
1386 }
1387 }
1388 }
1389 make_hbox(children)
1390}
1391
1392const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
1398
1399fn glyph_italic(lb: &LayoutBox) -> f64 {
1403 match &lb.content {
1404 BoxContent::Glyph { font_id, char_code } => {
1405 get_char_metrics(*font_id, *char_code)
1406 .map(|m| m.italic)
1407 .unwrap_or(0.0)
1408 }
1409 BoxContent::HBox(children) => {
1410 children.last().map(glyph_italic).unwrap_or(0.0)
1411 }
1412 _ => 0.0,
1413 }
1414}
1415
1416fn glyph_skew(lb: &LayoutBox) -> f64 {
1420 match &lb.content {
1421 BoxContent::Glyph { font_id, char_code } => {
1422 get_char_metrics(*font_id, *char_code)
1423 .map(|m| m.skew)
1424 .unwrap_or(0.0)
1425 }
1426 BoxContent::HBox(children) => {
1427 children.last().map(glyph_skew).unwrap_or(0.0)
1428 }
1429 _ => 0.0,
1430 }
1431}
1432
1433fn layout_accent(
1434 label: &str,
1435 base: &ParseNode,
1436 is_stretchy: bool,
1437 is_shifty: bool,
1438 is_below: bool,
1439 options: &LayoutOptions,
1440) -> LayoutBox {
1441 let body_box = layout_node(base, options);
1442 let base_w = body_box.width.max(0.5);
1443
1444 if label == "\\textcircled" {
1446 return layout_textcircled(body_box, options);
1447 }
1448
1449 if let Some((commands, w, h, fill)) =
1451 crate::katex_svg::katex_accent_path(label, base_w)
1452 {
1453 let accent_box = LayoutBox {
1455 width: w,
1456 height: 0.0,
1457 depth: h,
1458 content: BoxContent::SvgPath { commands, fill },
1459 color: options.color,
1460 };
1461 let gap = 0.08;
1465 let clearance = if is_below {
1466 body_box.height + body_box.depth + gap
1467 } else if label == "\\vec" {
1468 (body_box.height - options.metrics().x_height).max(0.0)
1471 } else {
1472 body_box.height + gap
1473 };
1474 let (height, depth) = if is_below {
1475 (body_box.height, body_box.depth + h + gap)
1476 } else if label == "\\vec" {
1477 (clearance + h, body_box.depth)
1479 } else {
1480 (body_box.height + gap + h, body_box.depth)
1481 };
1482 let vec_skew = if label == "\\vec" {
1483 (if is_shifty {
1484 glyph_skew(&body_box)
1485 } else {
1486 0.0
1487 }) + VEC_SKEW_EXTRA_RIGHT_EM
1488 } else {
1489 0.0
1490 };
1491 return LayoutBox {
1492 width: body_box.width,
1493 height,
1494 depth,
1495 content: BoxContent::Accent {
1496 base: Box::new(body_box),
1497 accent: Box::new(accent_box),
1498 clearance,
1499 skew: vec_skew,
1500 is_below,
1501 },
1502 color: options.color,
1503 };
1504 }
1505
1506 let use_arrow_path = is_stretchy && is_arrow_accent(label);
1508
1509 let accent_box = if use_arrow_path {
1510 let (commands, arrow_h, fill_arrow) =
1511 match crate::katex_svg::katex_stretchy_path(label, base_w) {
1512 Some((c, h)) => (c, h, true),
1513 None => {
1514 let h = 0.3_f64;
1515 let c = stretchy_accent_path(label, base_w, h);
1516 let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
1517 (c, h, fill)
1518 }
1519 };
1520 LayoutBox {
1521 width: base_w,
1522 height: arrow_h / 2.0,
1523 depth: arrow_h / 2.0,
1524 content: BoxContent::SvgPath {
1525 commands,
1526 fill: fill_arrow,
1527 },
1528 color: options.color,
1529 }
1530 } else {
1531 let accent_char = {
1533 let ch = resolve_symbol_char(label, Mode::Text);
1534 if ch == label.chars().next().unwrap_or('?') {
1535 resolve_symbol_char(label, Mode::Math)
1538 } else {
1539 ch
1540 }
1541 };
1542 let accent_code = accent_char as u32;
1543 let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
1544 let (accent_w, accent_h, accent_d) = match accent_metrics {
1545 Some(m) => (m.width, m.height, m.depth),
1546 None => (body_box.width, 0.25, 0.0),
1547 };
1548 LayoutBox {
1549 width: accent_w,
1550 height: accent_h,
1551 depth: accent_d,
1552 content: BoxContent::Glyph {
1553 font_id: FontId::MainRegular,
1554 char_code: accent_code,
1555 },
1556 color: options.color,
1557 }
1558 };
1559
1560 let skew = if use_arrow_path {
1561 0.0
1562 } else if is_shifty {
1563 glyph_skew(&body_box)
1566 } else {
1567 0.0
1568 };
1569
1570 let gap = if use_arrow_path {
1579 if label == "\\Overrightarrow" {
1580 0.21
1581 } else {
1582 0.26
1583 }
1584 } else {
1585 0.0
1586 };
1587
1588 let clearance = if is_below {
1589 body_box.height + body_box.depth + accent_box.depth + gap
1590 } else if use_arrow_path {
1591 body_box.height + gap
1592 } else {
1593 let base_clearance = match &body_box.content {
1603 BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
1604 if !is_below =>
1605 {
1606 if inner_accent.height <= 0.001 {
1612 let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
1618 let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
1619 katex_pos + correction
1620 } else {
1621 inner_cl + 0.3
1622 }
1623 }
1624 _ => body_box.height,
1625 };
1626 if label == "\\bar" || label == "\\=" {
1628 base_clearance - 0.2
1629 } else {
1630 base_clearance
1631 }
1632 };
1633
1634 let (height, depth) = if is_below {
1635 (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
1636 } else if use_arrow_path {
1637 (body_box.height + gap + accent_box.height, body_box.depth)
1638 } else {
1639 const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
1646 let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
1647 let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
1648 accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
1649 } else {
1650 body_box.height.max(accent_visual_top)
1651 };
1652 (h, body_box.depth)
1653 };
1654
1655 LayoutBox {
1656 width: body_box.width,
1657 height,
1658 depth,
1659 content: BoxContent::Accent {
1660 base: Box::new(body_box),
1661 accent: Box::new(accent_box),
1662 clearance,
1663 skew,
1664 is_below,
1665 },
1666 color: options.color,
1667 }
1668}
1669
1670fn node_contains_middle(node: &ParseNode) -> bool {
1676 match node {
1677 ParseNode::Middle { .. } => true,
1678 ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
1679 body.iter().any(node_contains_middle)
1680 }
1681 ParseNode::SupSub { base, sup, sub, .. } => {
1682 base.as_deref().is_some_and(node_contains_middle)
1683 || sup.as_deref().is_some_and(node_contains_middle)
1684 || sub.as_deref().is_some_and(node_contains_middle)
1685 }
1686 ParseNode::GenFrac { numer, denom, .. } => {
1687 node_contains_middle(numer) || node_contains_middle(denom)
1688 }
1689 ParseNode::Sqrt { body, index, .. } => {
1690 node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
1691 }
1692 ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
1693 node_contains_middle(base)
1694 }
1695 ParseNode::Op { body, .. } => body
1696 .as_ref()
1697 .is_some_and(|b| b.iter().any(node_contains_middle)),
1698 ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
1699 ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
1700 ParseNode::Font { body, .. } => node_contains_middle(body),
1701 ParseNode::Text { body, .. }
1702 | ParseNode::Color { body, .. }
1703 | ParseNode::Styling { body, .. }
1704 | ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
1705 ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
1706 node_contains_middle(body)
1707 }
1708 ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
1709 ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
1710 node_contains_middle(body)
1711 }
1712 ParseNode::Array { body, .. } => body
1713 .iter()
1714 .any(|row| row.iter().any(node_contains_middle)),
1715 ParseNode::Enclose { body, .. }
1716 | ParseNode::Lap { body, .. }
1717 | ParseNode::RaiseBox { body, .. }
1718 | ParseNode::VCenter { body, .. } => node_contains_middle(body),
1719 ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
1720 ParseNode::XArrow { body, below, .. } => {
1721 node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
1722 }
1723 ParseNode::MathChoice {
1724 display,
1725 text,
1726 script,
1727 scriptscript,
1728 ..
1729 } => {
1730 display.iter().any(node_contains_middle)
1731 || text.iter().any(node_contains_middle)
1732 || script.iter().any(node_contains_middle)
1733 || scriptscript.iter().any(node_contains_middle)
1734 }
1735 ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
1736 ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
1737 _ => false,
1738 }
1739}
1740
1741fn body_contains_middle(nodes: &[ParseNode]) -> bool {
1743 nodes.iter().any(node_contains_middle)
1744}
1745
1746fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
1749 let m = options.metrics();
1750 if options.style.is_display() {
1751 m.delim1
1752 } else if matches!(
1753 options.style,
1754 MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
1755 ) {
1756 options
1757 .with_style(MathStyle::Script)
1758 .metrics()
1759 .delim2
1760 } else {
1761 m.delim2
1762 }
1763}
1764
1765fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
1767 let metrics = options.metrics();
1768 let inner_height = inner.height;
1769 let inner_depth = inner.depth;
1770 let axis = metrics.axis_height;
1771 let max_dist = (inner_height - axis).max(inner_depth + axis);
1772 let delim_factor = 901.0;
1773 let delim_extend = 5.0 / metrics.pt_per_em;
1774 let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
1775 from_formula.max(inner_height + inner_depth)
1777}
1778
1779fn layout_left_right(
1780 body: &[ParseNode],
1781 left_delim: &str,
1782 right_delim: &str,
1783 options: &LayoutOptions,
1784) -> LayoutBox {
1785 let (inner, total_height) = if body_contains_middle(body) {
1786 let opts_first = LayoutOptions {
1788 leftright_delim_height: None,
1789 ..options.clone()
1790 };
1791 let inner_first = layout_expression(body, &opts_first, true);
1792 let total_height = left_right_delim_total_height(&inner_first, options);
1793 let opts_second = LayoutOptions {
1795 leftright_delim_height: Some(total_height),
1796 ..options.clone()
1797 };
1798 let inner_second = layout_expression(body, &opts_second, true);
1799 (inner_second, total_height)
1800 } else {
1801 let inner = layout_expression(body, options, true);
1802 let total_height = left_right_delim_total_height(&inner, options);
1803 (inner, total_height)
1804 };
1805
1806 let inner_height = inner.height;
1807 let inner_depth = inner.depth;
1808
1809 let left_box = make_stretchy_delim(left_delim, total_height, options);
1810 let right_box = make_stretchy_delim(right_delim, total_height, options);
1811
1812 let width = left_box.width + inner.width + right_box.width;
1813 let height = left_box.height.max(right_box.height).max(inner_height);
1814 let depth = left_box.depth.max(right_box.depth).max(inner_depth);
1815
1816 LayoutBox {
1817 width,
1818 height,
1819 depth,
1820 content: BoxContent::LeftRight {
1821 left: Box::new(left_box),
1822 right: Box::new(right_box),
1823 inner: Box::new(inner),
1824 },
1825 color: options.color,
1826 }
1827}
1828
1829const DELIM_FONT_SEQUENCE: [FontId; 5] = [
1830 FontId::MainRegular,
1831 FontId::Size1Regular,
1832 FontId::Size2Regular,
1833 FontId::Size3Regular,
1834 FontId::Size4Regular,
1835];
1836
1837fn normalize_delim(delim: &str) -> &str {
1839 match delim {
1840 "<" | "\\lt" | "\u{27E8}" => "\\langle",
1841 ">" | "\\gt" | "\u{27E9}" => "\\rangle",
1842 _ => delim,
1843 }
1844}
1845
1846fn is_vert_delim(delim: &str) -> bool {
1848 matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
1849}
1850
1851fn is_double_vert_delim(delim: &str) -> bool {
1853 matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
1854}
1855
1856fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
1860 let axis = options.metrics().axis_height;
1861 let depth = (total_height / 2.0 - axis).max(0.0);
1862 let height = total_height - depth;
1863 let width = if is_double { 0.556 } else { 0.333 };
1864
1865 let commands = if is_double {
1866 double_vert_delim_path(height, depth)
1867 } else {
1868 vert_delim_path(height, depth)
1869 };
1870
1871 LayoutBox {
1872 width,
1873 height,
1874 depth,
1875 content: BoxContent::SvgPath { commands, fill: true },
1876 color: options.color,
1877 }
1878}
1879
1880fn vert_delim_path(height: f64, depth: f64) -> Vec<PathCommand> {
1883 let xl = 0.145_f64;
1885 let xr = 0.188_f64;
1886 vec![
1887 PathCommand::MoveTo { x: xl, y: -height },
1888 PathCommand::LineTo { x: xr, y: -height },
1889 PathCommand::LineTo { x: xr, y: depth },
1890 PathCommand::LineTo { x: xl, y: depth },
1891 PathCommand::Close,
1892 ]
1893}
1894
1895fn double_vert_delim_path(height: f64, depth: f64) -> Vec<PathCommand> {
1897 let (xl1, xr1) = (0.145_f64, 0.188_f64);
1898 let (xl2, xr2) = (0.367_f64, 0.410_f64);
1899 vec![
1900 PathCommand::MoveTo { x: xl1, y: -height },
1901 PathCommand::LineTo { x: xr1, y: -height },
1902 PathCommand::LineTo { x: xr1, y: depth },
1903 PathCommand::LineTo { x: xl1, y: depth },
1904 PathCommand::Close,
1905 PathCommand::MoveTo { x: xl2, y: -height },
1906 PathCommand::LineTo { x: xr2, y: -height },
1907 PathCommand::LineTo { x: xr2, y: depth },
1908 PathCommand::LineTo { x: xl2, y: depth },
1909 PathCommand::Close,
1910 ]
1911}
1912
1913fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
1915 if delim == "." || delim.is_empty() {
1916 return LayoutBox::new_kern(0.0);
1917 }
1918
1919 const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
1924 return make_vert_delim_box(total_height, false, options);
1925 }
1926 if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
1927 return make_vert_delim_box(total_height, true, options);
1928 }
1929
1930 let delim = normalize_delim(delim);
1932
1933 let ch = resolve_symbol_char(delim, Mode::Math);
1934 let char_code = ch as u32;
1935
1936 let mut best_font = FontId::MainRegular;
1937 let mut best_w = 0.4;
1938 let mut best_h = 0.7;
1939 let mut best_d = 0.2;
1940
1941 for &font_id in &DELIM_FONT_SEQUENCE {
1942 if let Some(m) = get_char_metrics(font_id, char_code) {
1943 best_font = font_id;
1944 best_w = m.width;
1945 best_h = m.height;
1946 best_d = m.depth;
1947 if best_h + best_d >= total_height {
1948 break;
1949 }
1950 }
1951 }
1952
1953 let best_total = best_h + best_d;
1954 if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
1955 return stacked;
1956 }
1957
1958 LayoutBox {
1959 width: best_w,
1960 height: best_h,
1961 depth: best_d,
1962 content: BoxContent::Glyph {
1963 font_id: best_font,
1964 char_code,
1965 },
1966 color: options.color,
1967 }
1968}
1969
1970const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
1972
1973fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
1975 if delim == "." || delim.is_empty() {
1976 return LayoutBox::new_kern(0.0);
1977 }
1978
1979 if is_vert_delim(delim) {
1981 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
1982 return make_vert_delim_box(total, false, options);
1983 }
1984 if is_double_vert_delim(delim) {
1985 let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
1986 return make_vert_delim_box(total, true, options);
1987 }
1988
1989 let delim = normalize_delim(delim);
1991
1992 let ch = resolve_symbol_char(delim, Mode::Math);
1993 let char_code = ch as u32;
1994
1995 let font_id = match size {
1996 1 => FontId::Size1Regular,
1997 2 => FontId::Size2Regular,
1998 3 => FontId::Size3Regular,
1999 4 => FontId::Size4Regular,
2000 _ => FontId::Size1Regular,
2001 };
2002
2003 let metrics = get_char_metrics(font_id, char_code);
2004 let (width, height, depth, actual_font) = match metrics {
2005 Some(m) => (m.width, m.height, m.depth, font_id),
2006 None => {
2007 let m = get_char_metrics(FontId::MainRegular, char_code);
2008 match m {
2009 Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
2010 None => (0.4, 0.7, 0.2, FontId::MainRegular),
2011 }
2012 }
2013 };
2014
2015 LayoutBox {
2016 width,
2017 height,
2018 depth,
2019 content: BoxContent::Glyph {
2020 font_id: actual_font,
2021 char_code,
2022 },
2023 color: options.color,
2024 }
2025}
2026
2027#[allow(clippy::too_many_arguments)]
2032fn layout_array(
2033 body: &[Vec<ParseNode>],
2034 cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
2035 arraystretch: f64,
2036 add_jot: bool,
2037 row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
2038 hlines: &[Vec<bool>],
2039 col_sep_type: Option<&str>,
2040 hskip: bool,
2041 options: &LayoutOptions,
2042) -> LayoutBox {
2043 let metrics = options.metrics();
2044 let pt = 1.0 / metrics.pt_per_em;
2045 let baselineskip = 12.0 * pt;
2046 let jot = 3.0 * pt;
2047 let arrayskip = arraystretch * baselineskip;
2048 let arstrut_h = 0.7 * arrayskip;
2049 let arstrut_d = 0.3 * arrayskip;
2050 const ALIGN_RELATION_MU: f64 = 3.0;
2053 let col_gap = match col_sep_type {
2054 Some("align") | Some("alignat") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
2055 _ => 2.0 * 5.0 * pt, };
2057 let cell_options = match col_sep_type {
2058 Some("align") | Some("alignat") => LayoutOptions {
2059 align_relation_spacing: Some(ALIGN_RELATION_MU),
2060 ..options.clone()
2061 },
2062 _ => options.clone(),
2063 };
2064
2065 let num_rows = body.len();
2066 if num_rows == 0 {
2067 return LayoutBox::new_empty();
2068 }
2069
2070 let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
2071
2072 use ratex_parser::parse_node::AlignType;
2074 let col_aligns: Vec<u8> = {
2075 let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
2076 .map(|cs| {
2077 cs.iter()
2078 .filter(|s| matches!(s.align_type, AlignType::Align))
2079 .collect()
2080 })
2081 .unwrap_or_default();
2082 (0..num_cols)
2083 .map(|c| {
2084 align_specs
2085 .get(c)
2086 .and_then(|s| s.align.as_deref())
2087 .and_then(|a| a.bytes().next())
2088 .unwrap_or(b'c')
2089 })
2090 .collect()
2091 };
2092
2093 let col_separators: Vec<bool> = {
2096 let mut seps = vec![false; num_cols + 1];
2097 let mut align_count = 0usize;
2098 if let Some(cs) = cols {
2099 for spec in cs {
2100 match spec.align_type {
2101 AlignType::Align => align_count += 1,
2102 AlignType::Separator if spec.align.as_deref() == Some("|") => {
2103 if align_count <= num_cols {
2104 seps[align_count] = true;
2105 }
2106 }
2107 _ => {}
2108 }
2109 }
2110 }
2111 seps
2112 };
2113
2114 let rule_thickness = 0.4 * pt;
2115 let double_rule_sep = metrics.double_rule_sep;
2116
2117 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
2119 let mut col_widths = vec![0.0_f64; num_cols];
2120 let mut row_heights = Vec::with_capacity(num_rows);
2121 let mut row_depths = Vec::with_capacity(num_rows);
2122
2123 for row in body {
2124 let mut row_boxes = Vec::with_capacity(num_cols);
2125 let mut rh = arstrut_h;
2126 let mut rd = arstrut_d;
2127
2128 for (c, cell) in row.iter().enumerate() {
2129 let cell_nodes = match cell {
2130 ParseNode::OrdGroup { body, .. } => body.as_slice(),
2131 other => std::slice::from_ref(other),
2132 };
2133 let cell_box = layout_expression(cell_nodes, &cell_options, true);
2134 rh = rh.max(cell_box.height);
2135 rd = rd.max(cell_box.depth);
2136 if c < num_cols {
2137 col_widths[c] = col_widths[c].max(cell_box.width);
2138 }
2139 row_boxes.push(cell_box);
2140 }
2141
2142 while row_boxes.len() < num_cols {
2144 row_boxes.push(LayoutBox::new_empty());
2145 }
2146
2147 if add_jot {
2148 rd += jot;
2149 }
2150
2151 row_heights.push(rh);
2152 row_depths.push(rd);
2153 cell_boxes.push(row_boxes);
2154 }
2155
2156 for (r, gap) in row_gaps.iter().enumerate() {
2158 if r < row_depths.len() {
2159 if let Some(m) = gap {
2160 let gap_em = measurement_to_em(m, options);
2161 if gap_em > 0.0 {
2162 row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
2163 }
2164 }
2165 }
2166 }
2167
2168 let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
2170 while hlines_before_row.len() < num_rows + 1 {
2171 hlines_before_row.push(vec![]);
2172 }
2173
2174 for r in 0..=num_rows {
2180 let n = hlines_before_row[r].len();
2181 if n > 1 {
2182 let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
2183 if r == 0 {
2184 if num_rows > 0 {
2185 row_heights[0] += extra;
2186 }
2187 } else {
2188 row_depths[r - 1] += extra;
2189 }
2190 }
2191 }
2192
2193 let mut total_height = 0.0;
2195 let mut row_positions = Vec::with_capacity(num_rows);
2196 for r in 0..num_rows {
2197 total_height += row_heights[r];
2198 row_positions.push(total_height);
2199 total_height += row_depths[r];
2200 }
2201
2202 let offset = total_height / 2.0 + metrics.axis_height;
2203
2204 let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
2206
2207 let total_width: f64 = col_widths.iter().sum::<f64>()
2209 + col_gap * (num_cols.saturating_sub(1)) as f64
2210 + 2.0 * content_x_offset;
2211
2212 let height = offset;
2213 let depth = total_height - offset;
2214
2215 LayoutBox {
2216 width: total_width,
2217 height,
2218 depth,
2219 content: BoxContent::Array {
2220 cells: cell_boxes,
2221 col_widths: col_widths.clone(),
2222 col_aligns,
2223 row_heights: row_heights.clone(),
2224 row_depths: row_depths.clone(),
2225 col_gap,
2226 offset,
2227 content_x_offset,
2228 col_separators,
2229 hlines_before_row,
2230 rule_thickness,
2231 double_rule_sep,
2232 },
2233 color: options.color,
2234 }
2235}
2236
2237fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2242 let multiplier = match size {
2244 1 => 0.5,
2245 2 => 0.6,
2246 3 => 0.7,
2247 4 => 0.8,
2248 5 => 0.9,
2249 6 => 1.0,
2250 7 => 1.2,
2251 8 => 1.44,
2252 9 => 1.728,
2253 10 => 2.074,
2254 11 => 2.488,
2255 _ => 1.0,
2256 };
2257
2258 let inner = layout_expression(body, options, true);
2259 let ratio = multiplier / options.size_multiplier();
2260 if (ratio - 1.0).abs() < 0.001 {
2261 inner
2262 } else {
2263 LayoutBox {
2264 width: inner.width * ratio,
2265 height: inner.height * ratio,
2266 depth: inner.depth * ratio,
2267 content: BoxContent::Scaled {
2268 body: Box::new(inner),
2269 child_scale: ratio,
2270 },
2271 color: options.color,
2272 }
2273 }
2274}
2275
2276fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
2279 let metrics = options.metrics();
2280 let mut children = Vec::new();
2281 for c in body.chars() {
2282 let ch = if star && c == ' ' {
2283 '\u{2423}' } else {
2285 c
2286 };
2287 let code = ch as u32;
2288 let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
2289 Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
2290 None => match get_char_metrics(FontId::MainRegular, code) {
2291 Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
2292 None => (
2293 FontId::TypewriterRegular,
2294 0.5,
2295 metrics.x_height,
2296 0.0,
2297 ),
2298 },
2299 };
2300 children.push(LayoutBox {
2301 width: w,
2302 height: h,
2303 depth: d,
2304 content: BoxContent::Glyph {
2305 font_id,
2306 char_code: code,
2307 },
2308 color: options.color,
2309 });
2310 }
2311 let mut hbox = make_hbox(children);
2312 hbox.color = options.color;
2313 hbox
2314}
2315
2316fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2317 let mut children = Vec::new();
2318 for node in body {
2319 match node {
2320 ParseNode::TextOrd { text, .. } | ParseNode::MathOrd { text, .. } => {
2321 let ch = resolve_symbol_char(text, Mode::Text);
2322 let char_code = ch as u32;
2323 let m = get_char_metrics(FontId::MainRegular, char_code);
2324 let (w, h, d) = match m {
2325 Some(m) => (m.width, m.height, m.depth),
2326 None => missing_glyph_metrics_fallback(ch, options),
2327 };
2328 children.push(LayoutBox {
2329 width: w,
2330 height: h,
2331 depth: d,
2332 content: BoxContent::Glyph {
2333 font_id: FontId::MainRegular,
2334 char_code,
2335 },
2336 color: options.color,
2337 });
2338 }
2339 ParseNode::SpacingNode { text, .. } => {
2340 children.push(layout_spacing_command(text, options));
2341 }
2342 _ => {
2343 children.push(layout_node(node, options));
2344 }
2345 }
2346 }
2347 make_hbox(children)
2348}
2349
2350fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2353 let base = layout_expression(body, options, true);
2354 let w = base.width;
2355 let h = base.height;
2356 let d = base.depth;
2357
2358 let shadow = layout_expression(body, options, true);
2360 let shadow_shift_x = 0.02_f64;
2361 let _shadow_shift_y = 0.01_f64;
2362
2363 let kern_back = LayoutBox::new_kern(-w);
2367 let kern_x = LayoutBox::new_kern(shadow_shift_x);
2368
2369 let children = vec![
2376 kern_x,
2377 shadow,
2378 kern_back,
2379 base,
2380 ];
2381 let hbox = make_hbox(children);
2383 LayoutBox {
2385 width: w,
2386 height: h,
2387 depth: d,
2388 content: hbox.content,
2389 color: options.color,
2390 }
2391}
2392
2393fn layout_enclose(
2396 label: &str,
2397 background_color: Option<&str>,
2398 border_color: Option<&str>,
2399 body: &ParseNode,
2400 options: &LayoutOptions,
2401) -> LayoutBox {
2402 use crate::layout_box::BoxContent;
2403 use ratex_types::color::Color;
2404
2405 if label == "\\phase" {
2407 return layout_phase(body, options);
2408 }
2409
2410 if label == "\\angl" {
2412 return layout_angl(body, options);
2413 }
2414
2415 if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
2417 return layout_cancel(label, body, options);
2418 }
2419
2420 let metrics = options.metrics();
2422 let padding = 3.0 / metrics.pt_per_em;
2423 let border_thickness = 0.4 / metrics.pt_per_em;
2424
2425 let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
2426
2427 let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
2428 let border = border_color
2429 .and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
2430 .unwrap_or(Color::BLACK);
2431
2432 let inner = layout_node(body, options);
2433 let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
2434
2435 let width = inner.width + 2.0 * outer_pad;
2436 let height = inner.height + outer_pad;
2437 let depth = inner.depth + outer_pad;
2438
2439 LayoutBox {
2440 width,
2441 height,
2442 depth,
2443 content: BoxContent::Framed {
2444 body: Box::new(inner),
2445 padding,
2446 border_thickness,
2447 has_border,
2448 bg_color: bg,
2449 border_color: border,
2450 },
2451 color: options.color,
2452 }
2453}
2454
2455fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2457 use crate::layout_box::BoxContent;
2458 let inner = layout_node(body, options);
2459 let height = inner.height + shift;
2461 let depth = (inner.depth - shift).max(0.0);
2462 let width = inner.width;
2463 LayoutBox {
2464 width,
2465 height,
2466 depth,
2467 content: BoxContent::RaiseBox {
2468 body: Box::new(inner),
2469 shift,
2470 },
2471 color: options.color,
2472 }
2473}
2474
2475fn is_single_char_body(node: &ParseNode) -> bool {
2478 use ratex_parser::parse_node::ParseNode as PN;
2479 match node {
2480 PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2482 PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
2483 PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
2485 _ => false,
2486 }
2487}
2488
2489fn layout_cancel(
2495 label: &str,
2496 body: &ParseNode,
2497 options: &LayoutOptions,
2498) -> LayoutBox {
2499 use crate::layout_box::BoxContent;
2500 let inner = layout_node(body, options);
2501 let w = inner.width.max(0.01);
2502 let h = inner.height;
2503 let d = inner.depth;
2504
2505 let single = is_single_char_body(body);
2507 let v_pad = if single { 0.2 } else { 0.0 };
2508 let h_pad = if single { 0.0 } else { 0.2 };
2509
2510 let commands: Vec<PathCommand> = match label {
2514 "\\cancel" => vec![
2515 PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
2518 "\\bcancel" => vec![
2519 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
2522 "\\xcancel" => vec![
2523 PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
2524 PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
2525 PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
2526 PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
2527 ],
2528 "\\sout" => {
2529 let mid_y = -0.5 * options.metrics().x_height;
2531 vec![
2532 PathCommand::MoveTo { x: 0.0, y: mid_y },
2533 PathCommand::LineTo { x: w, y: mid_y },
2534 ]
2535 }
2536 _ => vec![],
2537 };
2538
2539 let line_w = w + 2.0 * h_pad;
2540 let line_h = h + v_pad;
2541 let line_d = d + v_pad;
2542 let line_box = LayoutBox {
2543 width: line_w,
2544 height: line_h,
2545 depth: line_d,
2546 content: BoxContent::SvgPath { commands, fill: false },
2547 color: options.color,
2548 };
2549
2550 let body_kern = -(line_w - h_pad);
2552 let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
2553 LayoutBox {
2554 width: w,
2555 height: h,
2556 depth: d,
2557 content: BoxContent::HBox(vec![line_box, body_shifted]),
2558 color: options.color,
2559 }
2560}
2561
2562fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2565 use crate::layout_box::BoxContent;
2566 let metrics = options.metrics();
2567 let inner = layout_node(body, options);
2568 let line_weight = 0.6_f64 / metrics.pt_per_em;
2570 let clearance = 0.35_f64 * metrics.x_height;
2571 let angle_height = inner.height + inner.depth + line_weight + clearance;
2572 let left_pad = angle_height / 2.0 + line_weight;
2573 let width = inner.width + left_pad;
2574
2575 let y_svg = (1000.0 * angle_height).floor().max(80.0);
2577
2578 let sy = angle_height / y_svg;
2580 let sx = sy;
2583 let right_x = (400_000.0_f64 * sx).min(width);
2584
2585 let bottom_y = inner.depth + line_weight + clearance;
2587 let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
2588
2589 let x_peak = y_svg / 2.0;
2591 let commands = vec![
2592 PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
2593 PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
2594 PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
2595 PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
2596 PathCommand::LineTo {
2597 x: 145.0 * sx,
2598 y: vy(y_svg - 80.0),
2599 },
2600 PathCommand::LineTo {
2601 x: right_x,
2602 y: vy(y_svg - 80.0),
2603 },
2604 PathCommand::Close,
2605 ];
2606
2607 let body_shifted = make_hbox(vec![
2608 LayoutBox::new_kern(left_pad),
2609 inner.clone(),
2610 ]);
2611
2612 let path_height = inner.height;
2613 let path_depth = bottom_y;
2614
2615 LayoutBox {
2616 width,
2617 height: path_height,
2618 depth: path_depth,
2619 content: BoxContent::HBox(vec![
2620 LayoutBox {
2621 width,
2622 height: path_height,
2623 depth: path_depth,
2624 content: BoxContent::SvgPath { commands, fill: true },
2625 color: options.color,
2626 },
2627 LayoutBox::new_kern(-width),
2628 body_shifted,
2629 ]),
2630 color: options.color,
2631 }
2632}
2633
2634fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2637 use crate::layout_box::BoxContent;
2638 let inner = layout_node(body, options);
2639 let w = inner.width.max(0.3);
2640 let clearance = 0.1_f64;
2642 let arc_h = inner.height + clearance;
2643
2644 let path_commands = vec![
2646 PathCommand::MoveTo { x: 0.0, y: -arc_h },
2647 PathCommand::LineTo { x: w, y: -arc_h },
2648 PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
2649 ];
2650
2651 let height = arc_h;
2652 LayoutBox {
2653 width: w,
2654 height,
2655 depth: inner.depth,
2656 content: BoxContent::Angl {
2657 path_commands,
2658 body: Box::new(inner),
2659 },
2660 color: options.color,
2661 }
2662}
2663
2664fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2665 let font_id = match font {
2666 "mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
2667 "mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
2668 "mathit" | "\\mathit" | "textit" | "\\textit" => Some(FontId::MainItalic),
2669 "mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
2670 "mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
2671 "mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
2672 "mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
2673 "mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
2674 "mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
2675 "boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
2676 _ => None,
2677 };
2678
2679 if let Some(fid) = font_id {
2680 layout_with_font(body, fid, options)
2681 } else {
2682 layout_node(body, options)
2683 }
2684}
2685
2686fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
2687 match node {
2688 ParseNode::OrdGroup { body, .. } => {
2689 let children: Vec<LayoutBox> = body.iter().map(|n| layout_with_font(n, font_id, options)).collect();
2690 make_hbox(children)
2691 }
2692 ParseNode::SupSub {
2693 base, sup, sub, ..
2694 } => {
2695 if let Some(base_node) = base.as_deref() {
2696 if should_use_op_limits(base_node, options) {
2697 return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
2698 }
2699 }
2700 layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
2701 }
2702 ParseNode::MathOrd { text, .. }
2703 | ParseNode::TextOrd { text, .. }
2704 | ParseNode::Atom { text, .. } => {
2705 let ch = resolve_symbol_char(text, Mode::Math);
2706 let char_code = ch as u32;
2707 if let Some(m) = get_char_metrics(font_id, char_code) {
2708 LayoutBox {
2709 width: m.width,
2710 height: m.height,
2711 depth: m.depth,
2712 content: BoxContent::Glyph { font_id, char_code },
2713 color: options.color,
2714 }
2715 } else {
2716 layout_node(node, options)
2718 }
2719 }
2720 _ => layout_node(node, options),
2721 }
2722}
2723
2724fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2729 let cramped = options.with_style(options.style.cramped());
2730 let body_box = layout_node(body, &cramped);
2731 let metrics = options.metrics();
2732 let rule = metrics.default_rule_thickness;
2733
2734 let height = body_box.height + 3.0 * rule;
2736 LayoutBox {
2737 width: body_box.width,
2738 height,
2739 depth: body_box.depth,
2740 content: BoxContent::Overline {
2741 body: Box::new(body_box),
2742 rule_thickness: rule,
2743 },
2744 color: options.color,
2745 }
2746}
2747
2748fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
2749 let body_box = layout_node(body, options);
2750 let metrics = options.metrics();
2751 let rule = metrics.default_rule_thickness;
2752
2753 let depth = body_box.depth + 3.0 * rule;
2755 LayoutBox {
2756 width: body_box.width,
2757 height: body_box.height,
2758 depth,
2759 content: BoxContent::Underline {
2760 body: Box::new(body_box),
2761 rule_thickness: rule,
2762 },
2763 color: options.color,
2764 }
2765}
2766
2767fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
2769 let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
2770 let body_opts = options.with_color(link_color);
2771 let body_box = layout_expression(body, &body_opts, true);
2772 layout_underline_laid_out(body_box, options, link_color)
2773}
2774
2775fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
2777 let metrics = options.metrics();
2778 let rule = metrics.default_rule_thickness;
2779 let depth = body_box.depth + 3.0 * rule;
2780 LayoutBox {
2781 width: body_box.width,
2782 height: body_box.height,
2783 depth,
2784 content: BoxContent::Underline {
2785 body: Box::new(body_box),
2786 rule_thickness: rule,
2787 },
2788 color,
2789 }
2790}
2791
2792fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
2797 let metrics = options.metrics();
2798 let mu = metrics.css_em_per_mu();
2799
2800 let width = match text {
2801 "\\," | "\\thinspace" => 3.0 * mu,
2802 "\\:" | "\\medspace" => 4.0 * mu,
2803 "\\;" | "\\thickspace" => 5.0 * mu,
2804 "\\!" | "\\negthinspace" => -3.0 * mu,
2805 "\\negmedspace" => -4.0 * mu,
2806 "\\negthickspace" => -5.0 * mu,
2807 " " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
2808 get_char_metrics(FontId::MainRegular, 160)
2812 .map(|m| m.width)
2813 .unwrap_or(0.25)
2814 }
2815 "\\quad" => metrics.quad,
2816 "\\qquad" => 2.0 * metrics.quad,
2817 "\\enspace" => metrics.quad / 2.0,
2818 _ => 0.0,
2819 };
2820
2821 LayoutBox::new_kern(width)
2822}
2823
2824fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
2829 let metrics = options.metrics();
2830 match m.unit.as_str() {
2831 "em" => m.number,
2832 "ex" => m.number * metrics.x_height,
2833 "mu" => m.number * metrics.css_em_per_mu(),
2834 "pt" => m.number / metrics.pt_per_em,
2835 "mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
2836 "cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
2837 "in" => m.number * 72.27 / metrics.pt_per_em,
2838 "bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
2839 "pc" => m.number * 12.0 / metrics.pt_per_em,
2840 "dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
2841 "cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
2842 "nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
2843 "nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
2844 "sp" => m.number / 65536.0 / metrics.pt_per_em,
2845 _ => m.number,
2846 }
2847}
2848
2849fn node_math_class(node: &ParseNode) -> Option<MathClass> {
2855 match node {
2856 ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
2857 ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
2858 ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
2859 ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
2860 ParseNode::GenFrac { .. } => Some(MathClass::Inner),
2861 ParseNode::Sqrt { .. } => Some(MathClass::Ord),
2862 ParseNode::SupSub { base, .. } => {
2863 base.as_ref().and_then(|b| node_math_class(b))
2864 }
2865 ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
2866 ParseNode::SpacingNode { .. } => None,
2867 ParseNode::Kern { .. } => None,
2868 ParseNode::HtmlMathMl { html, .. } => {
2869 for child in html {
2871 if let Some(cls) = node_math_class(child) {
2872 return Some(cls);
2873 }
2874 }
2875 None
2876 }
2877 ParseNode::Lap { .. } => None,
2878 ParseNode::LeftRight { .. } => Some(MathClass::Inner),
2879 ParseNode::AccentToken { .. } => Some(MathClass::Ord),
2880 ParseNode::XArrow { .. } => Some(MathClass::Rel),
2882 _ => Some(MathClass::Ord),
2883 }
2884}
2885
2886fn mclass_str_to_math_class(mclass: &str) -> MathClass {
2887 match mclass {
2888 "mord" => MathClass::Ord,
2889 "mop" => MathClass::Op,
2890 "mbin" => MathClass::Bin,
2891 "mrel" => MathClass::Rel,
2892 "mopen" => MathClass::Open,
2893 "mclose" => MathClass::Close,
2894 "mpunct" => MathClass::Punct,
2895 "minner" => MathClass::Inner,
2896 _ => MathClass::Ord,
2897 }
2898}
2899
2900fn is_character_box(node: &ParseNode) -> bool {
2902 matches!(
2903 node,
2904 ParseNode::MathOrd { .. }
2905 | ParseNode::TextOrd { .. }
2906 | ParseNode::Atom { .. }
2907 | ParseNode::AccentToken { .. }
2908 )
2909}
2910
2911fn family_to_math_class(family: AtomFamily) -> MathClass {
2912 match family {
2913 AtomFamily::Bin => MathClass::Bin,
2914 AtomFamily::Rel => MathClass::Rel,
2915 AtomFamily::Open => MathClass::Open,
2916 AtomFamily::Close => MathClass::Close,
2917 AtomFamily::Punct => MathClass::Punct,
2918 AtomFamily::Inner => MathClass::Inner,
2919 }
2920}
2921
2922fn layout_horiz_brace(
2927 base: &ParseNode,
2928 is_over: bool,
2929 options: &LayoutOptions,
2930) -> LayoutBox {
2931 let body_box = layout_node(base, options);
2932 let w = body_box.width.max(0.5);
2933
2934 let label = if is_over { "overbrace" } else { "underbrace" };
2935 let (raw_commands, brace_h, brace_fill) =
2938 match crate::katex_svg::katex_stretchy_path(label, w) {
2939 Some((c, h)) => (c, h, true),
2940 None => {
2941 let h = 0.35_f64;
2942 (horiz_brace_path(w, h, is_over), h, false)
2943 }
2944 };
2945
2946 let y_shift = if is_over { -brace_h / 2.0 } else { brace_h / 2.0 };
2950 let commands = shift_path_y(raw_commands, y_shift);
2951
2952 let brace_box = LayoutBox {
2953 width: w,
2954 height: if is_over { brace_h } else { 0.0 },
2955 depth: if is_over { 0.0 } else { brace_h },
2956 content: BoxContent::SvgPath {
2957 commands,
2958 fill: brace_fill,
2959 },
2960 color: options.color,
2961 };
2962
2963 let gap = 0.1;
2964 let (height, depth) = if is_over {
2965 (body_box.height + brace_h + gap, body_box.depth)
2966 } else {
2967 (body_box.height, body_box.depth + brace_h + gap)
2968 };
2969
2970 let clearance = if is_over {
2971 height - brace_h
2972 } else {
2973 body_box.height + body_box.depth + gap
2974 };
2975 let total_w = body_box.width;
2976
2977 LayoutBox {
2978 width: total_w,
2979 height,
2980 depth,
2981 content: BoxContent::Accent {
2982 base: Box::new(body_box),
2983 accent: Box::new(brace_box),
2984 clearance,
2985 skew: 0.0,
2986 is_below: !is_over,
2987 },
2988 color: options.color,
2989 }
2990}
2991
2992fn layout_xarrow(
2997 label: &str,
2998 body: &ParseNode,
2999 below: Option<&ParseNode>,
3000 options: &LayoutOptions,
3001) -> LayoutBox {
3002 let sup_style = options.style.superscript();
3003 let sub_style = options.style.subscript();
3004 let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
3005 let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
3006
3007 let sup_opts = options.with_style(sup_style);
3008 let body_box = layout_node(body, &sup_opts);
3009 let body_w = body_box.width * sup_ratio;
3010
3011 let below_box = below.map(|b| {
3012 let sub_opts = options.with_style(sub_style);
3013 layout_node(b, &sub_opts)
3014 });
3015 let below_w = below_box
3016 .as_ref()
3017 .map(|b| b.width * sub_ratio)
3018 .unwrap_or(0.0);
3019
3020 let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
3023 let upper_w = body_w + sup_ratio;
3024 let lower_w = if below_box.is_some() {
3025 below_w + sub_ratio
3026 } else {
3027 0.0
3028 };
3029 let arrow_w = upper_w.max(lower_w).max(min_w);
3030 let arrow_h = 0.3;
3031
3032 let (commands, actual_arrow_h, fill_arrow) =
3033 match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
3034 Some((c, h)) => (c, h, true),
3035 None => (
3036 stretchy_accent_path(label, arrow_w, arrow_h),
3037 arrow_h,
3038 label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
3039 ),
3040 };
3041 let arrow_box = LayoutBox {
3042 width: arrow_w,
3043 height: actual_arrow_h / 2.0,
3044 depth: actual_arrow_h / 2.0,
3045 content: BoxContent::SvgPath {
3046 commands,
3047 fill: fill_arrow,
3048 },
3049 color: options.color,
3050 };
3051
3052 let metrics = options.metrics();
3055 let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
3057 let gap = 0.111; let base_shift = -axis;
3061
3062 let sup_kern = gap;
3070 let sub_kern = gap;
3071
3072 let sup_h = body_box.height * sup_ratio;
3073 let sup_d = body_box.depth * sup_ratio;
3074
3075 let height = axis + arrow_half + gap + sup_h + sup_d;
3077 let mut depth = (arrow_half - axis).max(0.0);
3079
3080 if let Some(ref bel) = below_box {
3081 let sub_h = bel.height * sub_ratio;
3082 let sub_d = bel.depth * sub_ratio;
3083 depth = (arrow_half - axis) + gap + sub_h + sub_d;
3085 }
3086
3087 LayoutBox {
3088 width: arrow_w,
3089 height,
3090 depth,
3091 content: BoxContent::OpLimits {
3092 base: Box::new(arrow_box),
3093 sup: Some(Box::new(body_box)),
3094 sub: below_box.map(Box::new),
3095 base_shift,
3096 sup_kern,
3097 sub_kern,
3098 slant: 0.0,
3099 sup_scale: sup_ratio,
3100 sub_scale: sub_ratio,
3101 },
3102 color: options.color,
3103 }
3104}
3105
3106fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
3111 let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
3114 let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
3115 let diameter = radius * 2.0;
3116
3117 let cx = radius;
3119 let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
3122
3123 let circle_commands = vec![
3124 PathCommand::MoveTo { x: cx + r, y: cy },
3125 PathCommand::CubicTo {
3126 x1: cx + r, y1: cy - k * r,
3127 x2: cx + k * r, y2: cy - r,
3128 x: cx, y: cy - r,
3129 },
3130 PathCommand::CubicTo {
3131 x1: cx - k * r, y1: cy - r,
3132 x2: cx - r, y2: cy - k * r,
3133 x: cx - r, y: cy,
3134 },
3135 PathCommand::CubicTo {
3136 x1: cx - r, y1: cy + k * r,
3137 x2: cx - k * r, y2: cy + r,
3138 x: cx, y: cy + r,
3139 },
3140 PathCommand::CubicTo {
3141 x1: cx + k * r, y1: cy + r,
3142 x2: cx + r, y2: cy + k * r,
3143 x: cx + r, y: cy,
3144 },
3145 PathCommand::Close,
3146 ];
3147
3148 let circle_box = LayoutBox {
3149 width: diameter,
3150 height: r - cy.min(0.0),
3151 depth: (r + cy).max(0.0),
3152 content: BoxContent::SvgPath { commands: circle_commands, fill: false },
3153 color: options.color,
3154 };
3155
3156 let content_shift = (diameter - body_box.width) / 2.0;
3158 let children = vec![
3160 circle_box,
3161 LayoutBox::new_kern(-(diameter) + content_shift),
3162 body_box.clone(),
3163 ];
3164
3165 let height = r - cy.min(0.0);
3166 let depth = (r + cy).max(0.0);
3167
3168 LayoutBox {
3169 width: diameter,
3170 height,
3171 depth,
3172 content: BoxContent::HBox(children),
3173 color: options.color,
3174 }
3175}
3176
3177fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
3185 let cx = width / 2.0;
3186 let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
3191 PathCommand::MoveTo { x: cx + a, y: cy },
3192 PathCommand::CubicTo {
3193 x1: cx + a,
3194 y1: cy - k * b,
3195 x2: cx + k * a,
3196 y2: cy - b,
3197 x: cx,
3198 y: cy - b,
3199 },
3200 PathCommand::CubicTo {
3201 x1: cx - k * a,
3202 y1: cy - b,
3203 x2: cx - a,
3204 y2: cy - k * b,
3205 x: cx - a,
3206 y: cy,
3207 },
3208 PathCommand::CubicTo {
3209 x1: cx - a,
3210 y1: cy + k * b,
3211 x2: cx - k * a,
3212 y2: cy + b,
3213 x: cx,
3214 y: cy + b,
3215 },
3216 PathCommand::CubicTo {
3217 x1: cx + k * a,
3218 y1: cy + b,
3219 x2: cx + a,
3220 y2: cy + k * b,
3221 x: cx + a,
3222 y: cy,
3223 },
3224 PathCommand::Close,
3225 ]
3226}
3227
3228fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
3229 cmds.into_iter().map(|c| match c {
3230 PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
3231 PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
3232 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
3233 x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
3234 },
3235 PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
3236 x1, y1: y1 + dy, x, y: y + dy,
3237 },
3238 PathCommand::Close => PathCommand::Close,
3239 }).collect()
3240}
3241
3242fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
3243 if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
3244 return commands;
3245 }
3246 let ah = height * 0.35; let mid_y = -height / 2.0;
3248
3249 match label {
3250 "\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
3251 vec![
3252 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3253 PathCommand::LineTo { x: 0.0, y: mid_y },
3254 PathCommand::LineTo { x: ah, y: mid_y + ah },
3255 PathCommand::MoveTo { x: 0.0, y: mid_y },
3256 PathCommand::LineTo { x: width, y: mid_y },
3257 ]
3258 }
3259 "\\overleftrightarrow" | "\\underleftrightarrow"
3260 | "\\xleftrightarrow" | "\\xLeftrightarrow" => {
3261 vec![
3262 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3263 PathCommand::LineTo { x: 0.0, y: mid_y },
3264 PathCommand::LineTo { x: ah, y: mid_y + ah },
3265 PathCommand::MoveTo { x: 0.0, y: mid_y },
3266 PathCommand::LineTo { x: width, y: mid_y },
3267 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3268 PathCommand::LineTo { x: width, y: mid_y },
3269 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3270 ]
3271 }
3272 "\\xlongequal" => {
3273 let gap = 0.04;
3274 vec![
3275 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3276 PathCommand::LineTo { x: width, y: mid_y - gap },
3277 PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
3278 PathCommand::LineTo { x: width, y: mid_y + gap },
3279 ]
3280 }
3281 "\\xhookleftarrow" => {
3282 vec![
3283 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3284 PathCommand::LineTo { x: 0.0, y: mid_y },
3285 PathCommand::LineTo { x: ah, y: mid_y + ah },
3286 PathCommand::MoveTo { x: 0.0, y: mid_y },
3287 PathCommand::LineTo { x: width, y: mid_y },
3288 PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
3289 ]
3290 }
3291 "\\xhookrightarrow" => {
3292 vec![
3293 PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
3294 PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
3295 PathCommand::LineTo { x: width, y: mid_y },
3296 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3297 PathCommand::LineTo { x: width, y: mid_y },
3298 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3299 ]
3300 }
3301 "\\xrightharpoonup" | "\\xleftharpoonup" => {
3302 let right = label.contains("right");
3303 if right {
3304 vec![
3305 PathCommand::MoveTo { x: 0.0, y: mid_y },
3306 PathCommand::LineTo { x: width, y: mid_y },
3307 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3308 PathCommand::LineTo { x: width, y: mid_y },
3309 ]
3310 } else {
3311 vec![
3312 PathCommand::MoveTo { x: ah, y: mid_y - ah },
3313 PathCommand::LineTo { x: 0.0, y: mid_y },
3314 PathCommand::LineTo { x: width, y: mid_y },
3315 ]
3316 }
3317 }
3318 "\\xrightharpoondown" | "\\xleftharpoondown" => {
3319 let right = label.contains("right");
3320 if right {
3321 vec![
3322 PathCommand::MoveTo { x: 0.0, y: mid_y },
3323 PathCommand::LineTo { x: width, y: mid_y },
3324 PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
3325 PathCommand::LineTo { x: width, y: mid_y },
3326 ]
3327 } else {
3328 vec![
3329 PathCommand::MoveTo { x: ah, y: mid_y + ah },
3330 PathCommand::LineTo { x: 0.0, y: mid_y },
3331 PathCommand::LineTo { x: width, y: mid_y },
3332 ]
3333 }
3334 }
3335 "\\xrightleftharpoons" | "\\xleftrightharpoons" => {
3336 let gap = 0.06;
3337 vec![
3338 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3339 PathCommand::LineTo { x: width, y: mid_y - gap },
3340 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3341 PathCommand::LineTo { x: width, y: mid_y - gap },
3342 PathCommand::MoveTo { x: width, y: mid_y + gap },
3343 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3344 PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
3345 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3346 ]
3347 }
3348 "\\xtofrom" | "\\xrightleftarrows" => {
3349 let gap = 0.06;
3350 vec![
3351 PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
3352 PathCommand::LineTo { x: width, y: mid_y - gap },
3353 PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
3354 PathCommand::LineTo { x: width, y: mid_y - gap },
3355 PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
3356 PathCommand::MoveTo { x: width, y: mid_y + gap },
3357 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3358 PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
3359 PathCommand::LineTo { x: 0.0, y: mid_y + gap },
3360 PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
3361 ]
3362 }
3363 "\\overlinesegment" | "\\underlinesegment" => {
3364 vec![
3365 PathCommand::MoveTo { x: 0.0, y: mid_y },
3366 PathCommand::LineTo { x: width, y: mid_y },
3367 ]
3368 }
3369 _ => {
3370 vec![
3371 PathCommand::MoveTo { x: 0.0, y: mid_y },
3372 PathCommand::LineTo { x: width, y: mid_y },
3373 PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
3374 PathCommand::LineTo { x: width, y: mid_y },
3375 PathCommand::LineTo { x: width - ah, y: mid_y + ah },
3376 ]
3377 }
3378 }
3379}
3380
3381fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
3382 let mid = width / 2.0;
3383 let q = height * 0.6;
3384 if is_over {
3385 vec![
3386 PathCommand::MoveTo { x: 0.0, y: 0.0 },
3387 PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
3388 PathCommand::LineTo { x: mid - 0.05, y: -q },
3389 PathCommand::LineTo { x: mid, y: -height },
3390 PathCommand::LineTo { x: mid + 0.05, y: -q },
3391 PathCommand::LineTo { x: width - mid * 0.4, y: -q },
3392 PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
3393 ]
3394 } else {
3395 vec![
3396 PathCommand::MoveTo { x: 0.0, y: 0.0 },
3397 PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
3398 PathCommand::LineTo { x: mid - 0.05, y: q },
3399 PathCommand::LineTo { x: mid, y: height },
3400 PathCommand::LineTo { x: mid + 0.05, y: q },
3401 PathCommand::LineTo { x: width - mid * 0.4, y: q },
3402 PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
3403 ]
3404 }
3405}