1use std::ops::Range;
9use std::sync::Arc;
10
11use crate::text::metrics as text_metrics;
12use crate::tree::{Color, FontFamily, FontWeight, Rect, TextWrap};
13
14const DEFAULT_RULE_THICKNESS: f32 = 1.1;
15const SCRIPT_SCALE: f32 = 0.72;
16const LARGE_OPERATOR_SCALE: f32 = 1.35;
17const FRACTION_PAD_EM: f32 = 0.18;
18const FRACTION_GAP_EM: f32 = 0.18;
19const SQRT_GAP_EM: f32 = 0.10;
20const TABLE_COL_GAP_EM: f32 = 0.8;
21const TABLE_ROW_GAP_EM: f32 = 0.35;
22const CASES_COL_GAP_EM: f32 = 0.5;
23const RADICAL_GLYPH: char = '√';
24const THIN_MATH_SPACE_EM: f32 = 0.08;
25const MEDIUM_MATH_SPACE_EM: f32 = 0.18;
26const THICK_MATH_SPACE_EM: f32 = 0.28;
27const STRETCHY_VARIANT_CHARS: [char; 29] = [
28 '(',
29 ')',
30 '[',
31 ']',
32 '{',
33 '}',
34 '|',
35 '‖',
36 '⌊',
37 '⌋',
38 '⌈',
39 '⌉',
40 RADICAL_GLYPH,
41 '∑',
42 '∫',
43 '∏',
44 '⋂',
45 '⋃',
46 '∐',
47 '∮',
48 '∬',
49 '∭',
50 '⨁',
51 '⨂',
52 '⨀',
53 '⋁',
54 '⋀',
55 '⨄',
56 '⨆',
57];
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub enum MathDisplay {
61 #[default]
62 Inline,
63 Block,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
67pub enum MathColumnAlignment {
68 Left,
69 #[default]
70 Center,
71 Right,
72}
73
74#[derive(Clone, Debug, PartialEq)]
75#[non_exhaustive]
76pub enum MathExpr {
77 Row(Vec<MathExpr>),
78 Identifier(String),
79 Number(String),
80 Operator(String),
81 OperatorWithMetadata {
82 text: String,
83 lspace: Option<f32>,
84 rspace: Option<f32>,
85 large_operator: Option<bool>,
86 movable_limits: Option<bool>,
87 },
88 Text(String),
89 Space(f32),
90 Fraction {
91 numerator: Arc<MathExpr>,
92 denominator: Arc<MathExpr>,
93 },
94 Sqrt(Arc<MathExpr>),
95 Root {
96 base: Arc<MathExpr>,
97 index: Arc<MathExpr>,
98 },
99 Scripts {
100 base: Arc<MathExpr>,
101 sub: Option<Arc<MathExpr>>,
102 sup: Option<Arc<MathExpr>>,
103 },
104 UnderOver {
105 base: Arc<MathExpr>,
106 under: Option<Arc<MathExpr>>,
107 over: Option<Arc<MathExpr>>,
108 },
109 Accent {
110 base: Arc<MathExpr>,
111 accent: Arc<MathExpr>,
112 stretch: bool,
113 },
114 Fenced {
115 open: Option<String>,
116 close: Option<String>,
117 body: Arc<MathExpr>,
118 },
119 Table {
120 rows: Vec<Vec<MathExpr>>,
121 column_alignments: Vec<MathColumnAlignment>,
122 column_gap: Option<f32>,
123 row_gap: Option<f32>,
124 },
125 Source {
126 source: Range<usize>,
127 body: Arc<MathExpr>,
128 },
129 Error(String),
130}
131
132impl MathExpr {
133 pub fn row(children: impl IntoIterator<Item = MathExpr>) -> Self {
134 let mut children: Vec<MathExpr> = children.into_iter().collect();
135 match children.len() {
136 0 => MathExpr::Row(Vec::new()),
137 1 => children.pop().unwrap(),
138 _ => MathExpr::Row(children),
139 }
140 }
141
142 pub fn source_range(&self) -> Option<&Range<usize>> {
143 match self {
144 MathExpr::Source { source, .. } => Some(source),
145 _ => None,
146 }
147 }
148
149 pub fn without_source(&self) -> &MathExpr {
150 match self {
151 MathExpr::Source { body, .. } => body.without_source(),
152 _ => self,
153 }
154 }
155}
156
157#[derive(Clone, Debug, PartialEq)]
158pub struct MathLayout {
159 pub width: f32,
160 pub ascent: f32,
161 pub descent: f32,
162 pub atoms: Vec<MathAtom>,
163}
164
165impl MathLayout {
166 pub fn height(&self) -> f32 {
167 self.ascent + self.descent
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
172pub enum MathAtom {
173 Glyph {
174 text: String,
175 x: f32,
176 y_baseline: f32,
177 size: f32,
178 weight: FontWeight,
179 italic: bool,
180 },
181 GlyphId {
182 glyph_id: u16,
183 rect: Rect,
184 view_box: Rect,
185 },
186 Rule {
187 rect: Rect,
188 },
189 Radical {
190 points: [[f32; 2]; 5],
191 thickness: f32,
192 },
193 Delimiter {
194 delimiter: String,
195 rect: Rect,
196 thickness: f32,
197 },
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201enum MathOperatorClass {
202 Ordinary,
203 Binary,
204 Relation,
205 Large,
206 Punctuation,
207}
208
209#[derive(Clone, Copy, Debug, PartialEq)]
210struct MathOperatorInfo {
211 class: MathOperatorClass,
212 lspace_em: f32,
213 rspace_em: f32,
214 large_operator: bool,
215 movable_limits: bool,
216}
217
218impl MathOperatorInfo {
219 fn new(class: MathOperatorClass, lspace_em: f32, rspace_em: f32) -> Self {
220 Self {
221 class,
222 lspace_em,
223 rspace_em,
224 large_operator: false,
225 movable_limits: false,
226 }
227 }
228
229 fn large(mut self) -> Self {
230 self.large_operator = true;
231 self.movable_limits = true;
232 self
233 }
234
235 fn large_with_side_scripts(mut self) -> Self {
236 self.large_operator = true;
237 self.movable_limits = false;
238 self
239 }
240}
241
242fn operator_info(operator: &str) -> MathOperatorInfo {
243 use MathOperatorClass::*;
244 match operator {
245 "+" | "-" | "±" | "∓" | "·" | "×" | "÷" | "∪" | "∩" | "∧" | "∨" | "⊕" | "⊖" | "⊗" | "⊘"
246 | "⊙" | "⋆" | "∗" | "∘" | "•" | "⊔" | "⊓" | "⨿" | "≀" | "◁" | "▷" | "⋄" | "∖" => {
247 MathOperatorInfo::new(Binary, MEDIUM_MATH_SPACE_EM, MEDIUM_MATH_SPACE_EM)
248 }
249 "=" | "<" | ">" | "≤" | "≥" | "≠" | "≈" | "∼" | "→" | "←" | "↔" | "⇒" | "⇐" | "⇔" | "⟹"
250 | "⟸" | "⟺" | "⟶" | "⟵" | "⟷" | "↦" | "⟼" | "↪" | "↩" | "∈" | "∉" | "∋" | "∌" | "⊂"
251 | "⊃" | "⊆" | "⊇" | "⊊" | "⊋" | "≡" | "≃" | "≅" | "∝" | "≺" | "≻" | "⪯" | "⪰" | "≪"
252 | "≫" | "∥" | "⊥" | "≍" | "≐" | "⊨" | "⊢" | "⊣" | "∴" | "∵" => {
253 MathOperatorInfo::new(Relation, MEDIUM_MATH_SPACE_EM, MEDIUM_MATH_SPACE_EM)
254 }
255 "∑" | "∏" | "⋂" | "⋃" | "∐" | "⨁" | "⨂" | "⨀" | "⋁" | "⋀" | "⨄" | "⨆" => {
256 MathOperatorInfo::new(Large, THIN_MATH_SPACE_EM, THIN_MATH_SPACE_EM).large()
257 }
258 "∫" | "∮" | "∬" | "∭" => {
259 MathOperatorInfo::new(Large, THIN_MATH_SPACE_EM, THIN_MATH_SPACE_EM)
260 .large_with_side_scripts()
261 }
262 "," | "." | ";" | ":" => MathOperatorInfo::new(Punctuation, 0.0, THIN_MATH_SPACE_EM),
263 _ => MathOperatorInfo::new(Ordinary, 0.0, 0.0),
264 }
265}
266
267#[derive(Clone, Copy, Debug)]
268struct LayoutCtx {
269 size: f32,
270 display: MathDisplay,
271}
272
273impl LayoutCtx {
274 fn script(self) -> Self {
275 Self {
276 size: self.metrics().script_size(),
277 display: MathDisplay::Inline,
278 }
279 }
280
281 fn large_operator(self) -> Self {
282 Self {
283 size: self.metrics().large_operator_size(),
284 display: self.display,
285 }
286 }
287
288 fn metrics(self) -> MathMetrics {
289 MathMetrics {
290 size: self.size,
291 display: self.display,
292 }
293 }
294}
295
296#[derive(Clone, Copy, Debug)]
297struct MathMetrics {
298 size: f32,
299 display: MathDisplay,
300}
301
302impl MathMetrics {
303 fn font_constants(self) -> Option<OpenTypeMathConstants> {
304 open_type_math_constants()
305 }
306
307 fn script_size(self) -> f32 {
308 self.font_constants()
309 .and_then(|constants| constants.script_scale(self.size))
310 .unwrap_or(self.size * SCRIPT_SCALE)
311 .max(6.0)
312 }
313
314 fn large_operator_size(self) -> f32 {
315 self.size * LARGE_OPERATOR_SCALE
316 }
317
318 fn rule_thickness(self) -> f32 {
319 self.font_constants()
320 .and_then(|constants| constants.fraction_rule_thickness(self.size))
321 .unwrap_or(DEFAULT_RULE_THICKNESS * self.size / 16.0)
322 .max(0.75)
323 }
324
325 fn radical_rule_thickness(self) -> f32 {
326 self.font_constants()
327 .and_then(|constants| constants.radical_rule_thickness(self.size))
328 .unwrap_or_else(|| self.rule_thickness())
329 .max(0.75)
330 }
331
332 fn default_ascent(self) -> f32 {
333 self.size * 0.75
334 }
335
336 fn default_descent(self) -> f32 {
337 self.size * 0.25
338 }
339
340 fn glyph_ascent(self) -> f32 {
341 self.size * 0.82
342 }
343
344 fn glyph_descent(self) -> f32 {
345 self.size * 0.22
346 }
347
348 fn space_width(self, em: f32) -> f32 {
349 self.size * em
350 }
351
352 fn operator_spacing_with_overrides(
353 self,
354 operator: &str,
355 lspace_em: Option<f32>,
356 rspace_em: Option<f32>,
357 ) -> (f32, f32) {
358 let info = operator_info(operator);
359 (
360 self.size * lspace_em.unwrap_or(info.lspace_em),
361 self.size * rspace_em.unwrap_or(info.rspace_em),
362 )
363 }
364
365 fn fraction_pad(self) -> f32 {
366 self.size
367 * if matches!(self.display, MathDisplay::Block) {
368 FRACTION_PAD_EM
369 } else {
370 FRACTION_PAD_EM * 0.65
371 }
372 }
373
374 fn fraction_numerator_gap(self) -> f32 {
375 self.font_constants()
376 .and_then(|constants| {
377 constants
378 .fraction_numerator_gap(self.size, matches!(self.display, MathDisplay::Block))
379 })
380 .unwrap_or_else(|| self.fraction_gap_fallback())
381 }
382
383 fn fraction_denominator_gap(self) -> f32 {
384 self.font_constants()
385 .and_then(|constants| {
386 constants
387 .fraction_denominator_gap(self.size, matches!(self.display, MathDisplay::Block))
388 })
389 .unwrap_or_else(|| self.fraction_gap_fallback())
390 }
391
392 fn fraction_gap_fallback(self) -> f32 {
393 self.size
394 * if matches!(self.display, MathDisplay::Block) {
395 FRACTION_GAP_EM
396 } else {
397 FRACTION_GAP_EM * 0.55
398 }
399 }
400
401 fn fraction_numerator_shift(self) -> f32 {
402 self.font_constants()
403 .and_then(|constants| {
404 constants
405 .fraction_numerator_shift(self.size, matches!(self.display, MathDisplay::Block))
406 })
407 .unwrap_or(self.size * 0.55)
408 }
409
410 fn fraction_denominator_shift(self) -> f32 {
411 self.font_constants()
412 .and_then(|constants| {
413 constants.fraction_denominator_shift(
414 self.size,
415 matches!(self.display, MathDisplay::Block),
416 )
417 })
418 .unwrap_or(self.size * 0.55)
419 }
420
421 fn math_axis_shift(self) -> f32 {
422 self.font_constants()
423 .and_then(|constants| constants.axis_height(self.size))
424 .or_else(|| {
425 matches!(self.display, MathDisplay::Block)
426 .then(|| self.operator_axis_shift())
427 .flatten()
428 })
429 .unwrap_or(self.size * 0.28)
430 }
431
432 fn operator_axis_shift(self) -> Option<f32> {
433 let layout = math_glyph_layout("+", self.size, FontWeight::Regular);
434 let baseline = layout.lines.first()?.baseline;
435 Some((baseline - layout.line_height * 0.5).max(self.size * 0.2))
436 }
437
438 fn sqrt_gap(self) -> f32 {
439 self.font_constants()
440 .and_then(|constants| {
441 constants
442 .radical_vertical_gap(self.size, matches!(self.display, MathDisplay::Block))
443 })
444 .unwrap_or(self.size * SQRT_GAP_EM)
445 }
446
447 fn radical_width(self) -> f32 {
448 self.size * 0.72
449 }
450
451 fn radical_left_flair_y(self) -> f32 {
452 -self.size * 0.03
453 }
454
455 fn radical_hook_x(self) -> f32 {
456 self.size * 0.12
457 }
458
459 fn radical_hook_y(self) -> f32 {
460 -self.size * 0.1
461 }
462
463 fn radical_tick_x(self) -> f32 {
464 self.size * 0.24
465 }
466
467 fn radical_tick_y(self, inner_descent: f32) -> f32 {
468 (inner_descent * 0.75).max(self.size * 0.13)
469 }
470
471 fn radical_variant_for_height(self, target_height: f32) -> Option<OpenTypeDelimiterVariant> {
472 self.stretchy_variant_for_height(RADICAL_GLYPH, target_height)
473 }
474
475 fn large_operator_variant_for_height(
476 self,
477 operator: &str,
478 target_height: f32,
479 ) -> Option<OpenTypeDelimiterVariant> {
480 let operator = single_char(operator)?;
481 is_large_operator_symbol(operator)
482 .then(|| self.stretchy_variant_for_height(operator, target_height))?
483 }
484
485 fn root_offset_x(self, index_width: f32) -> f32 {
486 self.font_constants()
487 .map(|constants| {
488 let before = constants
489 .radical_kern_before_degree(self.size)
490 .unwrap_or(0.0);
491 let after = constants
492 .radical_kern_after_degree(self.size)
493 .unwrap_or(0.0);
494 (before + index_width + after).max(index_width * 0.35)
495 })
496 .unwrap_or(index_width * 0.55)
497 }
498
499 fn root_index_shift(self, root_ascent: f32, index_descent: f32) -> f32 {
500 self.font_constants()
501 .and_then(|constants| constants.radical_degree_bottom_raise_fraction())
502 .map(|raise| -root_ascent * raise - index_descent)
503 .unwrap_or(-root_ascent * 0.52)
504 }
505
506 fn script_gap(self) -> f32 {
507 self.font_constants()
508 .and_then(|constants| constants.space_after_script(self.size))
509 .unwrap_or(self.size * 0.06)
510 }
511
512 fn superscript_shift(self, base_ascent: f32, sup_descent: f32) -> f32 {
513 let min_shift = self
514 .font_constants()
515 .and_then(|constants| constants.superscript_shift_up(self.size))
516 .unwrap_or(0.0);
517 let bottom_min = self
518 .font_constants()
519 .and_then(|constants| constants.superscript_bottom_min(self.size))
520 .unwrap_or(self.size * 0.18);
521 -(base_ascent * 0.58)
522 .max(min_shift)
523 .max(sup_descent + bottom_min)
524 }
525
526 fn subscript_shift(self, base_descent: f32, sub_ascent: f32) -> f32 {
527 let min_shift = self
528 .font_constants()
529 .and_then(|constants| constants.subscript_shift_down(self.size))
530 .unwrap_or(self.size * 0.28);
531 (base_descent + sub_ascent * 0.72).max(min_shift)
532 }
533
534 fn sub_superscript_gap(self) -> f32 {
535 self.font_constants()
536 .and_then(|constants| constants.sub_superscript_gap_min(self.size))
537 .unwrap_or(self.size * 0.08)
538 }
539
540 fn under_over_gap(self) -> f32 {
541 self.size * 0.12
542 }
543
544 fn upper_limit_gap(self) -> f32 {
545 self.font_constants()
546 .and_then(|constants| constants.upper_limit_gap_min(self.size))
547 .unwrap_or_else(|| self.under_over_gap())
548 }
549
550 fn upper_limit_baseline_rise(self) -> f32 {
551 self.font_constants()
552 .and_then(|constants| constants.upper_limit_baseline_rise_min(self.size))
553 .unwrap_or(self.size * 0.35)
554 }
555
556 fn lower_limit_gap(self) -> f32 {
557 self.font_constants()
558 .and_then(|constants| constants.lower_limit_gap_min(self.size))
559 .unwrap_or_else(|| self.under_over_gap())
560 }
561
562 fn lower_limit_baseline_drop(self) -> f32 {
563 self.font_constants()
564 .and_then(|constants| constants.lower_limit_baseline_drop_min(self.size))
565 .unwrap_or(self.size * 0.35)
566 }
567
568 fn accent_gap(self) -> f32 {
569 self.size * 0.06
570 }
571
572 fn table_col_gap(self, gap_em: Option<f32>) -> f32 {
573 self.size * gap_em.unwrap_or(TABLE_COL_GAP_EM)
574 }
575
576 fn table_row_gap(self, gap_em: Option<f32>) -> f32 {
577 self.size * gap_em.unwrap_or(TABLE_ROW_GAP_EM)
578 }
579
580 fn delimiter_gap(self) -> f32 {
581 self.size * 0.08
582 }
583
584 fn delimiter_overshoot(self) -> f32 {
585 (self.size * 0.08).max(self.rule_thickness()).max(
586 self.font_constants()
587 .and_then(|constants| constants.min_connector_overlap(self.size))
588 .unwrap_or(0.0),
589 )
590 }
591
592 fn delimited_sub_formula_min_height(self) -> f32 {
593 self.font_constants()
594 .and_then(|constants| constants.delimited_sub_formula_min_height(self.size))
595 .unwrap_or(self.size * 1.5)
596 }
597
598 fn should_stretch_delimiter(self, body: &MathLayout) -> bool {
599 body.height() + self.delimiter_overshoot() * 2.0 >= self.delimited_sub_formula_min_height()
600 }
601
602 fn delimiter_variant_for_height(
603 self,
604 delimiter: char,
605 target_height: f32,
606 ) -> Option<OpenTypeDelimiterVariant> {
607 self.stretchy_variant_for_height(delimiter, target_height)
608 }
609
610 fn stretchy_variant_for_height(
611 self,
612 glyph: char,
613 target_height: f32,
614 ) -> Option<OpenTypeDelimiterVariant> {
615 self.font_constants().and_then(|constants| {
616 constants.stretchy_variant_for_height(glyph, target_height, self.size)
617 })
618 }
619
620 fn delimiter_assembly_parts(
621 self,
622 delimiter: char,
623 ) -> Option<Vec<OpenTypeDelimiterAssemblyPart>> {
624 self.font_constants()
625 .and_then(|constants| constants.delimiter_assembly_parts(delimiter))
626 }
627
628 fn delimiter_width(self) -> f32 {
629 self.size * 0.42
630 }
631}
632
633#[derive(Clone, Debug)]
634struct OpenTypeMathConstants {
635 units_per_em: f32,
636 script_percent_scale_down: i16,
637 axis_height: i16,
638 subscript_shift_down: i16,
639 superscript_shift_up: i16,
640 superscript_bottom_min: i16,
641 sub_superscript_gap_min: i16,
642 space_after_script: i16,
643 upper_limit_gap_min: i16,
644 upper_limit_baseline_rise_min: i16,
645 lower_limit_gap_min: i16,
646 lower_limit_baseline_drop_min: i16,
647 fraction_numerator_shift_up: i16,
648 fraction_numerator_display_style_shift_up: i16,
649 fraction_denominator_shift_down: i16,
650 fraction_denominator_display_style_shift_down: i16,
651 fraction_rule_thickness: i16,
652 fraction_numerator_gap_min: i16,
653 fraction_num_display_style_gap_min: i16,
654 fraction_denominator_gap_min: i16,
655 fraction_denom_display_style_gap_min: i16,
656 radical_rule_thickness: i16,
657 radical_vertical_gap: i16,
658 radical_display_style_vertical_gap: i16,
659 radical_kern_before_degree: i16,
660 radical_kern_after_degree: i16,
661 radical_degree_bottom_raise_percent: i16,
662 delimited_sub_formula_min_height: u16,
663 min_connector_overlap: u16,
664 #[cfg_attr(not(test), allow(dead_code))]
665 delimiter_variants: Vec<OpenTypeDelimiterVariants>,
666}
667
668#[cfg_attr(not(test), allow(dead_code))]
669#[derive(Clone, Debug)]
670struct OpenTypeDelimiterVariants {
671 delimiter: char,
672 variants: Vec<OpenTypeDelimiterVariant>,
673 assembly_parts: Vec<OpenTypeDelimiterAssemblyPart>,
674}
675
676#[cfg_attr(not(test), allow(dead_code))]
677#[derive(Clone, Copy, Debug)]
678struct OpenTypeDelimiterVariant {
679 glyph_id: u16,
680 advance: u16,
681 horizontal_advance: u16,
682 bbox: Option<OpenTypeGlyphBBox>,
683}
684
685#[cfg_attr(not(test), allow(dead_code))]
686#[derive(Clone, Copy, Debug)]
687struct OpenTypeDelimiterAssemblyPart {
688 glyph_id: u16,
689 start_connector_length: u16,
690 end_connector_length: u16,
691 full_advance: u16,
692 horizontal_advance: u16,
693 bbox: Option<OpenTypeGlyphBBox>,
694 extender: bool,
695}
696
697#[derive(Clone, Copy, Debug)]
698struct OpenTypeGlyphBBox {
699 x_min: i16,
700 y_min: i16,
701 x_max: i16,
702 y_max: i16,
703}
704
705impl OpenTypeDelimiterVariants {
706 fn max_advance(&self) -> u16 {
707 self.variants
708 .iter()
709 .map(|variant| variant.advance)
710 .chain(self.assembly_parts.iter().map(|part| part.full_advance))
711 .max()
712 .unwrap_or(0)
713 }
714}
715
716impl OpenTypeMathConstants {
717 fn font_units(&self, value: i16, size: f32) -> Option<f32> {
718 (value > 0 && self.units_per_em > 0.0).then(|| value as f32 / self.units_per_em * size)
719 }
720
721 fn signed_font_units(&self, value: i16, size: f32) -> Option<f32> {
722 (value != 0 && self.units_per_em > 0.0).then(|| value as f32 / self.units_per_em * size)
723 }
724
725 fn script_scale(&self, size: f32) -> Option<f32> {
726 (self.script_percent_scale_down > 0)
727 .then(|| size * self.script_percent_scale_down as f32 / 100.0)
728 }
729
730 fn fraction_rule_thickness(&self, size: f32) -> Option<f32> {
731 self.font_units(self.fraction_rule_thickness, size)
732 }
733
734 fn axis_height(&self, size: f32) -> Option<f32> {
735 self.font_units(self.axis_height, size)
736 }
737
738 fn subscript_shift_down(&self, size: f32) -> Option<f32> {
739 self.font_units(self.subscript_shift_down, size)
740 }
741
742 fn superscript_shift_up(&self, size: f32) -> Option<f32> {
743 self.font_units(self.superscript_shift_up, size)
744 }
745
746 fn superscript_bottom_min(&self, size: f32) -> Option<f32> {
747 self.font_units(self.superscript_bottom_min, size)
748 }
749
750 fn sub_superscript_gap_min(&self, size: f32) -> Option<f32> {
751 self.font_units(self.sub_superscript_gap_min, size)
752 }
753
754 fn space_after_script(&self, size: f32) -> Option<f32> {
755 self.font_units(self.space_after_script, size)
756 }
757
758 fn upper_limit_gap_min(&self, size: f32) -> Option<f32> {
759 self.font_units(self.upper_limit_gap_min, size)
760 }
761
762 fn upper_limit_baseline_rise_min(&self, size: f32) -> Option<f32> {
763 self.font_units(self.upper_limit_baseline_rise_min, size)
764 }
765
766 fn lower_limit_gap_min(&self, size: f32) -> Option<f32> {
767 self.font_units(self.lower_limit_gap_min, size)
768 }
769
770 fn lower_limit_baseline_drop_min(&self, size: f32) -> Option<f32> {
771 self.font_units(self.lower_limit_baseline_drop_min, size)
772 }
773
774 fn fraction_numerator_shift(&self, size: f32, display: bool) -> Option<f32> {
775 let value = if display {
776 self.fraction_numerator_display_style_shift_up
777 } else {
778 self.fraction_numerator_shift_up
779 };
780 self.font_units(value, size)
781 }
782
783 fn fraction_denominator_shift(&self, size: f32, display: bool) -> Option<f32> {
784 let value = if display {
785 self.fraction_denominator_display_style_shift_down
786 } else {
787 self.fraction_denominator_shift_down
788 };
789 self.font_units(value, size)
790 }
791
792 fn fraction_numerator_gap(&self, size: f32, display: bool) -> Option<f32> {
793 let value = if display {
794 self.fraction_num_display_style_gap_min
795 } else {
796 self.fraction_numerator_gap_min
797 };
798 self.font_units(value, size)
799 }
800
801 fn fraction_denominator_gap(&self, size: f32, display: bool) -> Option<f32> {
802 let value = if display {
803 self.fraction_denom_display_style_gap_min
804 } else {
805 self.fraction_denominator_gap_min
806 };
807 self.font_units(value, size)
808 }
809
810 fn radical_rule_thickness(&self, size: f32) -> Option<f32> {
811 self.font_units(self.radical_rule_thickness, size)
812 }
813
814 fn radical_vertical_gap(&self, size: f32, display: bool) -> Option<f32> {
815 let value = if display {
816 self.radical_display_style_vertical_gap
817 } else {
818 self.radical_vertical_gap
819 };
820 self.font_units(value, size)
821 }
822
823 fn radical_kern_before_degree(&self, size: f32) -> Option<f32> {
824 self.signed_font_units(self.radical_kern_before_degree, size)
825 }
826
827 fn radical_kern_after_degree(&self, size: f32) -> Option<f32> {
828 self.signed_font_units(self.radical_kern_after_degree, size)
829 }
830
831 fn radical_degree_bottom_raise_fraction(&self) -> Option<f32> {
832 (self.radical_degree_bottom_raise_percent > 0)
833 .then(|| self.radical_degree_bottom_raise_percent as f32 / 100.0)
834 }
835
836 #[cfg_attr(not(test), allow(dead_code))]
837 fn delimiter_variant_count(&self, delimiter: char) -> usize {
838 self.delimiter_variants
839 .iter()
840 .find(|variants| variants.delimiter == delimiter)
841 .map(|variants| variants.variants.len())
842 .unwrap_or(0)
843 }
844
845 #[cfg_attr(not(test), allow(dead_code))]
846 fn delimiter_assembly_part_count(&self, delimiter: char) -> usize {
847 self.delimiter_variants
848 .iter()
849 .find(|variants| variants.delimiter == delimiter)
850 .map(|variants| variants.assembly_parts.len())
851 .unwrap_or(0)
852 }
853
854 #[cfg_attr(not(test), allow(dead_code))]
855 fn delimiter_max_advance(&self, delimiter: char, size: f32) -> Option<f32> {
856 let advance = self
857 .delimiter_variants
858 .iter()
859 .find(|variants| variants.delimiter == delimiter)?
860 .max_advance();
861 (advance > 0 && self.units_per_em > 0.0).then(|| advance as f32 / self.units_per_em * size)
862 }
863
864 #[cfg_attr(not(test), allow(dead_code))]
865 fn delimiter_extender_part_count(&self, delimiter: char) -> usize {
866 self.delimiter_variants
867 .iter()
868 .find(|variants| variants.delimiter == delimiter)
869 .map(|variants| {
870 variants
871 .assembly_parts
872 .iter()
873 .filter(|part| part.extender)
874 .count()
875 })
876 .unwrap_or(0)
877 }
878
879 fn stretchy_variant_for_height(
880 &self,
881 glyph: char,
882 target_height: f32,
883 size: f32,
884 ) -> Option<OpenTypeDelimiterVariant> {
885 let variants = self
886 .delimiter_variants
887 .iter()
888 .find(|variants| variants.delimiter == glyph)?;
889 variants.variants.iter().copied().find(|variant| {
890 self.units_per_em > 0.0
891 && variant.advance as f32 / self.units_per_em * size >= target_height
892 })
893 }
894
895 fn delimiter_assembly_parts(
896 &self,
897 delimiter: char,
898 ) -> Option<Vec<OpenTypeDelimiterAssemblyPart>> {
899 let variants = self
900 .delimiter_variants
901 .iter()
902 .find(|variants| variants.delimiter == delimiter)?;
903 (!variants.assembly_parts.is_empty()).then(|| variants.assembly_parts.clone())
904 }
905
906 #[cfg_attr(not(test), allow(dead_code))]
907 fn delimiter_first_variant_glyph_id(&self, delimiter: char) -> Option<u16> {
908 self.delimiter_variants
909 .iter()
910 .find(|variants| variants.delimiter == delimiter)?
911 .variants
912 .first()
913 .map(|variant| variant.glyph_id)
914 }
915
916 #[cfg_attr(not(test), allow(dead_code))]
917 fn delimiter_has_assembly_connectors(&self, delimiter: char) -> bool {
918 self.delimiter_variants
919 .iter()
920 .find(|variants| variants.delimiter == delimiter)
921 .is_some_and(|variants| {
922 variants.assembly_parts.iter().any(|part| {
923 part.glyph_id > 0
924 && (part.start_connector_length > 0 || part.end_connector_length > 0)
925 })
926 })
927 }
928
929 fn min_connector_overlap(&self, size: f32) -> Option<f32> {
930 (self.min_connector_overlap > 0 && self.units_per_em > 0.0)
931 .then(|| self.min_connector_overlap as f32 / self.units_per_em * size)
932 }
933
934 fn delimited_sub_formula_min_height(&self, size: f32) -> Option<f32> {
935 (self.delimited_sub_formula_min_height > 0 && self.units_per_em > 0.0)
936 .then(|| self.delimited_sub_formula_min_height as f32 / self.units_per_em * size)
937 }
938}
939
940fn open_type_math_constants() -> Option<OpenTypeMathConstants> {
941 #[cfg(feature = "symbols")]
942 {
943 static CONSTANTS: std::sync::OnceLock<Option<OpenTypeMathConstants>> =
944 std::sync::OnceLock::new();
945 CONSTANTS
946 .get_or_init(|| parse_open_type_math_constants(damascene_fonts::NOTO_SANS_MATH_REGULAR))
947 .clone()
948 }
949 #[cfg(not(feature = "symbols"))]
950 {
951 None
952 }
953}
954
955#[cfg(feature = "symbols")]
956fn parse_open_type_math_constants(font: &[u8]) -> Option<OpenTypeMathConstants> {
957 let face = ttf_parser::Face::parse(font, 0).ok()?;
958 let math = face.tables().math?;
959 let constants = math.constants?;
960 Some(OpenTypeMathConstants {
961 units_per_em: face.units_per_em() as f32,
962 script_percent_scale_down: constants.script_percent_scale_down(),
963 axis_height: constants.axis_height().value,
964 subscript_shift_down: constants.subscript_shift_down().value,
965 superscript_shift_up: constants.superscript_shift_up().value,
966 superscript_bottom_min: constants.superscript_bottom_min().value,
967 sub_superscript_gap_min: constants.sub_superscript_gap_min().value,
968 space_after_script: constants.space_after_script().value,
969 upper_limit_gap_min: constants.upper_limit_gap_min().value,
970 upper_limit_baseline_rise_min: constants.upper_limit_baseline_rise_min().value,
971 lower_limit_gap_min: constants.lower_limit_gap_min().value,
972 lower_limit_baseline_drop_min: constants.lower_limit_baseline_drop_min().value,
973 fraction_numerator_shift_up: constants.fraction_numerator_shift_up().value,
974 fraction_numerator_display_style_shift_up: constants
975 .fraction_numerator_display_style_shift_up()
976 .value,
977 fraction_denominator_shift_down: constants.fraction_denominator_shift_down().value,
978 fraction_denominator_display_style_shift_down: constants
979 .fraction_denominator_display_style_shift_down()
980 .value,
981 fraction_rule_thickness: constants.fraction_rule_thickness().value,
982 fraction_numerator_gap_min: constants.fraction_numerator_gap_min().value,
983 fraction_num_display_style_gap_min: constants.fraction_num_display_style_gap_min().value,
984 fraction_denominator_gap_min: constants.fraction_denominator_gap_min().value,
985 fraction_denom_display_style_gap_min: constants
986 .fraction_denom_display_style_gap_min()
987 .value,
988 radical_rule_thickness: constants.radical_rule_thickness().value,
989 radical_vertical_gap: constants.radical_vertical_gap().value,
990 radical_display_style_vertical_gap: constants.radical_display_style_vertical_gap().value,
991 radical_kern_before_degree: constants.radical_kern_before_degree().value,
992 radical_kern_after_degree: constants.radical_kern_after_degree().value,
993 radical_degree_bottom_raise_percent: constants.radical_degree_bottom_raise_percent(),
994 delimited_sub_formula_min_height: constants.delimited_sub_formula_min_height(),
995 min_connector_overlap: math
996 .variants
997 .map(|variants| variants.min_connector_overlap)
998 .unwrap_or(0),
999 delimiter_variants: parse_open_type_delimiter_variants(&face, math.variants),
1000 })
1001}
1002
1003#[cfg(feature = "symbols")]
1004fn parse_open_type_delimiter_variants(
1005 face: &ttf_parser::Face<'_>,
1006 variants: Option<ttf_parser::math::Variants<'_>>,
1007) -> Vec<OpenTypeDelimiterVariants> {
1008 let Some(variants) = variants else {
1009 return Vec::new();
1010 };
1011 STRETCHY_VARIANT_CHARS
1012 .into_iter()
1013 .filter_map(|delimiter| {
1014 let glyph = face.glyph_index(delimiter)?;
1015 let construction = variants.vertical_constructions.get(glyph)?;
1016 let glyph_variants = construction
1017 .variants
1018 .into_iter()
1019 .map(|variant| OpenTypeDelimiterVariant {
1020 glyph_id: variant.variant_glyph.0,
1021 advance: variant.advance_measurement,
1022 horizontal_advance: face.glyph_hor_advance(variant.variant_glyph).unwrap_or(0),
1023 bbox: face.glyph_bounding_box(variant.variant_glyph).map(|bbox| {
1024 OpenTypeGlyphBBox {
1025 x_min: bbox.x_min,
1026 y_min: bbox.y_min,
1027 x_max: bbox.x_max,
1028 y_max: bbox.y_max,
1029 }
1030 }),
1031 })
1032 .collect();
1033 let assembly_parts = construction
1034 .assembly
1035 .map(|assembly| {
1036 assembly
1037 .parts
1038 .into_iter()
1039 .map(|part| OpenTypeDelimiterAssemblyPart {
1040 glyph_id: part.glyph_id.0,
1041 start_connector_length: part.start_connector_length,
1042 end_connector_length: part.end_connector_length,
1043 full_advance: part.full_advance,
1044 horizontal_advance: face.glyph_hor_advance(part.glyph_id).unwrap_or(0),
1045 bbox: face.glyph_bounding_box(part.glyph_id).map(|bbox| {
1046 OpenTypeGlyphBBox {
1047 x_min: bbox.x_min,
1048 y_min: bbox.y_min,
1049 x_max: bbox.x_max,
1050 y_max: bbox.y_max,
1051 }
1052 }),
1053 extender: part.part_flags.extender(),
1054 })
1055 .collect()
1056 })
1057 .unwrap_or_default();
1058 Some(OpenTypeDelimiterVariants {
1059 delimiter,
1060 variants: glyph_variants,
1061 assembly_parts,
1062 })
1063 })
1064 .collect()
1065}
1066
1067pub fn layout_math(expr: &MathExpr, size: f32, display: MathDisplay) -> MathLayout {
1068 layout_expr(expr, LayoutCtx { size, display })
1069}
1070
1071fn layout_expr(expr: &MathExpr, ctx: LayoutCtx) -> MathLayout {
1072 let metrics = ctx.metrics();
1073 match expr {
1074 MathExpr::Source { body, .. } => layout_expr(body, ctx),
1075 MathExpr::Row(children) => layout_row(children, ctx),
1076 MathExpr::Identifier(s) => layout_glyph(s, ctx, FontWeight::Regular, true),
1077 MathExpr::Number(s) => layout_glyph(s, ctx, FontWeight::Regular, false),
1078 MathExpr::Operator(s) => layout_operator(s, ctx),
1079 MathExpr::OperatorWithMetadata {
1080 text,
1081 lspace,
1082 rspace,
1083 large_operator,
1084 ..
1085 } => layout_operator_with_spacing(text, *lspace, *rspace, *large_operator, ctx),
1086 MathExpr::Text(s) => layout_glyph(s, ctx, FontWeight::Regular, false),
1087 MathExpr::Space(em) => MathLayout {
1088 width: metrics.space_width(*em),
1089 ascent: metrics.default_ascent(),
1090 descent: metrics.default_descent(),
1091 atoms: Vec::new(),
1092 },
1093 MathExpr::Fraction {
1094 numerator,
1095 denominator,
1096 } => layout_fraction(numerator, denominator, ctx),
1097 MathExpr::Sqrt(child) => layout_sqrt(child, ctx),
1098 MathExpr::Root { base, index } => layout_root(base, index, ctx),
1099 MathExpr::Scripts { base, sub, sup } => {
1100 layout_scripts(base, sub.as_deref(), sup.as_deref(), ctx)
1101 }
1102 MathExpr::UnderOver { base, under, over } => {
1103 layout_under_over(base, under.as_deref(), over.as_deref(), ctx)
1104 }
1105 MathExpr::Accent {
1106 base,
1107 accent,
1108 stretch,
1109 } => layout_accent(base, accent, *stretch, ctx),
1110 MathExpr::Fenced { open, close, body } => layout_fenced(open, close, body, ctx),
1111 MathExpr::Table {
1112 rows,
1113 column_alignments,
1114 column_gap,
1115 row_gap,
1116 } => layout_table(rows, column_alignments, *column_gap, *row_gap, ctx),
1117 MathExpr::Error(s) => layout_glyph(s, ctx, FontWeight::Regular, false),
1118 }
1119}
1120
1121fn layout_row(children: &[MathExpr], ctx: LayoutCtx) -> MathLayout {
1122 let mut width = 0.0;
1123 let metrics = ctx.metrics();
1124 let mut ascent: f32 = metrics.default_ascent();
1125 let mut descent: f32 = metrics.default_descent();
1126 let mut atoms = Vec::new();
1127 for child in children {
1128 let child_layout = layout_expr(child, ctx);
1129 translate_atoms(&mut atoms, child_layout.atoms, width, 0.0);
1130 width += child_layout.width;
1131 ascent = ascent.max(child_layout.ascent);
1132 descent = descent.max(child_layout.descent);
1133 }
1134 MathLayout {
1135 width,
1136 ascent,
1137 descent,
1138 atoms,
1139 }
1140}
1141
1142fn layout_glyph(s: &str, ctx: LayoutCtx, weight: FontWeight, italic: bool) -> MathLayout {
1143 if s.is_empty() {
1144 return MathLayout {
1145 width: 0.0,
1146 ascent: 0.0,
1147 descent: 0.0,
1148 atoms: Vec::new(),
1149 };
1150 }
1151 let measured = text_metrics::measure_text(s, ctx.size, weight, false, TextWrap::NoWrap, None);
1152 MathLayout {
1153 width: measured.width,
1154 ascent: ctx.metrics().glyph_ascent(),
1155 descent: ctx.metrics().glyph_descent(),
1156 atoms: vec![MathAtom::Glyph {
1157 text: s.to_string(),
1158 x: 0.0,
1159 y_baseline: 0.0,
1160 size: ctx.size,
1161 weight,
1162 italic,
1163 }],
1164 }
1165}
1166
1167fn layout_operator(s: &str, ctx: LayoutCtx) -> MathLayout {
1168 layout_operator_with_spacing(s, None, None, None, ctx)
1169}
1170
1171fn layout_operator_with_spacing(
1172 s: &str,
1173 lspace: Option<f32>,
1174 rspace: Option<f32>,
1175 large_operator: Option<bool>,
1176 ctx: LayoutCtx,
1177) -> MathLayout {
1178 let use_large_operator = large_operator.unwrap_or_else(|| is_large_operator_symbol_str(s));
1179 let glyph_ctx = if matches!(ctx.display, MathDisplay::Block) && use_large_operator {
1180 ctx.large_operator()
1181 } else {
1182 ctx
1183 };
1184 if matches!(ctx.display, MathDisplay::Block) && use_large_operator {
1185 let operator = MathExpr::OperatorWithMetadata {
1186 text: s.into(),
1187 lspace,
1188 rspace,
1189 large_operator: Some(true),
1190 movable_limits: None,
1191 };
1192 if let Some(layout) = layout_large_operator_variant(&operator, glyph_ctx) {
1193 return layout;
1194 }
1195 }
1196 layout_operator_glyph_with_spacing(s, lspace, rspace, glyph_ctx)
1197}
1198
1199fn layout_operator_glyph_with_spacing(
1200 s: &str,
1201 lspace: Option<f32>,
1202 rspace: Option<f32>,
1203 ctx: LayoutCtx,
1204) -> MathLayout {
1205 let mut layout = layout_glyph(s, ctx, FontWeight::Regular, false);
1206 let (lspace, rspace) = ctx
1207 .metrics()
1208 .operator_spacing_with_overrides(s, lspace, rspace);
1209 if lspace > 0.0 || rspace > 0.0 {
1210 for atom in &mut layout.atoms {
1211 if let MathAtom::Glyph { x, .. } = atom {
1212 *x += lspace;
1213 }
1214 }
1215 layout.width += lspace + rspace;
1216 }
1217 layout
1218}
1219
1220fn layout_operator_expr_glyph_fallback(expr: &MathExpr, ctx: LayoutCtx) -> Option<MathLayout> {
1221 match expr.without_source() {
1222 MathExpr::Operator(s) => Some(layout_operator_glyph_with_spacing(s, None, None, ctx)),
1223 MathExpr::OperatorWithMetadata {
1224 text,
1225 lspace,
1226 rspace,
1227 ..
1228 } => Some(layout_operator_glyph_with_spacing(
1229 text, *lspace, *rspace, ctx,
1230 )),
1231 _ => None,
1232 }
1233}
1234
1235fn layout_fraction(numerator: &MathExpr, denominator: &MathExpr, ctx: LayoutCtx) -> MathLayout {
1236 let metrics = ctx.metrics();
1237 let child_ctx = if matches!(ctx.display, MathDisplay::Block) {
1238 ctx
1239 } else {
1240 ctx.script()
1241 };
1242 let num = layout_expr(numerator, child_ctx);
1243 let den = layout_expr(denominator, child_ctx);
1244 let pad = metrics.fraction_pad();
1245 let num_gap = metrics.fraction_numerator_gap();
1246 let den_gap = metrics.fraction_denominator_gap();
1247 let rule = metrics.rule_thickness();
1248 let axis_shift = metrics.math_axis_shift();
1252 let rule_center_y = -axis_shift;
1253 let width = num.width.max(den.width) + pad * 2.0;
1254 let num_x = (width - num.width) * 0.5;
1255 let den_x = (width - den.width) * 0.5;
1256 let num_dy = (rule_center_y - num_gap - rule * 0.5 - num.descent)
1257 .min(-metrics.fraction_numerator_shift());
1258 let den_dy = (rule_center_y + den_gap + rule * 0.5 + den.ascent)
1259 .max(metrics.fraction_denominator_shift());
1260 let ascent = -num_dy + num.ascent;
1261 let descent = den_dy + den.descent;
1262 let mut atoms = Vec::new();
1263 translate_atoms(&mut atoms, num.atoms, num_x, num_dy);
1264 atoms.push(MathAtom::Rule {
1265 rect: Rect::new(0.0, rule_center_y - rule * 0.5, width, rule),
1266 });
1267 translate_atoms(&mut atoms, den.atoms, den_x, den_dy);
1268 MathLayout {
1269 width,
1270 ascent,
1271 descent,
1272 atoms,
1273 }
1274}
1275
1276fn layout_sqrt(child: &MathExpr, ctx: LayoutCtx) -> MathLayout {
1277 let metrics = ctx.metrics();
1278 let inner = layout_expr(child, ctx);
1279 let gap = metrics.sqrt_gap();
1280 let rule = metrics.radical_rule_thickness();
1281 if let Some(layout) = layout_open_type_sqrt(inner.clone(), gap, rule, ctx) {
1282 return layout;
1283 }
1284 layout_vector_sqrt(inner, gap, rule, ctx)
1285}
1286
1287fn layout_vector_sqrt(inner: MathLayout, gap: f32, rule: f32, ctx: LayoutCtx) -> MathLayout {
1288 let metrics = ctx.metrics();
1289 let radical_w = metrics.radical_width();
1290 let inner_x = radical_w + gap;
1291 let bar_y = -inner.ascent - gap - rule * 0.5;
1292 let tick_y = metrics.radical_tick_y(inner.descent);
1293 let end_x = inner_x + inner.width;
1294 let mut atoms = Vec::new();
1295 atoms.push(MathAtom::Radical {
1296 points: [
1297 [0.0, metrics.radical_left_flair_y()],
1298 [metrics.radical_hook_x(), metrics.radical_hook_y()],
1299 [metrics.radical_tick_x(), tick_y],
1300 [radical_w, bar_y],
1301 [end_x, bar_y],
1302 ],
1303 thickness: rule,
1304 });
1305 translate_atoms(&mut atoms, inner.atoms, inner_x, 0.0);
1306 MathLayout {
1307 width: end_x,
1308 ascent: -bar_y + rule * 0.5,
1309 descent: tick_y + rule * 0.5,
1310 atoms,
1311 }
1312}
1313
1314fn layout_open_type_sqrt(
1315 inner: MathLayout,
1316 gap: f32,
1317 rule: f32,
1318 ctx: LayoutCtx,
1319) -> Option<MathLayout> {
1320 let metrics = ctx.metrics();
1321 let bar_y = -inner.ascent - gap - rule * 0.5;
1322 let tick_y = metrics.radical_tick_y(inner.descent);
1323 let target_height = tick_y - bar_y + rule;
1324 let variant = metrics.radical_variant_for_height(target_height)?;
1325 let bbox = variant.bbox?;
1326 let constants = metrics.font_constants()?;
1327 let scale = metrics.size / constants.units_per_em;
1328 let view_box = glyph_advance_view_box(bbox, variant.horizontal_advance, None)?;
1329 if view_box.w <= 0.0 || view_box.h <= 0.0 {
1330 return None;
1331 }
1332 let radical_w = view_box.w * scale;
1333 let radical_h = view_box.h * scale;
1334 let radical_rect = Rect::new(0.0, bar_y - rule * 0.5, radical_w, radical_h);
1335 let inner_x = radical_w + gap;
1336 let end_x = inner_x + inner.width;
1337 let overbar_x = (radical_w - rule * 0.5).max(0.0);
1338 let mut atoms = Vec::new();
1339 atoms.push(MathAtom::GlyphId {
1340 glyph_id: variant.glyph_id,
1341 rect: radical_rect,
1342 view_box,
1343 });
1344 atoms.push(MathAtom::Rule {
1345 rect: Rect::new(
1346 overbar_x,
1347 bar_y - rule * 0.5,
1348 (end_x - overbar_x).max(rule),
1349 rule,
1350 ),
1351 });
1352 translate_atoms(&mut atoms, inner.atoms, inner_x, 0.0);
1353 Some(MathLayout {
1354 width: end_x,
1355 ascent: (-bar_y + rule * 0.5).max(-radical_rect.y),
1356 descent: (tick_y + rule * 0.5).max(radical_rect.y + radical_rect.h),
1357 atoms,
1358 })
1359}
1360
1361fn layout_root(base: &MathExpr, index: &MathExpr, ctx: LayoutCtx) -> MathLayout {
1362 let metrics = ctx.metrics();
1363 let root = layout_sqrt(base, ctx);
1364 let index = layout_expr(index, ctx.script());
1365 let root_x = metrics.root_offset_x(index.width);
1366 let index_dy = metrics.root_index_shift(root.ascent, index.descent);
1367 let mut atoms = Vec::new();
1368 translate_atoms(&mut atoms, index.atoms, 0.0, index_dy);
1369 translate_atoms(&mut atoms, root.atoms, root_x, 0.0);
1370 MathLayout {
1371 width: root_x + root.width,
1372 ascent: root.ascent.max(-index_dy + index.ascent),
1373 descent: root.descent.max(index_dy + index.descent),
1374 atoms,
1375 }
1376}
1377
1378fn layout_scripts(
1379 base: &MathExpr,
1380 sub: Option<&MathExpr>,
1381 sup: Option<&MathExpr>,
1382 ctx: LayoutCtx,
1383) -> MathLayout {
1384 if matches!(ctx.display, MathDisplay::Block) && is_display_limits_base(base) {
1385 return layout_under_over(base, sub, sup, ctx);
1386 }
1387 let display_large_operator =
1388 matches!(ctx.display, MathDisplay::Block) && is_large_operator_base(base);
1389 let base_ctx = if display_large_operator {
1390 ctx.large_operator()
1391 } else {
1392 ctx
1393 };
1394 let base_layout = if display_large_operator {
1395 layout_large_operator_variant(base, base_ctx)
1396 .or_else(|| layout_operator_expr_glyph_fallback(base, base_ctx))
1397 .unwrap_or_else(|| layout_expr(base, ctx))
1398 } else {
1399 layout_expr(base, base_ctx)
1400 };
1401 let script_ctx = ctx.script();
1402 let sub_layout = sub.map(|expr| layout_expr(expr, script_ctx));
1403 let sup_layout = sup.map(|expr| layout_expr(expr, script_ctx));
1404 let metrics = ctx.metrics();
1405 let script_gap = metrics.script_gap();
1406 let script_x = base_layout.width + script_gap;
1407 let sup_dy = sup_layout
1408 .as_ref()
1409 .map(|sup| metrics.superscript_shift(base_layout.ascent, sup.descent))
1410 .unwrap_or(0.0);
1411 let mut sub_dy = sub_layout
1412 .as_ref()
1413 .map(|sub| metrics.subscript_shift(base_layout.descent, sub.ascent))
1414 .unwrap_or(0.0);
1415 if let (Some(sub), Some(sup)) = (&sub_layout, &sup_layout) {
1416 let sup_bottom = sup_dy + sup.descent;
1417 let sub_top = sub_dy - sub.ascent;
1418 let gap = sub_top - sup_bottom;
1419 let min_gap = metrics.sub_superscript_gap();
1420 if gap < min_gap {
1421 sub_dy += min_gap - gap;
1422 }
1423 }
1424 let mut atoms = Vec::new();
1425 translate_atoms(&mut atoms, base_layout.atoms, 0.0, 0.0);
1426 let mut script_width: f32 = 0.0;
1427 let mut ascent = base_layout.ascent;
1428 let mut descent = base_layout.descent;
1429 if let Some(sup) = sup_layout {
1430 script_width = script_width.max(sup.width);
1431 ascent = ascent.max(-sup_dy + sup.ascent);
1432 translate_atoms(&mut atoms, sup.atoms, script_x, sup_dy);
1433 }
1434 if let Some(sub) = sub_layout {
1435 script_width = script_width.max(sub.width);
1436 descent = descent.max(sub_dy + sub.descent);
1437 translate_atoms(&mut atoms, sub.atoms, script_x, sub_dy);
1438 }
1439 MathLayout {
1440 width: base_layout.width + script_gap + script_width,
1441 ascent,
1442 descent,
1443 atoms,
1444 }
1445}
1446
1447fn layout_under_over(
1448 base: &MathExpr,
1449 under: Option<&MathExpr>,
1450 over: Option<&MathExpr>,
1451 ctx: LayoutCtx,
1452) -> MathLayout {
1453 let center_large_operator =
1454 matches!(ctx.display, MathDisplay::Block) && is_large_operator_base(base);
1455 let base_ctx = if center_large_operator {
1456 ctx.large_operator()
1457 } else {
1458 ctx
1459 };
1460 let base_layout = if center_large_operator {
1461 layout_large_operator_variant(base, base_ctx)
1462 .or_else(|| layout_operator_expr_glyph_fallback(base, base_ctx))
1463 .unwrap_or_else(|| layout_expr(base, ctx))
1464 } else {
1465 layout_expr(base, base_ctx)
1466 };
1467 let script_ctx = ctx.script();
1468 let under_layout = under.map(|expr| layout_expr(expr, script_ctx));
1469 let over_layout = over.map(|expr| layout_expr(expr, script_ctx));
1470 let metrics = ctx.metrics();
1471 let width = base_layout
1472 .width
1473 .max(under_layout.as_ref().map(|l| l.width).unwrap_or(0.0))
1474 .max(over_layout.as_ref().map(|l| l.width).unwrap_or(0.0));
1475 let base_x = (width - base_layout.width) * 0.5;
1476 let base_dy = if center_large_operator {
1477 base_ctx.metrics().math_axis_shift() - ctx.metrics().math_axis_shift()
1478 } else {
1479 0.0
1480 };
1481 let base_top = -base_layout.ascent + base_dy;
1482 let base_bottom = base_layout.descent + base_dy;
1483 let mut atoms = Vec::new();
1484 let mut ascent = -base_top;
1485 let mut descent = base_bottom;
1486 translate_atoms(&mut atoms, base_layout.atoms, base_x, base_dy);
1487 if let Some(over) = over_layout {
1488 let over_x = (width - over.width) * 0.5;
1489 let over_dy = (base_top - metrics.upper_limit_gap() - over.descent)
1490 .min(base_dy - metrics.upper_limit_baseline_rise());
1491 ascent = ascent.max(-over_dy + over.ascent);
1492 translate_atoms(&mut atoms, over.atoms, over_x, over_dy);
1493 }
1494 if let Some(under) = under_layout {
1495 let under_x = (width - under.width) * 0.5;
1496 let under_dy = (base_bottom + metrics.lower_limit_gap() + under.ascent)
1497 .max(base_dy + metrics.lower_limit_baseline_drop());
1498 descent = descent.max(under_dy + under.descent);
1499 translate_atoms(&mut atoms, under.atoms, under_x, under_dy);
1500 }
1501 MathLayout {
1502 width,
1503 ascent,
1504 descent,
1505 atoms,
1506 }
1507}
1508
1509fn layout_accent(base: &MathExpr, accent: &MathExpr, stretch: bool, ctx: LayoutCtx) -> MathLayout {
1510 let base_layout = layout_expr(base, ctx);
1511 if stretch && is_overline_accent(accent) {
1512 return layout_overline(base_layout, ctx);
1513 }
1514
1515 let accent_layout = layout_accent_mark(accent, ctx.script());
1516 let metrics = ctx.metrics();
1517 let gap = metrics.accent_gap();
1518 let width = base_layout.width.max(accent_layout.width);
1519 let base_x = (width - base_layout.width) * 0.5;
1520 let accent_x = (width - accent_layout.width) * 0.5;
1521 let accent_dy = -base_layout.ascent - gap - accent_layout.descent;
1522 let mut atoms = Vec::new();
1523 translate_atoms(&mut atoms, base_layout.atoms, base_x, 0.0);
1524 translate_atoms(&mut atoms, accent_layout.atoms, accent_x, accent_dy);
1525 MathLayout {
1526 width,
1527 ascent: base_layout.ascent.max(-accent_dy + accent_layout.ascent),
1528 descent: base_layout.descent,
1529 atoms,
1530 }
1531}
1532
1533fn layout_overline(base_layout: MathLayout, ctx: LayoutCtx) -> MathLayout {
1534 let metrics = ctx.metrics();
1535 let rule = metrics.rule_thickness();
1536 let gap = metrics.accent_gap();
1537 let rule_y = -base_layout.ascent - gap - rule;
1538 let mut atoms = Vec::new();
1539 translate_atoms(&mut atoms, base_layout.atoms, 0.0, 0.0);
1540 atoms.push(MathAtom::Rule {
1541 rect: Rect::new(0.0, rule_y, base_layout.width.max(rule), rule),
1542 });
1543 MathLayout {
1544 width: base_layout.width,
1545 ascent: (-rule_y).max(base_layout.ascent),
1546 descent: base_layout.descent,
1547 atoms,
1548 }
1549}
1550
1551fn is_overline_accent(expr: &MathExpr) -> bool {
1552 matches!(
1553 expr.without_source(),
1554 MathExpr::Operator(s) | MathExpr::Text(s) | MathExpr::Identifier(s)
1555 if matches!(s.as_str(), "¯" | "‾")
1556 )
1557}
1558
1559fn layout_accent_mark(accent: &MathExpr, ctx: LayoutCtx) -> MathLayout {
1560 match accent.without_source() {
1561 MathExpr::Operator(s) if s == "^" => layout_operator("ˆ", ctx),
1562 MathExpr::Operator(s) if s == "~" => layout_operator("˜", ctx),
1563 _ => layout_expr(accent, ctx),
1564 }
1565}
1566
1567fn is_display_limits_base(expr: &MathExpr) -> bool {
1568 match expr.without_source() {
1569 MathExpr::Operator(_) | MathExpr::OperatorWithMetadata { .. } => has_movable_limits(expr),
1570 MathExpr::Text(s) => matches!(
1571 s.as_str(),
1572 "lim" | "max" | "min" | "sup" | "inf" | "det" | "gcd" | "Pr" | "lim inf" | "lim sup"
1573 ),
1574 _ => false,
1575 }
1576}
1577
1578fn has_movable_limits(expr: &MathExpr) -> bool {
1579 match expr.without_source() {
1580 MathExpr::Operator(s) => operator_info(s).movable_limits,
1581 MathExpr::OperatorWithMetadata {
1582 text,
1583 movable_limits,
1584 ..
1585 } => movable_limits.unwrap_or_else(|| operator_info(text).movable_limits),
1586 _ => false,
1587 }
1588}
1589
1590fn is_large_operator_base(expr: &MathExpr) -> bool {
1591 match expr.without_source() {
1592 MathExpr::Operator(s) => is_large_operator_symbol_str(s),
1593 MathExpr::OperatorWithMetadata {
1594 text,
1595 large_operator,
1596 ..
1597 } => large_operator.unwrap_or_else(|| operator_info(text).large_operator),
1598 _ => false,
1599 }
1600}
1601
1602fn is_large_operator_symbol_str(s: &str) -> bool {
1603 operator_info(s).large_operator
1604}
1605
1606fn is_large_operator_symbol(ch: char) -> bool {
1607 operator_info(&ch.to_string()).large_operator
1608}
1609
1610fn layout_large_operator_variant(expr: &MathExpr, ctx: LayoutCtx) -> Option<MathLayout> {
1611 let (operator, lspace_override, rspace_override) = match expr.without_source() {
1612 MathExpr::Operator(operator) => (operator.as_str(), None, None),
1613 MathExpr::OperatorWithMetadata {
1614 text,
1615 lspace,
1616 rspace,
1617 ..
1618 } => (text.as_str(), *lspace, *rspace),
1619 _ => return None,
1620 };
1621 let metrics = ctx.metrics();
1622 let variant = metrics.large_operator_variant_for_height(operator, ctx.size)?;
1623 let bbox = variant.bbox?;
1624 let constants = metrics.font_constants()?;
1625 let scale = metrics.size / constants.units_per_em;
1626 let view_box = glyph_advance_view_box(bbox, variant.horizontal_advance, None)?;
1627 let glyph_width = view_box.w * scale;
1628 let glyph_height = view_box.h * scale;
1629 if glyph_width <= 0.0 || glyph_height <= 0.0 {
1630 return None;
1631 }
1632 let width = (variant.horizontal_advance as f32 * scale).max(glyph_width);
1633 let target_center_y = -metrics.math_axis_shift();
1634 let glyph_center_y = view_box.y * scale + glyph_height * 0.5;
1635 let glyph_y = target_center_y - glyph_center_y;
1636 let (lspace, rspace) =
1637 metrics.operator_spacing_with_overrides(operator, lspace_override, rspace_override);
1638 let rect = Rect::new(
1639 lspace + (width - glyph_width) * 0.5,
1640 glyph_y + view_box.y * scale,
1641 glyph_width,
1642 glyph_height,
1643 );
1644 Some(MathLayout {
1645 width: width + lspace + rspace,
1646 ascent: -rect.y,
1647 descent: rect.y + rect.h,
1648 atoms: vec![MathAtom::GlyphId {
1649 glyph_id: variant.glyph_id,
1650 rect,
1651 view_box,
1652 }],
1653 })
1654}
1655
1656fn single_char(s: &str) -> Option<char> {
1657 let mut chars = s.chars();
1658 let ch = chars.next()?;
1659 chars.next().is_none().then_some(ch)
1660}
1661
1662fn layout_fenced(
1663 open: &Option<String>,
1664 close: &Option<String>,
1665 body: &MathExpr,
1666 ctx: LayoutCtx,
1667) -> MathLayout {
1668 let body_layout = layout_expr(body, ctx);
1669 let delimiter_rect = delimiter_rect(&body_layout, ctx);
1670 let metrics = ctx.metrics();
1671 let gap = metrics.delimiter_gap();
1672 let stretch_delimiters = metrics.should_stretch_delimiter(&body_layout);
1673 let open_layout = open
1674 .as_deref()
1675 .map(|delimiter| layout_delimiter(delimiter, delimiter_rect, stretch_delimiters, ctx));
1676 let close_layout = close
1677 .as_deref()
1678 .map(|delimiter| layout_delimiter(delimiter, delimiter_rect, stretch_delimiters, ctx));
1679 let open_width = open_layout
1680 .as_ref()
1681 .map(|layout| layout.width + gap)
1682 .unwrap_or(0.0);
1683 let close_width = close_layout
1684 .as_ref()
1685 .map(|layout| layout.width + gap)
1686 .unwrap_or(0.0);
1687 let delimiter_ascent = open_layout
1688 .as_ref()
1689 .into_iter()
1690 .chain(close_layout.as_ref())
1691 .map(|layout| layout.ascent)
1692 .fold(0.0, f32::max);
1693 let delimiter_descent = open_layout
1694 .as_ref()
1695 .into_iter()
1696 .chain(close_layout.as_ref())
1697 .map(|layout| layout.descent)
1698 .fold(0.0, f32::max);
1699 let mut atoms = Vec::new();
1700 if let Some(open) = open_layout {
1701 translate_atoms(&mut atoms, open.atoms, 0.0, 0.0);
1702 }
1703 translate_atoms(&mut atoms, body_layout.atoms, open_width, 0.0);
1704 if let Some(close) = close_layout {
1705 translate_atoms(
1706 &mut atoms,
1707 close.atoms,
1708 open_width + body_layout.width + gap,
1709 0.0,
1710 );
1711 }
1712 MathLayout {
1713 width: open_width + body_layout.width + close_width,
1714 ascent: body_layout.ascent.max(delimiter_ascent),
1715 descent: body_layout.descent.max(delimiter_descent),
1716 atoms,
1717 }
1718}
1719
1720fn delimiter_rect(body: &MathLayout, ctx: LayoutCtx) -> Rect {
1721 let metrics = ctx.metrics();
1722 let overshoot = metrics.delimiter_overshoot();
1723 let top = -body.ascent - overshoot;
1724 let bottom = body.descent + overshoot;
1725 Rect::new(0.0, top, metrics.delimiter_width(), bottom - top)
1726}
1727
1728fn layout_delimiter(delimiter: &str, rect: Rect, stretch: bool, ctx: LayoutCtx) -> MathLayout {
1729 if !stretch || !is_vector_delimiter(delimiter) {
1730 return layout_glyph(delimiter, ctx, FontWeight::Regular, false);
1731 }
1732 if let Some(delimiter) = delimiter
1733 .chars()
1734 .next()
1735 .filter(|_| delimiter.chars().count() == 1)
1736 && let Some(variant) = ctx
1737 .metrics()
1738 .delimiter_variant_for_height(delimiter, rect.h)
1739 && let Some(layout) = layout_delimiter_variant(variant, rect, ctx)
1740 {
1741 return layout;
1742 }
1743 if let Some(delimiter) = delimiter
1744 .chars()
1745 .next()
1746 .filter(|_| delimiter.chars().count() == 1)
1747 && let Some(parts) = ctx.metrics().delimiter_assembly_parts(delimiter)
1748 && let Some(layout) = layout_delimiter_assembly(&parts, rect, ctx)
1749 {
1750 return layout;
1751 }
1752 MathLayout {
1753 width: rect.w,
1754 ascent: -rect.y,
1755 descent: rect.y + rect.h,
1756 atoms: vec![MathAtom::Delimiter {
1757 delimiter: delimiter.to_string(),
1758 rect,
1759 thickness: ctx.metrics().rule_thickness(),
1760 }],
1761 }
1762}
1763
1764fn is_vector_delimiter(delimiter: &str) -> bool {
1765 matches!(
1766 delimiter,
1767 "(" | ")" | "[" | "]" | "{" | "}" | "|" | "‖" | "⟨" | "⟩" | "⌊" | "⌋" | "⌈" | "⌉"
1768 )
1769}
1770
1771fn layout_delimiter_variant(
1772 variant: OpenTypeDelimiterVariant,
1773 target_rect: Rect,
1774 ctx: LayoutCtx,
1775) -> Option<MathLayout> {
1776 let bbox = variant.bbox?;
1777 let metrics = ctx.metrics();
1778 let constants = metrics.font_constants()?;
1779 let scale = metrics.size / constants.units_per_em;
1780 let width = (variant.horizontal_advance as f32 * scale).max(target_rect.w);
1781 let view_box = glyph_advance_view_box(bbox, variant.horizontal_advance, None)?;
1782 let glyph_height = view_box.h * scale;
1783 if view_box.w <= 0.0 || glyph_height <= 0.0 {
1784 return None;
1785 }
1786 let target_center_y = target_rect.y + target_rect.h * 0.5;
1787 let glyph_center_y = view_box.y * scale + glyph_height * 0.5;
1788 let glyph_y = target_center_y - glyph_center_y;
1789 let rect = Rect::new(
1790 (width - view_box.w * scale) * 0.5,
1791 glyph_y + view_box.y * scale,
1792 view_box.w * scale,
1793 glyph_height,
1794 );
1795 Some(MathLayout {
1796 width,
1797 ascent: (-rect.y).max(-target_rect.y),
1798 descent: (rect.y + rect.h).max(target_rect.y + target_rect.h),
1799 atoms: vec![MathAtom::GlyphId {
1800 glyph_id: variant.glyph_id,
1801 rect,
1802 view_box,
1803 }],
1804 })
1805}
1806
1807fn layout_delimiter_assembly(
1808 parts: &[OpenTypeDelimiterAssemblyPart],
1809 target_rect: Rect,
1810 ctx: LayoutCtx,
1811) -> Option<MathLayout> {
1812 let metrics = ctx.metrics();
1813 let constants = metrics.font_constants()?;
1814 if constants.units_per_em <= 0.0 {
1815 return None;
1816 }
1817 let scale = metrics.size / constants.units_per_em;
1818 let overlap_units = constants.min_connector_overlap.max(1);
1819 let target_units = target_rect.h / scale;
1820 let source_parts: Vec<OpenTypeDelimiterAssemblyPart> = parts.iter().rev().copied().collect();
1821 let mut assembly = source_parts.clone();
1822 let extender_parts: Vec<OpenTypeDelimiterAssemblyPart> = source_parts
1823 .iter()
1824 .copied()
1825 .filter(|part| part.extender)
1826 .collect();
1827 if extender_parts.is_empty() {
1828 return None;
1829 }
1830
1831 let mut extra_repeats = 0;
1832 while assembly_max_length_units(&assembly, overlap_units) < target_units {
1833 extra_repeats += 1;
1834 assembly = Vec::with_capacity(source_parts.len() + extra_repeats * extender_parts.len());
1835 for part in &source_parts {
1836 assembly.push(*part);
1837 if part.extender {
1838 assembly.extend(std::iter::repeat_n(*part, extra_repeats));
1839 }
1840 }
1841 }
1842
1843 let overlaps = assembly_overlaps_for_target(&assembly, target_units, overlap_units);
1844 let total_units = assembly_raw_advance_units(&assembly) - overlaps.iter().sum::<f32>();
1845 let total_height = total_units * scale;
1846 let target_center_y = target_rect.y + target_rect.h * 0.5;
1847 let top = target_center_y - total_height * 0.5;
1848 let width = assembly
1849 .iter()
1850 .filter_map(|part| {
1851 let bbox = part.bbox?;
1852 Some(
1853 (part.horizontal_advance as f32 * scale)
1854 .max((bbox.x_max - bbox.x_min) as f32 * scale),
1855 )
1856 })
1857 .fold(target_rect.w, f32::max);
1858
1859 let mut cursor_units = 0.0;
1860 let mut atoms = Vec::with_capacity(assembly.len());
1861 for (index, part) in assembly.iter().enumerate() {
1862 let bbox = part.bbox?;
1863 let slot_height = part.full_advance as f32 * scale;
1864 let view_box =
1865 glyph_advance_view_box(bbox, part.horizontal_advance, Some(part.full_advance))?;
1866 let glyph_width = view_box.w * scale;
1867 let glyph_height = view_box.h * scale;
1868 if glyph_width <= 0.0 || glyph_height <= 0.0 || slot_height <= 0.0 {
1869 return None;
1870 }
1871 let rect = Rect::new(
1872 (width - glyph_width) * 0.5,
1873 top + cursor_units * scale,
1874 glyph_width,
1875 slot_height.max(glyph_height),
1876 );
1877 atoms.push(MathAtom::GlyphId {
1878 glyph_id: part.glyph_id,
1879 rect,
1880 view_box,
1881 });
1882 if index + 1 < assembly.len() {
1883 cursor_units += part.full_advance as f32 - overlaps[index];
1884 }
1885 }
1886
1887 Some(MathLayout {
1888 width,
1889 ascent: (-top).max(-target_rect.y),
1890 descent: (top + total_height).max(target_rect.y + target_rect.h),
1891 atoms,
1892 })
1893}
1894
1895fn glyph_advance_view_box(
1896 bbox: OpenTypeGlyphBBox,
1897 horizontal_advance: u16,
1898 vertical_advance: Option<u16>,
1899) -> Option<Rect> {
1900 let x = (bbox.x_min as f32).min(0.0);
1901 let width = (horizontal_advance as f32)
1902 .max(bbox.x_max as f32 - x)
1903 .max((bbox.x_max - bbox.x_min) as f32);
1904 let y = -(bbox.y_max as f32);
1905 let height = vertical_advance
1906 .map(f32::from)
1907 .unwrap_or((bbox.y_max - bbox.y_min) as f32)
1908 .max((bbox.y_max - bbox.y_min) as f32);
1909 (width > 0.0 && height > 0.0).then(|| Rect::new(x, y, width, height))
1910}
1911
1912fn assembly_raw_advance_units(parts: &[OpenTypeDelimiterAssemblyPart]) -> f32 {
1913 parts.iter().map(|part| part.full_advance as f32).sum()
1914}
1915
1916fn assembly_max_length_units(parts: &[OpenTypeDelimiterAssemblyPart], min_overlap: u16) -> f32 {
1917 assembly_raw_advance_units(parts)
1918 - assembly_overlap_limits(parts, min_overlap)
1919 .iter()
1920 .map(|(min, _)| *min)
1921 .sum::<f32>()
1922}
1923
1924fn assembly_overlap_limits(
1925 parts: &[OpenTypeDelimiterAssemblyPart],
1926 min_overlap: u16,
1927) -> Vec<(f32, f32)> {
1928 parts
1929 .windows(2)
1930 .map(|pair| {
1931 let min = min_overlap as f32;
1932 let max = pair[0]
1933 .end_connector_length
1934 .min(pair[1].start_connector_length)
1935 .max(min_overlap) as f32;
1936 (min, max)
1937 })
1938 .collect()
1939}
1940
1941fn assembly_overlaps_for_target(
1942 parts: &[OpenTypeDelimiterAssemblyPart],
1943 target_units: f32,
1944 min_overlap: u16,
1945) -> Vec<f32> {
1946 let limits = assembly_overlap_limits(parts, min_overlap);
1947 if limits.is_empty() {
1948 return Vec::new();
1949 }
1950 let raw = assembly_raw_advance_units(parts);
1951 let min_sum: f32 = limits.iter().map(|(min, _)| *min).sum();
1952 let max_sum: f32 = limits.iter().map(|(_, max)| *max).sum();
1953 let desired_sum = (raw - target_units).clamp(min_sum, max_sum);
1954 let mut overlaps: Vec<f32> = limits.iter().map(|(min, _)| *min).collect();
1955 let mut remaining = desired_sum - min_sum;
1956
1957 while remaining > 0.001 {
1958 let adjustable: Vec<usize> = overlaps
1959 .iter()
1960 .zip(limits.iter())
1961 .enumerate()
1962 .filter_map(|(index, (overlap, (_, max)))| (*overlap < *max - 0.001).then_some(index))
1963 .collect();
1964 if adjustable.is_empty() {
1965 break;
1966 }
1967 let share = remaining / adjustable.len() as f32;
1968 let mut distributed = 0.0;
1969 for index in adjustable {
1970 let capacity = limits[index].1 - overlaps[index];
1971 let add = share.min(capacity);
1972 overlaps[index] += add;
1973 distributed += add;
1974 }
1975 if distributed <= 0.001 {
1976 break;
1977 }
1978 remaining -= distributed;
1979 }
1980
1981 overlaps
1982}
1983
1984fn layout_table(
1985 rows: &[Vec<MathExpr>],
1986 column_alignments: &[MathColumnAlignment],
1987 column_gap: Option<f32>,
1988 row_gap: Option<f32>,
1989 ctx: LayoutCtx,
1990) -> MathLayout {
1991 if rows.is_empty() {
1992 return MathLayout {
1993 width: 0.0,
1994 ascent: 0.0,
1995 descent: 0.0,
1996 atoms: Vec::new(),
1997 };
1998 }
1999 let cell_layouts: Vec<Vec<MathLayout>> = rows
2000 .iter()
2001 .map(|row| row.iter().map(|cell| layout_expr(cell, ctx)).collect())
2002 .collect();
2003 let metrics = ctx.metrics();
2004 let col_count = cell_layouts.iter().map(Vec::len).max().unwrap_or(0);
2005 let mut col_widths = vec![0.0_f32; col_count];
2006 let mut row_ascents = vec![metrics.default_ascent(); rows.len()];
2007 let mut row_descents = vec![metrics.default_descent(); rows.len()];
2008 for (row_index, row) in cell_layouts.iter().enumerate() {
2009 for (col_index, cell) in row.iter().enumerate() {
2010 col_widths[col_index] = col_widths[col_index].max(cell.width);
2011 row_ascents[row_index] = row_ascents[row_index].max(cell.ascent);
2012 row_descents[row_index] = row_descents[row_index].max(cell.descent);
2013 }
2014 }
2015 let col_gap = metrics.table_col_gap(column_gap);
2016 let row_gap = metrics.table_row_gap(row_gap);
2017 let width = col_widths.iter().sum::<f32>() + col_gap * col_count.saturating_sub(1) as f32;
2018 let row_heights: Vec<f32> = row_ascents
2019 .iter()
2020 .zip(row_descents.iter())
2021 .map(|(ascent, descent)| ascent + descent)
2022 .collect();
2023 let height = row_heights.iter().sum::<f32>() + row_gap * rows.len().saturating_sub(1) as f32;
2024 let baseline_origin = height * 0.5 + metrics.math_axis_shift();
2025 let mut atoms = Vec::new();
2026 let mut row_top = 0.0;
2027 for (row_index, row) in cell_layouts.into_iter().enumerate() {
2028 let row_baseline = row_top + row_ascents[row_index];
2029 let mut col_left = 0.0;
2030 for (col_index, cell) in row.into_iter().enumerate() {
2031 let col_extra = col_widths[col_index] - cell.width;
2032 let align = column_alignments
2033 .get(col_index)
2034 .copied()
2035 .unwrap_or_default();
2036 let cell_x = col_left
2037 + match align {
2038 MathColumnAlignment::Left => 0.0,
2039 MathColumnAlignment::Center => col_extra * 0.5,
2040 MathColumnAlignment::Right => col_extra,
2041 };
2042 translate_atoms(
2043 &mut atoms,
2044 cell.atoms,
2045 cell_x,
2046 row_baseline - baseline_origin,
2047 );
2048 col_left += col_widths[col_index] + col_gap;
2049 }
2050 row_top += row_heights[row_index] + row_gap;
2051 }
2052 MathLayout {
2053 width,
2054 ascent: baseline_origin,
2055 descent: height - baseline_origin,
2056 atoms,
2057 }
2058}
2059
2060fn translate_atoms(out: &mut Vec<MathAtom>, atoms: Vec<MathAtom>, dx: f32, dy: f32) {
2061 out.extend(atoms.into_iter().map(|atom| match atom {
2062 MathAtom::Glyph {
2063 text,
2064 x,
2065 y_baseline,
2066 size,
2067 weight,
2068 italic,
2069 } => MathAtom::Glyph {
2070 text,
2071 x: x + dx,
2072 y_baseline: y_baseline + dy,
2073 size,
2074 weight,
2075 italic,
2076 },
2077 MathAtom::GlyphId {
2078 glyph_id,
2079 rect,
2080 view_box,
2081 } => MathAtom::GlyphId {
2082 glyph_id,
2083 rect: Rect::new(rect.x + dx, rect.y + dy, rect.w, rect.h),
2084 view_box,
2085 },
2086 MathAtom::Rule { rect } => MathAtom::Rule {
2087 rect: Rect::new(rect.x + dx, rect.y + dy, rect.w, rect.h),
2088 },
2089 MathAtom::Radical { points, thickness } => MathAtom::Radical {
2090 points: points.map(|[x, y]| [x + dx, y + dy]),
2091 thickness,
2092 },
2093 MathAtom::Delimiter {
2094 delimiter,
2095 rect,
2096 thickness,
2097 } => MathAtom::Delimiter {
2098 delimiter,
2099 rect: Rect::new(rect.x + dx, rect.y + dy, rect.w, rect.h),
2100 thickness,
2101 },
2102 }));
2103}
2104
2105pub fn parse_tex(input: &str) -> Result<MathExpr, MathParseError> {
2106 let mut parser = TexParser::new(input);
2107 let expr = parser.parse_row(None)?;
2108 parser.skip_ws();
2109 if parser.peek().is_some() {
2110 return Err(parser.error("unexpected trailing input"));
2111 }
2112 Ok(expr)
2113}
2114
2115pub fn parse_tex_with_source_ranges(input: &str) -> Result<MathExpr, MathParseError> {
2116 let mut parser = TexParser::with_source_ranges(input);
2117 let expr = parser.parse_row(None)?;
2118 parser.skip_ws();
2119 if parser.peek().is_some() {
2120 return Err(parser.error("unexpected trailing input"));
2121 }
2122 Ok(expr)
2123}
2124
2125pub fn parse_mathml(input: &str) -> Result<MathExpr, MathParseError> {
2126 Ok(parse_mathml_with_display(input)?.0)
2127}
2128
2129pub fn parse_mathml_with_display(input: &str) -> Result<(MathExpr, MathDisplay), MathParseError> {
2130 let doc = roxmltree::Document::parse(input).map_err(|err| {
2131 let pos = err.pos();
2132 MathParseError {
2133 message: err.to_string(),
2134 byte: text_pos_to_byte(input, pos.row, pos.col),
2135 }
2136 })?;
2137 let root = doc.root_element();
2138 let display = match root.attribute("display") {
2139 Some("block") => MathDisplay::Block,
2140 _ => MathDisplay::Inline,
2141 };
2142 let expr = parse_mathml_node(root)?;
2143 Ok((expr, display))
2144}
2145
2146#[derive(Clone, Debug, PartialEq, Eq)]
2147pub struct MathParseError {
2148 pub message: String,
2149 pub byte: usize,
2150}
2151
2152fn parse_mathml_node(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2153 let name = node.tag_name().name();
2154 match name {
2155 "math" | "mrow" => Ok(MathExpr::row(parse_mathml_children(node)?)),
2156 "mi" => Ok(MathExpr::Identifier(normalized_node_text(node))),
2157 "mn" => Ok(MathExpr::Number(normalized_node_text(node))),
2158 "mo" => parse_mathml_operator(node),
2159 "mtext" => Ok(MathExpr::Text(normalized_node_text(node))),
2160 "mspace" => Ok(MathExpr::Space(parse_mathml_space(node))),
2161 "mfrac" => {
2162 let children = mathml_element_children(node);
2163 require_mathml_arity(node, &children, 2)?;
2164 Ok(MathExpr::Fraction {
2165 numerator: Arc::new(parse_mathml_node(children[0])?),
2166 denominator: Arc::new(parse_mathml_node(children[1])?),
2167 })
2168 }
2169 "msqrt" => Ok(MathExpr::Sqrt(Arc::new(MathExpr::row(
2170 parse_mathml_children(node)?,
2171 )))),
2172 "mroot" => {
2173 let children = mathml_element_children(node);
2174 require_mathml_arity(node, &children, 2)?;
2175 Ok(MathExpr::Root {
2176 base: Arc::new(parse_mathml_node(children[0])?),
2177 index: Arc::new(parse_mathml_node(children[1])?),
2178 })
2179 }
2180 "msub" => parse_mathml_scripts(node, true, false),
2181 "msup" => parse_mathml_scripts(node, false, true),
2182 "msubsup" => parse_mathml_scripts(node, true, true),
2183 "munder" => parse_mathml_under_over(node, true, false),
2184 "mover" if mathml_bool_attr(node.attribute("accent")) => parse_mathml_accent(node),
2185 "mover" => parse_mathml_under_over(node, false, true),
2186 "munderover" => parse_mathml_under_over(node, true, true),
2187 "mfenced" => parse_mathml_fenced(node),
2188 "semantics" => parse_mathml_semantics(node),
2189 "mtable" => parse_mathml_table(node),
2190 "mtr" => Ok(MathExpr::row(
2191 mathml_element_children(node)
2192 .into_iter()
2193 .map(parse_mathml_node)
2194 .collect::<Result<Vec<_>, _>>()?,
2195 )),
2196 "mtd" => Ok(MathExpr::row(parse_mathml_children(node)?)),
2197 unsupported => Ok(MathExpr::Error(format!(
2198 "unsupported MathML element <{unsupported}>"
2199 ))),
2200 }
2201}
2202
2203fn parse_mathml_children(node: roxmltree::Node<'_, '_>) -> Result<Vec<MathExpr>, MathParseError> {
2204 mathml_element_children(node)
2205 .into_iter()
2206 .map(parse_mathml_node)
2207 .collect()
2208}
2209
2210fn parse_mathml_operator(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2211 let operator = normalized_node_text(node);
2212 let lspace = node.attribute("lspace").and_then(parse_em_length);
2213 let rspace = node.attribute("rspace").and_then(parse_em_length);
2214 let large_operator = node.attribute("largeop").map(mathml_bool_attr_value);
2215 let movable_limits = node.attribute("movablelimits").map(mathml_bool_attr_value);
2216 if lspace.is_none() && rspace.is_none() && large_operator.is_none() && movable_limits.is_none()
2217 {
2218 return Ok(MathExpr::Operator(operator));
2219 }
2220 Ok(MathExpr::OperatorWithMetadata {
2221 text: operator,
2222 lspace,
2223 rspace,
2224 large_operator,
2225 movable_limits,
2226 })
2227}
2228
2229fn parse_mathml_semantics(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2230 let children = mathml_element_children(node);
2231 let Some(presentation) = children
2232 .into_iter()
2233 .find(|child| !matches!(child.tag_name().name(), "annotation" | "annotation-xml"))
2234 else {
2235 return Err(mathml_error_at(
2236 node,
2237 "<semantics> expected a presentation child".to_string(),
2238 ));
2239 };
2240 parse_mathml_node(presentation)
2241}
2242
2243fn mathml_element_children<'a, 'input>(
2244 node: roxmltree::Node<'a, 'input>,
2245) -> Vec<roxmltree::Node<'a, 'input>> {
2246 node.children()
2247 .filter(roxmltree::Node::is_element)
2248 .collect()
2249}
2250
2251fn require_mathml_arity(
2252 node: roxmltree::Node<'_, '_>,
2253 children: &[roxmltree::Node<'_, '_>],
2254 expected: usize,
2255) -> Result<(), MathParseError> {
2256 if children.len() == expected {
2257 Ok(())
2258 } else {
2259 Err(mathml_error_at(
2260 node,
2261 format!(
2262 "<{}> expected {expected} element children, got {}",
2263 node.tag_name().name(),
2264 children.len()
2265 ),
2266 ))
2267 }
2268}
2269
2270fn parse_mathml_scripts(
2271 node: roxmltree::Node<'_, '_>,
2272 has_sub: bool,
2273 has_sup: bool,
2274) -> Result<MathExpr, MathParseError> {
2275 let children = mathml_element_children(node);
2276 let expected = 1 + usize::from(has_sub) + usize::from(has_sup);
2277 require_mathml_arity(node, &children, expected)?;
2278 let base = Arc::new(parse_mathml_node(children[0])?);
2279 let sub = has_sub.then(|| {
2280 let index = 1;
2281 parse_mathml_node(children[index]).map(Arc::new)
2282 });
2283 let sup = has_sup.then(|| {
2284 let index = if has_sub { 2 } else { 1 };
2285 parse_mathml_node(children[index]).map(Arc::new)
2286 });
2287 Ok(MathExpr::Scripts {
2288 base,
2289 sub: sub.transpose()?,
2290 sup: sup.transpose()?,
2291 })
2292}
2293
2294fn parse_mathml_under_over(
2295 node: roxmltree::Node<'_, '_>,
2296 has_under: bool,
2297 has_over: bool,
2298) -> Result<MathExpr, MathParseError> {
2299 let children = mathml_element_children(node);
2300 let expected = 1 + usize::from(has_under) + usize::from(has_over);
2301 require_mathml_arity(node, &children, expected)?;
2302 let base = Arc::new(parse_mathml_node(children[0])?);
2303 let under = has_under.then(|| {
2304 let index = 1;
2305 parse_mathml_node(children[index]).map(Arc::new)
2306 });
2307 let over = has_over.then(|| {
2308 let index = if has_under { 2 } else { 1 };
2309 parse_mathml_node(children[index]).map(Arc::new)
2310 });
2311 Ok(MathExpr::UnderOver {
2312 base,
2313 under: under.transpose()?,
2314 over: over.transpose()?,
2315 })
2316}
2317
2318fn parse_mathml_accent(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2319 let children = mathml_element_children(node);
2320 require_mathml_arity(node, &children, 2)?;
2321 let accent = parse_mathml_node(children[1])?;
2322 let stretch =
2323 mathml_bool_attr(children[1].attribute("stretchy")) || is_overline_accent(&accent);
2324 Ok(MathExpr::Accent {
2325 base: Arc::new(parse_mathml_node(children[0])?),
2326 accent: Arc::new(accent),
2327 stretch,
2328 })
2329}
2330
2331fn mathml_bool_attr(value: Option<&str>) -> bool {
2332 value.is_some_and(mathml_bool_attr_value)
2333}
2334
2335fn mathml_bool_attr_value(value: &str) -> bool {
2336 matches!(value.trim(), "true" | "1")
2337}
2338
2339fn parse_mathml_table(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2340 let mut rows = Vec::new();
2341 for row_node in mathml_element_children(node) {
2342 if !matches!(row_node.tag_name().name(), "mtr" | "mlabeledtr") {
2343 return Err(mathml_error_at(
2344 row_node,
2345 format!(
2346 "<mtable> expected row element children, got <{}>",
2347 row_node.tag_name().name()
2348 ),
2349 ));
2350 }
2351 let mut row = Vec::new();
2352 for cell_node in mathml_element_children(row_node) {
2353 require_mathml_tag(cell_node, "mtd")?;
2354 row.push(MathExpr::row(parse_mathml_children(cell_node)?));
2355 }
2356 rows.push(row);
2357 }
2358 let column_alignments = parse_mathml_column_alignments(node.attribute("columnalign"))?;
2359 let column_gap = parse_mathml_table_spacing(node.attribute("columnspacing"))?;
2360 let row_gap = parse_mathml_table_spacing(node.attribute("rowspacing"))?;
2361 Ok(MathExpr::Table {
2362 rows,
2363 column_alignments,
2364 column_gap,
2365 row_gap,
2366 })
2367}
2368
2369fn parse_mathml_column_alignments(
2370 value: Option<&str>,
2371) -> Result<Vec<MathColumnAlignment>, MathParseError> {
2372 let Some(value) = value else {
2373 return Ok(Vec::new());
2374 };
2375 value
2376 .split_whitespace()
2377 .map(|token| match token {
2378 "left" => Ok(MathColumnAlignment::Left),
2379 "center" => Ok(MathColumnAlignment::Center),
2380 "right" => Ok(MathColumnAlignment::Right),
2381 "decimal" => Ok(MathColumnAlignment::Right),
2382 other => Err(MathParseError {
2383 message: format!("unsupported MathML columnalign value {other:?}"),
2384 byte: 0,
2385 }),
2386 })
2387 .collect()
2388}
2389
2390fn parse_mathml_table_spacing(value: Option<&str>) -> Result<Option<f32>, MathParseError> {
2391 let Some(value) = value else {
2392 return Ok(None);
2393 };
2394 let Some(first) = value.split_whitespace().next() else {
2395 return Ok(None);
2396 };
2397 parse_mathml_em_length(first).map(Some)
2398}
2399
2400fn parse_mathml_em_length(value: &str) -> Result<f32, MathParseError> {
2401 let number = value.strip_suffix("em").unwrap_or(value);
2402 let parsed = number.parse::<f32>().map_err(|_| MathParseError {
2403 message: format!("unsupported MathML table spacing value {value:?}"),
2404 byte: 0,
2405 })?;
2406 if parsed.is_sign_negative() {
2407 return Err(MathParseError {
2408 message: format!("negative MathML table spacing value {value:?}"),
2409 byte: 0,
2410 });
2411 }
2412 Ok(parsed)
2413}
2414
2415fn parse_mathml_fenced(node: roxmltree::Node<'_, '_>) -> Result<MathExpr, MathParseError> {
2416 let open = parse_fence_attr(node.attribute("open").unwrap_or("("));
2417 let close = parse_fence_attr(node.attribute("close").unwrap_or(")"));
2418 let separator = match node.attribute("separators") {
2419 Some(value) => value
2420 .chars()
2421 .find(|ch| !ch.is_whitespace())
2422 .map(|ch| ch.to_string()),
2423 None => Some(",".to_string()),
2424 };
2425 let children = parse_mathml_children(node)?;
2426 let mut body = Vec::new();
2427 for (index, child) in children.into_iter().enumerate() {
2428 if index > 0
2429 && let Some(separator) = &separator
2430 {
2431 body.push(MathExpr::Operator(separator.clone()));
2432 }
2433 body.push(child);
2434 }
2435 Ok(MathExpr::Fenced {
2436 open,
2437 close,
2438 body: Arc::new(MathExpr::row(body)),
2439 })
2440}
2441
2442fn parse_fence_attr(value: &str) -> Option<String> {
2443 let value = value.trim();
2444 if value.is_empty() || value == "." {
2445 None
2446 } else {
2447 Some(value.to_string())
2448 }
2449}
2450
2451fn require_mathml_tag(node: roxmltree::Node<'_, '_>, expected: &str) -> Result<(), MathParseError> {
2452 if node.tag_name().name() == expected {
2453 Ok(())
2454 } else {
2455 Err(mathml_error_at(
2456 node,
2457 format!(
2458 "expected <{expected}> element, got <{}>",
2459 node.tag_name().name()
2460 ),
2461 ))
2462 }
2463}
2464
2465fn normalized_node_text(node: roxmltree::Node<'_, '_>) -> String {
2466 node.descendants()
2467 .filter(roxmltree::Node::is_text)
2468 .filter_map(|n| n.text())
2469 .collect::<String>()
2470 .split_whitespace()
2471 .collect::<Vec<_>>()
2472 .join(" ")
2473}
2474
2475fn parse_mathml_space(node: roxmltree::Node<'_, '_>) -> f32 {
2476 node.attribute("width")
2477 .and_then(parse_em_length)
2478 .unwrap_or(0.3)
2479}
2480
2481fn parse_em_length(s: &str) -> Option<f32> {
2482 let trimmed = s.trim();
2483 if let Some(number) = trimmed.strip_suffix("em") {
2484 return number.trim().parse().ok();
2485 }
2486 if let Some(number) = trimmed.strip_suffix("px") {
2487 return number.trim().parse::<f32>().ok().map(|px| px / 16.0);
2488 }
2489 trimmed.parse().ok()
2490}
2491
2492fn mathml_error_at(node: roxmltree::Node<'_, '_>, message: String) -> MathParseError {
2493 MathParseError {
2494 message,
2495 byte: node.range().start,
2496 }
2497}
2498
2499fn text_pos_to_byte(input: &str, row: u32, col: u32) -> usize {
2500 let mut current_row = 1;
2501 let mut current_col = 1;
2502 for (byte, ch) in input.char_indices() {
2503 if current_row == row && current_col == col {
2504 return byte;
2505 }
2506 if ch == '\n' {
2507 current_row += 1;
2508 current_col = 1;
2509 } else {
2510 current_col += 1;
2511 }
2512 }
2513 input.len()
2514}
2515
2516struct TexParser<'a> {
2517 input: &'a str,
2518 pos: usize,
2519 source_ranges: bool,
2520}
2521
2522impl<'a> TexParser<'a> {
2523 fn new(input: &'a str) -> Self {
2524 Self {
2525 input,
2526 pos: 0,
2527 source_ranges: false,
2528 }
2529 }
2530
2531 fn with_source_ranges(input: &'a str) -> Self {
2532 Self {
2533 input,
2534 pos: 0,
2535 source_ranges: true,
2536 }
2537 }
2538
2539 fn source_wrap(&self, start: usize, expr: MathExpr) -> MathExpr {
2540 if self.source_ranges {
2541 MathExpr::Source {
2542 source: start..self.pos,
2543 body: Arc::new(expr),
2544 }
2545 } else {
2546 expr
2547 }
2548 }
2549
2550 fn parse_row(&mut self, until: Option<char>) -> Result<MathExpr, MathParseError> {
2551 let start = self.pos;
2552 let mut items = Vec::new();
2553 loop {
2554 self.skip_ws();
2555 if self.starts_with_command("right") {
2556 return Err(self.error("unexpected \\right"));
2557 }
2558 match self.peek() {
2559 None => {
2560 if until.is_some() {
2561 return Err(self.error("unclosed group"));
2562 }
2563 break;
2564 }
2565 Some(ch) if Some(ch) == until => {
2566 self.bump();
2567 break;
2568 }
2569 Some('}') => return Err(self.error("unexpected closing brace")),
2570 _ => {
2571 let atom = self.parse_atom_with_scripts()?;
2572 items.push(atom);
2573 }
2574 }
2575 }
2576 let expr = MathExpr::row(items);
2577 Ok(self.source_wrap(start, expr))
2578 }
2579
2580 fn parse_row_until_right(&mut self) -> Result<MathExpr, MathParseError> {
2581 let start = self.pos;
2582 let mut items = Vec::new();
2583 loop {
2584 self.skip_ws();
2585 if self.peek().is_none() {
2586 return Err(self.error("unclosed \\left"));
2587 }
2588 if self.starts_with_command("right") {
2589 break;
2590 }
2591 if self.peek() == Some('}') {
2592 return Err(self.error("unexpected closing brace"));
2593 }
2594 let atom = self.parse_atom_with_scripts()?;
2595 items.push(atom);
2596 }
2597 let expr = MathExpr::row(items);
2598 Ok(self.source_wrap(start, expr))
2599 }
2600
2601 fn parse_table_environment(
2602 &mut self,
2603 env: &str,
2604 column_alignments: Vec<MathColumnAlignment>,
2605 column_gap: Option<f32>,
2606 row_gap: Option<f32>,
2607 ) -> Result<MathExpr, MathParseError> {
2608 let mut rows = Vec::new();
2609 let mut row = Vec::new();
2610 let mut cell = Vec::new();
2611
2612 loop {
2613 self.skip_ws();
2614 if self.peek().is_none() {
2615 return Err(self.error(&format!("unclosed \\begin{{{env}}}")));
2616 }
2617 if self.starts_with_command("end") {
2618 self.consume_environment_end(env)?;
2619 if !row.is_empty() || !cell.is_empty() || rows.is_empty() {
2620 row.push(MathExpr::row(std::mem::take(&mut cell)));
2621 rows.push(row);
2622 }
2623 break;
2624 }
2625 if self.peek() == Some('&') {
2626 self.bump();
2627 row.push(MathExpr::row(std::mem::take(&mut cell)));
2628 continue;
2629 }
2630 if self.starts_with_row_separator() {
2631 self.consume_row_separator()?;
2632 row.push(MathExpr::row(std::mem::take(&mut cell)));
2633 rows.push(std::mem::take(&mut row));
2634 continue;
2635 }
2636
2637 cell.push(self.parse_atom_with_scripts()?);
2638 }
2639
2640 self.validate_tex_table_shape(env, &rows, &column_alignments)?;
2641
2642 let table = MathExpr::Table {
2643 rows,
2644 column_alignments,
2645 column_gap,
2646 row_gap,
2647 };
2648 Ok(match env {
2649 "matrix" | "array" | "aligned" | "align" => table,
2650 "pmatrix" => MathExpr::Fenced {
2651 open: Some("(".into()),
2652 close: Some(")".into()),
2653 body: Arc::new(table),
2654 },
2655 "bmatrix" => MathExpr::Fenced {
2656 open: Some("[".into()),
2657 close: Some("]".into()),
2658 body: Arc::new(table),
2659 },
2660 "Bmatrix" => MathExpr::Fenced {
2661 open: Some("{".into()),
2662 close: Some("}".into()),
2663 body: Arc::new(table),
2664 },
2665 "vmatrix" => MathExpr::Fenced {
2666 open: Some("|".into()),
2667 close: Some("|".into()),
2668 body: Arc::new(table),
2669 },
2670 "Vmatrix" => MathExpr::Fenced {
2671 open: Some("‖".into()),
2672 close: Some("‖".into()),
2673 body: Arc::new(table),
2674 },
2675 "cases" => MathExpr::Fenced {
2676 open: Some("{".into()),
2677 close: None,
2678 body: Arc::new(table),
2679 },
2680 _ => return Err(self.error(&format!("unsupported math environment {env}"))),
2681 })
2682 }
2683
2684 fn validate_tex_table_shape(
2685 &self,
2686 env: &str,
2687 rows: &[Vec<MathExpr>],
2688 column_alignments: &[MathColumnAlignment],
2689 ) -> Result<(), MathParseError> {
2690 let Some(first_row) = rows.first() else {
2691 return Ok(());
2692 };
2693 let expected_cols = first_row.len();
2694 for (row_index, row) in rows.iter().enumerate().skip(1) {
2695 if row.len() != expected_cols {
2696 return Err(self.error(&format!(
2697 "inconsistent column count in {env}: row {} has {}, expected {expected_cols}",
2698 row_index + 1,
2699 row.len()
2700 )));
2701 }
2702 }
2703 if !column_alignments.is_empty() && column_alignments.len() != expected_cols {
2704 return Err(self.error(&format!(
2705 "{env} alignment spec has {} columns, but table has {expected_cols}",
2706 column_alignments.len()
2707 )));
2708 }
2709 Ok(())
2710 }
2711
2712 fn parse_atom_with_scripts(&mut self) -> Result<MathExpr, MathParseError> {
2713 let start = self.pos;
2714 let mut base = self.parse_atom()?;
2715 let mut sub = None;
2716 let mut sup = None;
2717 loop {
2718 self.skip_ws();
2719 match self.peek() {
2720 Some('_') => {
2721 self.bump();
2722 sub = Some(Arc::new(self.parse_script_arg()?));
2723 }
2724 Some('^') => {
2725 self.bump();
2726 sup = Some(Arc::new(self.parse_script_arg()?));
2727 }
2728 _ => break,
2729 }
2730 }
2731 if sub.is_some() || sup.is_some() {
2732 base = MathExpr::Scripts {
2733 base: Arc::new(base),
2734 sub,
2735 sup,
2736 };
2737 base = self.source_wrap(start, base);
2738 }
2739 Ok(base)
2740 }
2741
2742 fn parse_script_arg(&mut self) -> Result<MathExpr, MathParseError> {
2743 self.skip_ws();
2744 if self.peek() == Some('{') {
2745 self.bump();
2746 self.parse_row(Some('}'))
2747 } else {
2748 self.parse_atom()
2749 }
2750 }
2751
2752 fn parse_atom(&mut self) -> Result<MathExpr, MathParseError> {
2753 self.skip_ws();
2754 let start = self.pos;
2755 match self.peek() {
2756 Some('{') => {
2757 self.bump();
2758 let expr = self.parse_row(Some('}'))?;
2759 Ok(self.source_wrap(start, expr))
2760 }
2761 Some('\\') => self.parse_command(),
2762 Some(ch) if ch.is_ascii_digit() => {
2763 let text = self.take_while(|c| c.is_ascii_digit() || c == '.');
2764 Ok(self.source_wrap(start, MathExpr::Number(text)))
2765 }
2766 Some(ch) if ch.is_alphabetic() => {
2767 self.bump();
2768 Ok(self.source_wrap(start, MathExpr::Identifier(ch.to_string())))
2769 }
2770 Some(ch) => {
2771 self.bump();
2772 let expr = if ch.is_whitespace() {
2773 MathExpr::Space(0.3)
2774 } else {
2775 MathExpr::Operator(ch.to_string())
2776 };
2777 Ok(self.source_wrap(start, expr))
2778 }
2779 None => Err(self.error("expected math atom")),
2780 }
2781 }
2782
2783 fn parse_command(&mut self) -> Result<MathExpr, MathParseError> {
2784 let start = self.pos;
2785 let expr = self.parse_command_unwrapped()?;
2786 Ok(self.source_wrap(start, expr))
2787 }
2788
2789 fn parse_command_unwrapped(&mut self) -> Result<MathExpr, MathParseError> {
2790 self.expect('\\')?;
2791 let name = self.take_while(|c| c.is_ascii_alphabetic());
2792 if name.is_empty() {
2793 let escaped = self
2794 .bump()
2795 .ok_or_else(|| self.error("expected escaped character"))?;
2796 return Ok(match escaped {
2797 ',' => MathExpr::Space(THIN_MATH_SPACE_EM),
2798 ':' => MathExpr::Space(MEDIUM_MATH_SPACE_EM),
2799 ';' => MathExpr::Space(THICK_MATH_SPACE_EM),
2800 '!' => MathExpr::Space(-THIN_MATH_SPACE_EM),
2801 ' ' => MathExpr::Space(MEDIUM_MATH_SPACE_EM),
2802 _ => MathExpr::Operator(escaped.to_string()),
2803 });
2804 }
2805 match name.as_str() {
2806 "frac" | "tfrac" | "dfrac" => {
2807 let numerator = Arc::new(self.parse_required_group()?);
2808 let denominator = Arc::new(self.parse_required_group()?);
2809 Ok(MathExpr::Fraction {
2810 numerator,
2811 denominator,
2812 })
2813 }
2814 "binom" => {
2815 let numerator = self.parse_required_group()?;
2816 let denominator = self.parse_required_group()?;
2817 Ok(MathExpr::Fenced {
2818 open: Some("(".into()),
2819 close: Some(")".into()),
2820 body: Arc::new(MathExpr::Table {
2821 rows: vec![vec![numerator], vec![denominator]],
2822 column_alignments: Vec::new(),
2823 column_gap: None,
2824 row_gap: None,
2825 }),
2826 })
2827 }
2828 "sqrt" => {
2829 let index = self.parse_optional_bracket_group()?;
2830 let base = Arc::new(self.parse_required_group()?);
2831 Ok(match index {
2832 Some(index) => MathExpr::Root {
2833 base,
2834 index: Arc::new(index),
2835 },
2836 None => MathExpr::Sqrt(base),
2837 })
2838 }
2839 "hat" | "widehat" => Ok(MathExpr::Accent {
2840 base: Arc::new(self.parse_required_group()?),
2841 accent: Arc::new(MathExpr::Operator("ˆ".into())),
2842 stretch: false,
2843 }),
2844 "bar" => Ok(MathExpr::Accent {
2845 base: Arc::new(self.parse_required_group()?),
2846 accent: Arc::new(MathExpr::Operator("¯".into())),
2847 stretch: false,
2848 }),
2849 "overline" => Ok(MathExpr::Accent {
2850 base: Arc::new(self.parse_required_group()?),
2851 accent: Arc::new(MathExpr::Operator("‾".into())),
2852 stretch: true,
2853 }),
2854 "vec" => Ok(MathExpr::Accent {
2855 base: Arc::new(self.parse_required_group()?),
2856 accent: Arc::new(MathExpr::Operator("→".into())),
2857 stretch: false,
2858 }),
2859 "tilde" | "widetilde" => Ok(MathExpr::Accent {
2860 base: Arc::new(self.parse_required_group()?),
2861 accent: Arc::new(MathExpr::Operator("˜".into())),
2862 stretch: false,
2863 }),
2864 "left" => {
2865 let open = self.parse_delimiter()?;
2866 let body = Arc::new(self.parse_row_until_right()?);
2867 self.consume_command("right")?;
2868 let close = self.parse_delimiter()?;
2869 Ok(MathExpr::Fenced { open, close, body })
2870 }
2871 "right" => Err(self.error("unexpected \\right")),
2872 "begin" => {
2873 let env = self.parse_environment_name()?;
2874 match env.as_str() {
2875 "matrix" | "pmatrix" | "bmatrix" | "Bmatrix" | "vmatrix" | "Vmatrix"
2876 | "cases" | "aligned" | "align" => {
2877 let options = default_tex_table_options(&env);
2878 self.parse_table_environment(
2879 &env,
2880 options.column_alignments,
2881 options.column_gap,
2882 options.row_gap,
2883 )
2884 }
2885 "array" => {
2886 let column_alignments = self.parse_array_column_alignments()?;
2887 self.parse_table_environment(&env, column_alignments, None, None)
2888 }
2889 _ => Err(self.error(&format!("unsupported math environment {env}"))),
2890 }
2891 }
2892 "end" => Err(self.error("unexpected \\end")),
2893 "text" | "mathrm" | "operatorname" => Ok(MathExpr::Text(self.parse_text_group()?)),
2894 "mathbf" | "boldsymbol" | "mathcal" => self.parse_required_group(),
2895 "mathbb" => {
2896 let expr = self.parse_required_group()?;
2897 Ok(map_mathbb_expr(expr))
2898 }
2899 "alpha" => Ok(MathExpr::Identifier("α".into())),
2901 "beta" => Ok(MathExpr::Identifier("β".into())),
2902 "gamma" => Ok(MathExpr::Identifier("γ".into())),
2903 "delta" => Ok(MathExpr::Identifier("δ".into())),
2904 "varepsilon" | "epsilon" => Ok(MathExpr::Identifier("ε".into())),
2905 "zeta" => Ok(MathExpr::Identifier("ζ".into())),
2906 "eta" => Ok(MathExpr::Identifier("η".into())),
2907 "theta" => Ok(MathExpr::Identifier("θ".into())),
2908 "vartheta" => Ok(MathExpr::Identifier("ϑ".into())),
2909 "iota" => Ok(MathExpr::Identifier("ι".into())),
2910 "kappa" => Ok(MathExpr::Identifier("κ".into())),
2911 "varkappa" => Ok(MathExpr::Identifier("ϰ".into())),
2912 "lambda" => Ok(MathExpr::Identifier("λ".into())),
2913 "mu" => Ok(MathExpr::Identifier("μ".into())),
2914 "nu" => Ok(MathExpr::Identifier("ν".into())),
2915 "xi" => Ok(MathExpr::Identifier("ξ".into())),
2916 "pi" => Ok(MathExpr::Identifier("π".into())),
2917 "varpi" => Ok(MathExpr::Identifier("ϖ".into())),
2918 "rho" => Ok(MathExpr::Identifier("ρ".into())),
2919 "varrho" => Ok(MathExpr::Identifier("ϱ".into())),
2920 "sigma" => Ok(MathExpr::Identifier("σ".into())),
2921 "varsigma" => Ok(MathExpr::Identifier("ς".into())),
2922 "tau" => Ok(MathExpr::Identifier("τ".into())),
2923 "upsilon" => Ok(MathExpr::Identifier("υ".into())),
2924 "phi" | "varphi" => Ok(MathExpr::Identifier("φ".into())),
2925 "chi" => Ok(MathExpr::Identifier("χ".into())),
2926 "psi" => Ok(MathExpr::Identifier("ψ".into())),
2927 "omega" => Ok(MathExpr::Identifier("ω".into())),
2928 "Gamma" => Ok(MathExpr::Identifier("Γ".into())),
2930 "Delta" => Ok(MathExpr::Identifier("Δ".into())),
2931 "Theta" => Ok(MathExpr::Identifier("Θ".into())),
2932 "Lambda" => Ok(MathExpr::Identifier("Λ".into())),
2933 "Xi" => Ok(MathExpr::Identifier("Ξ".into())),
2934 "Pi" => Ok(MathExpr::Identifier("Π".into())),
2935 "Sigma" => Ok(MathExpr::Identifier("Σ".into())),
2936 "Upsilon" => Ok(MathExpr::Identifier("Υ".into())),
2937 "Phi" => Ok(MathExpr::Identifier("Φ".into())),
2938 "Psi" => Ok(MathExpr::Identifier("Ψ".into())),
2939 "Omega" => Ok(MathExpr::Identifier("Ω".into())),
2940 "partial" => Ok(MathExpr::Identifier("∂".into())),
2942 "infty" => Ok(MathExpr::Identifier("∞".into())),
2943 "hbar" => Ok(MathExpr::Identifier("ℏ".into())),
2944 "Re" => Ok(MathExpr::Identifier("ℜ".into())),
2945 "Im" => Ok(MathExpr::Identifier("ℑ".into())),
2946 "aleph" => Ok(MathExpr::Identifier("ℵ".into())),
2947 "beth" => Ok(MathExpr::Identifier("ℶ".into())),
2948 "wp" => Ok(MathExpr::Identifier("℘".into())),
2949 "ell" => Ok(MathExpr::Identifier("ℓ".into())),
2950 "emptyset" | "varnothing" => Ok(MathExpr::Identifier("∅".into())),
2951 "triangle" => Ok(MathExpr::Identifier("△".into())),
2952 "square" => Ok(MathExpr::Identifier("□".into())),
2953 "flat" => Ok(MathExpr::Identifier("♭".into())),
2954 "sharp" => Ok(MathExpr::Identifier("♯".into())),
2955 "natural" => Ok(MathExpr::Identifier("♮".into())),
2956 "pm" => Ok(MathExpr::Operator("±".into())),
2958 "mp" => Ok(MathExpr::Operator("∓".into())),
2959 "cdot" => Ok(MathExpr::Operator("·".into())),
2960 "times" => Ok(MathExpr::Operator("×".into())),
2961 "div" => Ok(MathExpr::Operator("÷".into())),
2962 "cup" => Ok(MathExpr::Operator("∪".into())),
2963 "cap" => Ok(MathExpr::Operator("∩".into())),
2964 "wedge" | "land" => Ok(MathExpr::Operator("∧".into())),
2965 "vee" | "lor" => Ok(MathExpr::Operator("∨".into())),
2966 "oplus" => Ok(MathExpr::Operator("⊕".into())),
2967 "ominus" => Ok(MathExpr::Operator("⊖".into())),
2968 "otimes" => Ok(MathExpr::Operator("⊗".into())),
2969 "oslash" => Ok(MathExpr::Operator("⊘".into())),
2970 "odot" => Ok(MathExpr::Operator("⊙".into())),
2971 "circ" => Ok(MathExpr::Operator("∘".into())),
2972 "bullet" => Ok(MathExpr::Operator("•".into())),
2973 "star" => Ok(MathExpr::Operator("⋆".into())),
2974 "ast" => Ok(MathExpr::Operator("∗".into())),
2975 "sqcup" => Ok(MathExpr::Operator("⊔".into())),
2976 "sqcap" => Ok(MathExpr::Operator("⊓".into())),
2977 "amalg" => Ok(MathExpr::Operator("⨿".into())),
2978 "wr" => Ok(MathExpr::Operator("≀".into())),
2979 "triangleleft" => Ok(MathExpr::Operator("◁".into())),
2980 "triangleright" => Ok(MathExpr::Operator("▷".into())),
2981 "diamond" => Ok(MathExpr::Operator("⋄".into())),
2982 "setminus" | "smallsetminus" => Ok(MathExpr::Operator("∖".into())),
2983 "approx" => Ok(MathExpr::Operator("≈".into())),
2985 "sim" => Ok(MathExpr::Operator("∼".into())),
2986 "simeq" => Ok(MathExpr::Operator("≃".into())),
2987 "cong" => Ok(MathExpr::Operator("≅".into())),
2988 "equiv" => Ok(MathExpr::Operator("≡".into())),
2989 "propto" => Ok(MathExpr::Operator("∝".into())),
2990 "le" | "leq" => Ok(MathExpr::Operator("≤".into())),
2991 "ge" | "geq" => Ok(MathExpr::Operator("≥".into())),
2992 "ne" | "neq" => Ok(MathExpr::Operator("≠".into())),
2993 "ll" => Ok(MathExpr::Operator("≪".into())),
2994 "gg" => Ok(MathExpr::Operator("≫".into())),
2995 "prec" => Ok(MathExpr::Operator("≺".into())),
2996 "succ" => Ok(MathExpr::Operator("≻".into())),
2997 "preceq" => Ok(MathExpr::Operator("⪯".into())),
2998 "succeq" => Ok(MathExpr::Operator("⪰".into())),
2999 "parallel" => Ok(MathExpr::Operator("∥".into())),
3000 "perp" => Ok(MathExpr::Operator("⊥".into())),
3001 "asymp" => Ok(MathExpr::Operator("≍".into())),
3002 "doteq" => Ok(MathExpr::Operator("≐".into())),
3003 "models" => Ok(MathExpr::Operator("⊨".into())),
3004 "vdash" => Ok(MathExpr::Operator("⊢".into())),
3005 "dashv" => Ok(MathExpr::Operator("⊣".into())),
3006 "therefore" => Ok(MathExpr::Operator("∴".into())),
3007 "because" => Ok(MathExpr::Operator("∵".into())),
3008 "in" => Ok(MathExpr::Operator("∈".into())),
3010 "notin" => Ok(MathExpr::Operator("∉".into())),
3011 "ni" => Ok(MathExpr::Operator("∋".into())),
3012 "subset" => Ok(MathExpr::Operator("⊂".into())),
3013 "supset" => Ok(MathExpr::Operator("⊃".into())),
3014 "subseteq" => Ok(MathExpr::Operator("⊆".into())),
3015 "supseteq" => Ok(MathExpr::Operator("⊇".into())),
3016 "subsetneq" => Ok(MathExpr::Operator("⊊".into())),
3017 "supsetneq" => Ok(MathExpr::Operator("⊋".into())),
3018 "complement" => Ok(MathExpr::Operator("∁".into())),
3019 "forall" => Ok(MathExpr::Operator("∀".into())),
3021 "exists" => Ok(MathExpr::Operator("∃".into())),
3022 "nexists" => Ok(MathExpr::Operator("∄".into())),
3023 "neg" | "lnot" => Ok(MathExpr::Operator("¬".into())),
3024 "top" => Ok(MathExpr::Operator("⊤".into())),
3025 "bot" => Ok(MathExpr::Operator("⊥".into())),
3026 "implies" => Ok(extra_wide_operator("⟹")),
3027 "impliedby" => Ok(extra_wide_operator("⟸")),
3028 "iff" => Ok(extra_wide_operator("⟺")),
3029 "to" | "rightarrow" => Ok(MathExpr::Operator("→".into())),
3031 "leftarrow" | "gets" => Ok(MathExpr::Operator("←".into())),
3032 "leftrightarrow" => Ok(MathExpr::Operator("↔".into())),
3033 "Rightarrow" => Ok(MathExpr::Operator("⇒".into())),
3034 "Leftarrow" => Ok(MathExpr::Operator("⇐".into())),
3035 "Leftrightarrow" => Ok(MathExpr::Operator("⇔".into())),
3036 "longrightarrow" => Ok(MathExpr::Operator("⟶".into())),
3037 "longleftarrow" => Ok(MathExpr::Operator("⟵".into())),
3038 "longleftrightarrow" => Ok(MathExpr::Operator("⟷".into())),
3039 "Longrightarrow" => Ok(MathExpr::Operator("⟹".into())),
3040 "Longleftarrow" => Ok(MathExpr::Operator("⟸".into())),
3041 "Longleftrightarrow" => Ok(MathExpr::Operator("⟺".into())),
3042 "uparrow" => Ok(MathExpr::Operator("↑".into())),
3043 "downarrow" => Ok(MathExpr::Operator("↓".into())),
3044 "updownarrow" => Ok(MathExpr::Operator("↕".into())),
3045 "Uparrow" => Ok(MathExpr::Operator("⇑".into())),
3046 "Downarrow" => Ok(MathExpr::Operator("⇓".into())),
3047 "Updownarrow" => Ok(MathExpr::Operator("⇕".into())),
3048 "mapsto" => Ok(MathExpr::Operator("↦".into())),
3049 "longmapsto" => Ok(MathExpr::Operator("⟼".into())),
3050 "hookrightarrow" => Ok(MathExpr::Operator("↪".into())),
3051 "hookleftarrow" => Ok(MathExpr::Operator("↩".into())),
3052 "sum" => Ok(MathExpr::Operator("∑".into())),
3054 "prod" => Ok(MathExpr::Operator("∏".into())),
3055 "coprod" => Ok(MathExpr::Operator("∐".into())),
3056 "int" => Ok(MathExpr::Operator("∫".into())),
3057 "oint" => Ok(MathExpr::Operator("∮".into())),
3058 "iint" => Ok(MathExpr::Operator("∬".into())),
3059 "iiint" => Ok(MathExpr::Operator("∭".into())),
3060 "bigcup" => Ok(MathExpr::Operator("⋃".into())),
3061 "bigcap" => Ok(MathExpr::Operator("⋂".into())),
3062 "biguplus" => Ok(MathExpr::Operator("⨄".into())),
3063 "bigsqcup" => Ok(MathExpr::Operator("⨆".into())),
3064 "bigvee" => Ok(MathExpr::Operator("⋁".into())),
3065 "bigwedge" => Ok(MathExpr::Operator("⋀".into())),
3066 "bigoplus" => Ok(MathExpr::Operator("⨁".into())),
3067 "bigotimes" => Ok(MathExpr::Operator("⨂".into())),
3068 "bigodot" => Ok(MathExpr::Operator("⨀".into())),
3069 "nabla" => Ok(MathExpr::Operator("∇".into())),
3071 "dagger" => Ok(MathExpr::Operator("†".into())),
3072 "ddagger" => Ok(MathExpr::Operator("‡".into())),
3073 "mid" => Ok(MathExpr::Operator("|".into())),
3074 "angle" => Ok(MathExpr::Operator("∠".into())),
3075 "measuredangle" => Ok(MathExpr::Operator("∡".into())),
3076 "ldots" | "dots" => Ok(MathExpr::Text("...".into())),
3078 "cdots" => Ok(MathExpr::Operator("⋯".into())),
3079 "vdots" => Ok(MathExpr::Operator("⋮".into())),
3080 "ddots" => Ok(MathExpr::Operator("⋱".into())),
3081 "sin" | "cos" | "tan" | "cot" | "sec" | "csc" | "sinh" | "cosh" | "tanh" | "coth"
3083 | "arcsin" | "arccos" | "arctan" | "log" | "lg" | "ln" | "exp" | "lim" | "max"
3084 | "min" | "sup" | "inf" | "det" | "arg" | "deg" | "dim" | "hom" | "ker" => {
3085 Ok(MathExpr::Text(name))
3086 }
3087 "gcd" => Ok(MathExpr::Text("gcd".into())),
3088 "Pr" => Ok(MathExpr::Text("Pr".into())),
3089 "liminf" => Ok(MathExpr::Text("lim inf".into())),
3090 "limsup" => Ok(MathExpr::Text("lim sup".into())),
3091 "quad" => Ok(MathExpr::Space(1.0)),
3093 "qquad" => Ok(MathExpr::Space(2.0)),
3094 "thinspace" => Ok(MathExpr::Space(THIN_MATH_SPACE_EM)),
3095 "medspace" => Ok(MathExpr::Space(MEDIUM_MATH_SPACE_EM)),
3096 "thickspace" => Ok(MathExpr::Space(THICK_MATH_SPACE_EM)),
3097 "negthinspace" => Ok(MathExpr::Space(-THIN_MATH_SPACE_EM)),
3098 "negmedspace" => Ok(MathExpr::Space(-MEDIUM_MATH_SPACE_EM)),
3099 "negthickspace" => Ok(MathExpr::Space(-THICK_MATH_SPACE_EM)),
3100 "enspace" => Ok(MathExpr::Space(0.5)),
3101 "space" => Ok(MathExpr::Space(MEDIUM_MATH_SPACE_EM)),
3102 _ => match delimiter_command(&name) {
3103 Some(symbol) => Ok(MathExpr::Operator(symbol)),
3104 None => Ok(MathExpr::Identifier(format!("\\{name}"))),
3105 },
3106 }
3107 }
3108
3109 fn parse_required_group(&mut self) -> Result<MathExpr, MathParseError> {
3110 self.skip_ws();
3111 self.expect('{')?;
3112 self.parse_row(Some('}'))
3113 }
3114
3115 fn parse_text_group(&mut self) -> Result<String, MathParseError> {
3116 self.skip_ws();
3117 self.expect('{')?;
3118 let mut depth = 1;
3119 let mut text = String::new();
3120 while let Some(ch) = self.bump() {
3121 match ch {
3122 '\\' => {
3123 let escaped = self
3124 .bump()
3125 .ok_or_else(|| self.error("unclosed text group"))?;
3126 text.push(escaped);
3127 }
3128 '{' => {
3129 depth += 1;
3130 text.push(ch);
3131 }
3132 '}' => {
3133 depth -= 1;
3134 if depth == 0 {
3135 return Ok(text.split_whitespace().collect::<Vec<_>>().join(" "));
3136 }
3137 text.push(ch);
3138 }
3139 _ => text.push(ch),
3140 }
3141 }
3142 Err(self.error("unclosed text group"))
3143 }
3144
3145 fn parse_optional_bracket_group(&mut self) -> Result<Option<MathExpr>, MathParseError> {
3146 self.skip_ws();
3147 if self.peek() != Some('[') {
3148 return Ok(None);
3149 }
3150 self.bump();
3151 self.parse_row(Some(']')).map(Some)
3152 }
3153
3154 fn parse_delimiter(&mut self) -> Result<Option<String>, MathParseError> {
3155 self.skip_ws();
3156 let delimiter = match self.bump() {
3157 Some('.') => return Ok(None),
3158 Some('\\') => {
3159 let name = self.take_while(|c| c.is_ascii_alphabetic());
3160 if name.is_empty() {
3161 self.bump()
3162 .ok_or_else(|| self.error("expected delimiter after escape"))?
3163 .to_string()
3164 } else {
3165 delimiter_command(&name).unwrap_or_else(|| format!("\\{name}"))
3166 }
3167 }
3168 Some(ch) => ch.to_string(),
3169 None => return Err(self.error("expected delimiter")),
3170 };
3171 Ok(Some(delimiter))
3172 }
3173
3174 fn parse_environment_name(&mut self) -> Result<String, MathParseError> {
3175 self.skip_ws();
3176 self.expect('{')?;
3177 let name = self.take_while(|c| c != '}');
3178 self.expect('}')?;
3179 if name.is_empty() {
3180 return Err(self.error("expected environment name"));
3181 }
3182 Ok(name)
3183 }
3184
3185 fn parse_array_column_alignments(
3186 &mut self,
3187 ) -> Result<Vec<MathColumnAlignment>, MathParseError> {
3188 self.skip_ws();
3189 self.expect('{')?;
3190 let mut alignments = Vec::new();
3191 loop {
3192 match self.bump() {
3193 Some('}') => break,
3194 Some('l') => alignments.push(MathColumnAlignment::Left),
3195 Some('c') => alignments.push(MathColumnAlignment::Center),
3196 Some('r') => alignments.push(MathColumnAlignment::Right),
3197 Some('|') | Some(' ') | Some('\t') | Some('\n') | Some('\r') => {}
3198 Some(ch) => {
3199 return Err(
3200 self.error(&format!("unsupported array alignment specifier {ch:?}"))
3201 );
3202 }
3203 None => return Err(self.error("unclosed array alignment spec")),
3204 }
3205 }
3206 Ok(alignments)
3207 }
3208
3209 fn consume_environment_end(&mut self, expected: &str) -> Result<(), MathParseError> {
3210 self.consume_command("end")?;
3211 let found = self.parse_environment_name()?;
3212 if found == expected {
3213 Ok(())
3214 } else {
3215 Err(self.error(&format!("expected \\end{{{expected}}}")))
3216 }
3217 }
3218
3219 fn starts_with_row_separator(&self) -> bool {
3220 self.input[self.pos..].starts_with(r"\\")
3221 }
3222
3223 fn consume_row_separator(&mut self) -> Result<(), MathParseError> {
3224 if !self.starts_with_row_separator() {
3225 return Err(self.error(r"expected \\"));
3226 }
3227 self.expect('\\')?;
3228 self.expect('\\')
3229 }
3230
3231 fn skip_ws(&mut self) {
3232 while matches!(self.peek(), Some(ch) if ch.is_whitespace()) {
3233 self.bump();
3234 }
3235 }
3236
3237 fn expect(&mut self, expected: char) -> Result<(), MathParseError> {
3238 match self.bump() {
3239 Some(ch) if ch == expected => Ok(()),
3240 _ => Err(self.error(&format!("expected '{expected}'"))),
3241 }
3242 }
3243
3244 fn take_while(&mut self, mut f: impl FnMut(char) -> bool) -> String {
3245 let start = self.pos;
3246 while matches!(self.peek(), Some(ch) if f(ch)) {
3247 self.bump();
3248 }
3249 self.input[start..self.pos].to_string()
3250 }
3251
3252 fn starts_with_command(&self, command: &str) -> bool {
3253 let rest = &self.input[self.pos..];
3254 let Some(after_slash) = rest.strip_prefix('\\') else {
3255 return false;
3256 };
3257 let Some(after_command) = after_slash.strip_prefix(command) else {
3258 return false;
3259 };
3260 !matches!(after_command.chars().next(), Some(ch) if ch.is_ascii_alphabetic())
3261 }
3262
3263 fn consume_command(&mut self, command: &str) -> Result<(), MathParseError> {
3264 if !self.starts_with_command(command) {
3265 return Err(self.error(&format!("expected \\{command}")));
3266 }
3267 self.expect('\\')?;
3268 let found = self.take_while(|c| c.is_ascii_alphabetic());
3269 if found == command {
3270 Ok(())
3271 } else {
3272 Err(self.error(&format!("expected \\{command}")))
3273 }
3274 }
3275
3276 fn peek(&self) -> Option<char> {
3277 self.input[self.pos..].chars().next()
3278 }
3279
3280 fn bump(&mut self) -> Option<char> {
3281 let ch = self.peek()?;
3282 self.pos += ch.len_utf8();
3283 Some(ch)
3284 }
3285
3286 fn error(&self, message: &str) -> MathParseError {
3287 MathParseError {
3288 message: message.to_string(),
3289 byte: self.pos,
3290 }
3291 }
3292}
3293
3294fn extra_wide_operator(symbol: &str) -> MathExpr {
3295 let spacing = MEDIUM_MATH_SPACE_EM + THICK_MATH_SPACE_EM;
3296 MathExpr::OperatorWithMetadata {
3297 text: symbol.into(),
3298 lspace: Some(spacing),
3299 rspace: Some(spacing),
3300 large_operator: None,
3301 movable_limits: None,
3302 }
3303}
3304
3305fn delimiter_command(command: &str) -> Option<String> {
3306 let delimiter = match command {
3307 "lbrace" => "{",
3308 "rbrace" => "}",
3309 "lparen" => "(",
3310 "rparen" => ")",
3311 "lbrack" => "[",
3312 "rbrack" => "]",
3313 "langle" => "⟨",
3314 "rangle" => "⟩",
3315 "vert" => "|",
3316 "Vert" => "‖",
3317 "lfloor" => "⌊",
3318 "rfloor" => "⌋",
3319 "lceil" => "⌈",
3320 "rceil" => "⌉",
3321 _ => return None,
3322 };
3323 Some(delimiter.to_string())
3324}
3325
3326fn map_mathbb_expr(expr: MathExpr) -> MathExpr {
3327 match expr {
3328 MathExpr::Identifier(text) => MathExpr::Identifier(map_mathbb_text(&text)),
3329 MathExpr::Text(text) => MathExpr::Text(map_mathbb_text(&text)),
3330 MathExpr::Row(children) => MathExpr::row(children.into_iter().map(map_mathbb_expr)),
3331 other => other,
3332 }
3333}
3334
3335fn map_mathbb_text(text: &str) -> String {
3336 text.chars().map(map_mathbb_char).collect()
3337}
3338
3339fn map_mathbb_char(ch: char) -> char {
3340 match ch {
3341 'A' => '𝔸',
3342 'B' => '𝔹',
3343 'C' => 'ℂ',
3344 'D' => '𝔻',
3345 'E' => '𝔼',
3346 'F' => '𝔽',
3347 'G' => '𝔾',
3348 'H' => 'ℍ',
3349 'I' => '𝕀',
3350 'J' => '𝕁',
3351 'K' => '𝕂',
3352 'L' => '𝕃',
3353 'M' => '𝕄',
3354 'N' => 'ℕ',
3355 'O' => '𝕆',
3356 'P' => 'ℙ',
3357 'Q' => 'ℚ',
3358 'R' => 'ℝ',
3359 'S' => '𝕊',
3360 'T' => '𝕋',
3361 'U' => '𝕌',
3362 'V' => '𝕍',
3363 'W' => '𝕎',
3364 'X' => '𝕏',
3365 'Y' => '𝕐',
3366 'Z' => 'ℤ',
3367 _ => ch,
3368 }
3369}
3370
3371struct TexTableOptions {
3372 column_alignments: Vec<MathColumnAlignment>,
3373 column_gap: Option<f32>,
3374 row_gap: Option<f32>,
3375}
3376
3377fn default_tex_table_options(env: &str) -> TexTableOptions {
3378 match env {
3379 "cases" => TexTableOptions {
3380 column_alignments: vec![MathColumnAlignment::Left, MathColumnAlignment::Left],
3381 column_gap: Some(CASES_COL_GAP_EM),
3382 row_gap: None,
3383 },
3384 "aligned" | "align" => TexTableOptions {
3385 column_alignments: vec![MathColumnAlignment::Right, MathColumnAlignment::Left],
3386 column_gap: Some(MEDIUM_MATH_SPACE_EM),
3387 row_gap: None,
3388 },
3389 _ => TexTableOptions {
3390 column_alignments: Vec::new(),
3391 column_gap: None,
3392 row_gap: None,
3393 },
3394 }
3395}
3396
3397pub(crate) fn math_glyph_layout(
3398 text: &str,
3399 size: f32,
3400 weight: FontWeight,
3401) -> text_metrics::TextLayout {
3402 text_metrics::layout_text_with_line_height_and_family(
3403 text,
3404 size,
3405 text_metrics::line_height(size),
3406 FontFamily::Inter,
3407 weight,
3408 false,
3409 TextWrap::NoWrap,
3410 None,
3411 )
3412}
3413
3414pub(crate) fn resolved_math_color(color: Option<Color>) -> Color {
3415 color.unwrap_or(crate::tokens::FOREGROUND)
3416}
3417
3418#[cfg(test)]
3419mod tests {
3420 use super::*;
3421
3422 fn has_radical_shape(layout: &MathLayout) -> bool {
3423 layout
3424 .atoms
3425 .iter()
3426 .any(|atom| matches!(atom, MathAtom::Radical { .. } | MathAtom::GlyphId { .. }))
3427 }
3428
3429 fn expect_source(expr: &MathExpr, expected: Range<usize>) -> &MathExpr {
3430 let MathExpr::Source { source, body } = expr else {
3431 panic!("expected source wrapper, got {expr:?}");
3432 };
3433 assert_eq!(*source, expected);
3434 body
3435 }
3436
3437 fn assert_no_unknown_tex_commands(expr: &MathExpr) {
3438 match expr {
3439 MathExpr::Identifier(text) => {
3440 assert!(
3441 !text.starts_with('\\'),
3442 "unexpected raw TeX command identifier {text:?} in {expr:?}"
3443 );
3444 }
3445 MathExpr::Row(children) => {
3446 for child in children {
3447 assert_no_unknown_tex_commands(child);
3448 }
3449 }
3450 MathExpr::Fraction {
3451 numerator,
3452 denominator,
3453 } => {
3454 assert_no_unknown_tex_commands(numerator);
3455 assert_no_unknown_tex_commands(denominator);
3456 }
3457 MathExpr::Sqrt(child) => assert_no_unknown_tex_commands(child),
3458 MathExpr::Root { base, index } => {
3459 assert_no_unknown_tex_commands(base);
3460 assert_no_unknown_tex_commands(index);
3461 }
3462 MathExpr::Scripts { base, sub, sup } => {
3463 assert_no_unknown_tex_commands(base);
3464 if let Some(sub) = sub {
3465 assert_no_unknown_tex_commands(sub);
3466 }
3467 if let Some(sup) = sup {
3468 assert_no_unknown_tex_commands(sup);
3469 }
3470 }
3471 MathExpr::UnderOver { base, under, over } => {
3472 assert_no_unknown_tex_commands(base);
3473 if let Some(under) = under {
3474 assert_no_unknown_tex_commands(under);
3475 }
3476 if let Some(over) = over {
3477 assert_no_unknown_tex_commands(over);
3478 }
3479 }
3480 MathExpr::Accent { base, accent, .. } => {
3481 assert_no_unknown_tex_commands(base);
3482 assert_no_unknown_tex_commands(accent);
3483 }
3484 MathExpr::Fenced { body, .. } => assert_no_unknown_tex_commands(body),
3485 MathExpr::Table { rows, .. } => {
3486 for row in rows {
3487 for cell in row {
3488 assert_no_unknown_tex_commands(cell);
3489 }
3490 }
3491 }
3492 MathExpr::Source { body, .. } => assert_no_unknown_tex_commands(body),
3493 MathExpr::Operator(_)
3494 | MathExpr::OperatorWithMetadata { .. }
3495 | MathExpr::Text(_)
3496 | MathExpr::Number(_)
3497 | MathExpr::Space(_)
3498 | MathExpr::Error(_) => {}
3499 }
3500 }
3501
3502 #[test]
3503 fn tex_source_ranges_are_opt_in_and_do_not_change_layout() {
3504 let input = r"\frac{x_1}{2}";
3505 let plain = parse_tex(input).expect("plain tex");
3506 let sourced = parse_tex_with_source_ranges(input).expect("source-backed tex");
3507
3508 assert!(!matches!(plain, MathExpr::Source { .. }));
3509 assert_eq!(
3510 layout_math(&plain, 16.0, MathDisplay::Block),
3511 layout_math(&sourced, 16.0, MathDisplay::Block)
3512 );
3513 assert!(matches!(
3514 expect_source(&sourced, 0..input.len()).without_source(),
3515 MathExpr::Fraction { .. }
3516 ));
3517 }
3518
3519 #[test]
3520 fn tex_source_ranges_track_script_components() {
3521 let expr = parse_tex_with_source_ranges("x_1^2").expect("source-backed tex");
3522 let root = expect_source(&expr, 0..5);
3523 let body = expect_source(root, 0..5);
3524 let MathExpr::Scripts { base, sub, sup } = body else {
3525 panic!("expected scripts, got {body:?}");
3526 };
3527
3528 assert_eq!(
3529 expect_source(base, 0..1).without_source(),
3530 &MathExpr::Identifier("x".into())
3531 );
3532 assert_eq!(
3533 expect_source(sub.as_deref().expect("subscript"), 2..3).without_source(),
3534 &MathExpr::Number("1".into())
3535 );
3536 assert_eq!(
3537 expect_source(sup.as_deref().expect("superscript"), 4..5).without_source(),
3538 &MathExpr::Number("2".into())
3539 );
3540 }
3541
3542 #[cfg(feature = "symbols")]
3543 #[test]
3544 fn loads_bundled_open_type_math_constants() {
3545 let constants = open_type_math_constants().expect("bundled math font has a MATH table");
3546 assert!(
3547 constants
3548 .script_scale(16.0)
3549 .is_some_and(|size| size > 6.0 && size < 16.0),
3550 "script scale should come from Noto Sans Math"
3551 );
3552 assert!(
3553 constants
3554 .fraction_rule_thickness(16.0)
3555 .is_some_and(|thickness| thickness > 0.75 && thickness < 2.0),
3556 "fraction rule thickness should come from Noto Sans Math"
3557 );
3558 assert!(
3559 constants
3560 .axis_height(16.0)
3561 .is_some_and(|axis| axis > 1.0 && axis < 8.0),
3562 "axis height should come from Noto Sans Math"
3563 );
3564 assert!(
3565 constants
3566 .superscript_shift_up(16.0)
3567 .is_some_and(|shift| shift > 1.0 && shift < 16.0),
3568 "superscript shift should come from Noto Sans Math"
3569 );
3570 assert!(
3571 constants
3572 .subscript_shift_down(16.0)
3573 .is_some_and(|shift| shift > 1.0 && shift < 16.0),
3574 "subscript shift should come from Noto Sans Math"
3575 );
3576 assert!(
3577 constants
3578 .space_after_script(16.0)
3579 .is_some_and(|space| space > 0.1 && space < 4.0),
3580 "script spacing should come from Noto Sans Math"
3581 );
3582 assert!(
3583 constants
3584 .upper_limit_gap_min(16.0)
3585 .is_some_and(|gap| gap > 0.5 && gap < 8.0),
3586 "upper limit gap should come from Noto Sans Math"
3587 );
3588 assert!(
3589 constants
3590 .lower_limit_baseline_drop_min(16.0)
3591 .is_some_and(|drop| drop > 1.0 && drop < 20.0),
3592 "lower limit baseline drop should come from Noto Sans Math"
3593 );
3594 assert!(
3595 constants
3596 .fraction_numerator_gap(16.0, true)
3597 .is_some_and(|gap| gap > 0.5 && gap < 8.0),
3598 "display numerator gap should come from Noto Sans Math"
3599 );
3600 assert!(
3601 constants
3602 .fraction_denominator_gap(16.0, true)
3603 .is_some_and(|gap| gap > 0.5 && gap < 8.0),
3604 "display denominator gap should come from Noto Sans Math"
3605 );
3606 assert!(
3607 constants
3608 .fraction_numerator_shift(16.0, true)
3609 .is_some_and(|shift| shift > 1.0 && shift < 24.0),
3610 "display numerator shift should come from Noto Sans Math"
3611 );
3612 assert!(
3613 constants
3614 .fraction_denominator_shift(16.0, true)
3615 .is_some_and(|shift| shift > 1.0 && shift < 24.0),
3616 "display denominator shift should come from Noto Sans Math"
3617 );
3618 assert!(
3619 constants
3620 .radical_rule_thickness(16.0)
3621 .is_some_and(|thickness| thickness > 0.75 && thickness < 2.0),
3622 "radical rule thickness should come from Noto Sans Math"
3623 );
3624 assert!(
3625 constants
3626 .radical_vertical_gap(16.0, true)
3627 .is_some_and(|gap| gap > 0.5 && gap < 8.0),
3628 "display radical gap should come from Noto Sans Math"
3629 );
3630 assert!(
3631 constants
3632 .radical_kern_before_degree(16.0)
3633 .is_some_and(|kern| kern > 0.0 && kern < 8.0),
3634 "radical degree before-kern should come from Noto Sans Math"
3635 );
3636 assert!(
3637 constants
3638 .radical_kern_after_degree(16.0)
3639 .is_some_and(|kern| kern < 0.0 && kern > -8.0),
3640 "radical degree after-kern should come from Noto Sans Math"
3641 );
3642 assert!(
3643 constants
3644 .radical_degree_bottom_raise_fraction()
3645 .is_some_and(|raise| raise > 0.0 && raise < 1.0),
3646 "radical degree raise should come from Noto Sans Math"
3647 );
3648 assert!(
3649 constants
3650 .min_connector_overlap(16.0)
3651 .is_some_and(|overlap| overlap > 0.0),
3652 "delimiter connector overlap should come from Noto Sans Math"
3653 );
3654 assert!(
3655 constants
3656 .delimited_sub_formula_min_height(16.0)
3657 .is_some_and(|height| height > 8.0 && height < 40.0),
3658 "delimiter stretch threshold should come from Noto Sans Math"
3659 );
3660 assert!(
3661 constants.delimiter_variant_count('(') > 0,
3662 "left paren should expose vertical delimiter variants"
3663 );
3664 assert!(
3665 constants.delimiter_variant_count(RADICAL_GLYPH) > 0,
3666 "radical should expose vertical math glyph variants"
3667 );
3668 assert!(
3669 constants.delimiter_variant_count('∑') > 0,
3670 "summation should expose vertical math glyph variants"
3671 );
3672 assert!(
3673 constants.delimiter_variant_count('∫') > 0,
3674 "integral should expose vertical math glyph variants"
3675 );
3676 assert!(
3677 constants
3678 .delimiter_first_variant_glyph_id('(')
3679 .is_some_and(|glyph_id| glyph_id > 0),
3680 "left paren variants should preserve glyph IDs"
3681 );
3682 assert!(
3683 constants.delimiter_assembly_part_count('{') > 0,
3684 "left brace should expose a vertical delimiter assembly"
3685 );
3686 assert!(
3687 constants.delimiter_extender_part_count('{') > 0,
3688 "left brace assembly should expose extender parts"
3689 );
3690 assert!(
3691 constants.delimiter_has_assembly_connectors('{'),
3692 "left brace assembly should preserve connector metadata"
3693 );
3694 assert!(
3695 constants
3696 .delimiter_max_advance('(', 16.0)
3697 .is_some_and(|advance| advance > 16.0),
3698 "delimiter variant advances should scale into px"
3699 );
3700 }
3701
3702 #[test]
3703 fn parses_fraction_with_scripts() {
3704 let expr = parse_tex(r"\frac{a^2+b^2}{\sqrt{x_1+x_2}}").expect("valid tex");
3705 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
3706 assert!(layout.width > 20.0, "width = {}", layout.width);
3707 assert!(layout.ascent > 10.0, "ascent = {}", layout.ascent);
3708 assert!(layout.descent > 10.0, "descent = {}", layout.descent);
3709 assert!(
3710 layout
3711 .atoms
3712 .iter()
3713 .any(|atom| matches!(atom, MathAtom::Rule { .. })),
3714 "fraction should emit rule atoms"
3715 );
3716 assert!(
3717 has_radical_shape(&layout),
3718 "sqrt should emit a radical shape atom"
3719 );
3720 }
3721
3722 #[test]
3723 fn display_fraction_honors_baseline_shifts() {
3724 let layout = layout_math(
3725 &parse_tex(r"\frac{1}{2}").unwrap(),
3726 16.0,
3727 MathDisplay::Block,
3728 );
3729 let metrics = LayoutCtx {
3730 size: 16.0,
3731 display: MathDisplay::Block,
3732 }
3733 .metrics();
3734 let numerator_y = layout
3735 .atoms
3736 .iter()
3737 .find_map(|atom| match atom {
3738 MathAtom::Glyph {
3739 text, y_baseline, ..
3740 } if text == "1" => Some(*y_baseline),
3741 _ => None,
3742 })
3743 .expect("numerator baseline");
3744 let denominator_y = layout
3745 .atoms
3746 .iter()
3747 .find_map(|atom| match atom {
3748 MathAtom::Glyph {
3749 text, y_baseline, ..
3750 } if text == "2" => Some(*y_baseline),
3751 _ => None,
3752 })
3753 .expect("denominator baseline");
3754
3755 assert!(
3756 -numerator_y >= metrics.fraction_numerator_shift() - 0.1,
3757 "numerator shift = {}, min = {}",
3758 -numerator_y,
3759 metrics.fraction_numerator_shift()
3760 );
3761 assert!(
3762 denominator_y >= metrics.fraction_denominator_shift() - 0.1,
3763 "denominator shift = {denominator_y}, min = {}",
3764 metrics.fraction_denominator_shift()
3765 );
3766 }
3767
3768 #[test]
3769 fn scripts_with_sub_and_sup_keep_minimum_gap() {
3770 let layout = layout_math(&parse_tex(r"x_1^2").unwrap(), 16.0, MathDisplay::Inline);
3771 let sub_top = layout
3772 .atoms
3773 .iter()
3774 .find_map(|atom| match atom {
3775 MathAtom::Glyph {
3776 text,
3777 y_baseline,
3778 size,
3779 ..
3780 } if text == "1" => Some(
3781 y_baseline
3782 - LayoutCtx {
3783 size: *size,
3784 display: MathDisplay::Inline,
3785 }
3786 .metrics()
3787 .glyph_ascent(),
3788 ),
3789 _ => None,
3790 })
3791 .expect("subscript top");
3792 let sup_bottom = layout
3793 .atoms
3794 .iter()
3795 .find_map(|atom| match atom {
3796 MathAtom::Glyph {
3797 text,
3798 y_baseline,
3799 size,
3800 ..
3801 } if text == "2" => Some(
3802 y_baseline
3803 + LayoutCtx {
3804 size: *size,
3805 display: MathDisplay::Inline,
3806 }
3807 .metrics()
3808 .glyph_descent(),
3809 ),
3810 _ => None,
3811 })
3812 .expect("superscript bottom");
3813 let min_gap = LayoutCtx {
3814 size: 16.0,
3815 display: MathDisplay::Inline,
3816 }
3817 .metrics()
3818 .sub_superscript_gap();
3819
3820 assert!(
3821 sub_top - sup_bottom >= min_gap - 0.1,
3822 "script gap = {}, min = {min_gap}",
3823 sub_top - sup_bottom
3824 );
3825 }
3826
3827 #[test]
3828 fn parses_indexed_tex_root() {
3829 let expr = parse_tex(r"\sqrt[3]{x+1}").expect("valid tex");
3830 match expr {
3831 MathExpr::Root { base, index } => {
3832 assert_eq!(*index, MathExpr::Number("3".into()));
3833 assert!(matches!(*base, MathExpr::Row(_)));
3834 }
3835 other => panic!("expected indexed root, got {other:?}"),
3836 }
3837 let layout = layout_math(
3838 &parse_tex(r"\sqrt[3]{x+1}").unwrap(),
3839 16.0,
3840 MathDisplay::Inline,
3841 );
3842 assert!(
3843 has_radical_shape(&layout),
3844 "indexed root should emit a radical shape atom"
3845 );
3846 }
3847
3848 #[test]
3849 fn indexed_root_uses_open_type_degree_metrics() {
3850 let ctx = LayoutCtx {
3851 size: 16.0,
3852 display: MathDisplay::Inline,
3853 };
3854 let metrics = ctx.metrics();
3855 let base = parse_tex(r"x+1").expect("valid root base");
3856 let index_expr = MathExpr::Number("3".into());
3857 let root = layout_sqrt(&base, ctx);
3858 let index = layout_expr(&index_expr, ctx.script());
3859 let layout = layout_root(&base, &index_expr, ctx);
3860 let constants = metrics.font_constants().expect("bundled math constants");
3861 let expected_root_x = (constants
3862 .radical_kern_before_degree(ctx.size)
3863 .unwrap_or(0.0)
3864 + index.width
3865 + constants.radical_kern_after_degree(ctx.size).unwrap_or(0.0))
3866 .max(index.width * 0.35);
3867 let expected_index_dy = -root.ascent
3868 * constants
3869 .radical_degree_bottom_raise_fraction()
3870 .expect("root degree raise")
3871 - index.descent;
3872 let index_atom = layout
3873 .atoms
3874 .iter()
3875 .find_map(|atom| match atom {
3876 MathAtom::Glyph {
3877 text,
3878 x,
3879 y_baseline,
3880 ..
3881 } if text == "3" => Some((*x, *y_baseline)),
3882 _ => None,
3883 })
3884 .expect("root index glyph");
3885 let root_x = layout
3886 .atoms
3887 .iter()
3888 .find_map(|atom| match atom {
3889 MathAtom::GlyphId { rect, .. } => Some(rect.x),
3890 MathAtom::Radical { points, .. } => Some(points[0][0]),
3891 _ => None,
3892 })
3893 .expect("root radical atom");
3894
3895 assert!(
3896 (index_atom.0 - 0.0).abs() < 0.1,
3897 "index x = {}",
3898 index_atom.0
3899 );
3900 assert!(
3901 (index_atom.1 - expected_index_dy).abs() < 0.1,
3902 "index baseline = {}, expected {expected_index_dy}",
3903 index_atom.1
3904 );
3905 assert!(
3906 (root_x - expected_root_x).abs() < 0.1,
3907 "root x = {root_x}, expected {expected_root_x}"
3908 );
3909 }
3910
3911 #[test]
3912 fn parses_tex_accents() {
3913 let expr = parse_tex(r"\hat{x} + \overline{ab} + \vec{v}").expect("valid tex accents");
3914 let MathExpr::Row(children) = expr else {
3915 panic!("expected row expression");
3916 };
3917 assert!(
3918 children
3919 .iter()
3920 .filter(|child| matches!(child, MathExpr::Accent { .. }))
3921 .count()
3922 >= 3,
3923 "expected accent expressions in {children:?}"
3924 );
3925
3926 let overline = layout_math(
3927 &parse_tex(r"\overline{ab}").unwrap(),
3928 16.0,
3929 MathDisplay::Inline,
3930 );
3931 assert!(
3932 overline
3933 .atoms
3934 .iter()
3935 .any(|atom| matches!(atom, MathAtom::Rule { rect } if rect.y < -10.0)),
3936 "overline should emit a rule above the base"
3937 );
3938 }
3939
3940 #[test]
3941 fn parses_tex_text_groups() {
3942 let expr = parse_tex(r"x \text{ if } y \operatorname{max}").expect("valid tex text");
3943 let MathExpr::Row(children) = expr else {
3944 panic!("expected row expression");
3945 };
3946 assert!(
3947 children
3948 .iter()
3949 .any(|child| matches!(child, MathExpr::Text(text) if text == "if")),
3950 "expected text group in {children:?}"
3951 );
3952 assert!(
3953 children
3954 .iter()
3955 .any(|child| matches!(child, MathExpr::Text(text) if text == "max")),
3956 "expected operatorname text in {children:?}"
3957 );
3958 }
3959
3960 #[test]
3961 fn parses_common_tex_symbol_commands() {
3962 let expr =
3963 parse_tex(r"\alpha+\beta\to\gamma+\emptyset+\varnothing").expect("valid tex symbols");
3964 let MathExpr::Row(children) = expr else {
3965 panic!("expected row expression");
3966 };
3967 assert!(
3968 children
3969 .iter()
3970 .any(|child| matches!(child, MathExpr::Identifier(text) if text == "∅")),
3971 "expected empty-set symbol in {children:?}"
3972 );
3973 assert!(
3974 children.iter().all(
3975 |child| !matches!(child, MathExpr::Identifier(text) if text.starts_with('\\'))
3976 ),
3977 "expected supported symbol commands in {children:?}"
3978 );
3979 }
3980
3981 #[test]
3982 fn parses_aligned_tex_environment() {
3983 let expr = parse_tex(
3984 r"\begin{aligned}
3985(a + b)^2 &= a^2 + 2ab + b^2 \\
3986(a - b)^2 &= a^2 - 2ab + b^2 \\
3987(a+b)(a-b) &= a^2 - b^2
3988\end{aligned}",
3989 )
3990 .expect("valid aligned environment");
3991
3992 let MathExpr::Table {
3993 rows,
3994 column_alignments,
3995 ..
3996 } = expr
3997 else {
3998 panic!("expected aligned environment to parse as table");
3999 };
4000 assert_eq!(rows.len(), 3);
4001 assert!(rows.iter().all(|row| row.len() == 2), "rows = {rows:?}");
4002 assert_eq!(
4003 column_alignments,
4004 vec![MathColumnAlignment::Right, MathColumnAlignment::Left]
4005 );
4006 }
4007
4008 #[test]
4009 fn parses_markdown_math_stress_tex_commands() {
4010 let formulas = [
4011 r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
4012 r"\int_{-\infty}^{\infty} e^{-x^2}\, dx = \sqrt{\pi}",
4013 r"\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x)\, e^{-2\pi i x \xi}\, dx",
4014 r"\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}",
4015 r"\nabla \times \mathbf{B} = \mu_0 \mathbf{J} + \mu_0 \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t}",
4016 r"\begin{aligned}
4017S &= \sum_{k=0}^{n} r^k = 1 + r + r^2 + \cdots + r^n \\
4018rS &= r + r^2 + \cdots + r^{n+1} \\
4019S - rS &= 1 - r^{n+1} \\
4020S &= \frac{1 - r^{n+1}}{1 - r}, \quad r \neq 1
4021\end{aligned}",
4022 r"R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}",
4023 r"\det(A) = \begin{vmatrix} a & b & c \\ d & e & f \\ g & h & i \end{vmatrix}",
4024 r"f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}",
4025 r"P(A \mid B) = \frac{P(B \mid A)\, P(A)}{P(B)}",
4026 r"f(x \mid \mu, \sigma^2) = \frac{1}{\sqrt{2\pi\sigma^2}}",
4027 r"\mathbb{E}[X] = \int_{-\infty}^{\infty} x\, f(x)\, dx, \qquad \operatorname{Var}(X) = \mathbb{E}[X^2] - (\mathbb{E}[X])^2",
4028 r"( x + y )^n = \sum_{k=0}^{n} \binom{n}{k} x^{n-k} y^k",
4029 r"\varphi(n) = n \prod_{p \mid n} \left(1 - \frac{1}{p}\right)",
4030 r"A = A^\dagger",
4031 r"E_n = \frac{n^2 \pi^2 \hbar^2}{2mL^2}",
4032 r"|\langle \mathbf{u}, \mathbf{v} \rangle|^2 \leq \langle \mathbf{u}, \mathbf{u} \rangle \cdot \langle \mathbf{v}, \mathbf{v} \rangle",
4033 r"\alpha,\ \beta,\ \gamma,\ \delta,\ \varepsilon,\ \zeta,\ \eta,\ \theta,\ \iota,\ \kappa,\ \lambda,\ \mu,\ \nu,\ \xi,\ \pi,\ \rho,\ \sigma,\ \tau,\ \upsilon,\ \phi,\ \chi,\ \psi,\ \omega",
4034 r"\Gamma(n) = (n-1)! \qquad \Gamma\!\left(\tfrac{1}{2}\right) = \sqrt{\pi}",
4035 r"\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s}, \quad \Re(s) > 1",
4036 ];
4037
4038 for formula in formulas {
4039 let expr = parse_tex(formula)
4040 .unwrap_or_else(|err| panic!("failed to parse {formula:?}: {}", err.message));
4041 assert_no_unknown_tex_commands(&expr);
4042 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4043 assert!(
4044 layout.width.is_finite() && layout.height().is_finite(),
4045 "layout should be finite for {formula:?}: {layout:?}"
4046 );
4047 }
4048 }
4049
4050 #[test]
4051 fn operator_metadata_covers_spacing_and_large_ops() {
4052 let plus = operator_info("+");
4053 assert_eq!(plus.class, MathOperatorClass::Binary);
4054 assert!(plus.lspace_em > 0.0);
4055 assert!(plus.rspace_em > 0.0);
4056
4057 let comma = operator_info(",");
4058 assert_eq!(comma.class, MathOperatorClass::Punctuation);
4059 assert_eq!(comma.lspace_em, 0.0);
4060 assert!(comma.rspace_em > 0.0);
4061
4062 let sum = operator_info("∑");
4063 assert_eq!(sum.class, MathOperatorClass::Large);
4064 assert!(sum.large_operator);
4065 assert!(sum.movable_limits);
4066
4067 let integral = operator_info("∫");
4068 assert_eq!(integral.class, MathOperatorClass::Large);
4069 assert!(integral.large_operator);
4070 assert!(!integral.movable_limits);
4071
4072 let wedge = operator_info("∧");
4074 assert_eq!(wedge.class, MathOperatorClass::Binary);
4075 let oplus = operator_info("⊕");
4076 assert_eq!(oplus.class, MathOperatorClass::Binary);
4077
4078 let element_of = operator_info("∈");
4079 assert_eq!(element_of.class, MathOperatorClass::Relation);
4080 let double_right_arrow = operator_info("⇒");
4081 assert_eq!(double_right_arrow.class, MathOperatorClass::Relation);
4082
4083 let coproduct = operator_info("∐");
4084 assert_eq!(coproduct.class, MathOperatorClass::Large);
4085 assert!(coproduct.movable_limits);
4086
4087 let contour_integral = operator_info("∮");
4088 assert_eq!(contour_integral.class, MathOperatorClass::Large);
4089 assert!(contour_integral.large_operator);
4090 assert!(!contour_integral.movable_limits);
4091 }
4092
4093 #[test]
4094 fn parses_logic_and_set_theory_commands() {
4095 let expr = parse_tex(r"\forall x \in S, \exists y \notin T \implies x \neq y")
4096 .expect("valid logic tex");
4097 assert_no_unknown_tex_commands(&expr);
4098
4099 let MathExpr::Row(children) = &expr else {
4100 panic!("expected row");
4101 };
4102
4103 assert!(
4104 children
4105 .iter()
4106 .any(|c| matches!(c.without_source(), MathExpr::Operator(s) if s == "∀")),
4107 "expected ∀ in {children:?}"
4108 );
4109 assert!(
4110 children
4111 .iter()
4112 .any(|c| matches!(c.without_source(), MathExpr::Operator(s) if s == "∃")),
4113 "expected ∃ in {children:?}"
4114 );
4115 assert!(
4116 children
4117 .iter()
4118 .any(|c| matches!(c.without_source(), MathExpr::Operator(s) if s == "∈")),
4119 "expected ∈ in {children:?}"
4120 );
4121 assert!(
4122 children
4123 .iter()
4124 .any(|c| matches!(c.without_source(), MathExpr::Operator(s) if s == "∉")),
4125 "expected ∉ in {children:?}"
4126 );
4127
4128 let implies = children
4129 .iter()
4130 .find_map(|child| match child.without_source() {
4131 MathExpr::OperatorWithMetadata {
4132 text,
4133 lspace,
4134 rspace,
4135 ..
4136 } if text == "⟹" => Some((*lspace, *rspace)),
4137 _ => None,
4138 })
4139 .expect("expected \\implies operator with metadata");
4140 let (lspace, rspace) = implies;
4141 assert!(
4143 lspace.is_some_and(|v| v > MEDIUM_MATH_SPACE_EM),
4144 "lspace = {lspace:?}"
4145 );
4146 assert!(
4147 rspace.is_some_and(|v| v > MEDIUM_MATH_SPACE_EM),
4148 "rspace = {rspace:?}"
4149 );
4150 }
4151
4152 #[test]
4153 fn parses_function_like_operators_with_limits() {
4154 let expr = parse_tex(r"\gcd_{p \mid n}(p) + \liminf_{n \to \infty} a_n")
4155 .expect("valid function tex");
4156 assert_no_unknown_tex_commands(&expr);
4157
4158 let layout = layout_math(&expr, 22.0, MathDisplay::Block);
4159 assert!(layout.width.is_finite() && layout.height().is_finite());
4160
4161 assert!(is_display_limits_base(&MathExpr::Text("gcd".into())));
4163 assert!(is_display_limits_base(&MathExpr::Text("Pr".into())));
4164 assert!(is_display_limits_base(&MathExpr::Text("det".into())));
4165 assert!(is_display_limits_base(&MathExpr::Text("lim inf".into())));
4166 assert!(is_display_limits_base(&MathExpr::Text("lim sup".into())));
4167 }
4168
4169 #[test]
4170 fn parses_bare_delimiter_commands_as_operators() {
4171 let expr = parse_tex(r"\lfloor x \rfloor + \lceil y \rceil + \langle u, v \rangle")
4172 .expect("valid bare delimiter tex");
4173 assert_no_unknown_tex_commands(&expr);
4174 let MathExpr::Row(children) = &expr else {
4175 panic!("expected row");
4176 };
4177 for symbol in ["⌊", "⌋", "⌈", "⌉", "⟨", "⟩"] {
4178 assert!(
4179 children
4180 .iter()
4181 .any(|c| matches!(c.without_source(), MathExpr::Operator(s) if s == symbol)),
4182 "expected {symbol} in {children:?}"
4183 );
4184 }
4185 }
4186
4187 #[test]
4188 fn parses_arrows_and_big_operators() {
4189 let formulas = [
4190 r"f : A \hookrightarrow B",
4191 r"x \mapsto x^2",
4192 r"a \Rightarrow b \Leftrightarrow c",
4193 r"\bigoplus_{i=1}^{n} V_i \cong \bigotimes_{j=1}^{m} W_j",
4194 r"\oint_{\partial \Omega} \omega = \iint_{\Omega} d\omega",
4195 r"\coprod_{i \in I} X_i",
4196 r"\bigvee_{k} a_k \wedge \bigwedge_{k} b_k",
4197 r"A \subseteq B \subset C \supseteq D \supset E",
4198 r"x \equiv y",
4199 r"\therefore \forall \epsilon > 0, \exists \delta > 0",
4200 r"\neg p \vee q \iff p \implies q",
4201 r"\gcd(a, b) \cdot \mathrm{lcm}(a, b) = a \cdot b",
4202 r"\Pr(A \cup B) \leq \Pr(A) + \Pr(B)",
4203 r"\liminf_{n \to \infty} a_n \leq \limsup_{n \to \infty} a_n",
4204 r"\sin^2\theta + \cos^2\theta = 1, \quad \tan\theta = \frac{\sin\theta}{\cos\theta}",
4205 r"\arcsin x + \arccos x = \frac{\pi}{2}",
4206 r"\sinh x = \frac{e^x - e^{-x}}{2}, \quad \cosh x = \frac{e^x + e^{-x}}{2}",
4207 r"\Pi_{i} a_i \prec \Sigma_{i} a_i \succ \Phi_{i} a_i",
4208 r"a \parallel b, \quad u \perp v",
4209 r"\vec{v} \cdot \vec{w} = \|\vec{v}\| \|\vec{w}\| \cos\theta",
4210 r"x \ll y \ll z, \quad a \gg b \gg c",
4211 r"\aleph_0 < 2^{\aleph_0}",
4212 r"\Im(z) + i\,\Re(z), \quad \ell^2(\mathbb{N})",
4213 r"x \star y \ne y \star x, \quad a \oplus b \otimes c",
4214 r"p \models \phi \vdash \psi \dashv \rho",
4215 ];
4216
4217 for formula in formulas {
4218 let expr = parse_tex(formula)
4219 .unwrap_or_else(|err| panic!("failed to parse {formula:?}: {}", err.message));
4220 assert_no_unknown_tex_commands(&expr);
4221 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4222 assert!(
4223 layout.width.is_finite() && layout.height().is_finite(),
4224 "layout finite for {formula:?}"
4225 );
4226 }
4227 }
4228
4229 #[test]
4230 fn display_sum_scripts_layout_as_limits() {
4231 let expr = parse_tex(r"\sum_{i=1}^{n} x_i").expect("valid tex");
4232 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4233 let metrics = LayoutCtx {
4234 size: 16.0,
4235 display: MathDisplay::Block,
4236 }
4237 .metrics();
4238 let sum_center_y = layout
4239 .atoms
4240 .iter()
4241 .find_map(|atom| match atom {
4242 MathAtom::Glyph {
4243 text, y_baseline, ..
4244 } if text == "∑" => Some(*y_baseline),
4245 MathAtom::GlyphId { rect, .. } => Some(rect.y + rect.h * 0.5),
4246 _ => None,
4247 })
4248 .expect("sum center");
4249 let upper_y = layout
4250 .atoms
4251 .iter()
4252 .find_map(|atom| match atom {
4253 MathAtom::Glyph {
4254 text, y_baseline, ..
4255 } if text == "n" => Some(*y_baseline),
4256 _ => None,
4257 })
4258 .expect("upper limit baseline");
4259 let lower_y = layout
4260 .atoms
4261 .iter()
4262 .find_map(|atom| match atom {
4263 MathAtom::Glyph {
4264 text, y_baseline, ..
4265 } if text == "i" => Some(*y_baseline),
4266 _ => None,
4267 })
4268 .expect("lower limit baseline");
4269 assert!(
4270 layout
4271 .atoms
4272 .iter()
4273 .any(|atom| matches!(atom, MathAtom::Glyph { text, y_baseline, .. } if text == "n" && *y_baseline < 0.0)),
4274 "sum upper limit should sit above the operator"
4275 );
4276 assert!(
4277 layout
4278 .atoms
4279 .iter()
4280 .any(|atom| matches!(atom, MathAtom::Glyph { text, y_baseline, .. } if text == "i" && *y_baseline > 0.0)),
4281 "sum lower limit should sit below the operator"
4282 );
4283 assert!(
4284 sum_center_y - upper_y >= metrics.upper_limit_baseline_rise() - 0.1,
4285 "upper limit rise = {}, min = {}",
4286 sum_center_y - upper_y,
4287 metrics.upper_limit_baseline_rise()
4288 );
4289 assert!(
4290 lower_y - sum_center_y >= metrics.lower_limit_baseline_drop() - 0.1,
4291 "lower limit drop = {}, min = {}",
4292 lower_y - sum_center_y,
4293 metrics.lower_limit_baseline_drop()
4294 );
4295 assert!(
4296 layout
4297 .atoms
4298 .iter()
4299 .any(|atom| matches!(atom, MathAtom::GlyphId { .. })),
4300 "display sum should use an OpenType operator variant"
4301 );
4302 assert!(
4303 (sum_center_y + metrics.math_axis_shift()).abs() < 0.75,
4304 "display sum should center on the parent math axis"
4305 );
4306 }
4307
4308 #[test]
4309 fn display_integral_uses_open_type_variant() {
4310 let display = layout_math(&parse_tex(r"\int").unwrap(), 16.0, MathDisplay::Block);
4311 let inline = layout_math(&parse_tex(r"\int").unwrap(), 16.0, MathDisplay::Inline);
4312 assert!(
4313 display
4314 .atoms
4315 .iter()
4316 .any(|atom| matches!(atom, MathAtom::GlyphId { .. })),
4317 "display integral should use an OpenType operator variant"
4318 );
4319 assert!(
4320 display.height() > inline.height() * 1.4,
4321 "display integral height = {}, inline height = {}",
4322 display.height(),
4323 inline.height()
4324 );
4325 }
4326
4327 #[test]
4328 fn mathml_largeop_false_keeps_integral_unexpanded() {
4329 let expr = parse_mathml(r#"<math><mo largeop="false">∫</mo></math>"#)
4330 .expect("valid MathML integral");
4331 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4332 assert!(
4333 !layout
4334 .atoms
4335 .iter()
4336 .any(|atom| matches!(atom, MathAtom::GlyphId { .. })),
4337 "largeop=false should keep display integral on the ordinary glyph path"
4338 );
4339 }
4340
4341 #[test]
4342 fn display_integral_scripts_stay_on_side_of_large_operator() {
4343 let layout = layout_math(
4344 &parse_tex(r"\int_0^1 f(x)dx").unwrap(),
4345 16.0,
4346 MathDisplay::Block,
4347 );
4348 let integral_rect = layout
4349 .atoms
4350 .iter()
4351 .find_map(|atom| match atom {
4352 MathAtom::GlyphId { rect, .. } => Some(*rect),
4353 _ => None,
4354 })
4355 .expect("large integral glyph");
4356 let lower = layout
4357 .atoms
4358 .iter()
4359 .find_map(|atom| match atom {
4360 MathAtom::Glyph { text, x, .. } if text == "0" => Some(*x),
4361 _ => None,
4362 })
4363 .expect("lower integral script");
4364 let upper = layout
4365 .atoms
4366 .iter()
4367 .find_map(|atom| match atom {
4368 MathAtom::Glyph { text, x, .. } if text == "1" => Some(*x),
4369 _ => None,
4370 })
4371 .expect("upper integral script");
4372
4373 assert!(
4374 lower >= integral_rect.right() - 0.5 && upper >= integral_rect.right() - 0.5,
4375 "integral scripts should stay to the side, rect = {integral_rect:?}, lower x = {lower}, upper x = {upper}"
4376 );
4377 }
4378
4379 #[test]
4380 fn parses_tex_left_right_fences() {
4381 let expr = parse_tex(r"\left(\frac{a}{b}\right)").expect("valid fenced tex");
4382 match expr {
4383 MathExpr::Fenced { open, close, body } => {
4384 assert_eq!(open.as_deref(), Some("("));
4385 assert_eq!(close.as_deref(), Some(")"));
4386 assert!(matches!(*body, MathExpr::Fraction { .. }));
4387 }
4388 other => panic!("expected fenced expression, got {other:?}"),
4389 }
4390 let layout = layout_math(
4391 &parse_tex(r"\left(\begin{matrix}a\\b\\c\end{matrix}\right)").unwrap(),
4392 16.0,
4393 MathDisplay::Inline,
4394 );
4395 assert!(
4396 layout
4397 .atoms
4398 .iter()
4399 .any(|atom| matches!(atom, MathAtom::GlyphId { rect, .. } if rect.h > 16.0)),
4400 "fence should emit a stretched OpenType delimiter variant glyph"
4401 );
4402 }
4403
4404 #[test]
4405 fn simple_tex_left_right_fences_remain_glyphs() {
4406 let layout = layout_math(
4407 &parse_tex(r"\left(x\right)").unwrap(),
4408 16.0,
4409 MathDisplay::Inline,
4410 );
4411 assert!(
4412 !layout
4413 .atoms
4414 .iter()
4415 .any(|atom| matches!(atom, MathAtom::Delimiter { .. })),
4416 "simple fences should stay as glyphs below the font stretch threshold"
4417 );
4418 assert!(
4419 layout
4420 .atoms
4421 .iter()
4422 .any(|atom| matches!(atom, MathAtom::Glyph { text, .. } if text == "(")),
4423 "left fence should emit a glyph atom"
4424 );
4425 assert!(
4426 layout
4427 .atoms
4428 .iter()
4429 .any(|atom| matches!(atom, MathAtom::Glyph { text, .. } if text == ")")),
4430 "right fence should emit a glyph atom"
4431 );
4432 }
4433
4434 #[test]
4435 fn stretched_tex_fences_use_open_type_variant_glyphs() {
4436 let layout = layout_math(
4437 &parse_tex(r"\left(\begin{matrix}a&b\\c&d\end{matrix}\right)").unwrap(),
4438 16.0,
4439 MathDisplay::Inline,
4440 );
4441 assert!(
4442 layout
4443 .atoms
4444 .iter()
4445 .any(|atom| matches!(atom, MathAtom::GlyphId { .. })),
4446 "moderately stretched fences should use exact OpenType delimiter variant glyphs"
4447 );
4448 }
4449
4450 #[test]
4451 fn very_tall_tex_fences_use_open_type_assembly_parts() {
4452 let expr =
4453 parse_tex(r"\left\{\begin{matrix}a\\b\\c\\d\\e\\f\\g\\h\end{matrix}\right.").unwrap();
4454 let layout = layout_math(&expr, 16.0, MathDisplay::Inline);
4455 let glyph_id_count = layout
4456 .atoms
4457 .iter()
4458 .filter(|atom| matches!(atom, MathAtom::GlyphId { .. }))
4459 .count();
4460 assert!(
4461 glyph_id_count > 2,
4462 "very tall fences should use repeated OpenType assembly glyph parts"
4463 );
4464 assert!(
4465 !layout
4466 .atoms
4467 .iter()
4468 .any(|atom| matches!(atom, MathAtom::Delimiter { .. })),
4469 "font assembly should avoid the hand-drawn delimiter fallback"
4470 );
4471 let MathExpr::Fenced { body, .. } = expr else {
4472 panic!("expected fenced expression");
4473 };
4474 let ctx = LayoutCtx {
4475 size: 16.0,
4476 display: MathDisplay::Inline,
4477 };
4478 let target_rect = delimiter_rect(&layout_expr(&body, ctx), ctx);
4479 let assembled_top = layout
4480 .atoms
4481 .iter()
4482 .filter_map(|atom| match atom {
4483 MathAtom::GlyphId { rect, .. } => Some(rect.y),
4484 _ => None,
4485 })
4486 .fold(f32::INFINITY, f32::min);
4487 let assembled_bottom = layout
4488 .atoms
4489 .iter()
4490 .filter_map(|atom| match atom {
4491 MathAtom::GlyphId { rect, .. } => Some(rect.y + rect.h),
4492 _ => None,
4493 })
4494 .fold(f32::NEG_INFINITY, f32::max);
4495 assert!(
4496 assembled_bottom - assembled_top <= target_rect.h + 0.5,
4497 "assembled delimiter height should track target height"
4498 );
4499 }
4500
4501 #[test]
4502 fn rejects_unmatched_tex_right_fence() {
4503 let err = parse_tex(r"x \right)").expect_err("invalid unmatched fence");
4504 assert!(err.message.contains("unexpected \\right"));
4505 }
4506
4507 #[test]
4508 fn parses_tex_matrix_environment() {
4509 let expr = parse_tex(r"\begin{matrix}a&b\\c&d\end{matrix}").expect("valid matrix");
4510 match expr {
4511 MathExpr::Table {
4512 rows,
4513 column_alignments,
4514 ..
4515 } => {
4516 assert_eq!(rows.len(), 2);
4517 assert_eq!(rows[0].len(), 2);
4518 assert_eq!(rows[1].len(), 2);
4519 assert_eq!(rows[0][0], MathExpr::Identifier("a".into()));
4520 assert_eq!(rows[1][1], MathExpr::Identifier("d".into()));
4521 assert!(column_alignments.is_empty());
4522 }
4523 other => panic!("expected table expression, got {other:?}"),
4524 }
4525 }
4526
4527 #[test]
4528 fn parses_tex_bmatrix_as_fenced_table() {
4529 let expr =
4530 parse_tex(r"\begin{bmatrix}a&b\\c&d\end{bmatrix}").expect("valid bracketed matrix");
4531 match expr {
4532 MathExpr::Fenced { open, close, body } => {
4533 assert_eq!(open.as_deref(), Some("["));
4534 assert_eq!(close.as_deref(), Some("]"));
4535 match body.as_ref() {
4536 MathExpr::Table { rows, .. } => {
4537 assert_eq!(rows.len(), 2);
4538 assert_eq!(rows[0].len(), 2);
4539 }
4540 other => panic!("expected table body, got {other:?}"),
4541 }
4542 }
4543 other => panic!("expected fenced matrix, got {other:?}"),
4544 }
4545 }
4546
4547 #[test]
4548 fn parses_tex_cases_as_left_braced_table() {
4549 let expr = parse_tex(r"\begin{cases}x&x>0\\-x&x<0\end{cases}").expect("valid cases");
4550 match expr {
4551 MathExpr::Fenced { open, close, body } => {
4552 assert_eq!(open.as_deref(), Some("{"));
4553 assert_eq!(close.as_deref(), None);
4554 match body.as_ref() {
4555 MathExpr::Table {
4556 column_alignments,
4557 column_gap,
4558 ..
4559 } => {
4560 assert_eq!(
4561 column_alignments,
4562 &vec![MathColumnAlignment::Left, MathColumnAlignment::Left]
4563 );
4564 assert_eq!(*column_gap, Some(CASES_COL_GAP_EM));
4565 }
4566 other => panic!("expected table body, got {other:?}"),
4567 }
4568 }
4569 other => panic!("expected left-braced cases table, got {other:?}"),
4570 }
4571 }
4572
4573 #[test]
4574 fn parses_tex_array_column_alignments() {
4575 let expr = parse_tex(r"\begin{array}{lr}x&100\\xx&2\end{array}").expect("valid array");
4576 match expr {
4577 MathExpr::Table {
4578 rows,
4579 column_alignments,
4580 ..
4581 } => {
4582 assert_eq!(rows.len(), 2);
4583 assert_eq!(
4584 column_alignments,
4585 vec![MathColumnAlignment::Left, MathColumnAlignment::Right]
4586 );
4587 }
4588 other => panic!("expected array table, got {other:?}"),
4589 }
4590 }
4591
4592 #[test]
4593 fn ignores_trailing_tex_table_row_separator() {
4594 let expr = parse_tex(r"\begin{matrix}a&b\\c&d\\\end{matrix}")
4595 .expect("valid matrix with trailing row separator");
4596 match expr {
4597 MathExpr::Table { rows, .. } => {
4598 assert_eq!(rows.len(), 2);
4599 assert_eq!(rows[0].len(), 2);
4600 assert_eq!(rows[1].len(), 2);
4601 }
4602 other => panic!("expected table expression, got {other:?}"),
4603 }
4604 }
4605
4606 #[test]
4607 fn rejects_inconsistent_tex_table_columns() {
4608 let err =
4609 parse_tex(r"\begin{matrix}a&b\\c\end{matrix}").expect_err("invalid ragged matrix");
4610 assert!(err.message.contains("inconsistent column count"));
4611 }
4612
4613 #[test]
4614 fn rejects_mismatched_tex_array_alignment_spec() {
4615 let err = parse_tex(r"\begin{array}{lr}x&100&z\\xx&2&y\end{array}")
4616 .expect_err("invalid array alignment spec");
4617 assert!(err.message.contains("alignment spec has 2 columns"));
4618 }
4619
4620 #[test]
4621 fn table_layout_honors_column_alignment() {
4622 let left_aligned = layout_math(
4623 &MathExpr::Table {
4624 rows: vec![
4625 vec![MathExpr::Identifier("x".into())],
4626 vec![MathExpr::Identifier("xxxx".into())],
4627 ],
4628 column_alignments: vec![MathColumnAlignment::Left],
4629 column_gap: None,
4630 row_gap: None,
4631 },
4632 16.0,
4633 MathDisplay::Inline,
4634 );
4635 let right_aligned = layout_math(
4636 &MathExpr::Table {
4637 rows: vec![
4638 vec![MathExpr::Identifier("x".into())],
4639 vec![MathExpr::Identifier("xxxx".into())],
4640 ],
4641 column_alignments: vec![MathColumnAlignment::Right],
4642 column_gap: None,
4643 row_gap: None,
4644 },
4645 16.0,
4646 MathDisplay::Inline,
4647 );
4648 let left_x = left_aligned
4649 .atoms
4650 .iter()
4651 .find_map(|atom| match atom {
4652 MathAtom::Glyph { text, x, .. } if text == "x" => Some(*x),
4653 _ => None,
4654 })
4655 .expect("left-aligned first cell glyph");
4656 let right_x = right_aligned
4657 .atoms
4658 .iter()
4659 .find_map(|atom| match atom {
4660 MathAtom::Glyph { text, x, .. } if text == "x" => Some(*x),
4661 _ => None,
4662 })
4663 .expect("right-aligned first cell glyph");
4664
4665 assert!(left_x < 0.1, "left-aligned glyph x = {left_x}");
4666 assert!(
4667 right_x > left_x + 10.0,
4668 "right alignment should shift narrow cells across wider columns"
4669 );
4670 }
4671
4672 #[test]
4673 fn table_layout_honors_table_spacing() {
4674 let loose = layout_math(
4675 &MathExpr::Table {
4676 rows: vec![
4677 vec![
4678 MathExpr::Identifier("a".into()),
4679 MathExpr::Identifier("b".into()),
4680 ],
4681 vec![
4682 MathExpr::Identifier("c".into()),
4683 MathExpr::Identifier("d".into()),
4684 ],
4685 ],
4686 column_alignments: Vec::new(),
4687 column_gap: Some(2.0),
4688 row_gap: Some(1.0),
4689 },
4690 16.0,
4691 MathDisplay::Inline,
4692 );
4693 let tight = layout_math(
4694 &MathExpr::Table {
4695 rows: vec![
4696 vec![
4697 MathExpr::Identifier("a".into()),
4698 MathExpr::Identifier("b".into()),
4699 ],
4700 vec![
4701 MathExpr::Identifier("c".into()),
4702 MathExpr::Identifier("d".into()),
4703 ],
4704 ],
4705 column_alignments: Vec::new(),
4706 column_gap: Some(0.25),
4707 row_gap: Some(0.1),
4708 },
4709 16.0,
4710 MathDisplay::Inline,
4711 );
4712
4713 assert!(
4714 loose.width > tight.width + 20.0,
4715 "loose width = {}, tight width = {}",
4716 loose.width,
4717 tight.width
4718 );
4719 assert!(
4720 loose.height() > tight.height() + 10.0,
4721 "loose height = {}, tight height = {}",
4722 loose.height(),
4723 tight.height()
4724 );
4725 }
4726
4727 #[test]
4728 fn table_layout_centers_on_math_axis() {
4729 let layout = layout_math(
4730 &MathExpr::Table {
4731 rows: vec![
4732 vec![
4733 MathExpr::Identifier("a".into()),
4734 MathExpr::Identifier("b".into()),
4735 ],
4736 vec![
4737 MathExpr::Identifier("c".into()),
4738 MathExpr::Identifier("d".into()),
4739 ],
4740 ],
4741 column_alignments: Vec::new(),
4742 column_gap: None,
4743 row_gap: None,
4744 },
4745 16.0,
4746 MathDisplay::Block,
4747 );
4748 let visual_center_y = (layout.descent - layout.ascent) * 0.5;
4749 assert!(
4750 visual_center_y < -2.0,
4751 "table visual center should sit on the math axis above baseline, got {visual_center_y}"
4752 );
4753 }
4754
4755 #[test]
4756 fn math_axis_prefers_open_type_axis_height() {
4757 let size = 14.0;
4758 let metrics = LayoutCtx {
4759 size,
4760 display: MathDisplay::Block,
4761 }
4762 .metrics();
4763 let expected = metrics
4764 .font_constants()
4765 .and_then(|constants| constants.axis_height(size))
4766 .unwrap_or_else(|| {
4767 metrics
4768 .operator_axis_shift()
4769 .expect("operator axis fallback")
4770 });
4771
4772 assert!(
4773 (metrics.math_axis_shift() - expected).abs() < 0.1,
4774 "axis = {}, expected = {expected}",
4775 metrics.math_axis_shift()
4776 );
4777 }
4778
4779 #[test]
4780 fn rejects_mismatched_tex_environment_end() {
4781 let err = parse_tex(r"\begin{matrix}a\end{pmatrix}").expect_err("invalid environment");
4782 assert!(err.message.contains(r"expected \end{matrix}"));
4783 }
4784
4785 #[test]
4786 fn reports_unclosed_group() {
4787 let err = parse_tex(r"\frac{1}{x").expect_err("invalid tex");
4788 assert!(err.message.contains("unclosed group"));
4789 }
4790
4791 #[test]
4792 fn parses_mathml_fraction_with_scripts() {
4793 let expr = parse_mathml(
4794 r#"
4795 <math>
4796 <mfrac>
4797 <mrow>
4798 <msup><mi>a</mi><mn>2</mn></msup>
4799 <mo>+</mo>
4800 <msup><mi>b</mi><mn>2</mn></msup>
4801 </mrow>
4802 <msqrt>
4803 <msub><mi>x</mi><mn>1</mn></msub>
4804 <mo>+</mo>
4805 <msub><mi>x</mi><mn>2</mn></msub>
4806 </msqrt>
4807 </mfrac>
4808 </math>
4809 "#,
4810 )
4811 .expect("valid mathml");
4812 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4813 assert!(layout.width > 20.0, "width = {}", layout.width);
4814 assert!(
4815 layout
4816 .atoms
4817 .iter()
4818 .any(|atom| matches!(atom, MathAtom::Rule { .. })),
4819 "fraction should emit rule atoms"
4820 );
4821 assert!(
4822 has_radical_shape(&layout),
4823 "sqrt should emit a radical shape atom"
4824 );
4825 }
4826
4827 #[test]
4828 fn parses_mathml_indexed_root() {
4829 let expr = parse_mathml(
4830 r#"
4831 <math>
4832 <mroot>
4833 <mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow>
4834 <mn>3</mn>
4835 </mroot>
4836 </math>
4837 "#,
4838 )
4839 .expect("valid mathml");
4840 match expr {
4841 MathExpr::Root { base, index } => {
4842 assert_eq!(*index, MathExpr::Number("3".into()));
4843 assert!(matches!(*base, MathExpr::Row(_)));
4844 }
4845 other => panic!("expected indexed root, got {other:?}"),
4846 }
4847 }
4848
4849 #[test]
4850 fn parses_mathml_under_over() {
4851 let expr = parse_mathml(
4852 r#"
4853 <math>
4854 <munderover>
4855 <mo>∑</mo>
4856 <mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow>
4857 <mi>n</mi>
4858 </munderover>
4859 </math>
4860 "#,
4861 )
4862 .expect("valid mathml");
4863 match expr {
4864 MathExpr::UnderOver { base, under, over } => {
4865 assert_eq!(*base, MathExpr::Operator("∑".into()));
4866 assert!(matches!(*under.unwrap(), MathExpr::Row(_)));
4867 assert_eq!(*over.unwrap(), MathExpr::Identifier("n".into()));
4868 }
4869 other => panic!("expected under/over expression, got {other:?}"),
4870 }
4871 }
4872
4873 #[test]
4874 fn parses_mathml_operator_spacing_attributes() {
4875 let expr = parse_mathml(r#"<math><mo lspace="0em" rspace="0.5em">+</mo></math>"#)
4876 .expect("valid spaced operator");
4877 assert_eq!(
4878 expr,
4879 MathExpr::OperatorWithMetadata {
4880 text: "+".into(),
4881 lspace: Some(0.0),
4882 rspace: Some(0.5),
4883 large_operator: None,
4884 movable_limits: None,
4885 }
4886 );
4887
4888 let default_width =
4889 layout_math(&MathExpr::Operator("+".into()), 16.0, MathDisplay::Inline).width;
4890 let custom_width = layout_math(&expr, 16.0, MathDisplay::Inline).width;
4891 assert!(
4892 custom_width > default_width,
4893 "custom width = {custom_width}, default width = {default_width}"
4894 );
4895 }
4896
4897 #[test]
4898 fn parses_mathml_operator_limit_attributes() {
4899 let expr = parse_mathml(
4900 r#"
4901 <math>
4902 <msub>
4903 <mo movablelimits="true">lim</mo>
4904 <mi>x</mi>
4905 </msub>
4906 </math>
4907 "#,
4908 )
4909 .expect("valid movable limits operator");
4910 let layout = layout_math(&expr, 16.0, MathDisplay::Block);
4911 assert!(
4912 layout
4913 .atoms
4914 .iter()
4915 .any(|atom| matches!(atom, MathAtom::Glyph { text, y_baseline, .. } if text == "x" && *y_baseline > 0.0)),
4916 "movablelimits operator should place display subscript underneath"
4917 );
4918
4919 let large = parse_mathml(r#"<math><mo largeop="true">∫</mo></math>"#)
4920 .expect("valid large operator");
4921 assert!(matches!(
4922 large,
4923 MathExpr::OperatorWithMetadata {
4924 large_operator: Some(true),
4925 ..
4926 }
4927 ));
4928 }
4929
4930 #[test]
4931 fn parses_mathml_accent_mover() {
4932 let expr = parse_mathml(
4933 r#"
4934 <math>
4935 <mover accent="true">
4936 <mi>x</mi>
4937 <mo>^</mo>
4938 </mover>
4939 </math>
4940 "#,
4941 )
4942 .expect("valid mathml accent");
4943 match expr {
4944 MathExpr::Accent {
4945 base,
4946 accent,
4947 stretch,
4948 } => {
4949 assert_eq!(*base, MathExpr::Identifier("x".into()));
4950 assert_eq!(*accent, MathExpr::Operator("^".into()));
4951 assert!(!stretch);
4952 }
4953 other => panic!("expected accent expression, got {other:?}"),
4954 }
4955 }
4956
4957 #[test]
4958 fn parses_mathml_semantics_wrapper() {
4959 let expr = parse_mathml(
4960 r#"
4961 <math>
4962 <semantics>
4963 <mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow>
4964 <annotation encoding="application/x-tex">x+1</annotation>
4965 </semantics>
4966 </math>
4967 "#,
4968 )
4969 .expect("valid mathml semantics wrapper");
4970 match expr {
4971 MathExpr::Row(children) => {
4972 assert_eq!(children.len(), 3);
4973 assert_eq!(children[0], MathExpr::Identifier("x".into()));
4974 assert_eq!(children[2], MathExpr::Number("1".into()));
4975 }
4976 other => panic!("expected row expression, got {other:?}"),
4977 }
4978 }
4979
4980 #[test]
4981 fn rejects_mathml_semantics_without_presentation_child() {
4982 let err = parse_mathml(
4983 r#"
4984 <math>
4985 <semantics>
4986 <annotation encoding="application/x-tex">x+1</annotation>
4987 </semantics>
4988 </math>
4989 "#,
4990 )
4991 .expect_err("invalid mathml semantics wrapper");
4992 assert!(
4993 err.message
4994 .contains("<semantics> expected a presentation child")
4995 );
4996 }
4997
4998 #[test]
4999 fn parses_mathml_fenced_expression() {
5000 let expr = parse_mathml(
5001 r#"
5002 <math>
5003 <mfenced open="[" close="]" separators=",">
5004 <mi>a</mi>
5005 <mi>b</mi>
5006 </mfenced>
5007 </math>
5008 "#,
5009 )
5010 .expect("valid mathml fenced expression");
5011 match expr {
5012 MathExpr::Fenced { open, close, body } => {
5013 assert_eq!(open.as_deref(), Some("["));
5014 assert_eq!(close.as_deref(), Some("]"));
5015 match body.as_ref() {
5016 MathExpr::Row(children) => {
5017 assert_eq!(children.len(), 3);
5018 assert_eq!(children[1], MathExpr::Operator(",".into()));
5019 }
5020 other => panic!("expected row body, got {other:?}"),
5021 }
5022 }
5023 other => panic!("expected fenced expression, got {other:?}"),
5024 }
5025 }
5026
5027 #[test]
5028 fn parses_mathml_table() {
5029 let expr = parse_mathml(
5030 r#"
5031 <math>
5032 <mtable>
5033 <mtr>
5034 <mtd><mi>a</mi></mtd>
5035 <mtd><mi>b</mi></mtd>
5036 </mtr>
5037 <mtr>
5038 <mtd><mi>c</mi></mtd>
5039 <mtd><mi>d</mi></mtd>
5040 </mtr>
5041 </mtable>
5042 </math>
5043 "#,
5044 )
5045 .expect("valid mathml");
5046 match expr {
5047 MathExpr::Table { rows, .. } => {
5048 assert_eq!(rows.len(), 2);
5049 assert_eq!(rows[0].len(), 2);
5050 assert_eq!(rows[1].len(), 2);
5051 }
5052 other => panic!("expected table expression, got {other:?}"),
5053 }
5054 let layout = layout_math(
5055 &parse_mathml(
5056 r#"<math><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd><mtd><mi>d</mi></mtd></mtr></mtable></math>"#,
5057 )
5058 .unwrap(),
5059 16.0,
5060 MathDisplay::Block,
5061 );
5062 assert!(layout.width > 20.0, "width = {}", layout.width);
5063 assert!(layout.ascent > 10.0, "ascent = {}", layout.ascent);
5064 assert!(layout.descent > 10.0, "descent = {}", layout.descent);
5065 }
5066
5067 #[test]
5068 fn parses_mathml_table_column_alignment() {
5069 let expr = parse_mathml(
5070 r#"
5071 <math>
5072 <mtable columnalign="left right">
5073 <mtr>
5074 <mtd><mi>x</mi></mtd>
5075 <mtd><mn>100</mn></mtd>
5076 </mtr>
5077 </mtable>
5078 </math>
5079 "#,
5080 )
5081 .expect("valid aligned mathml table");
5082 match expr {
5083 MathExpr::Table {
5084 column_alignments, ..
5085 } => {
5086 assert_eq!(
5087 column_alignments,
5088 vec![MathColumnAlignment::Left, MathColumnAlignment::Right]
5089 );
5090 }
5091 other => panic!("expected table expression, got {other:?}"),
5092 }
5093 }
5094
5095 #[test]
5096 fn parses_mathml_table_spacing() {
5097 let expr = parse_mathml(
5098 r#"
5099 <math>
5100 <mtable columnspacing="0.5em" rowspacing="0.2em">
5101 <mtr>
5102 <mtd><mi>a</mi></mtd>
5103 <mtd><mi>b</mi></mtd>
5104 </mtr>
5105 <mtr>
5106 <mtd><mi>c</mi></mtd>
5107 <mtd><mi>d</mi></mtd>
5108 </mtr>
5109 </mtable>
5110 </math>
5111 "#,
5112 )
5113 .expect("valid spaced mathml table");
5114 match expr {
5115 MathExpr::Table {
5116 column_gap,
5117 row_gap,
5118 ..
5119 } => {
5120 assert_eq!(column_gap, Some(0.5));
5121 assert_eq!(row_gap, Some(0.2));
5122 }
5123 other => panic!("expected table expression, got {other:?}"),
5124 }
5125 }
5126
5127 #[test]
5128 fn parses_mathml_display_attribute() {
5129 let (expr, display) = parse_mathml_with_display(
5130 r#"<math display="block"><msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup></math>"#,
5131 )
5132 .expect("valid mathml");
5133 assert_eq!(display, MathDisplay::Block);
5134 match expr {
5135 MathExpr::Scripts { base, sub, sup } => {
5136 assert_eq!(*base, MathExpr::Identifier("x".into()));
5137 assert_eq!(*sub.unwrap(), MathExpr::Number("1".into()));
5138 assert_eq!(*sup.unwrap(), MathExpr::Number("2".into()));
5139 }
5140 other => panic!("expected scripts expression, got {other:?}"),
5141 }
5142 }
5143
5144 #[test]
5145 fn rejects_wrong_mathml_arity() {
5146 let err =
5147 parse_mathml(r#"<math><mfrac><mi>a</mi></mfrac></math>"#).expect_err("invalid arity");
5148 assert!(err.message.contains("expected 2 element children"));
5149 }
5150}