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