typst_layout/math/
mod.rs

1#[macro_use]
2mod shared;
3mod accent;
4mod attach;
5mod cancel;
6mod frac;
7mod fragment;
8mod lr;
9mod mat;
10mod root;
11mod run;
12mod stretch;
13mod text;
14mod underover;
15
16use rustybuzz::Feature;
17use ttf_parser::Tag;
18use typst_library::diag::{bail, SourceResult};
19use typst_library::engine::Engine;
20use typst_library::foundations::{
21    Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
22};
23use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
24use typst_library::layout::{
25    Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
26    InlineItem, OuterHAlignment, PlaceElem, Point, Region, Regions, Size, Spacing,
27    SpecificAlignment, VAlignment,
28};
29use typst_library::math::*;
30use typst_library::model::ParElem;
31use typst_library::routines::{Arenas, RealizationKind};
32use typst_library::text::{
33    families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
34};
35use typst_library::World;
36use typst_syntax::Span;
37use typst_utils::Numeric;
38use unicode_math_class::MathClass;
39
40use self::fragment::{
41    FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment,
42};
43use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
44use self::shared::*;
45use self::stretch::{stretch_fragment, stretch_glyph};
46
47/// Layout an inline equation (in a paragraph).
48#[typst_macros::time(span = elem.span())]
49pub fn layout_equation_inline(
50    elem: &Packed<EquationElem>,
51    engine: &mut Engine,
52    locator: Locator,
53    styles: StyleChain,
54    region: Size,
55) -> SourceResult<Vec<InlineItem>> {
56    assert!(!elem.block(styles));
57
58    let font = find_math_font(engine, styles, elem.span())?;
59
60    let mut locator = locator.split();
61    let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
62
63    let scale_style = style_for_script_scale(&ctx);
64    let styles = styles.chain(&scale_style);
65
66    let run = ctx.layout_into_run(&elem.body, styles)?;
67
68    let mut items = if run.row_count() == 1 {
69        run.into_par_items()
70    } else {
71        vec![InlineItem::Frame(run.into_fragment(styles).into_frame())]
72    };
73
74    // An empty equation should have a height, so we still create a frame
75    // (which is then resized in the loop).
76    if items.is_empty() {
77        items.push(InlineItem::Frame(Frame::soft(Size::zero())));
78    }
79
80    for item in &mut items {
81        let InlineItem::Frame(frame) = item else { continue };
82
83        let slack = ParElem::leading_in(styles) * 0.7;
84
85        let (t, b) = font.edges(
86            TextElem::top_edge_in(styles),
87            TextElem::bottom_edge_in(styles),
88            TextElem::size_in(styles),
89            TextEdgeBounds::Frame(frame),
90        );
91
92        let ascent = t.max(frame.ascent() - slack);
93        let descent = b.max(frame.descent() - slack);
94        frame.translate(Point::with_y(ascent - frame.baseline()));
95        frame.size_mut().y = ascent + descent;
96    }
97
98    Ok(items)
99}
100
101/// Layout a block-level equation (in a flow).
102#[typst_macros::time(span = elem.span())]
103pub fn layout_equation_block(
104    elem: &Packed<EquationElem>,
105    engine: &mut Engine,
106    locator: Locator,
107    styles: StyleChain,
108    regions: Regions,
109) -> SourceResult<Fragment> {
110    assert!(elem.block(styles));
111
112    let span = elem.span();
113    let font = find_math_font(engine, styles, span)?;
114
115    let mut locator = locator.split();
116    let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
117
118    let scale_style = style_for_script_scale(&ctx);
119    let styles = styles.chain(&scale_style);
120
121    let full_equation_builder = ctx
122        .layout_into_run(&elem.body, styles)?
123        .multiline_frame_builder(styles);
124    let width = full_equation_builder.size.x;
125
126    let equation_builders = if BlockElem::breakable_in(styles) {
127        let mut rows = full_equation_builder.frames.into_iter().peekable();
128        let mut equation_builders = vec![];
129        let mut last_first_pos = Point::zero();
130        let mut regions = regions;
131
132        loop {
133            // Keep track of the position of the first row in this region,
134            // so that the offset can be reverted later.
135            let Some(&(_, first_pos)) = rows.peek() else { break };
136            last_first_pos = first_pos;
137
138            let mut frames = vec![];
139            let mut height = Abs::zero();
140            while let Some((sub, pos)) = rows.peek() {
141                let mut pos = *pos;
142                pos.y -= first_pos.y;
143
144                // Finish this region if the line doesn't fit. Only do it if
145                // we placed at least one line _or_ we still have non-last
146                // regions. Crucially, we don't want to infinitely create
147                // new regions which are too small.
148                if !regions.size.y.fits(sub.height() + pos.y)
149                    && (regions.may_progress()
150                        || (regions.may_break() && !frames.is_empty()))
151                {
152                    break;
153                }
154
155                let (sub, _) = rows.next().unwrap();
156                height = height.max(pos.y + sub.height());
157                frames.push((sub, pos));
158            }
159
160            equation_builders
161                .push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
162            regions.next();
163        }
164
165        // Append remaining rows to the equation builder of the last region.
166        if let Some(equation_builder) = equation_builders.last_mut() {
167            equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
168                pos.y -= last_first_pos.y;
169                (frame, pos)
170            }));
171
172            let height = equation_builder
173                .frames
174                .iter()
175                .map(|(frame, pos)| frame.height() + pos.y)
176                .max()
177                .unwrap_or(equation_builder.size.y);
178
179            equation_builder.size.y = height;
180        }
181
182        // Ensure that there is at least one frame, even for empty equations.
183        if equation_builders.is_empty() {
184            equation_builders
185                .push(MathRunFrameBuilder { frames: vec![], size: Size::zero() });
186        }
187
188        equation_builders
189    } else {
190        vec![full_equation_builder]
191    };
192
193    let Some(numbering) = (**elem).numbering(styles) else {
194        let frames = equation_builders
195            .into_iter()
196            .map(MathRunFrameBuilder::build)
197            .collect();
198        return Ok(Fragment::frames(frames));
199    };
200
201    let pod = Region::new(regions.base(), Axes::splat(false));
202    let counter = Counter::of(EquationElem::elem())
203        .display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
204        .spanned(span);
205    let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
206
207    static NUMBER_GUTTER: Em = Em::new(0.5);
208    let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
209
210    let number_align = match elem.number_align(styles) {
211        SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
212        SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
213        SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
214    };
215
216    // Add equation numbers to each equation region.
217    let region_count = equation_builders.len();
218    let frames = equation_builders
219        .into_iter()
220        .map(|builder| {
221            if builder.frames.is_empty() && region_count > 1 {
222                // Don't number empty regions, but do number empty equations.
223                return builder.build();
224            }
225            add_equation_number(
226                builder,
227                number.clone(),
228                number_align.resolve(styles),
229                AlignElem::alignment_in(styles).resolve(styles).x,
230                regions.size.x,
231                full_number_width,
232            )
233        })
234        .collect();
235
236    Ok(Fragment::frames(frames))
237}
238
239fn find_math_font(
240    engine: &mut Engine<'_>,
241    styles: StyleChain,
242    span: Span,
243) -> SourceResult<Font> {
244    let variant = variant(styles);
245    let world = engine.world;
246    let Some(font) = families(styles).find_map(|family| {
247        let id = world.book().select(family.as_str(), variant)?;
248        let font = world.font(id)?;
249        let _ = font.ttf().tables().math?.constants?;
250        Some(font)
251    }) else {
252        bail!(span, "current font does not support math");
253    };
254    Ok(font)
255}
256
257fn add_equation_number(
258    equation_builder: MathRunFrameBuilder,
259    number: Frame,
260    number_align: Axes<FixedAlignment>,
261    equation_align: FixedAlignment,
262    region_size_x: Abs,
263    full_number_width: Abs,
264) -> Frame {
265    let first =
266        equation_builder.frames.first().map_or(
267            (equation_builder.size, Point::zero(), Abs::zero()),
268            |(frame, pos)| (frame.size(), *pos, frame.baseline()),
269        );
270    let last =
271        equation_builder.frames.last().map_or(
272            (equation_builder.size, Point::zero(), Abs::zero()),
273            |(frame, pos)| (frame.size(), *pos, frame.baseline()),
274        );
275    let line_count = equation_builder.frames.len();
276    let mut equation = equation_builder.build();
277
278    let width = if region_size_x.is_finite() {
279        region_size_x
280    } else {
281        equation.width() + 2.0 * full_number_width
282    };
283
284    let is_multiline = line_count >= 2;
285    let resizing_offset = resize_equation(
286        &mut equation,
287        &number,
288        number_align,
289        equation_align,
290        width,
291        is_multiline,
292        [first, last],
293    );
294    equation.translate(Point::with_x(match (equation_align, number_align.x) {
295        (FixedAlignment::Start, FixedAlignment::Start) => full_number_width,
296        (FixedAlignment::End, FixedAlignment::End) => -full_number_width,
297        _ => Abs::zero(),
298    }));
299
300    let x = match number_align.x {
301        FixedAlignment::Start => Abs::zero(),
302        FixedAlignment::End => equation.width() - number.width(),
303        _ => unreachable!(),
304    };
305    let y = {
306        let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| {
307            resizing_offset.y + pos.y + baseline - number.baseline()
308        };
309        match number_align.y {
310            FixedAlignment::Start => align_baselines(first, &number),
311            FixedAlignment::Center if !is_multiline => align_baselines(first, &number),
312            // In this case, the center lines (not baselines) of the number frame
313            // and the equation frame shall be aligned.
314            FixedAlignment::Center => (equation.height() - number.height()) / 2.0,
315            FixedAlignment::End => align_baselines(last, &number),
316        }
317    };
318
319    equation.push_frame(Point::new(x, y), number);
320    equation
321}
322
323/// Resize the equation's frame accordingly so that it encompasses the number.
324fn resize_equation(
325    equation: &mut Frame,
326    number: &Frame,
327    number_align: Axes<FixedAlignment>,
328    equation_align: FixedAlignment,
329    width: Abs,
330    is_multiline: bool,
331    [first, last]: [(Axes<Abs>, Point, Abs); 2],
332) -> Point {
333    if matches!(number_align.y, FixedAlignment::Center if is_multiline) {
334        // In this case, the center lines (not baselines) of the number frame
335        // and the equation frame shall be aligned.
336        return equation.resize(
337            Size::new(width, equation.height().max(number.height())),
338            Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center),
339        );
340    }
341
342    let excess_above = Abs::zero().max({
343        if !is_multiline || matches!(number_align.y, FixedAlignment::Start) {
344            let (.., baseline) = first;
345            number.baseline() - baseline
346        } else {
347            Abs::zero()
348        }
349    });
350    let excess_below = Abs::zero().max({
351        if !is_multiline || matches!(number_align.y, FixedAlignment::End) {
352            let (size, .., baseline) = last;
353            (number.height() - number.baseline()) - (size.y - baseline)
354        } else {
355            Abs::zero()
356        }
357    });
358
359    // The vertical expansion is asymmetric on the top and bottom edges, so we
360    // first align at the top then translate the content downward later.
361    let resizing_offset = equation.resize(
362        Size::new(width, equation.height() + excess_above + excess_below),
363        Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start),
364    );
365    equation.translate(Point::with_y(excess_above));
366    resizing_offset + Point::with_y(excess_above)
367}
368
369/// The context for math layout.
370struct MathContext<'a, 'v, 'e> {
371    // External.
372    engine: &'v mut Engine<'e>,
373    locator: &'v mut SplitLocator<'a>,
374    region: Region,
375    // Font-related.
376    font: &'a Font,
377    ttf: &'a ttf_parser::Face<'a>,
378    table: ttf_parser::math::Table<'a>,
379    constants: ttf_parser::math::Constants<'a>,
380    dtls_table: Option<GlyphwiseSubsts<'a>>,
381    flac_table: Option<GlyphwiseSubsts<'a>>,
382    ssty_table: Option<GlyphwiseSubsts<'a>>,
383    glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
384    space_width: Em,
385    // Mutable.
386    fragments: Vec<MathFragment>,
387}
388
389impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
390    /// Create a new math context.
391    fn new(
392        engine: &'v mut Engine<'e>,
393        locator: &'v mut SplitLocator<'a>,
394        styles: StyleChain<'a>,
395        base: Size,
396        font: &'a Font,
397    ) -> Self {
398        let math_table = font.ttf().tables().math.unwrap();
399        let gsub_table = font.ttf().tables().gsub;
400        let constants = math_table.constants.unwrap();
401
402        let feat = |tag: &[u8; 4]| {
403            GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..))
404        };
405
406        let features = features(styles);
407        let glyphwise_tables = Some(
408            features
409                .into_iter()
410                .filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature))
411                .collect(),
412        );
413
414        let ttf = font.ttf();
415        let space_width = ttf
416            .glyph_index(' ')
417            .and_then(|id| ttf.glyph_hor_advance(id))
418            .map(|advance| font.to_em(advance))
419            .unwrap_or(THICK);
420
421        Self {
422            engine,
423            locator,
424            region: Region::new(base, Axes::splat(false)),
425            font,
426            ttf,
427            table: math_table,
428            constants,
429            dtls_table: feat(b"dtls"),
430            flac_table: feat(b"flac"),
431            ssty_table: feat(b"ssty"),
432            glyphwise_tables,
433            space_width,
434            fragments: vec![],
435        }
436    }
437
438    /// Push a fragment.
439    fn push(&mut self, fragment: impl Into<MathFragment>) {
440        self.fragments.push(fragment.into());
441    }
442
443    /// Push multiple fragments.
444    fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) {
445        self.fragments.extend(fragments);
446    }
447
448    /// Layout the given element and return the result as a [`MathRun`].
449    fn layout_into_run(
450        &mut self,
451        elem: &Content,
452        styles: StyleChain,
453    ) -> SourceResult<MathRun> {
454        Ok(MathRun::new(self.layout_into_fragments(elem, styles)?))
455    }
456
457    /// Layout the given element and return the resulting [`MathFragment`]s.
458    fn layout_into_fragments(
459        &mut self,
460        elem: &Content,
461        styles: StyleChain,
462    ) -> SourceResult<Vec<MathFragment>> {
463        // The element's layout_math() changes the fragments held in this
464        // MathContext object, but for convenience this function shouldn't change
465        // them, so we restore the MathContext's fragments after obtaining the
466        // layout result.
467        let prev = std::mem::take(&mut self.fragments);
468        self.layout_into_self(elem, styles)?;
469        Ok(std::mem::replace(&mut self.fragments, prev))
470    }
471
472    /// Layout the given element and return the result as a
473    /// unified [`MathFragment`].
474    fn layout_into_fragment(
475        &mut self,
476        elem: &Content,
477        styles: StyleChain,
478    ) -> SourceResult<MathFragment> {
479        Ok(self.layout_into_run(elem, styles)?.into_fragment(styles))
480    }
481
482    /// Layout the given element and return the result as a [`Frame`].
483    fn layout_into_frame(
484        &mut self,
485        elem: &Content,
486        styles: StyleChain,
487    ) -> SourceResult<Frame> {
488        Ok(self.layout_into_fragment(elem, styles)?.into_frame())
489    }
490
491    /// Layout arbitrary content.
492    fn layout_into_self(
493        &mut self,
494        content: &Content,
495        styles: StyleChain,
496    ) -> SourceResult<()> {
497        let arenas = Arenas::default();
498        let pairs = (self.engine.routines.realize)(
499            RealizationKind::Math,
500            self.engine,
501            self.locator,
502            &arenas,
503            content,
504            styles,
505        )?;
506
507        let outer = styles;
508        for (elem, styles) in pairs {
509            // Hack because the font is fixed in math.
510            if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
511                let frame = layout_external(elem, self, styles)?;
512                self.push(FrameFragment::new(styles, frame).with_spaced(true));
513                continue;
514            }
515
516            layout_realized(elem, self, styles)?;
517        }
518
519        Ok(())
520    }
521}
522
523/// Lays out a leaf element resulting from realization.
524fn layout_realized(
525    elem: &Content,
526    ctx: &mut MathContext,
527    styles: StyleChain,
528) -> SourceResult<()> {
529    if let Some(elem) = elem.to_packed::<TagElem>() {
530        ctx.push(MathFragment::Tag(elem.tag.clone()));
531    } else if elem.is::<SpaceElem>() {
532        ctx.push(MathFragment::Space(ctx.space_width.resolve(styles)));
533    } else if elem.is::<LinebreakElem>() {
534        ctx.push(MathFragment::Linebreak);
535    } else if let Some(elem) = elem.to_packed::<HElem>() {
536        layout_h(elem, ctx, styles)?;
537    } else if let Some(elem) = elem.to_packed::<TextElem>() {
538        self::text::layout_text(elem, ctx, styles)?;
539    } else if let Some(elem) = elem.to_packed::<SymbolElem>() {
540        self::text::layout_symbol(elem, ctx, styles)?;
541    } else if let Some(elem) = elem.to_packed::<BoxElem>() {
542        layout_box(elem, ctx, styles)?;
543    } else if elem.is::<AlignPointElem>() {
544        ctx.push(MathFragment::Align);
545    } else if let Some(elem) = elem.to_packed::<ClassElem>() {
546        layout_class(elem, ctx, styles)?;
547    } else if let Some(elem) = elem.to_packed::<AccentElem>() {
548        self::accent::layout_accent(elem, ctx, styles)?;
549    } else if let Some(elem) = elem.to_packed::<AttachElem>() {
550        self::attach::layout_attach(elem, ctx, styles)?;
551    } else if let Some(elem) = elem.to_packed::<PrimesElem>() {
552        self::attach::layout_primes(elem, ctx, styles)?;
553    } else if let Some(elem) = elem.to_packed::<ScriptsElem>() {
554        self::attach::layout_scripts(elem, ctx, styles)?;
555    } else if let Some(elem) = elem.to_packed::<LimitsElem>() {
556        self::attach::layout_limits(elem, ctx, styles)?;
557    } else if let Some(elem) = elem.to_packed::<CancelElem>() {
558        self::cancel::layout_cancel(elem, ctx, styles)?
559    } else if let Some(elem) = elem.to_packed::<FracElem>() {
560        self::frac::layout_frac(elem, ctx, styles)?;
561    } else if let Some(elem) = elem.to_packed::<BinomElem>() {
562        self::frac::layout_binom(elem, ctx, styles)?;
563    } else if let Some(elem) = elem.to_packed::<LrElem>() {
564        self::lr::layout_lr(elem, ctx, styles)?
565    } else if let Some(elem) = elem.to_packed::<MidElem>() {
566        self::lr::layout_mid(elem, ctx, styles)?
567    } else if let Some(elem) = elem.to_packed::<VecElem>() {
568        self::mat::layout_vec(elem, ctx, styles)?
569    } else if let Some(elem) = elem.to_packed::<MatElem>() {
570        self::mat::layout_mat(elem, ctx, styles)?
571    } else if let Some(elem) = elem.to_packed::<CasesElem>() {
572        self::mat::layout_cases(elem, ctx, styles)?
573    } else if let Some(elem) = elem.to_packed::<OpElem>() {
574        layout_op(elem, ctx, styles)?
575    } else if let Some(elem) = elem.to_packed::<RootElem>() {
576        self::root::layout_root(elem, ctx, styles)?
577    } else if let Some(elem) = elem.to_packed::<StretchElem>() {
578        self::stretch::layout_stretch(elem, ctx, styles)?
579    } else if let Some(elem) = elem.to_packed::<UnderlineElem>() {
580        self::underover::layout_underline(elem, ctx, styles)?
581    } else if let Some(elem) = elem.to_packed::<OverlineElem>() {
582        self::underover::layout_overline(elem, ctx, styles)?
583    } else if let Some(elem) = elem.to_packed::<UnderbraceElem>() {
584        self::underover::layout_underbrace(elem, ctx, styles)?
585    } else if let Some(elem) = elem.to_packed::<OverbraceElem>() {
586        self::underover::layout_overbrace(elem, ctx, styles)?
587    } else if let Some(elem) = elem.to_packed::<UnderbracketElem>() {
588        self::underover::layout_underbracket(elem, ctx, styles)?
589    } else if let Some(elem) = elem.to_packed::<OverbracketElem>() {
590        self::underover::layout_overbracket(elem, ctx, styles)?
591    } else if let Some(elem) = elem.to_packed::<UnderparenElem>() {
592        self::underover::layout_underparen(elem, ctx, styles)?
593    } else if let Some(elem) = elem.to_packed::<OverparenElem>() {
594        self::underover::layout_overparen(elem, ctx, styles)?
595    } else if let Some(elem) = elem.to_packed::<UndershellElem>() {
596        self::underover::layout_undershell(elem, ctx, styles)?
597    } else if let Some(elem) = elem.to_packed::<OvershellElem>() {
598        self::underover::layout_overshell(elem, ctx, styles)?
599    } else {
600        let mut frame = layout_external(elem, ctx, styles)?;
601        if !frame.has_baseline() {
602            let axis = scaled!(ctx, styles, axis_height);
603            frame.set_baseline(frame.height() / 2.0 + axis);
604        }
605        ctx.push(
606            FrameFragment::new(styles, frame)
607                .with_spaced(true)
608                .with_ignorant(elem.is::<PlaceElem>()),
609        );
610    }
611
612    Ok(())
613}
614
615/// Lays out an [`BoxElem`].
616fn layout_box(
617    elem: &Packed<BoxElem>,
618    ctx: &mut MathContext,
619    styles: StyleChain,
620) -> SourceResult<()> {
621    let frame = crate::inline::layout_box(
622        elem,
623        ctx.engine,
624        ctx.locator.next(&elem.span()),
625        styles,
626        ctx.region.size,
627    )?;
628    ctx.push(FrameFragment::new(styles, frame).with_spaced(true));
629    Ok(())
630}
631
632/// Lays out an [`HElem`].
633fn layout_h(
634    elem: &Packed<HElem>,
635    ctx: &mut MathContext,
636    styles: StyleChain,
637) -> SourceResult<()> {
638    if let Spacing::Rel(rel) = elem.amount {
639        if rel.rel.is_zero() {
640            ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
641        }
642    }
643    Ok(())
644}
645
646/// Lays out a [`ClassElem`].
647#[typst_macros::time(name = "math.class", span = elem.span())]
648fn layout_class(
649    elem: &Packed<ClassElem>,
650    ctx: &mut MathContext,
651    styles: StyleChain,
652) -> SourceResult<()> {
653    let style = EquationElem::set_class(Some(elem.class)).wrap();
654    let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
655    fragment.set_class(elem.class);
656    fragment.set_limits(Limits::for_class(elem.class));
657    ctx.push(fragment);
658    Ok(())
659}
660
661/// Lays out an [`OpElem`].
662#[typst_macros::time(name = "math.op", span = elem.span())]
663fn layout_op(
664    elem: &Packed<OpElem>,
665    ctx: &mut MathContext,
666    styles: StyleChain,
667) -> SourceResult<()> {
668    let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
669    let italics = fragment.italics_correction();
670    let accent_attach = fragment.accent_attach();
671    let text_like = fragment.is_text_like();
672
673    ctx.push(
674        FrameFragment::new(styles, fragment.into_frame())
675            .with_class(MathClass::Large)
676            .with_italics_correction(italics)
677            .with_accent_attach(accent_attach)
678            .with_text_like(text_like)
679            .with_limits(if elem.limits(styles) {
680                Limits::Display
681            } else {
682                Limits::Never
683            }),
684    );
685    Ok(())
686}
687
688/// Layout into a frame with normal layout.
689fn layout_external(
690    content: &Content,
691    ctx: &mut MathContext,
692    styles: StyleChain,
693) -> SourceResult<Frame> {
694    crate::layout_frame(
695        ctx.engine,
696        content,
697        ctx.locator.next(&content.span()),
698        styles,
699        ctx.region,
700    )
701}