1use core::cell::Cell;
4use core::cmp::Ordering;
5
6use crate::atoms::symbol_atom_kind;
7use crate::color::Color;
8use crate::dim::Dim;
9use crate::error::Error;
10use crate::font::MathFont;
11use crate::layout::metrics::MathParams;
12use crate::layout::numbering::NumberingState;
13use crate::layout::space::{atom_space_mu, convert_bin, space_width};
14use crate::layout::style::MathStyle;
15use crate::layout::{BoxContent, MathBox};
16use crate::parser::DEFAULT_MAX_NESTING_DEPTH;
17use crate::parser::{
18 AccentKind, AtomKind, ColSpec, DelimSize, Delimiter, EnvRow, IntegralKind, MathNode,
19 MatrixStyle, PhantomKind, SpaceKind, TextStyle,
20};
21use crate::style_map::styled_char;
22use crate::symbols::lookup;
23
24pub fn layout(node: &MathNode, font: &MathFont, style: MathStyle) -> Result<MathBox, Error> {
56 let mut state = NumberingState::default();
57 layout_with_numbering(node, font, style, &mut state)
58}
59
60pub fn layout_with_numbering(
89 node: &MathNode,
90 font: &MathFont,
91 style: MathStyle,
92 state: &mut NumberingState,
93) -> Result<MathBox, Error> {
94 layout_impl(node, font, style, state, DEFAULT_MAX_NESTING_DEPTH)
95}
96
97pub fn layout_with_max_depth(
119 node: &MathNode,
120 font: &MathFont,
121 style: MathStyle,
122 max_depth: usize,
123) -> Result<MathBox, Error> {
124 let mut state = NumberingState::default();
125 layout_impl(node, font, style, &mut state, max_depth)
126}
127
128fn layout_impl(
129 node: &MathNode,
130 font: &MathFont,
131 style: MathStyle,
132 state: &mut NumberingState,
133 max_depth: usize,
134) -> Result<MathBox, Error> {
135 let params = MathParams::from_font(font)?;
136 let start = state.collect(node);
137 Engine {
138 font,
139 params,
140 numbers: state,
141 idx: Cell::new(start),
142 depth: Cell::new(0),
143 max_depth,
144 }
145 .layout(node, style)
146}
147
148struct Engine<'a> {
149 font: &'a MathFont,
150 params: MathParams,
151 numbers: &'a NumberingState,
152 idx: Cell<usize>,
153 depth: Cell<usize>,
155 max_depth: usize,
157}
158
159struct Item {
160 bx: MathBox,
161 class: Option<AtomKind>,
162}
163
164impl Engine<'_> {
165 fn layout(&self, node: &MathNode, style: MathStyle) -> Result<MathBox, Error> {
166 Ok(self.item(node, style)?.bx)
167 }
168
169 fn item(&self, node: &MathNode, style: MathStyle) -> Result<Item, Error> {
175 let depth = self.depth.get();
176 if depth >= self.max_depth {
177 return Err(Error::Unsupported {
178 what: format!("tree nests deeper than {} levels", self.max_depth),
179 });
180 }
181 self.depth.set(depth + 1);
182 let out = self.item_inner(node, style);
183 self.depth.set(depth);
184 out
185 }
186
187 fn item_inner(&self, node: &MathNode, style: MathStyle) -> Result<Item, Error> {
188 match node {
189 MathNode::Atom(c, k) => {
190 let bx = self.glyph(math_italic(math_char(*c)), style)?;
191 Ok(Item {
192 bx,
193 class: Some(*k),
194 })
195 }
196 MathNode::Symbol(name) => {
197 let ch = math_italic(symbol_char(name)?);
198 let bx = self.glyph(ch, style)?;
199 Ok(Item {
200 bx,
201 class: Some(symbol_class(name)),
202 })
203 }
204 MathNode::Row(items) => self.row(items, style),
205 MathNode::Fraction(num, den) => self.fraction(num, den, style),
206 MathNode::Radical(deg, rad) => self.radical(deg.as_deref(), rad, style),
207 MathNode::Superscript(base, exp) => self.scripts(base, None, Some(exp), style),
208 MathNode::Subscript(base, sub) => self.scripts(base, Some(sub), None, style),
209 MathNode::SubSup(base, sub, exp) => self.scripts(base, Some(sub), Some(exp), style),
210 MathNode::Delimited(open, body, close) => self.delimited(open, body, close, style),
211 MathNode::SizedDelim(d, size, k) => {
212 let needed = self.explicit_delim_span(*size, style);
213 Ok(Item {
214 class: Some(*k),
215 bx: self.delim_box(d, &needed, style)?,
216 })
217 }
218 MathNode::Space(kind) => Ok(Item {
219 bx: MathBox::kern(self.space_dim(kind, style)),
220 class: None,
221 }),
222 MathNode::Strut(h, d) => {
223 let s = self.params.scale(style);
224 Ok(Item {
225 bx: MathBox {
226 width: Dim::zero(),
227 height: h * &s,
228 depth: d * &s,
229 italic: Dim::zero(),
230 shift: Dim::zero(),
231 content: BoxContent::Empty,
232 },
233 class: Some(AtomKind::Ord),
234 })
235 }
236 MathNode::Phantom(kind, inner) => {
237 let mut bx = self.layout(inner, style)?;
238 match kind {
239 PhantomKind::Full => bx.content = BoxContent::Empty,
240 PhantomKind::Vertical => {
241 bx.width = Dim::zero();
242 bx.content = BoxContent::Empty;
243 }
244 PhantomKind::Horizontal => {
245 bx.height = Dim::zero();
246 bx.depth = Dim::zero();
247 bx.content = BoxContent::Empty;
248 }
249 }
250 Ok(Item {
251 class: class_of(inner),
252 bx,
253 })
254 }
255 MathNode::Text(s, ts) => self.text_run(s, *ts, style),
256 MathNode::Operator(name, limits) => self.operator(name, *limits, style),
257 MathNode::Sum(lo, hi) => self.large_op('∑', lo.as_deref(), hi.as_deref(), style, true),
258 MathNode::Product(lo, hi) => {
259 self.large_op('∏', lo.as_deref(), hi.as_deref(), style, true)
260 }
261 MathNode::Integral(k, lo, hi) => {
262 let ch = match k {
263 IntegralKind::Int => '∫',
264 IntegralKind::Iint => '∬',
265 IntegralKind::Iiint => '∭',
266 IntegralKind::Oint => '∮',
267 IntegralKind::Oiint => '∯',
268 };
269 self.large_op(ch, lo.as_deref(), hi.as_deref(), style, false)
270 }
271 MathNode::Limit(sub) => {
272 let op = self.text_run("lim", TextStyle::Rm, style)?;
273 if let Some(s) = sub {
274 self.attach_limits(op.bx, None, Some(s), style, true)
275 } else {
276 Ok(Item {
277 bx: op.bx,
278 class: Some(AtomKind::Op),
279 })
280 }
281 }
282 MathNode::OverUnder(base, over, under) => {
283 let mut b = self.layout(base, style)?;
284 let mut needed = b.width.clone();
285 if let Some(o) = over {
286 needed = needed.max(&self.layout(o, style.into_script())?.width);
287 }
288 if let Some(u) = under {
289 needed = needed.max(&self.layout(u, style.into_script())?.width);
290 }
291 b = self.stretch_h(b, &needed, style)?;
292 self.attach_limits(b, over.as_deref(), under.as_deref(), style, true)
293 }
294 MathNode::Accent(base, kind) => self.accent(base, *kind, style),
295 MathNode::CancelTo(value, expr) => self.cancelto(value, expr, style),
296 MathNode::Matrix(ms, spec, rows) => self.matrix(*ms, spec, rows, style),
297 MathNode::Substack(lines) => self.substack(lines, style),
298 MathNode::Ref(key) => self.reference(key, style),
299 MathNode::Tag { star, body } => self.tag_box(*star, body, style),
300 MathNode::Label(_) | MathNode::NoNumber => Ok(Item {
301 bx: MathBox::empty(),
302 class: None,
303 }),
304 MathNode::Hline => Err(Error::Unsupported {
305 what: "hline outside array".into(),
306 }),
307 MathNode::Intertext(n) => Ok(Item {
308 class: Some(AtomKind::Ord),
309 bx: self.layout(n, MathStyle::Text)?,
310 }),
311 MathNode::Color(c, body) | MathNode::TextColor(c, body) => {
312 let inner = self.layout(body, style)?;
313 Ok(Item {
314 class: class_of(body),
315 bx: color_wrap(*c, inner),
316 })
317 }
318 MathNode::ColorBox(c, body) => {
319 let inner = self.layout(body, style)?;
320 let pad = self.params.mu(style) * Dim::from_i64(3);
321 Ok(Item {
322 class: Some(AtomKind::Inner),
323 bx: back_color_wrap(*c, pad_box(inner, &pad)),
324 })
325 }
326 MathNode::FColorBox(border, fill, body) => {
327 let inner = self.layout(body, style)?;
328 let pad = self.params.mu(style) * Dim::from_i64(3);
329 let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
330 Ok(Item {
331 class: Some(AtomKind::Inner),
332 bx: back_color_wrap(
333 *fill,
334 frame_wrap(thick, Some(*border), pad_box(inner, &pad)),
335 ),
336 })
337 }
338 }
339 }
340
341 fn glyph(&self, ch: char, style: MathStyle) -> Result<MathBox, Error> {
342 let g = self.font.glyph(ch)?;
343 let s = self.params.scale(style);
344 let italic = self.font.italic_correction(g.glyph_id);
345 Ok(MathBox {
346 width: &g.advance * &s,
347 height: &g.height * &s,
348 depth: &g.depth * &s,
349 italic: &italic * &s,
350 shift: Dim::zero(),
351 content: BoxContent::glyph(ch, g.glyph_id, s),
352 })
353 }
354
355 fn glyph_id(&self, ch: char, gid: u16, style: MathStyle) -> Result<MathBox, Error> {
356 let g = self.font.glyph_id(ch, gid)?;
357 let s = self.params.scale(style);
358 let italic = self.font.italic_correction(gid);
359 Ok(MathBox {
360 width: &g.advance * &s,
361 height: &g.height * &s,
362 depth: &g.depth * &s,
363 italic: &italic * &s,
364 shift: Dim::zero(),
365 content: BoxContent::glyph(ch, gid, s),
366 })
367 }
368
369 fn space_dim(&self, kind: &SpaceKind, style: MathStyle) -> Dim {
370 let mu = self.params.mu(style);
371 match kind {
372 SpaceKind::Thin => mu * Dim::from_i64(3),
373 SpaceKind::Medium => mu * Dim::from_i64(4),
374 SpaceKind::Thick => mu * Dim::from_i64(5),
375 SpaceKind::NegThin => -(mu * Dim::from_i64(3)),
376 SpaceKind::Quad => self.params.em(style),
377 SpaceKind::Qquad => self.params.em(style) * Dim::from_i64(2),
378 SpaceKind::ControlSpace => self.params.em(style) / Dim::from_i64(3),
379 SpaceKind::Hspace(d) => d * &self.params.scale(style),
380 }
381 }
382
383 fn text_run(&self, s: &str, ts: TextStyle, style: MathStyle) -> Result<Item, Error> {
384 if ts == TextStyle::Pmb {
385 return self.pmb(s, style);
386 }
387 let mut kids = Vec::new();
388 for c in s.chars() {
389 if c == ' ' {
390 kids.push(MathBox::kern(self.params.mu(style) * Dim::from_i64(4)));
391 } else {
392 kids.push(self.glyph(styled_char(c, ts), style)?);
393 }
394 }
395 Ok(Item {
396 bx: MathBox::hpack(kids),
397 class: Some(AtomKind::Ord),
398 })
399 }
400
401 fn pmb(&self, s: &str, style: MathStyle) -> Result<Item, Error> {
402 let base = self.text_run(s, TextStyle::Rm, style)?;
403 let dx = self.params.em(style) / Dim::from_i64(25);
404 let shifted = MathBox::hpack(vec![MathBox::kern(dx.clone()), base.bx.clone()]);
405 Ok(Item {
406 class: Some(AtomKind::Ord),
407 bx: MathBox {
408 width: &base.bx.width + &dx,
409 height: base.bx.height.clone(),
410 depth: base.bx.depth.clone(),
411 italic: Dim::zero(),
412 shift: Dim::zero(),
413 content: BoxContent::Overlap(vec![base.bx, shifted]),
414 },
415 })
416 }
417
418 fn operator(&self, name: &str, limits: bool, style: MathStyle) -> Result<Item, Error> {
419 if let Some(ch) = single_glyph(name) {
420 if !ch.is_ascii_alphabetic() {
421 return self.large_op(ch, None, None, style, limits);
422 }
423 }
424 self.text_run(name, TextStyle::Rm, style).map(|mut it| {
425 it.class = Some(AtomKind::Op);
426 it
427 })
428 }
429
430 fn row(&self, items: &[MathNode], style: MathStyle) -> Result<Item, Error> {
431 if items.is_empty() {
432 return Ok(Item {
433 bx: MathBox::empty(),
434 class: Some(AtomKind::Ord),
435 });
436 }
437 let mut laid = Vec::new();
438 for n in items {
439 laid.push(self.item(n, style)?);
440 }
441 let n = laid.len();
442 let mut classes: Vec<Option<AtomKind>> = Vec::with_capacity(n);
443 for i in 0..n {
444 let prev = if i == 0 { None } else { laid[i - 1].class };
445 let next = if i + 1 < n { laid[i + 1].class } else { None };
446 classes.push(laid[i].class.map(|c| convert_bin(prev, c, next)));
447 }
448 let mut out = Vec::new();
449 for i in 0..n {
450 if i > 0 {
451 if let (Some(l), Some(r)) = (classes[i - 1], classes[i]) {
452 let mu = atom_space_mu(l, r, style);
453 let w = space_width(mu, &self.params, style);
454 if !w.is_zero() {
455 out.push(MathBox::kern(w));
456 }
457 }
458 }
459 out.push(laid[i].bx.clone());
460 }
461 let class = if n == 1 {
462 classes[0]
463 } else {
464 Some(AtomKind::Ord)
465 };
466 Ok(Item {
467 bx: MathBox::hpack(out),
468 class,
469 })
470 }
471
472 fn fraction(&self, num: &MathNode, den: &MathNode, style: MathStyle) -> Result<Item, Error> {
473 let num_b = self.layout(num, style.numerator())?;
474 let den_b = self.layout(den, style.denominator())?;
475 let s = self.params.scale(style);
476 let axis = &self.params.axis_height * &s;
477 let thick = &self.params.fraction_rule_thickness * &s;
478 let half = &thick / &Dim::from_i64(2);
479 let (shift_up0, shift_dn0, gap_num, gap_den) = if style.is_display() {
480 (
481 &self.params.fraction_numerator_display_style_shift_up * &s,
482 &self.params.fraction_denominator_display_style_shift_down * &s,
483 &self.params.fraction_num_display_style_gap_min * &s,
484 &self.params.fraction_denom_display_style_gap_min * &s,
485 )
486 } else {
487 (
488 &self.params.fraction_numerator_shift_up * &s,
489 &self.params.fraction_denominator_shift_down * &s,
490 &self.params.fraction_numerator_gap_min * &s,
491 &self.params.fraction_denominator_gap_min * &s,
492 )
493 };
494 let num_shift = shift_up0.max(&(&axis + &half + &gap_num + &num_b.depth));
495 let den_shift = shift_dn0.max(&(&den_b.height + &gap_den + &half - &axis).clamp_nonneg());
496 let width = num_b.width.max(&den_b.width);
497 let num_c = center_in(num_b, &width);
498 let den_c = center_in(den_b, &width);
499 let num_h = num_c.height.clone();
500 let den_d = den_c.depth.clone();
501 let bar =
502 MathBox::rule(width.clone(), thick.clone(), Dim::zero()).with_shift(&axis - &half);
503 Ok(Item {
504 class: Some(AtomKind::Inner),
505 bx: MathBox {
506 width,
507 height: &num_shift + &num_h,
508 depth: &den_shift + &den_d,
509 italic: Dim::zero(),
510 shift: Dim::zero(),
511 content: BoxContent::Overlap(vec![
512 num_c.with_shift(num_shift),
513 bar,
514 den_c.with_shift(-den_shift),
515 ]),
516 },
517 })
518 }
519
520 fn radical(
521 &self,
522 deg: Option<&MathNode>,
523 rad: &MathNode,
524 style: MathStyle,
525 ) -> Result<Item, Error> {
526 let rad_b = self.layout(rad, style.cramp())?;
527 let s = self.params.scale(style);
528 let thick = &self.params.radical_rule_thickness * &s;
529 let extra = &self.params.radical_extra_ascender * &s;
530 let gap = if style.is_display() {
531 &self.params.radical_display_style_vertical_gap * &s
532 } else {
533 &self.params.radical_vertical_gap * &s
534 };
535 let needed = &rad_b.height + &rad_b.depth + &gap + &thick + &extra;
536 let surd = self.sized_glyph('√', &needed, style)?;
537 let bar = MathBox::rule(rad_b.width.clone(), thick.clone(), Dim::zero())
538 .with_shift(&rad_b.height + &gap);
539 let rad_col = MathBox {
540 width: rad_b.width.clone(),
541 height: &rad_b.height + &gap + &thick + &extra,
542 depth: rad_b.depth.clone(),
543 italic: Dim::zero(),
544 shift: Dim::zero(),
545 content: BoxContent::Overlap(vec![bar, rad_b.clone()]),
546 };
547 let mut kids = vec![surd, rad_col];
548 let mut width = MathBox::hpack(vec![kids[0].clone(), kids[1].clone()]).width;
549 let height = (&rad_b.height + &gap + &thick + &extra).max(&kids[0].height);
550 let depth = rad_b.depth.max(&kids[0].depth);
551 if let Some(d) = deg {
552 let db = self.layout(d, MathStyle::ScriptScript)?;
553 let before = &self.params.radical_kern_before_degree * &s;
554 let after = &self.params.radical_kern_after_degree * &s;
555 let pct = Dim::from_i64(i64::from(self.params.radical_degree_bottom_raise_percent))
556 / Dim::from_i64(100);
557 let raise = &height * &pct;
558 let deg_box = db.with_shift(raise);
559 kids = vec![
560 MathBox::kern(before),
561 deg_box,
562 MathBox::kern(after),
563 kids[0].clone(),
564 kids[1].clone(),
565 ];
566 width = MathBox::hpack(kids.clone()).width;
567 }
568 Ok(Item {
569 class: Some(AtomKind::Inner),
570 bx: MathBox {
571 width,
572 height,
573 depth,
574 italic: Dim::zero(),
575 shift: Dim::zero(),
576 content: BoxContent::HList(kids),
577 },
578 })
579 }
580
581 fn scripts(
582 &self,
583 base: &MathNode,
584 sub: Option<&MathNode>,
585 sup: Option<&MathNode>,
586 style: MathStyle,
587 ) -> Result<Item, Error> {
588 let base_it = self.item(base, style)?;
589 self.attach_scripts_to_box(base_it.bx, base_it.class, sub, sup, style)
590 }
591
592 fn attach_scripts_to_box(
593 &self,
594 base: MathBox,
595 class: Option<AtomKind>,
596 sub: Option<&MathNode>,
597 sup: Option<&MathNode>,
598 style: MathStyle,
599 ) -> Result<Item, Error> {
600 if sub.is_none() && sup.is_none() {
601 return Ok(Item { bx: base, class });
602 }
603 let s = self.params.scale(style);
604 let ss = style.into_script();
605 let after = &self.params.space_after_script * &self.params.scale(ss);
606 let mut sup_shift = Dim::zero();
607 let mut sub_shift = Dim::zero();
608 let sup_laid = if let Some(e) = sup {
609 sup_shift = if style.is_cramped() {
610 &self.params.superscript_shift_up_cramped * &s
611 } else {
612 &self.params.superscript_shift_up * &s
613 };
614 Some(self.layout(e, ss.cramp())?)
615 } else {
616 None
617 };
618 let sub_laid = if let Some(u) = sub {
619 sub_shift = &self.params.subscript_shift_down * &s;
620 Some(self.layout(u, ss)?)
621 } else {
622 None
623 };
624 if let (Some(sp), Some(sb)) = (&sup_laid, &sub_laid) {
625 let gap = &sup_shift + &sub_shift - &sp.depth - &sb.height;
626 let min_gap = &self.params.sub_superscript_gap_min * &s;
627 if matches!(gap.cmp(&min_gap), Some(Ordering::Less)) {
628 sub_shift = &sub_shift + (&min_gap - &gap);
629 }
630 }
631 let mut kids = vec![base.clone()];
632 let mut width = base.width.clone();
633 if sup_laid.is_some() && !base.italic.is_zero() {
634 kids.push(MathBox::kern(base.italic.clone()));
635 width = &width + &base.italic;
636 }
637 let mut slot_w = Dim::zero();
638 let mut slot_h = Dim::zero();
639 let mut slot_d = Dim::zero();
640 let mut slot_kids = Vec::new();
641 if let Some(sp) = sup_laid {
642 slot_w = slot_w.max(&sp.width);
643 slot_h = slot_h.max(&(&sp.height + &sup_shift));
644 slot_d = slot_d.max(&(&sp.depth - &sup_shift).clamp_nonneg());
645 slot_kids.push(sp.with_shift(sup_shift));
646 }
647 if let Some(sb) = sub_laid {
648 slot_w = slot_w.max(&sb.width);
649 slot_h = slot_h.max(&(&sb.height - &sub_shift).clamp_nonneg());
650 slot_d = slot_d.max(&(&sb.depth + &sub_shift));
651 slot_kids.push(sb.with_shift(-sub_shift));
652 }
653 kids.push(MathBox {
654 width: slot_w.clone(),
655 height: slot_h.clone(),
656 depth: slot_d.clone(),
657 italic: Dim::zero(),
658 shift: Dim::zero(),
659 content: BoxContent::Overlap(slot_kids),
660 });
661 if !after.is_zero() {
662 kids.push(MathBox::kern(after.clone()));
663 }
664 width = &width + &slot_w + &after;
665 Ok(Item {
666 class,
667 bx: MathBox {
668 width,
669 height: base.height.max(&slot_h),
670 depth: base.depth.max(&slot_d),
671 italic: Dim::zero(),
672 shift: Dim::zero(),
673 content: BoxContent::HList(kids),
674 },
675 })
676 }
677
678 fn delimited(
679 &self,
680 open: &Delimiter,
681 body: &MathNode,
682 close: &Delimiter,
683 style: MathStyle,
684 ) -> Result<Item, Error> {
685 let body_b = self.layout(body, style)?;
686 let s = self.params.scale(style);
687 let axis = &self.params.axis_height * &s;
688 let above = (&body_b.height - &axis).clamp_nonneg();
689 let below = &body_b.depth + &axis;
690 let needed = above.max(&below) * Dim::from_i64(2);
691 let left = self.delim_box(open, &needed, style)?;
692 let right = self.delim_box(close, &needed, style)?;
693 Ok(Item {
694 class: Some(AtomKind::Inner),
695 bx: MathBox::hpack(vec![left, body_b, right]),
696 })
697 }
698
699 fn explicit_delim_span(&self, size: DelimSize, style: MathStyle) -> Dim {
700 let em = self.params.em(style);
701 let n = match size {
702 DelimSize::Big => 12,
703 DelimSize::Big2 => 18,
704 DelimSize::Bigg => 24,
705 DelimSize::Bigg2 => 30,
706 };
707 em * Dim::from_i64(n) / Dim::from_i64(10)
708 }
709
710 fn delim_box(&self, d: &Delimiter, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
711 match d {
712 Delimiter::Empty => Ok(MathBox::empty()),
713 Delimiter::Char(c) => self.sized_glyph(*c, needed, style),
714 Delimiter::Named(n) => {
715 let ch = named_delim(n)?;
716 self.sized_glyph(ch, needed, style)
717 }
718 }
719 }
720
721 fn sized_glyph(&self, ch: char, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
722 let base = self.font.glyph(ch)?;
723 let mut best = self.glyph(ch, style)?;
724 let mut best_span = &best.height + &best.depth;
725 for gid in self.font.vertical_variants(base.glyph_id) {
726 let cand = self.glyph_id(ch, gid, style)?;
727 let span = &cand.height + &cand.depth;
728 let meets = span.cmp(needed).is_some_and(|o| o != Ordering::Less);
729 let best_short = best_span.cmp(needed).is_some_and(|o| o == Ordering::Less);
730 let tighter = span.cmp(&best_span).is_some_and(|o| o == Ordering::Less);
731 let taller = span.cmp(&best_span).is_some_and(|o| o == Ordering::Greater);
732 if (meets && (best_short || tighter)) || (best_short && taller) {
733 best_span = span;
734 best = cand;
735 }
736 }
737 Ok(best)
738 }
739
740 fn stretch_h(&self, base: MathBox, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
741 let BoxContent::Glyph { ch, glyph_id, .. } = base.content else {
742 return Ok(base);
743 };
744 let mut best = base;
745 let mut best_w = best.width.clone();
746 for gid in self.font.horizontal_variants(glyph_id) {
747 let cand = self.glyph_id(ch, gid, style)?;
748 let wider_needed = best_w.cmp(needed).is_some_and(|o| o == Ordering::Less);
749 let fits = cand.width.cmp(needed).is_some_and(|o| o != Ordering::Less);
750 let tighter = cand.width.cmp(&best_w).is_some_and(|o| o == Ordering::Less);
751 let longer = cand
752 .width
753 .cmp(&best_w)
754 .is_some_and(|o| o == Ordering::Greater);
755 if (fits && (wider_needed || tighter)) || (wider_needed && longer) {
756 best_w = cand.width.clone();
757 best = cand;
758 }
759 }
760 if best_w.cmp(needed).is_some_and(|o| o != Ordering::Less) {
761 return Ok(best);
762 }
763 if let Some(asm) = self.assemble_h(ch, glyph_id, needed, style) {
764 if asm
765 .width
766 .cmp(&best_w)
767 .is_some_and(|o| o == Ordering::Greater)
768 {
769 return Ok(asm);
770 }
771 }
772 Ok(best)
773 }
774
775 fn assemble_h(&self, ch: char, gid: u16, needed: &Dim, style: MathStyle) -> Option<MathBox> {
776 let parts = self.font.horizontal_assembly_parts(gid);
777 if parts.is_empty() {
778 return None;
779 }
780 let s = self.params.scale(style);
781 let fu = |n: u16| Dim::from_font_units(i64::from(n), self.params.units_per_em) * &s;
782 let sequence = |copies: u32| {
783 let mut v = Vec::new();
784 for &(id, start, end, adv, ext) in &parts {
785 if ext {
786 for _ in 0..copies {
787 v.push((id, start, end, adv));
788 }
789 } else {
790 v.push((id, start, end, adv));
791 }
792 }
793 v
794 };
795 let width_of = |seq: &[(u16, u16, u16, u16)]| {
796 if seq.is_empty() {
797 return Dim::zero();
798 }
799 let mut w = fu(seq[0].3);
800 for i in 1..seq.len() {
801 let overlap = seq[i - 1].2.min(seq[i].1);
802 w = &w + &fu(seq[i].3) - &fu(overlap);
803 }
804 w
805 };
806 let mut copies = 0u32;
807 while copies < 64 {
808 if width_of(&sequence(copies))
809 .cmp(needed)
810 .is_some_and(|o| o != Ordering::Less)
811 {
812 break;
813 }
814 copies += 1;
815 }
816 let seq = sequence(copies);
817 if seq.is_empty() {
818 return None;
819 }
820 let mut kids = Vec::new();
821 for (i, &(id, start, _, _)) in seq.iter().enumerate() {
822 if i > 0 {
823 let overlap = seq[i - 1].2.min(start);
824 kids.push(MathBox::kern(-(fu(overlap))));
825 }
826 kids.push(self.glyph_id(ch, id, style).ok()?);
827 }
828 Some(MathBox::hpack(kids))
829 }
830
831 fn large_op(
832 &self,
833 ch: char,
834 lo: Option<&MathNode>,
835 hi: Option<&MathNode>,
836 style: MathStyle,
837 limits_in_display: bool,
838 ) -> Result<Item, Error> {
839 let min_h = if style.is_display() {
840 self.params.display_operator_min_height.clone() * self.params.scale(style)
841 } else {
842 Dim::zero()
843 };
844 let op = self.sized_glyph(ch, &min_h, style)?;
845 let use_limits = limits_in_display && style.is_display();
846 self.attach_limits(op, hi, lo, style, use_limits)
847 }
848
849 fn attach_limits(
850 &self,
851 op: MathBox,
852 over: Option<&MathNode>,
853 under: Option<&MathNode>,
854 style: MathStyle,
855 as_limits: bool,
856 ) -> Result<Item, Error> {
857 if !as_limits {
858 return self.attach_scripts_to_box(op, Some(AtomKind::Op), under, over, style);
859 }
860 let s = self.params.scale(style);
861 let op_h = op.height.clone();
862 let op_d = op.depth.clone();
863 let over_b = match over {
864 Some(o) => Some(self.layout(o, style.into_script())?),
865 None => None,
866 };
867 let under_b = match under {
868 Some(u) => Some(self.layout(u, style.into_script())?),
869 None => None,
870 };
871 let mut width = op.width.clone();
872 if let Some(ref o) = over_b {
873 width = width.max(&o.width);
874 }
875 if let Some(ref u) = under_b {
876 width = width.max(&u.width);
877 }
878 let mut height = op_h.clone();
879 let mut depth = op_d.clone();
880 let mut kids = vec![center_in(op, &width)];
881 if let Some(ob) = over_b {
882 let gap = self.params.upper_limit_gap_min.clone() * &s;
883 let rise = self.params.upper_limit_baseline_rise_min.clone() * &s;
884 let extra = gap.max(&rise);
885 height = &height + &ob.height + &ob.depth + &extra;
886 let sh = &op_h + &extra + &ob.depth;
887 kids.push(center_in(ob, &width).with_shift(sh));
888 }
889 if let Some(ub) = under_b {
890 let gap = self.params.lower_limit_gap_min.clone() * &s;
891 let drop = self.params.lower_limit_baseline_drop_min.clone() * &s;
892 let extra = gap.max(&drop);
893 depth = &depth + &ub.height + &ub.depth + &extra;
894 let sh = -(&op_d + &extra + &ub.height);
895 kids.push(center_in(ub, &width).with_shift(sh));
896 }
897 Ok(Item {
898 class: Some(AtomKind::Op),
899 bx: MathBox {
900 width,
901 height,
902 depth,
903 italic: Dim::zero(),
904 shift: Dim::zero(),
905 content: BoxContent::Overlap(kids),
906 },
907 })
908 }
909
910 fn not_overlay(&self, base: &MathNode, b: MathBox, style: MathStyle) -> Result<Item, Error> {
911 let slash = match self.glyph('\u{0338}', style) {
912 Ok(bx) => bx,
913 Err(_) => self.glyph('/', style)?,
914 };
915 let width = b.width.max(&slash.width);
916 let two = Dim::from_i64(2);
917 let b_axis = &(&b.height - &b.depth) / &two;
918 let s_axis = &(&slash.height - &slash.depth) / &two;
919 let raise = &b_axis - &s_axis;
920 let height = b.height.max(&(&slash.height + &raise).max(&slash.height));
921 let depth = b.depth.max(&(&slash.depth - &raise).clamp_nonneg());
922 Ok(Item {
923 class: class_of(base),
924 bx: MathBox {
925 width: width.clone(),
926 height,
927 depth,
928 italic: Dim::zero(),
929 shift: Dim::zero(),
930 content: BoxContent::Overlap(vec![
931 center_in(b, &width),
932 center_in(slash, &width).with_shift(raise),
933 ]),
934 },
935 })
936 }
937
938 fn accent(&self, base: &MathNode, kind: AccentKind, style: MathStyle) -> Result<Item, Error> {
939 let nucleus_style = if is_tex_accent(kind) {
940 style.cramp()
941 } else {
942 style
943 };
944 let b = self.layout(base, nucleus_style)?;
945 if kind == AccentKind::Not {
946 return self.not_overlay(base, b, style);
947 }
948 if kind == AccentKind::Boxed {
949 return Ok(Item {
950 class: Some(AtomKind::Ord),
951 bx: self.boxed_frame(b, style),
952 });
953 }
954 if matches!(
955 kind,
956 AccentKind::Cancel | AccentKind::BCancel | AccentKind::XCancel
957 ) {
958 let mut kids = vec![b.clone()];
959 kids.extend(self.cancel_lines(&b, kind, style));
960 return Ok(Item {
961 class: Some(AtomKind::Ord),
962 bx: MathBox {
963 width: b.width.clone(),
964 height: b.height.clone(),
965 depth: b.depth.clone(),
966 italic: Dim::zero(),
967 shift: Dim::zero(),
968 content: BoxContent::Overlap(kids),
969 },
970 });
971 }
972 if matches!(kind, AccentKind::Overline | AccentKind::Underline) {
973 return self.bar_rule(b, kind == AccentKind::Underline, style);
974 }
975 let mut acc = self.accent_glyph(kind, style)?;
976 let stretchy = is_stretchy_accent(kind);
977 if stretchy {
978 acc = self.stretch_h(acc, &b.width, style)?;
979 }
980 if is_under_accent(kind) {
981 return Ok(Item {
982 class: Some(AtomKind::Ord),
983 bx: self.place_under(b, acc, style),
984 });
985 }
986 let x_off = if stretchy {
987 let extra = &b.width - &acc.width;
988 &extra / &Dim::from_i64(2)
989 } else {
990 self.accent_x_off(&b, &acc, kind)
991 };
992 let raise = self.accent_raise(&b, kind, style);
993 Ok(Item {
994 class: Some(AtomKind::Ord),
995 bx: overlay_accent(b, acc, x_off, raise),
996 })
997 }
998
999 fn accent_glyph(&self, kind: AccentKind, style: MathStyle) -> Result<MathBox, Error> {
1000 for &ch in accent_candidates(kind) {
1001 if let Ok(bx) = self.glyph(ch, style) {
1002 return Ok(bx);
1003 }
1004 }
1005 Err(Error::Unsupported {
1006 what: format!("accent {}", kind.gold()),
1007 })
1008 }
1009
1010 fn cancelto(&self, value: &MathNode, expr: &MathNode, style: MathStyle) -> Result<Item, Error> {
1011 let b = self.layout(expr, style)?;
1012 let val = self.layout(value, style.into_script())?;
1013 let mut kids = vec![b.clone()];
1014 kids.extend(self.cancel_lines(&b, AccentKind::Cancel, style));
1015 let gap = self.params.space_after_script.clone() * self.params.scale(style);
1016 let val_x = &b.width + ⪆
1017 let val_shift = &b.height + &val.depth;
1018 let val_w = val.width.clone();
1019 let val_h = val.height.clone();
1020 kids.push(shift_x(val, val_x.clone()).with_shift(val_shift.clone()));
1021 let width = &val_x + &val_w;
1022 let height = b.height.max(&(&val_shift + &val_h));
1023 Ok(Item {
1024 class: Some(AtomKind::Ord),
1025 bx: MathBox {
1026 width,
1027 height,
1028 depth: b.depth.clone(),
1029 italic: Dim::zero(),
1030 shift: Dim::zero(),
1031 content: BoxContent::Overlap(kids),
1032 },
1033 })
1034 }
1035
1036 fn boxed_frame(&self, inner: MathBox, style: MathStyle) -> MathBox {
1037 let pad = self.params.mu(style) * Dim::from_i64(3);
1038 let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
1039 frame_wrap(thick, None, pad_box(inner, &pad))
1040 }
1041
1042 fn bar_rule(&self, b: MathBox, under: bool, style: MathStyle) -> Result<Item, Error> {
1043 let s = self.params.scale(style);
1044 let gap = if under {
1045 self.params.underbar_vertical_gap.clone() * &s
1046 } else {
1047 self.params.overbar_vertical_gap.clone() * &s
1048 };
1049 let thick = if under {
1050 self.params.underbar_rule_thickness.clone() * &s
1051 } else {
1052 self.params.overbar_rule_thickness.clone() * &s
1053 };
1054 let extra = if under {
1055 self.params.underbar_extra_descender.clone() * &s
1056 } else {
1057 self.params.overbar_extra_ascender.clone() * &s
1058 };
1059 let mut height = b.height.clone();
1060 let mut depth = b.depth.clone();
1061 let bar = if under {
1062 depth = &depth + &gap + &thick + &extra;
1063 MathBox::rule(b.width.clone(), thick, Dim::zero()).with_shift(-(&b.depth + &gap))
1064 } else {
1065 height = &height + &gap + &thick + &extra;
1066 MathBox::rule(b.width.clone(), thick, Dim::zero()).with_shift(&b.height + &gap)
1067 };
1068 Ok(Item {
1069 class: Some(AtomKind::Ord),
1070 bx: MathBox {
1071 width: b.width.clone(),
1072 height,
1073 depth,
1074 italic: Dim::zero(),
1075 shift: Dim::zero(),
1076 content: BoxContent::Overlap(vec![b, bar]),
1077 },
1078 })
1079 }
1080
1081 fn cancel_lines(&self, b: &MathBox, kind: AccentKind, style: MathStyle) -> Vec<MathBox> {
1082 let t = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
1083 let w = b.width.clone();
1084 let h = b.height.clone();
1085 let d = b.depth.clone();
1086 let mk = |x1: Dim, y1: Dim, x2: Dim, y2: Dim| MathBox {
1087 width: w.clone(),
1088 height: h.clone(),
1089 depth: d.clone(),
1090 italic: Dim::zero(),
1091 shift: Dim::zero(),
1092 content: BoxContent::Line {
1093 x1,
1094 y1,
1095 x2,
1096 y2,
1097 thickness: t.clone(),
1098 },
1099 };
1100 match kind {
1101 AccentKind::Cancel => vec![mk(Dim::zero(), -d.clone(), w.clone(), h.clone())],
1102 AccentKind::BCancel => vec![mk(Dim::zero(), h.clone(), w.clone(), -d.clone())],
1103 AccentKind::XCancel => vec![
1104 mk(Dim::zero(), -d.clone(), w.clone(), h.clone()),
1105 mk(Dim::zero(), h.clone(), w.clone(), -d.clone()),
1106 ],
1107 _ => Vec::new(),
1108 }
1109 }
1110
1111 fn accent_x_off(&self, base: &MathBox, acc: &MathBox, kind: AccentKind) -> Dim {
1112 let two = Dim::from_i64(2);
1113 let base_att = first_glyph_id(base)
1114 .and_then(|id| self.font.top_accent_attachment(id))
1115 .unwrap_or_else(|| (&base.width + &base.italic) / &two);
1116 let acc_att = first_glyph_id(acc)
1117 .and_then(|id| self.font.top_accent_attachment(id))
1118 .unwrap_or_else(|| &acc.width / &two);
1119 match kind {
1120 AccentKind::Vec | AccentKind::Overrightarrow | AccentKind::Underrightarrow => {
1121 &base.width + &base.italic - &acc.width
1122 }
1123 AccentKind::Acute => &base_att - &acc_att + &(&acc.width / &Dim::from_i64(4)),
1124 AccentKind::Grave => &base_att - &acc_att - &(&acc.width / &Dim::from_i64(4)),
1125 _ => &base_att - &acc_att,
1126 }
1127 }
1128
1129 fn accent_raise(&self, base: &MathBox, kind: AccentKind, style: MathStyle) -> Dim {
1137 let s = self.params.scale(style);
1138 let abh = if style.is_cramped() {
1139 &self.params.flattened_accent_base_height * &s
1140 } else {
1141 &self.params.accent_base_height * &s
1142 };
1143 if is_diacritic_accent(kind) {
1144 (&base.height - &abh).clamp_nonneg()
1145 } else {
1146 base.height.max(&abh)
1147 }
1148 }
1149
1150 fn place_under(&self, base: MathBox, acc: MathBox, style: MathStyle) -> MathBox {
1151 let s = self.params.scale(style);
1152 let gap = self.params.underbar_vertical_gap.clone() * &s;
1153 let extra = self.params.underbar_extra_descender.clone() * &s;
1154 let raise = -(&base.depth + &gap + &acc.height);
1155 let width = base.width.max(&acc.width);
1156 let depth = &base.depth + &gap + &acc.height + &acc.depth + &extra;
1157 MathBox {
1158 width: width.clone(),
1159 height: base.height.clone(),
1160 depth,
1161 italic: Dim::zero(),
1162 shift: Dim::zero(),
1163 content: BoxContent::Overlap(vec![
1164 center_in(base, &width),
1165 center_in(acc, &width).with_shift(raise),
1166 ]),
1167 }
1168 }
1169
1170 fn take_number(&self) -> Option<String> {
1171 let i = self.idx.get();
1172 self.idx.set(i + 1);
1173 self.numbers.assigned(i).map(str::to_string)
1174 }
1175
1176 fn number_box(&self, s: &str, style: MathStyle) -> Result<MathBox, Error> {
1177 Ok(self.text_run(s, TextStyle::Rm, style)?.bx)
1178 }
1179
1180 fn attach_number(&self, body: MathBox, num: Option<MathBox>, style: MathStyle) -> MathBox {
1181 let Some(num) = num else {
1182 return body;
1183 };
1184 let gap = self.params.em(style);
1185 MathBox::hpack(vec![body, MathBox::kern(gap), num])
1186 }
1187
1188 fn reference(&self, key: &str, style: MathStyle) -> Result<Item, Error> {
1189 let s = self.numbers.lookup(key).ok_or_else(|| Error::Unsupported {
1190 what: format!("undefined label {key}"),
1191 })?;
1192 self.text_run(s, TextStyle::Rm, style)
1193 }
1194
1195 fn tag_box(&self, star: bool, body: &MathNode, style: MathStyle) -> Result<Item, Error> {
1196 let inner = self.layout(body, MathStyle::Text)?;
1197 let bx = if star {
1198 inner
1199 } else {
1200 let open = self.glyph('(', style)?;
1201 let close = self.glyph(')', style)?;
1202 MathBox::hpack(vec![open, inner, close])
1203 };
1204 Ok(Item {
1205 class: Some(AtomKind::Ord),
1206 bx,
1207 })
1208 }
1209
1210 fn substack(&self, lines: &[MathNode], style: MathStyle) -> Result<Item, Error> {
1211 let ss = style.into_script();
1212 let sep = self.params.em(ss) / Dim::from_i64(5);
1213 let mut laid = Vec::new();
1214 for ln in lines {
1215 laid.push(self.layout(ln, ss)?);
1216 }
1217 let width = laid.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1218 let mut rows = Vec::new();
1219 for (i, b) in laid.into_iter().enumerate() {
1220 if i > 0 {
1221 rows.push(MathBox {
1222 width: Dim::zero(),
1223 height: Dim::zero(),
1224 depth: sep.clone(),
1225 italic: Dim::zero(),
1226 shift: Dim::zero(),
1227 content: BoxContent::Empty,
1228 });
1229 }
1230 rows.push(align_in(b, &width, ColSpec::Center));
1231 }
1232 Ok(Item {
1233 class: Some(AtomKind::Ord),
1234 bx: MathBox::vpack(rows),
1235 })
1236 }
1237
1238 fn matrix(
1239 &self,
1240 style_m: MatrixStyle,
1241 spec: &[ColSpec],
1242 rows: &[EnvRow],
1243 style: MathStyle,
1244 ) -> Result<Item, Error> {
1245 if rows.is_empty() {
1246 return Ok(Item {
1247 bx: MathBox::empty(),
1248 class: Some(AtomKind::Inner),
1249 });
1250 }
1251 let body_style = if style_m.is_display_env() {
1252 MathStyle::Display
1253 } else {
1254 style
1255 };
1256 match style_m {
1257 MatrixStyle::Align => self.align_env(rows, body_style, true),
1258 MatrixStyle::Aligned | MatrixStyle::Split => self.align_env(rows, body_style, false),
1259 MatrixStyle::Gather => self.gather_env(rows, body_style),
1260 MatrixStyle::Multline => self.multline_env(rows, body_style),
1261 MatrixStyle::Equation => self.equation_env(rows, body_style),
1262 MatrixStyle::Array => self.array_env(spec, rows, body_style),
1263 MatrixStyle::Cases => self.cases_env(rows, body_style),
1264 _ => self.centered_matrix(style_m, rows, body_style),
1265 }
1266 }
1267
1268 fn centered_matrix(
1269 &self,
1270 style_m: MatrixStyle,
1271 rows: &[EnvRow],
1272 style: MathStyle,
1273 ) -> Result<Item, Error> {
1274 let data = data_cells(rows)?;
1275 self.grid(
1276 &data,
1277 style,
1278 None,
1279 self.params.mu(style) * Dim::from_i64(10),
1280 ColSpec::Center,
1281 matrix_delims(style_m),
1282 false,
1283 )
1284 }
1285
1286 fn cases_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1287 let data = data_cells(rows)?;
1288 self.grid(
1289 &data,
1290 style,
1291 None,
1292 self.params.em(style),
1293 ColSpec::Left,
1294 (Some('{'), None),
1295 false,
1296 )
1297 }
1298
1299 fn align_env(&self, rows: &[EnvRow], style: MathStyle, numbered: bool) -> Result<Item, Error> {
1300 let mut math_rows: Vec<Vec<MathBox>> = Vec::new();
1301 let mut nums: Vec<Option<MathBox>> = Vec::new();
1302 let mut extras: Vec<RowKind> = Vec::new();
1303 let mut ncols = 0;
1304 for row in rows {
1305 match row {
1306 EnvRow::Hline => {
1307 extras.push(RowKind::Hline);
1308 }
1309 EnvRow::Intertext(n) => {
1310 extras.push(RowKind::Intertext(Box::new(
1311 self.layout(n, MathStyle::Text)?,
1312 )));
1313 }
1314 EnvRow::Cells { cells, .. } => {
1315 let mut rboxes = Vec::new();
1316 for c in cells {
1317 rboxes.push(self.layout(c, style)?);
1318 }
1319 ncols = ncols.max(rboxes.len());
1320 math_rows.push(rboxes);
1321 extras.push(RowKind::Cells);
1322 if numbered {
1323 nums.push(match self.take_number() {
1324 Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1325 None => None,
1326 });
1327 }
1328 }
1329 }
1330 }
1331 let mut col_w = vec![Dim::zero(); ncols];
1332 for row in &math_rows {
1333 for (j, cell) in row.iter().enumerate() {
1334 col_w[j] = col_w[j].max(&cell.width);
1335 }
1336 }
1337 let pair_sep = self.params.em(style);
1338 let row_sep = self.params.em(style) / Dim::from_i64(5);
1339 let mut packed = Vec::new();
1340 let mut mi = 0;
1341 for extra in extras {
1342 match extra {
1343 RowKind::Hline => {
1344 return Err(Error::Unsupported {
1345 what: "hline in align".into(),
1346 });
1347 }
1348 RowKind::Intertext(t) => {
1349 if !packed.is_empty() {
1350 packed.push(sep_row(row_sep.clone()));
1351 }
1352 packed.push(*t);
1353 }
1354 RowKind::Cells => {
1355 if !packed.is_empty() {
1356 packed.push(sep_row(row_sep.clone()));
1357 }
1358 let mut parts = Vec::new();
1359 let row = pad_row(math_rows[mi].clone(), ncols);
1360 for (j, cell) in row.into_iter().enumerate() {
1361 if j > 0 && j % 2 == 0 {
1362 parts.push(MathBox::kern(pair_sep.clone()));
1363 }
1364 let align = if j % 2 == 0 {
1365 ColSpec::Right
1366 } else {
1367 ColSpec::Left
1368 };
1369 parts.push(align_in(cell, &col_w[j], align));
1370 }
1371 let mut body = MathBox::hpack(parts);
1372 if numbered {
1373 body = self.attach_number(body, nums[mi].clone(), style);
1374 }
1375 packed.push(body);
1376 mi += 1;
1377 }
1378 }
1379 }
1380 Ok(Item {
1381 class: Some(AtomKind::Inner),
1382 bx: MathBox::vpack(packed),
1383 })
1384 }
1385
1386 fn gather_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1387 let row_sep = self.params.em(style) / Dim::from_i64(5);
1388 let mut bodies = Vec::new();
1389 let mut kinds = Vec::new();
1390 for row in rows {
1391 match row {
1392 EnvRow::Hline => {
1393 return Err(Error::Unsupported {
1394 what: "hline in gather".into(),
1395 });
1396 }
1397 EnvRow::Intertext(n) => {
1398 kinds.push(RowKind::Intertext(Box::new(
1399 self.layout(n, MathStyle::Text)?,
1400 )));
1401 }
1402 EnvRow::Cells { cells, .. } => {
1403 let node = if cells.len() == 1 {
1404 cells[0].clone()
1405 } else {
1406 MathNode::Row(cells.clone())
1407 };
1408 bodies.push(self.layout(&node, style)?);
1409 kinds.push(RowKind::Cells);
1410 }
1411 }
1412 }
1413 let max_w = bodies.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1414 let mut packed = Vec::new();
1415 let mut bi = 0;
1416 for kind in kinds {
1417 if !packed.is_empty() {
1418 packed.push(sep_row(row_sep.clone()));
1419 }
1420 match kind {
1421 RowKind::Intertext(t) => packed.push(*t),
1422 RowKind::Cells => {
1423 let body = align_in(bodies[bi].clone(), &max_w, ColSpec::Center);
1424 let num = match self.take_number() {
1425 Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1426 None => None,
1427 };
1428 packed.push(self.attach_number(body, num, style));
1429 bi += 1;
1430 }
1431 RowKind::Hline => {}
1432 }
1433 }
1434 Ok(Item {
1435 class: Some(AtomKind::Inner),
1436 bx: MathBox::vpack(packed),
1437 })
1438 }
1439
1440 fn multline_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1441 let data = data_cells(rows)?;
1442 if data.is_empty() {
1443 let _ = self.take_number();
1444 return Ok(Item {
1445 bx: MathBox::empty(),
1446 class: Some(AtomKind::Inner),
1447 });
1448 }
1449 let mut bodies = Vec::new();
1450 for row in &data {
1451 let node = if row.len() == 1 {
1452 row[0].clone()
1453 } else {
1454 MathNode::Row(row.clone())
1455 };
1456 bodies.push(self.layout(&node, style)?);
1457 }
1458 let max_w = bodies.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1459 let n = bodies.len();
1460 let row_sep = self.params.em(style) / Dim::from_i64(5);
1461 let mut packed = Vec::new();
1462 for (i, b) in bodies.into_iter().enumerate() {
1463 if i > 0 {
1464 packed.push(sep_row(row_sep.clone()));
1465 }
1466 let align = if i == 0 {
1467 ColSpec::Left
1468 } else if i + 1 == n {
1469 ColSpec::Right
1470 } else {
1471 ColSpec::Center
1472 };
1473 packed.push(align_in(b, &max_w, align));
1474 }
1475 let mut inner = MathBox::vpack(packed);
1476 let num = match self.take_number() {
1477 Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1478 None => None,
1479 };
1480 inner = self.attach_number(inner, num, style);
1481 Ok(Item {
1482 class: Some(AtomKind::Inner),
1483 bx: inner,
1484 })
1485 }
1486
1487 fn equation_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1488 let data = data_cells(rows)?;
1489 let mut parts = Vec::new();
1490 for row in &data {
1491 for (i, c) in row.iter().enumerate() {
1492 if i > 0 {
1493 parts.push(MathNode::Space(SpaceKind::Quad));
1494 }
1495 parts.push(c.clone());
1496 }
1497 }
1498 let node = wrap_nodes(parts);
1499 let body = self.layout(&node, style)?;
1500 let num = match self.take_number() {
1501 Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1502 None => None,
1503 };
1504 Ok(Item {
1505 class: Some(AtomKind::Inner),
1506 bx: self.attach_number(body, num, style),
1507 })
1508 }
1509
1510 fn array_env(
1511 &self,
1512 spec: &[ColSpec],
1513 rows: &[EnvRow],
1514 style: MathStyle,
1515 ) -> Result<Item, Error> {
1516 let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
1517 let col_sep = self.params.mu(style) * Dim::from_i64(10);
1518 let row_sep = self.params.em(style) / Dim::from_i64(5);
1519 let mut kinds = Vec::new();
1520 let mut data: Vec<Vec<MathBox>> = Vec::new();
1521 let mut ncols_data = 0;
1522 for row in rows {
1523 match row {
1524 EnvRow::Hline => kinds.push(RowKind::Hline),
1525 EnvRow::Intertext(n) => {
1526 kinds.push(RowKind::Intertext(Box::new(
1527 self.layout(n, MathStyle::Text)?,
1528 )));
1529 }
1530 EnvRow::Cells { cells, .. } => {
1531 let mut rboxes = Vec::new();
1532 for c in cells {
1533 rboxes.push(self.layout(c, style)?);
1534 }
1535 ncols_data = ncols_data.max(rboxes.len());
1536 data.push(rboxes);
1537 kinds.push(RowKind::Cells);
1538 }
1539 }
1540 }
1541 let mut spec: Vec<ColSpec> = if spec.is_empty() {
1542 vec![ColSpec::Center; ncols_data]
1543 } else {
1544 spec.to_vec()
1545 };
1546 let mut ndata = spec.iter().filter(|c| !c.is_rule()).count();
1547 while ndata < ncols_data {
1548 spec.push(ColSpec::Center);
1549 ndata += 1;
1550 }
1551 for row in &mut data {
1552 while row.len() < ndata {
1553 row.push(MathBox::empty());
1554 }
1555 }
1556 let mut col_w: Vec<Dim> = spec
1557 .iter()
1558 .map(|c| {
1559 if c.is_rule() {
1560 thick.clone()
1561 } else {
1562 Dim::zero()
1563 }
1564 })
1565 .collect();
1566 for row in &data {
1567 let mut dj = 0;
1568 for (j, sp) in spec.iter().enumerate() {
1569 if sp.is_rule() {
1570 continue;
1571 }
1572 if dj < row.len() {
1573 col_w[j] = col_w[j].max(&row[dj].width);
1574 }
1575 dj += 1;
1576 }
1577 }
1578 let mut table_w = Dim::zero();
1579 for (j, _) in spec.iter().enumerate() {
1580 if j > 0 && !spec[j].is_rule() && !spec[j - 1].is_rule() {
1581 table_w = &table_w + &col_sep;
1582 }
1583 table_w = &table_w + &col_w[j];
1584 }
1585 let mut packed = Vec::new();
1586 let mut di = 0;
1587 for kind in kinds {
1588 match kind {
1589 RowKind::Hline => {
1590 packed.push(MathBox::rule(table_w.clone(), thick.clone(), Dim::zero()));
1591 }
1592 RowKind::Intertext(t) => {
1593 if !packed.is_empty() {
1594 packed.push(sep_row(row_sep.clone()));
1595 }
1596 packed.push(*t);
1597 }
1598 RowKind::Cells => {
1599 if !packed.is_empty()
1600 && !matches!(packed.last().map(|b| &b.content), Some(BoxContent::Rule))
1601 {
1602 packed.push(sep_row(row_sep.clone()));
1603 }
1604 let row = &data[di];
1605 let mut rh = Dim::zero();
1606 let mut rd = Dim::zero();
1607 for c in row {
1608 rh = rh.max(&c.height);
1609 rd = rd.max(&c.depth);
1610 }
1611 let mut parts = Vec::new();
1612 let mut dj = 0;
1613 for (j, sp) in spec.iter().enumerate() {
1614 if j > 0 && !sp.is_rule() && !spec[j - 1].is_rule() {
1615 parts.push(MathBox::kern(col_sep.clone()));
1616 }
1617 if sp.is_rule() {
1618 parts.push(MathBox {
1619 width: thick.clone(),
1620 height: rh.clone(),
1621 depth: rd.clone(),
1622 italic: Dim::zero(),
1623 shift: Dim::zero(),
1624 content: BoxContent::Rule,
1625 });
1626 } else {
1627 let cell = row.get(dj).cloned().unwrap_or_else(MathBox::empty);
1628 parts.push(align_in(cell, &col_w[j], *sp));
1629 dj += 1;
1630 }
1631 }
1632 packed.push(MathBox::hpack(parts));
1633 di += 1;
1634 }
1635 }
1636 }
1637 Ok(Item {
1638 class: Some(AtomKind::Inner),
1639 bx: MathBox::vpack(packed),
1640 })
1641 }
1642
1643 #[allow(clippy::too_many_arguments)]
1644 fn grid(
1645 &self,
1646 rows: &[Vec<MathNode>],
1647 style: MathStyle,
1648 spec: Option<&[ColSpec]>,
1649 col_sep: Dim,
1650 default_align: ColSpec,
1651 delims: (Option<char>, Option<char>),
1652 numbered: bool,
1653 ) -> Result<Item, Error> {
1654 if rows.is_empty() {
1655 return Ok(Item {
1656 bx: MathBox::empty(),
1657 class: Some(AtomKind::Inner),
1658 });
1659 }
1660 let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
1661 let mut cells: Vec<Vec<MathBox>> = Vec::new();
1662 for row in rows {
1663 let mut rboxes = Vec::new();
1664 for c in 0..ncols {
1665 if c < row.len() {
1666 rboxes.push(self.layout(&row[c], style)?);
1667 } else {
1668 rboxes.push(MathBox::empty());
1669 }
1670 }
1671 cells.push(rboxes);
1672 }
1673 let mut col_w: Vec<Dim> = vec![Dim::zero(); ncols];
1674 for row in &cells {
1675 for (j, cell) in row.iter().enumerate() {
1676 col_w[j] = col_w[j].max(&cell.width);
1677 }
1678 }
1679 let row_sep = self.params.em(style) / Dim::from_i64(5);
1680 let mut row_boxes = Vec::new();
1681 for (ri, row) in cells.into_iter().enumerate() {
1682 let mut parts = Vec::new();
1683 for (j, cell) in row.into_iter().enumerate() {
1684 if j > 0 {
1685 parts.push(MathBox::kern(col_sep.clone()));
1686 }
1687 let align = spec
1688 .and_then(|s| s.get(j).copied())
1689 .unwrap_or(default_align);
1690 parts.push(align_in(cell, &col_w[j], align));
1691 }
1692 if ri > 0 {
1693 row_boxes.push(sep_row(row_sep.clone()));
1694 }
1695 let mut packed = MathBox::hpack(parts);
1696 if numbered {
1697 let num = match self.take_number() {
1698 Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1699 None => None,
1700 };
1701 packed = self.attach_number(packed, num, style);
1702 }
1703 row_boxes.push(packed);
1704 }
1705 let mut inner = MathBox::vpack(row_boxes);
1706 let needed = &inner.height + &inner.depth;
1707 let (ld, rd) = delims;
1708 if let Some(l) = ld {
1709 let left = self.sized_glyph(l, &needed, style)?;
1710 let right = match rd {
1711 Some(r) => self.sized_glyph(r, &needed, style)?,
1712 None => MathBox::empty(),
1713 };
1714 inner = MathBox::hpack(vec![left, inner, right]);
1715 }
1716 Ok(Item {
1717 class: Some(AtomKind::Inner),
1718 bx: inner,
1719 })
1720 }
1721}
1722
1723enum RowKind {
1724 Cells,
1725 Hline,
1726 Intertext(Box<MathBox>),
1727}
1728
1729fn data_cells(rows: &[EnvRow]) -> Result<Vec<Vec<MathNode>>, Error> {
1730 let mut out = Vec::new();
1731 for r in rows {
1732 match r {
1733 EnvRow::Cells { cells, .. } => out.push(cells.clone()),
1734 EnvRow::Hline => {
1735 return Err(Error::Unsupported {
1736 what: "hline in this environment".into(),
1737 });
1738 }
1739 EnvRow::Intertext(_) => {
1740 return Err(Error::Unsupported {
1741 what: "intertext in this environment".into(),
1742 });
1743 }
1744 }
1745 }
1746 Ok(out)
1747}
1748
1749fn sep_row(depth: Dim) -> MathBox {
1750 MathBox {
1751 width: Dim::zero(),
1752 height: Dim::zero(),
1753 depth,
1754 italic: Dim::zero(),
1755 shift: Dim::zero(),
1756 content: BoxContent::Empty,
1757 }
1758}
1759
1760fn pad_row(mut row: Vec<MathBox>, ncols: usize) -> Vec<MathBox> {
1761 while row.len() < ncols {
1762 row.push(MathBox::empty());
1763 }
1764 row
1765}
1766
1767fn wrap_nodes(items: Vec<MathNode>) -> MathNode {
1768 if items.len() == 1 {
1769 items
1770 .into_iter()
1771 .next()
1772 .unwrap_or(MathNode::Row(Vec::new()))
1773 } else {
1774 MathNode::Row(items)
1775 }
1776}
1777
1778fn align_in(inner: MathBox, width: &Dim, align: ColSpec) -> MathBox {
1779 if matches!(align, ColSpec::VRule) || inner.width.eq_dim(width) {
1780 return inner;
1781 }
1782 let extra = width - &inner.width;
1783 match align {
1784 ColSpec::Center => center_in(inner, width),
1785 ColSpec::Left => {
1786 let h = inner.height.clone();
1787 let d = inner.depth.clone();
1788 let packed = MathBox::hpack(vec![inner, MathBox::kern(extra)]);
1789 MathBox {
1790 width: packed.width,
1791 height: h,
1792 depth: d,
1793 italic: Dim::zero(),
1794 shift: Dim::zero(),
1795 content: packed.content,
1796 }
1797 }
1798 ColSpec::Right => {
1799 let h = inner.height.clone();
1800 let d = inner.depth.clone();
1801 let packed = MathBox::hpack(vec![MathBox::kern(extra), inner]);
1802 MathBox {
1803 width: packed.width,
1804 height: h,
1805 depth: d,
1806 italic: Dim::zero(),
1807 shift: Dim::zero(),
1808 content: packed.content,
1809 }
1810 }
1811 ColSpec::VRule => inner,
1812 }
1813}
1814
1815fn color_wrap(c: Color, inner: MathBox) -> MathBox {
1816 MathBox {
1817 width: inner.width.clone(),
1818 height: inner.height.clone(),
1819 depth: inner.depth.clone(),
1820 italic: inner.italic.clone(),
1821 shift: inner.shift.clone(),
1822 content: BoxContent::Color(c, Box::new(inner)),
1823 }
1824}
1825
1826fn back_color_wrap(c: Color, inner: MathBox) -> MathBox {
1827 MathBox {
1828 width: inner.width.clone(),
1829 height: inner.height.clone(),
1830 depth: inner.depth.clone(),
1831 italic: inner.italic.clone(),
1832 shift: inner.shift.clone(),
1833 content: BoxContent::BackColor(c, Box::new(inner)),
1834 }
1835}
1836
1837fn frame_wrap(thickness: Dim, stroke: Option<Color>, inner: MathBox) -> MathBox {
1838 MathBox {
1839 width: inner.width.clone(),
1840 height: inner.height.clone(),
1841 depth: inner.depth.clone(),
1842 italic: Dim::zero(),
1843 shift: Dim::zero(),
1844 content: BoxContent::Frame {
1845 thickness,
1846 stroke,
1847 inner: Box::new(inner),
1848 },
1849 }
1850}
1851
1852fn pad_box(inner: MathBox, pad: &Dim) -> MathBox {
1853 let w = &inner.width + pad + pad;
1854 let h = &inner.height + pad;
1855 let d = &inner.depth + pad;
1856 MathBox {
1857 width: w,
1858 height: h,
1859 depth: d,
1860 italic: Dim::zero(),
1861 shift: Dim::zero(),
1862 content: BoxContent::HList(vec![
1863 MathBox::kern(pad.clone()),
1864 inner,
1865 MathBox::kern(pad.clone()),
1866 ]),
1867 }
1868}
1869
1870fn center_in(inner: MathBox, width: &Dim) -> MathBox {
1871 if inner.width.eq_dim(width) {
1872 return inner;
1873 }
1874 let extra = width - &inner.width;
1875 let half = &extra / &Dim::from_i64(2);
1876 let h = inner.height.clone();
1877 let d = inner.depth.clone();
1878 let packed = MathBox::hpack(vec![
1879 MathBox::kern(half.clone()),
1880 inner,
1881 MathBox::kern(&extra - &half),
1882 ]);
1883 MathBox {
1884 width: packed.width,
1885 height: h,
1886 depth: d,
1887 italic: Dim::zero(),
1888 shift: Dim::zero(),
1889 content: packed.content,
1890 }
1891}
1892
1893fn shift_x(inner: MathBox, x: Dim) -> MathBox {
1894 if x.is_zero() {
1895 return inner;
1896 }
1897 let h = inner.height.clone();
1898 let d = inner.depth.clone();
1899 let packed = MathBox::hpack(vec![MathBox::kern(x), inner]);
1900 MathBox {
1901 width: packed.width,
1902 height: h,
1903 depth: d,
1904 italic: Dim::zero(),
1905 shift: Dim::zero(),
1906 content: packed.content,
1907 }
1908}
1909
1910fn overlay_accent(base: MathBox, acc: MathBox, x_off: Dim, raise: Dim) -> MathBox {
1911 let origin = (-x_off.clone()).clamp_nonneg();
1912 let base_x = origin.clone();
1913 let acc_x = &origin + &x_off;
1914 let width = (&base_x + &base.width).max(&(&acc_x + &acc.width));
1915 let acc_top = &acc.height + &raise;
1916 let acc_bot = &raise - &acc.depth;
1917 let height = base.height.max(&acc_top);
1918 let depth = base.depth.max(&(-acc_bot).clamp_nonneg());
1919 MathBox {
1920 width,
1921 height,
1922 depth,
1923 italic: Dim::zero(),
1924 shift: Dim::zero(),
1925 content: BoxContent::Overlap(vec![
1926 shift_x(base, base_x),
1927 shift_x(acc, acc_x).with_shift(raise),
1928 ]),
1929 }
1930}
1931
1932fn first_glyph_id(b: &MathBox) -> Option<u16> {
1933 match &b.content {
1934 BoxContent::Glyph { glyph_id, .. } => Some(*glyph_id),
1935 BoxContent::HList(v) | BoxContent::VList(v) | BoxContent::Overlap(v) => {
1936 v.iter().find_map(first_glyph_id)
1937 }
1938 BoxContent::Color(_, inner)
1939 | BoxContent::BackColor(_, inner)
1940 | BoxContent::Frame { inner, .. } => first_glyph_id(inner),
1941 _ => None,
1942 }
1943}
1944
1945fn is_tex_accent(kind: AccentKind) -> bool {
1946 matches!(
1947 kind,
1948 AccentKind::Hat
1949 | AccentKind::Check
1950 | AccentKind::Breve
1951 | AccentKind::Acute
1952 | AccentKind::Grave
1953 | AccentKind::Tilde
1954 | AccentKind::Bar
1955 | AccentKind::Vec
1956 | AccentKind::Dot
1957 | AccentKind::Ddot
1958 | AccentKind::Dddot
1959 | AccentKind::Ddddot
1960 | AccentKind::Ring
1961 | AccentKind::WideHat
1962 | AccentKind::WideTilde
1963 | AccentKind::Overleftarrow
1964 | AccentKind::Overrightarrow
1965 | AccentKind::Overleftrightarrow
1966 | AccentKind::Underleftarrow
1967 | AccentKind::Underrightarrow
1968 | AccentKind::Underleftrightarrow
1969 | AccentKind::Overbrace
1970 | AccentKind::Underbrace
1971 )
1972}
1973
1974fn is_stretchy_accent(kind: AccentKind) -> bool {
1975 matches!(
1976 kind,
1977 AccentKind::WideHat
1978 | AccentKind::WideTilde
1979 | AccentKind::Overleftarrow
1980 | AccentKind::Overrightarrow
1981 | AccentKind::Overleftrightarrow
1982 | AccentKind::Underleftarrow
1983 | AccentKind::Underrightarrow
1984 | AccentKind::Underleftrightarrow
1985 | AccentKind::Overbrace
1986 | AccentKind::Underbrace
1987 )
1988}
1989
1990fn is_diacritic_accent(kind: AccentKind) -> bool {
1991 matches!(
1992 kind,
1993 AccentKind::Hat
1994 | AccentKind::WideHat
1995 | AccentKind::Check
1996 | AccentKind::Breve
1997 | AccentKind::Acute
1998 | AccentKind::Grave
1999 | AccentKind::Tilde
2000 | AccentKind::WideTilde
2001 | AccentKind::Bar
2002 | AccentKind::Dot
2003 | AccentKind::Ddot
2004 | AccentKind::Dddot
2005 | AccentKind::Ddddot
2006 | AccentKind::Ring
2007 )
2008}
2009
2010fn is_under_accent(kind: AccentKind) -> bool {
2011 matches!(
2012 kind,
2013 AccentKind::Underleftarrow
2014 | AccentKind::Underrightarrow
2015 | AccentKind::Underleftrightarrow
2016 | AccentKind::Underbrace
2017 )
2018}
2019
2020fn accent_candidates(kind: AccentKind) -> &'static [char] {
2021 match kind {
2022 AccentKind::Hat | AccentKind::WideHat => &['ˆ', '\u{0302}'],
2023 AccentKind::Check => &['ˇ', '\u{030C}'],
2024 AccentKind::Breve => &['˘', '\u{0306}'],
2025 AccentKind::Acute => &['´', '\u{0301}'],
2026 AccentKind::Grave => &['`', '\u{0300}'],
2027 AccentKind::Tilde | AccentKind::WideTilde => &['˜', '\u{0303}'],
2028 AccentKind::Bar => &['¯', '\u{0304}'],
2029 AccentKind::Vec => &['→', '\u{20D7}'],
2030 AccentKind::Dot => &['˙', '\u{0307}'],
2031 AccentKind::Ddot => &['¨', '\u{0308}'],
2032 AccentKind::Dddot => &['\u{20DB}'],
2033 AccentKind::Ddddot => &['\u{20DC}'],
2034 AccentKind::Ring => &['˚', '\u{030A}'],
2035 AccentKind::Overleftarrow | AccentKind::Underleftarrow => &['←', '\u{27F5}'],
2036 AccentKind::Overrightarrow | AccentKind::Underrightarrow => &['→', '\u{27F6}'],
2037 AccentKind::Overleftrightarrow | AccentKind::Underleftrightarrow => &['↔', '\u{27F7}'],
2038 AccentKind::Overbrace => &['⏞'],
2039 AccentKind::Underbrace => &['⏟'],
2040 AccentKind::Not
2041 | AccentKind::Overline
2042 | AccentKind::Underline
2043 | AccentKind::Cancel
2044 | AccentKind::BCancel
2045 | AccentKind::XCancel
2046 | AccentKind::Boxed => &[],
2047 }
2048}
2049
2050fn class_of(n: &MathNode) -> Option<AtomKind> {
2051 match n {
2052 MathNode::Atom(_, k) => Some(*k),
2053 MathNode::Symbol(s) => Some(symbol_class(s)),
2054 MathNode::Operator(_, _)
2055 | MathNode::Sum(_, _)
2056 | MathNode::Product(_, _)
2057 | MathNode::Integral(_, _, _)
2058 | MathNode::Limit(_) => Some(AtomKind::Op),
2059 MathNode::Fraction(_, _)
2060 | MathNode::Radical(_, _)
2061 | MathNode::Matrix(_, _, _)
2062 | MathNode::Substack(_)
2063 | MathNode::Delimited(_, _, _) => Some(AtomKind::Inner),
2064 MathNode::SizedDelim(_, _, k) => Some(*k),
2065 MathNode::Superscript(b, _)
2066 | MathNode::Subscript(b, _)
2067 | MathNode::SubSup(b, _, _)
2068 | MathNode::Accent(b, _)
2069 | MathNode::OverUnder(b, _, _)
2070 | MathNode::CancelTo(_, b)
2071 | MathNode::Tag { body: b, .. }
2072 | MathNode::Intertext(b) => class_of(b),
2073 MathNode::Text(_, _) | MathNode::Ref(_) => Some(AtomKind::Ord),
2074 MathNode::Color(_, b)
2075 | MathNode::TextColor(_, b)
2076 | MathNode::ColorBox(_, b)
2077 | MathNode::FColorBox(_, _, b)
2078 | MathNode::Phantom(_, b) => class_of(b),
2079 MathNode::Row(v) if v.len() == 1 => class_of(&v[0]),
2080 MathNode::Row(_) => Some(AtomKind::Ord),
2081 MathNode::Space(_)
2082 | MathNode::Strut(_, _)
2083 | MathNode::Label(_)
2084 | MathNode::NoNumber
2085 | MathNode::Hline => None,
2086 }
2087}
2088
2089fn single_glyph(name: &str) -> Option<char> {
2090 let e = lookup(name)?;
2091 let mut chars = e.glyph.chars();
2092 match (chars.next(), chars.next()) {
2093 (Some(c), None) => Some(c),
2094 _ => None,
2095 }
2096}
2097
2098fn math_char(c: char) -> char {
2104 match c {
2105 '-' => '\u{2212}',
2106 other => other,
2107 }
2108}
2109
2110fn math_italic(c: char) -> char {
2117 let lower_greek = ('\u{03B1}'..='\u{03C9}').contains(&c);
2118 if c.is_ascii_alphabetic() || lower_greek || matches!(c, 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ')
2119 {
2120 styled_char(c, TextStyle::It)
2121 } else {
2122 c
2123 }
2124}
2125
2126fn symbol_char(name: &str) -> Result<char, Error> {
2127 single_glyph(name).ok_or_else(|| Error::Unsupported {
2128 what: format!("symbol \\{name}"),
2129 })
2130}
2131
2132fn symbol_class(name: &str) -> AtomKind {
2133 symbol_atom_kind(name)
2134}
2135
2136fn named_delim(n: &str) -> Result<char, Error> {
2137 let c = match n {
2138 "{" => '{',
2139 "}" => '}',
2140 "|" | "Vert" | "lVert" | "rVert" => '‖',
2141 "vert" | "lvert" | "rvert" => '|',
2142 "langle" => '⟨',
2143 "rangle" => '⟩',
2144 "lfloor" => '⌊',
2145 "rfloor" => '⌋',
2146 "lceil" => '⌈',
2147 "rceil" => '⌉',
2148 "backslash" => '\\',
2149 "uparrow" => '↑',
2150 "downarrow" => '↓',
2151 "Uparrow" => '⇑',
2152 "Downarrow" => '⇓',
2153 "updownarrow" => '↕',
2154 "Updownarrow" => '⇕',
2155 other => {
2156 return Err(Error::Unsupported {
2157 what: format!("delimiter {other}"),
2158 });
2159 }
2160 };
2161 Ok(c)
2162}
2163
2164fn matrix_delims(s: MatrixStyle) -> (Option<char>, Option<char>) {
2165 match s {
2166 MatrixStyle::Pmatrix => (Some('('), Some(')')),
2167 MatrixStyle::Bmatrix => (Some('['), Some(']')),
2168 MatrixStyle::Vmatrix => (Some('|'), Some('|')),
2169 MatrixStyle::VVmatrix => (Some('‖'), Some('‖')),
2170 MatrixStyle::BBmatrix => (Some('{'), Some('}')),
2171 MatrixStyle::Cases => (Some('{'), None),
2172 _ => (None, None),
2173 }
2174}