typst_realize/
lib.rs

1//! Typst's realization subsystem.
2//!
3//! *Realization* is the process of recursively applying styling and, in
4//! particular, show rules to produce well-known elements that can be processed
5//! further.
6
7use std::borrow::Cow;
8use std::cell::LazyCell;
9
10use arrayvec::ArrayVec;
11use bumpalo::Bump;
12use bumpalo::collections::{CollectIn, String as BumpString, Vec as BumpVec};
13use comemo::Track;
14use ecow::EcoString;
15use typst_library::diag::{At, SourceResult, bail, warning};
16use typst_library::engine::Engine;
17use typst_library::foundations::{
18    Content, Context, ContextElem, Element, NativeElement, NativeShowRule, Packed,
19    Recipe, RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem,
20    Styles, SymbolElem, Synthesize, TargetElem, Transformation,
21};
22use typst_library::introspection::{
23    Locatable, LocationKey, SplitLocator, Tag, TagElem, TagFlags, Tagged,
24};
25use typst_library::layout::{
26    AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
27};
28use typst_library::math::{EquationElem, Mathy};
29use typst_library::model::{
30    CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike,
31    ParElem, ParbreakElem, TermsElem,
32};
33use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
34use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
35use typst_syntax::Span;
36use typst_utils::{ListSet, SliceExt, SmallBitSet};
37
38/// Realize content into a flat list of well-known, styled items.
39#[typst_macros::time(name = "realize")]
40pub fn realize<'a>(
41    kind: RealizationKind,
42    engine: &mut Engine,
43    locator: &mut SplitLocator,
44    arenas: &'a Arenas,
45    content: &'a Content,
46    styles: StyleChain<'a>,
47) -> SourceResult<Vec<Pair<'a>>> {
48    let mut s = State {
49        engine,
50        locator,
51        arenas,
52        rules: match kind {
53            RealizationKind::LayoutDocument { .. } => LAYOUT_RULES,
54            RealizationKind::LayoutFragment { .. } => LAYOUT_RULES,
55            RealizationKind::LayoutPar => LAYOUT_PAR_RULES,
56            RealizationKind::HtmlDocument { .. } => HTML_DOCUMENT_RULES,
57            RealizationKind::HtmlFragment { .. } => HTML_FRAGMENT_RULES,
58            RealizationKind::Math => MATH_RULES,
59        },
60        sink: vec![],
61        groupings: ArrayVec::new(),
62        outside: kind.is_document(),
63        may_attach: false,
64        saw_parbreak: false,
65        kind,
66    };
67
68    visit(&mut s, content, styles)?;
69    finish(&mut s)?;
70
71    Ok(s.sink)
72}
73
74/// Mutable state for realization.
75///
76/// Sadly, we need that many lifetimes because &mut references are invariant and
77/// it would force the lifetimes of e.g. engine and locator to be equal if they
78/// shared a lifetime. We can get around it by enforcing the lifetimes on
79/// `fn realize`, but that makes it less flexible on the call site, which isn't
80/// worth it.
81///
82/// The only interesting lifetime is 'a, which is that of the content that comes
83/// in and goes out. It's the same 'a as on `fn realize`.
84struct State<'a, 'x, 'y, 'z> {
85    /// Defines what kind of realization we are performing.
86    kind: RealizationKind<'x>,
87    /// The engine.
88    engine: &'x mut Engine<'y>,
89    /// Assigns unique locations to elements.
90    locator: &'x mut SplitLocator<'z>,
91    /// Temporary storage arenas for lifetime extension during realization.
92    arenas: &'a Arenas,
93    /// The output elements of well-known types.
94    sink: Vec<Pair<'a>>,
95    /// Grouping rules used for realization.
96    rules: &'x [&'x GroupingRule],
97    /// Currently active groupings.
98    groupings: ArrayVec<Grouping<'x>, MAX_GROUP_NESTING>,
99    /// Whether we are currently not within any container or show rule output.
100    /// This is used to determine page styles during layout.
101    outside: bool,
102    /// Whether now following attach spacing can survive.
103    may_attach: bool,
104    /// Whether we visited any paragraph breaks.
105    saw_parbreak: bool,
106}
107
108/// Defines a rule for how certain elements shall be grouped during realization.
109struct GroupingRule {
110    /// When an element is visited that matches a rule with higher priority
111    /// than one that is currently grouped, we start a nested group.
112    priority: u8,
113    /// Whether the grouping handles tags itself. If this is set to `false`,
114    /// realization will transparently take care of tags and they will not
115    /// be visible to `finish`.
116    tags: bool,
117    /// Defines which kinds of elements start and make up this kind of grouping.
118    trigger: fn(&Content, &State) -> bool,
119    /// Defines elements that may appear in the interior of the grouping, but
120    /// not at the edges.
121    inner: fn(&Content) -> bool,
122    /// Defines whether styles for this kind of element interrupt the grouping.
123    interrupt: fn(Element) -> bool,
124    /// Should convert the accumulated elements in `s.sink[start..]` into
125    /// the grouped element.
126    finish: fn(Grouped) -> SourceResult<()>,
127}
128
129/// A started grouping of some elements.
130struct Grouping<'a> {
131    /// The position in `s.sink` where the group starts.
132    start: usize,
133    /// Only applies to `PAR` grouping: Whether this paragraph group is
134    /// interrupted, but not yet finished because it may be ignored due to being
135    /// fully inline.
136    interrupted: bool,
137    /// The rule used for this grouping.
138    rule: &'a GroupingRule,
139}
140
141/// The result of grouping.
142struct Grouped<'a, 'x, 'y, 'z, 's> {
143    /// The realization state.
144    s: &'s mut State<'a, 'x, 'y, 'z>,
145    /// The position in `s.sink` where the group starts.
146    start: usize,
147}
148
149/// What to do with an element when encountering it during realization.
150struct Verdict<'a> {
151    /// Whether the element is already prepared (i.e. things that should only
152    /// happen once have happened).
153    prepared: bool,
154    /// A map of styles to apply to the element.
155    map: Styles,
156    /// An optional show rule transformation to apply to the element.
157    step: Option<ShowStep<'a>>,
158}
159
160/// A show rule transformation to apply to the element.
161enum ShowStep<'a> {
162    /// A user-defined transformational show rule.
163    Recipe(&'a Recipe, RecipeIndex),
164    /// The built-in show rule.
165    Builtin(NativeShowRule),
166}
167
168/// A match of a regex show rule.
169struct RegexMatch<'a> {
170    /// The offset in the string that matched.
171    offset: usize,
172    /// The text that matched.
173    text: EcoString,
174    /// The style chain of the matching grouping.
175    styles: StyleChain<'a>,
176    /// The index of the recipe that matched.
177    id: RecipeIndex,
178    /// The recipe that matched.
179    recipe: &'a Recipe,
180}
181
182/// State kept for space collapsing.
183#[derive(Debug, Copy, Clone, Eq, PartialEq)]
184enum SpaceState {
185    /// A following space will be collapsed.
186    Destructive,
187    /// A following space will be kept unless a destructive element follows.
188    Supportive,
189    /// A space exists at this index.
190    Space(usize),
191}
192
193impl<'a> State<'a, '_, '_, '_> {
194    /// Lifetime-extends some content.
195    fn store(&self, content: Content) -> &'a Content {
196        self.arenas.content.alloc(content)
197    }
198
199    /// Lifetime-extends some pairs.
200    ///
201    /// By using a `BumpVec` instead of a `alloc_slice_copy` we can reuse
202    /// the space if no other bump allocations have been made by the time
203    /// the `BumpVec` is dropped.
204    fn store_slice(&self, pairs: &[Pair<'a>]) -> BumpVec<'a, Pair<'a>> {
205        let mut vec = BumpVec::new_in(&self.arenas.bump);
206        vec.extend_from_slice_copy(pairs);
207        vec
208    }
209}
210
211impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> {
212    /// Accesses the grouped elements.
213    fn get(&self) -> &[Pair<'a>] {
214        &self.s.sink[self.start..]
215    }
216
217    /// Accesses the grouped elements mutably.
218    fn get_mut(&mut self) -> (&mut Vec<Pair<'a>>, usize) {
219        (&mut self.s.sink, self.start)
220    }
221
222    /// Removes the grouped elements from the sink and retrieves back the state
223    /// with which resulting elements can be visited.
224    fn end(self) -> &'s mut State<'a, 'x, 'y, 'z> {
225        self.s.sink.truncate(self.start);
226        self.s
227    }
228}
229
230/// Handles an arbitrary piece of content during realization.
231fn visit<'a>(
232    s: &mut State<'a, '_, '_, '_>,
233    content: &'a Content,
234    styles: StyleChain<'a>,
235) -> SourceResult<()> {
236    // Tags can always simply be pushed.
237    if content.is::<TagElem>() {
238        s.sink.push((content, styles));
239        return Ok(());
240    }
241
242    // Transformations for content based on the realization kind. Needs
243    // to happen before show rules.
244    if visit_kind_rules(s, content, styles)? {
245        return Ok(());
246    }
247
248    // Apply show rules and preparation.
249    if visit_show_rules(s, content, styles)? {
250        return Ok(());
251    }
252
253    // Recurse into sequences. Styled elements and sequences can currently also
254    // have labels, so this needs to happen before they are handled.
255    if let Some(sequence) = content.to_packed::<SequenceElem>() {
256        for elem in &sequence.children {
257            visit(s, elem, styles)?;
258        }
259        return Ok(());
260    }
261
262    // Recurse into styled elements.
263    if let Some(styled) = content.to_packed::<StyledElem>() {
264        return visit_styled(s, &styled.child, Cow::Borrowed(&styled.styles), styles);
265    }
266
267    // Apply grouping --- where multiple elements are collected and then
268    // processed together (typically being transformed into one).
269    if visit_grouping_rules(s, content, styles)? {
270        return Ok(());
271    }
272
273    // Some elements are skipped based on specific circumstances.
274    if visit_filter_rules(s, content, styles)? {
275        return Ok(());
276    }
277
278    // No further transformations to apply, so we can finally just push it to
279    // the output!
280    s.sink.push((content, styles));
281
282    Ok(())
283}
284
285// Handles transformations based on the realization kind.
286fn visit_kind_rules<'a>(
287    s: &mut State<'a, '_, '_, '_>,
288    content: &'a Content,
289    styles: StyleChain<'a>,
290) -> SourceResult<bool> {
291    if let RealizationKind::Math = s.kind {
292        // Transparently recurse into equations nested in math, so that things
293        // like this work:
294        // ```
295        // #let my = $pi$
296        // $ my r^2 $
297        // ```
298        if let Some(elem) = content.to_packed::<EquationElem>() {
299            visit(s, &elem.body, styles)?;
300            return Ok(true);
301        }
302
303        // In normal realization, we apply regex show rules to consecutive
304        // textual elements via `TEXTUAL` grouping. However, in math, this is
305        // not desirable, so we just do it on a per-element basis.
306        if let Some(elem) = content.to_packed::<SymbolElem>() {
307            if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) {
308                visit_regex_match(s, &[(content, styles)], m)?;
309                return Ok(true);
310            }
311        } else if let Some(elem) = content.to_packed::<TextElem>()
312            && let Some(m) = find_regex_match_in_str(&elem.text, styles)
313        {
314            visit_regex_match(s, &[(content, styles)], m)?;
315            return Ok(true);
316        }
317    } else {
318        // Transparently wrap mathy content into equations.
319        if content.can::<dyn Mathy>() && !content.is::<EquationElem>() {
320            let eq = EquationElem::new(content.clone()).pack().spanned(content.span());
321            visit(s, s.store(eq), styles)?;
322            return Ok(true);
323        }
324
325        // Symbols in non-math content transparently convert to `TextElem` so we
326        // don't have to handle them in non-math layout.
327        if let Some(elem) = content.to_packed::<SymbolElem>() {
328            let mut text = TextElem::packed(elem.text.clone()).spanned(elem.span());
329            if let Some(label) = elem.label() {
330                text.set_label(label);
331            }
332            visit(s, s.store(text), styles)?;
333            return Ok(true);
334        }
335    }
336
337    Ok(false)
338}
339
340/// Tries to apply show rules to or prepare content. Returns `true` if the
341/// element was handled.
342fn visit_show_rules<'a>(
343    s: &mut State<'a, '_, '_, '_>,
344    content: &'a Content,
345    styles: StyleChain<'a>,
346) -> SourceResult<bool> {
347    // Determines whether and how to proceed with show rule application.
348    let Some(Verdict { prepared, mut map, step }) = verdict(s.engine, content, styles)
349    else {
350        return Ok(false);
351    };
352
353    // Create a fresh copy that we can mutate.
354    let mut output = Cow::Borrowed(content);
355
356    // If the element isn't yet prepared (we're seeing it for the first time),
357    // prepare it.
358    let mut tags = None;
359    if !prepared {
360        tags = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
361    }
362
363    // Apply a show rule step, if there is one.
364    if let Some(step) = step {
365        let chained = styles.chain(&map);
366        let result = match step {
367            // Apply a user-defined show rule.
368            ShowStep::Recipe(recipe, guard) => {
369                let context = Context::new(output.location(), Some(chained));
370                recipe.apply(
371                    s.engine,
372                    context.track(),
373                    output.into_owned().guarded(guard),
374                )
375            }
376
377            // Apply a built-in show rule.
378            ShowStep::Builtin(rule) => {
379                let _scope = typst_timing::TimingScope::new(output.elem().name());
380                rule.apply(&output, s.engine, chained)
381                    .map(|content| content.spanned(output.span()))
382            }
383        };
384
385        // Errors in show rules don't terminate compilation immediately. We just
386        // continue with empty content for them and show all errors together, if
387        // they remain by the end of the introspection loop.
388        //
389        // This way, we can ignore errors that only occur in earlier iterations
390        // and also show more useful errors at once.
391        output = Cow::Owned(s.engine.delay(result));
392    }
393
394    // Lifetime-extend the realized content if necessary.
395    let realized = match output {
396        Cow::Borrowed(realized) => realized,
397        Cow::Owned(realized) => s.store(realized),
398    };
399
400    // Push start tag.
401    let (start, end) = tags.unzip();
402    if let Some(tag) = start {
403        visit(s, s.store(TagElem::packed(tag)), styles)?;
404    }
405
406    let prev_outside = s.outside;
407    s.outside &= content.is::<ContextElem>();
408    s.engine.route.increase();
409    s.engine.route.check_show_depth().at(content.span())?;
410
411    visit_styled(s, realized, Cow::Owned(map), styles)?;
412
413    s.outside = prev_outside;
414    s.engine.route.decrease();
415
416    // Push end tag.
417    if let Some(tag) = end {
418        visit(s, s.store(TagElem::packed(tag)), styles)?;
419    }
420
421    Ok(true)
422}
423
424/// Inspects an element and the current styles and determines how to proceed
425/// with the styling.
426fn verdict<'a>(
427    engine: &mut Engine,
428    elem: &'a Content,
429    styles: StyleChain<'a>,
430) -> Option<Verdict<'a>> {
431    let prepared = elem.is_prepared();
432    let mut map = Styles::new();
433    let mut step = None;
434
435    // Do pre-synthesis on a cloned element to be able to match on synthesized
436    // fields before real synthesis runs (during preparation). It's really
437    // unfortunate that we have to do this, but otherwise
438    // `show figure.where(kind: table)` won't work :(
439    let mut elem = elem;
440    let mut slot;
441    if !prepared && elem.can::<dyn Synthesize>() {
442        slot = elem.clone();
443        slot.with_mut::<dyn Synthesize>()
444            .unwrap()
445            .synthesize(engine, styles)
446            .ok();
447        elem = &slot;
448    }
449
450    // Lazily computes the total number of recipes in the style chain. We need
451    // it to determine whether a particular show rule was already applied to the
452    // `elem` previously. For this purpose, show rules are indexed from the
453    // top of the chain as the chain might grow to the bottom.
454    let depth = LazyCell::new(|| styles.recipes().count());
455
456    for (r, recipe) in styles.recipes().enumerate() {
457        // We're not interested in recipes that don't match.
458        if !recipe
459            .selector()
460            .is_some_and(|selector| selector.matches(elem, Some(styles)))
461        {
462            continue;
463        }
464
465        // Special handling for show-set rules.
466        if let Transformation::Style(transform) = recipe.transform() {
467            if !prepared {
468                map.apply(transform.clone());
469            }
470            continue;
471        }
472
473        // If we already have a show step, don't look for one.
474        if step.is_some() {
475            continue;
476        }
477
478        // Check whether this show rule was already applied to the element.
479        let index = RecipeIndex(*depth - r);
480        if elem.is_guarded(index) {
481            continue;
482        }
483
484        // We'll apply this recipe.
485        step = Some(ShowStep::Recipe(recipe, index));
486
487        // If we found a show rule and are already prepared, there is nothing
488        // else to do, so we can just break. If we are not yet prepared,
489        // continue searching for potential show-set styles.
490        if prepared {
491            break;
492        }
493    }
494
495    // If we found no user-defined rule, also consider the built-in show rule.
496    if step.is_none() {
497        let target = styles.get(TargetElem::target);
498        if let Some(rule) = engine.routines.rules.get(target, elem) {
499            step = Some(ShowStep::Builtin(rule));
500        }
501    }
502
503    // If there's no nothing to do, there is also no verdict.
504    if step.is_none()
505        && map.is_empty()
506        && (prepared || {
507            elem.label().is_none()
508                && elem.location().is_none()
509                && !elem.can::<dyn ShowSet>()
510                && !elem.can::<dyn Locatable>()
511                && !elem.can::<dyn Tagged>()
512                && !elem.can::<dyn Synthesize>()
513        })
514    {
515        return None;
516    }
517
518    Some(Verdict { prepared, map, step })
519}
520
521/// This is only executed the first time an element is visited.
522fn prepare(
523    engine: &mut Engine,
524    locator: &mut SplitLocator,
525    elem: &mut Content,
526    map: &mut Styles,
527    styles: StyleChain,
528) -> SourceResult<Option<(Tag, Tag)>> {
529    // Generate a location for the element, which uniquely identifies it in
530    // the document. This has some overhead, so we only do it for elements
531    // that are explicitly marked as locatable and labelled elements.
532    //
533    // The element could already have a location even if it is not prepared
534    // when it stems from a query.
535    let key = typst_utils::hash128(&elem);
536    let flags = TagFlags {
537        introspectable: elem.can::<dyn Locatable>()
538            || elem.label().is_some()
539            || elem.location().is_some(),
540        tagged: elem.can::<dyn Tagged>(),
541    };
542    if elem.location().is_none() && flags.any() {
543        let loc = locator.next_location(engine.introspector, key);
544        elem.set_location(loc);
545    }
546
547    // Apply built-in show-set rules. User-defined show-set rules are already
548    // considered in the map built while determining the verdict.
549    if let Some(show_settable) = elem.with::<dyn ShowSet>() {
550        map.apply(show_settable.show_set(styles));
551    }
552
553    // If necessary, generated "synthesized" fields (which are derived from
554    // other fields or queries). Do this after show-set so that show-set styles
555    // are respected.
556    if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() {
557        synthesizable.synthesize(engine, styles.chain(map))?;
558    }
559
560    // Copy style chain fields into the element itself, so that they are
561    // available in rules.
562    elem.materialize(styles.chain(map));
563
564    // If the element is locatable, create start and end tags to be able to find
565    // the element in the frames after layout. Do this after synthesis and
566    // materialization, so that it includes the synthesized fields. Do it before
567    // marking as prepared so that show-set rules will apply to this element
568    // when queried.
569    let tags = elem
570        .location()
571        .map(|loc| (Tag::Start(elem.clone(), flags), Tag::End(loc, key, flags)));
572
573    // Ensure that this preparation only runs once by marking the element as
574    // prepared.
575    elem.mark_prepared();
576
577    Ok(tags)
578}
579
580/// Handles a styled element.
581fn visit_styled<'a>(
582    s: &mut State<'a, '_, '_, '_>,
583    content: &'a Content,
584    mut local: Cow<'a, Styles>,
585    outer: StyleChain<'a>,
586) -> SourceResult<()> {
587    // Nothing to do if the styles are actually empty.
588    if local.is_empty() {
589        return visit(s, content, outer);
590    }
591
592    // Check for document and page styles.
593    let mut pagebreak = false;
594    for style in local.iter() {
595        let Some(elem) = style.element() else { continue };
596        if elem == DocumentElem::ELEM {
597            if let Some(info) = s.kind.as_document_mut() {
598                info.populate(&local)
599            } else {
600                bail!(
601                    style.span(),
602                    "document set rules are not allowed inside of containers"
603                );
604            }
605        } else if elem == TextElem::ELEM {
606            // Infer the document locale from the first toplevel set rule.
607            if let Some(info) = s.kind.as_document_mut() {
608                info.populate_locale(&local)
609            }
610        } else if elem == PageElem::ELEM {
611            match s.kind {
612                RealizationKind::LayoutDocument { .. } => {
613                    // When there are page styles, we "break free" from our show
614                    // rule cage.
615                    pagebreak = true;
616                    s.outside = true;
617                }
618                RealizationKind::HtmlDocument { .. } => s.engine.sink.warn(warning!(
619                    style.span(),
620                    "page set rule was ignored during HTML export"
621                )),
622                _ => bail!(
623                    style.span(),
624                    "page configuration is not allowed inside of containers"
625                ),
626            }
627        }
628    }
629
630    // If we are not within a container or show rule, mark the styles as
631    // "outside". This will allow them to be lifted to the page level.
632    if s.outside {
633        local = Cow::Owned(local.into_owned().outside());
634    }
635
636    // Lifetime-extend the styles if necessary.
637    let outer = s.arenas.bump.alloc(outer);
638    let local = match local {
639        Cow::Borrowed(map) => map,
640        Cow::Owned(owned) => &*s.arenas.styles.alloc(owned),
641    };
642
643    // Generate a weak pagebreak if there is a page interruption. For the
644    // starting pagebreak we only want the styles before and including the
645    // interruptions, not trailing styles that happen to be in the same `Styles`
646    // list, so we trim the local styles.
647    if pagebreak {
648        let relevant = local
649            .as_slice()
650            .trim_end_matches(|style| style.element() != Some(PageElem::ELEM));
651        visit(s, PagebreakElem::shared_weak(), outer.chain(relevant))?;
652    }
653
654    finish_interrupted(s, local)?;
655    visit(s, content, outer.chain(local))?;
656    finish_interrupted(s, local)?;
657
658    // Generate a weak "boundary" pagebreak at the end. In comparison to a
659    // normal weak pagebreak, the styles of this are ignored during layout, so
660    // it doesn't really matter what we use here.
661    if pagebreak {
662        visit(s, PagebreakElem::shared_boundary(), *outer)?;
663    }
664
665    Ok(())
666}
667
668/// Tries to group the content in an active group or start a new one if any
669/// grouping rule matches. Returns `true` if the element was grouped.
670fn visit_grouping_rules<'a>(
671    s: &mut State<'a, '_, '_, '_>,
672    content: &'a Content,
673    styles: StyleChain<'a>,
674) -> SourceResult<bool> {
675    let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, s));
676
677    // Try to continue or finish an existing grouping.
678    let mut i = 0;
679    while let Some(active) = s.groupings.last() {
680        // Start a nested group if a rule with higher priority matches.
681        if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
682            break;
683        }
684
685        // If the element can be added to the active grouping, do it.
686        if !active.interrupted
687            && ((active.rule.trigger)(content, s) || (active.rule.inner)(content))
688        {
689            s.sink.push((content, styles));
690            return Ok(true);
691        }
692
693        finish_innermost_grouping(s)?;
694        i += 1;
695        if i > 512 {
696            // It seems like this case is only hit when there is a cycle between
697            // a show rule and a grouping rule. The show rule produces content
698            // that is matched by a grouping rule, which is then again processed
699            // by the show rule, and so on. The two must be at an equilibrium,
700            // otherwise either the "maximum show rule depth" or "maximum
701            // grouping depth" errors are triggered.
702            bail!(content.span(), "maximum grouping depth exceeded");
703        }
704    }
705
706    // Start a new grouping.
707    if let Some(rule) = matching {
708        let start = s.sink.len();
709        s.groupings.push(Grouping { start, rule, interrupted: false });
710        s.sink.push((content, styles));
711        return Ok(true);
712    }
713
714    Ok(false)
715}
716
717/// Some elements don't make it to the sink depending on the realization kind
718/// and current state.
719fn visit_filter_rules<'a>(
720    s: &mut State<'a, '_, '_, '_>,
721    content: &'a Content,
722    styles: StyleChain<'a>,
723) -> SourceResult<bool> {
724    if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) {
725        return Ok(false);
726    }
727
728    if content.is::<SpaceElem>() {
729        // Outside of maths and paragraph realization, spaces that were not
730        // collected by the paragraph grouper don't interest us.
731        return Ok(true);
732    } else if content.is::<ParbreakElem>() {
733        // Paragraph breaks are only a boundary for paragraph grouping, we don't
734        // need to store them.
735        s.may_attach = false;
736        s.saw_parbreak = true;
737        return Ok(true);
738    } else if !s.may_attach
739        && content
740            .to_packed::<VElem>()
741            .is_some_and(|elem| elem.attach.get(styles))
742    {
743        // Attach spacing collapses if not immediately following a paragraph.
744        return Ok(true);
745    }
746
747    // Remember whether following attach spacing can survive.
748    s.may_attach = content.is::<ParElem>();
749
750    Ok(false)
751}
752
753/// Finishes all grouping.
754fn finish(s: &mut State) -> SourceResult<()> {
755    finish_grouping_while(s, |s| {
756        // If this is a fragment realization and all we've got is inline
757        // content, don't turn it into a paragraph.
758        if is_fully_inline(s) {
759            *s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline;
760            s.groupings.pop();
761            collapse_spaces(&mut s.sink, 0);
762            false
763        } else {
764            !s.groupings.is_empty()
765        }
766    })?;
767
768    // In paragraph and math realization, spaces are top-level.
769    if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) {
770        collapse_spaces(&mut s.sink, 0);
771    }
772
773    Ok(())
774}
775
776/// Finishes groupings while any active group is interrupted by the styles.
777fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> {
778    let mut last = None;
779    for elem in local.iter().filter_map(|style| style.element()) {
780        if last == Some(elem) {
781            continue;
782        }
783        finish_grouping_while(s, |s| {
784            s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem))
785                && if is_fully_inline(s) {
786                    s.groupings[0].interrupted = true;
787                    false
788                } else {
789                    true
790                }
791        })?;
792        last = Some(elem);
793    }
794    Ok(())
795}
796
797/// Finishes groupings while `f` returns `true`.
798fn finish_grouping_while<F>(s: &mut State, mut f: F) -> SourceResult<()>
799where
800    F: FnMut(&mut State) -> bool,
801{
802    // Finishing of a group may result in new content and new grouping. This
803    // can, in theory, go on for a bit. To prevent it from becoming an infinite
804    // loop, we keep track of the iteration count.
805    let mut i = 0;
806    while f(s) {
807        finish_innermost_grouping(s)?;
808        i += 1;
809        if i > 512 {
810            bail!(Span::detached(), "maximum grouping depth exceeded");
811        }
812    }
813    Ok(())
814}
815
816/// Finishes the currently innermost grouping.
817fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
818    // The grouping we are interrupting.
819    let Grouping { mut start, rule, .. } = s.groupings.pop().unwrap();
820
821    // Trim trailing non-trigger elements. At the start, they are already not
822    // included precisely because they are not triggers.
823    let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s));
824    let mut end = start + trimmed.len();
825
826    // Tags that are opened within or at the start boundary of the grouping
827    // should have their closing tag included if it is at the end boundary.
828    // Similarly, tags that are closed within or at the end boundary should have
829    // their opening tag included if it is at the start boundary. Finally, tags
830    // that are sandwiched between an opening tag with a matching closing tag
831    // should also be included.
832    if rule.tags {
833        // The trailing part of the sink can contain a mix of inner elements and
834        // tags. If there is a closing tag with a matching start tag, but there
835        // is an inner element in between, that's in principle a situation with
836        // overlapping tags. However, if the inner element would immediately be
837        // destructed anyways, there isn't really a problem. So we try to
838        // anticipate that and destruct it eagerly.
839        if std::ptr::eq(rule, &PAR) {
840            for _ in s.sink.extract_if(end.., |(c, _)| c.is::<SpaceElem>()) {}
841        }
842
843        // Find tags before, within, and after the grouping range.
844        let bump = &s.arenas.bump;
845        let before = tag_set(bump, s.sink[..start].iter().rev().map_while(to_tag));
846        let within = tag_set(bump, s.sink[start..end].iter().filter_map(to_tag));
847        let after = tag_set(bump, s.sink[end..].iter().map_while(to_tag));
848
849        // Include all tags at the start that are closed within or after.
850        for (k, (c, _)) in s.sink[..start].iter().enumerate().rev() {
851            let Some(elem) = c.to_packed::<TagElem>() else { break };
852            let key = elem.tag.location().into();
853            if within.contains(&key) || after.contains(&key) {
854                start = k;
855            }
856        }
857
858        // Include all tags at the end that are opened within or before.
859        for (k, (c, _)) in s.sink.iter().enumerate().skip(end) {
860            let Some(elem) = c.to_packed::<TagElem>() else { break };
861            let key = elem.tag.location().into();
862            if within.contains(&key) || before.contains(&key) {
863                end = k + 1;
864            }
865        }
866    }
867
868    let tail = s.store_slice(&s.sink[end..]);
869    s.sink.truncate(end);
870
871    // If the grouping is not interested in tags, remove and collect them.
872    let mut tags = BumpVec::<Pair>::new_in(&s.arenas.bump);
873    if !rule.tags {
874        let mut k = start;
875        for i in start..end {
876            if s.sink[i].0.is::<TagElem>() {
877                tags.push(s.sink[i]);
878                continue;
879            }
880
881            if k < i {
882                s.sink[k] = s.sink[i];
883            }
884            k += 1;
885        }
886        s.sink.truncate(k);
887    }
888
889    // Execute the grouping's finisher rule.
890    (rule.finish)(Grouped { s, start })?;
891
892    // Visit the tags and staged elements again.
893    for &(content, styles) in tags.iter().chain(&tail) {
894        visit(s, content, styles)?;
895    }
896
897    Ok(())
898}
899
900/// Extracts the locations of all tags in the given `list` into a bump-allocated
901/// set.
902fn tag_set<'a>(
903    bump: &'a Bump,
904    iter: impl IntoIterator<Item = &'a Packed<TagElem>>,
905) -> ListSet<BumpVec<'a, LocationKey>> {
906    ListSet::new(
907        iter.into_iter()
908            .map(|elem| LocationKey::new(elem.tag.location()))
909            .collect_in::<BumpVec<_>>(bump),
910    )
911}
912
913/// Tries to convert a pair to a tag.
914fn to_tag<'a>((c, _): &Pair<'a>) -> Option<&'a Packed<TagElem>> {
915    c.to_packed::<TagElem>()
916}
917
918/// The maximum number of nested groups that are possible. Corresponds to the
919/// number of unique priority levels.
920const MAX_GROUP_NESTING: usize = 3;
921
922/// Grouping rules used in layout realization.
923static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
924
925/// Grouping rules used in paragraph layout realization.
926static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
927
928/// Grouping rules used in HTML root realization.
929static HTML_DOCUMENT_RULES: &[&GroupingRule] =
930    &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
931
932/// Grouping rules used in HTML fragment realization.
933static HTML_FRAGMENT_RULES: &[&GroupingRule] =
934    &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
935
936/// Grouping rules used in math realization.
937static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
938
939/// Groups adjacent textual elements for text show rule application.
940static TEXTUAL: GroupingRule = GroupingRule {
941    priority: 3,
942    tags: true,
943    trigger: |content, _| {
944        let elem = content.elem();
945        // Note that `SymbolElem` converts into `TextElem` before textual show
946        // rules run, and we apply textual rules to elements manually during
947        // math realization, so we don't check for it here.
948        elem == TextElem::ELEM
949            || elem == LinebreakElem::ELEM
950            || elem == SmartQuoteElem::ELEM
951    },
952    inner: |content| content.elem() == SpaceElem::ELEM,
953    // Any kind of style interrupts this kind of grouping since regex show
954    // rules cannot match over style changes anyway.
955    interrupt: |_| true,
956    finish: finish_textual,
957};
958
959/// Collects inline-level elements into a `ParElem`.
960static PAR: GroupingRule = GroupingRule {
961    priority: 1,
962    tags: true,
963    trigger: |content, state| {
964        let elem = content.elem();
965        elem == TextElem::ELEM
966            || elem == HElem::ELEM
967            || elem == LinebreakElem::ELEM
968            || elem == SmartQuoteElem::ELEM
969            || elem == InlineElem::ELEM
970            || elem == BoxElem::ELEM
971            || match state.kind {
972                RealizationKind::HtmlDocument { is_inline, .. }
973                | RealizationKind::HtmlFragment { is_inline, .. } => is_inline(content),
974                _ => false,
975            }
976    },
977    inner: |content| content.elem() == SpaceElem::ELEM,
978    interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM,
979    finish: finish_par,
980};
981
982/// Collects `CiteElem`s into `CiteGroup`s.
983static CITES: GroupingRule = GroupingRule {
984    priority: 2,
985    tags: false,
986    trigger: |content, _| content.elem() == CiteElem::ELEM,
987    inner: |content| content.elem() == SpaceElem::ELEM,
988    interrupt: |elem| {
989        elem == CiteGroup::ELEM || elem == ParElem::ELEM || elem == AlignElem::ELEM
990    },
991    finish: finish_cites,
992};
993
994/// Builds a `ListElem` from grouped `ListItems`s.
995static LIST: GroupingRule = list_like_grouping::<ListElem>();
996
997/// Builds an `EnumElem` from grouped `EnumItem`s.
998static ENUM: GroupingRule = list_like_grouping::<EnumElem>();
999
1000/// Builds a `TermsElem` from grouped `TermItem`s.
1001static TERMS: GroupingRule = list_like_grouping::<TermsElem>();
1002
1003/// Collects `ListItemLike` elements into a `ListLike` element.
1004const fn list_like_grouping<T: ListLike>() -> GroupingRule {
1005    GroupingRule {
1006        priority: 2,
1007        tags: false,
1008        trigger: |content, _| content.elem() == T::Item::ELEM,
1009        inner: |content| {
1010            let elem = content.elem();
1011            elem == SpaceElem::ELEM || elem == ParbreakElem::ELEM
1012        },
1013        interrupt: |elem| elem == T::ELEM || elem == AlignElem::ELEM,
1014        finish: finish_list_like::<T>,
1015    }
1016}
1017
1018/// Processes grouped textual elements.
1019///
1020/// Specifically, it searches for regex matches in grouped textual elements and
1021/// - if there was a match, visits the results recursively,
1022/// - if there was no match, tries to simply implicitly use the grouped elements
1023///   as part of a paragraph grouping,
1024/// - if that's not possible because another grouping is active, temporarily
1025///   disables textual grouping and revisits the elements.
1026fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
1027    // Try to find a regex match in the grouped textual elements. Returns early
1028    // if there is one.
1029    if visit_textual(s, start)? {
1030        return Ok(());
1031    }
1032
1033    // There was no regex match, so we need to collect the text into a paragraph
1034    // grouping. To do that, we first terminate all non-paragraph groupings.
1035    if in_non_par_grouping(s) {
1036        let elems = s.store_slice(&s.sink[start..]);
1037        s.sink.truncate(start);
1038        finish_grouping_while(s, in_non_par_grouping)?;
1039        start = s.sink.len();
1040        s.sink.extend(elems);
1041    }
1042
1043    // Now, there are only two options:
1044    // 1. We are already in a paragraph group. In this case, the elements just
1045    //    transparently become part of it.
1046    // 2. There is no group at all. In this case, we create one.
1047    if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
1048        s.groupings.push(Grouping { start, rule: &PAR, interrupted: false });
1049    }
1050
1051    Ok(())
1052}
1053
1054/// Whether there is an active grouping, but it is not a `PAR` grouping.
1055fn in_non_par_grouping(s: &mut State) -> bool {
1056    s.groupings.last().is_some_and(|grouping| {
1057        !std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted
1058    })
1059}
1060
1061/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it
1062/// spans the whole sink (with the exception of leading tags).
1063fn is_fully_inline(s: &State) -> bool {
1064    s.kind.is_fragment()
1065        && !s.saw_parbreak
1066        && match s.groupings.as_slice() {
1067            [grouping] => {
1068                std::ptr::eq(grouping.rule, &PAR)
1069                    && s.sink[..grouping.start].iter().all(|(c, _)| c.is::<TagElem>())
1070            }
1071            _ => false,
1072        }
1073}
1074
1075/// Builds the `ParElem` from inline-level elements.
1076fn finish_par(mut grouped: Grouped) -> SourceResult<()> {
1077    // Collapse unsupported spaces in-place.
1078    let (sink, start) = grouped.get_mut();
1079    collapse_spaces(sink, start);
1080
1081    // Collect the children.
1082    let elems = grouped.get();
1083    let span = select_span(elems);
1084    let (body, trunk) = repack(elems);
1085
1086    // Create and visit the paragraph.
1087    let s = grouped.end();
1088    let elem = ParElem::new(body).pack().spanned(span);
1089    visit(s, s.store(elem), trunk)
1090}
1091
1092/// Builds the `CiteGroup` from `CiteElem`s.
1093fn finish_cites(grouped: Grouped) -> SourceResult<()> {
1094    // Collect the children.
1095    let elems = grouped.get();
1096    let span = select_span(elems);
1097    let trunk = elems[0].1;
1098    let children = elems
1099        .iter()
1100        .filter_map(|(c, _)| c.to_packed::<CiteElem>())
1101        .cloned()
1102        .collect();
1103
1104    // Create and visit the citation group.
1105    let s = grouped.end();
1106    let elem = CiteGroup::new(children).pack().spanned(span);
1107    visit(s, s.store(elem), trunk)
1108}
1109
1110/// Builds the `ListLike` element from `ListItemLike` elements.
1111fn finish_list_like<T: ListLike>(grouped: Grouped) -> SourceResult<()> {
1112    // Collect the children.
1113    let elems = grouped.get();
1114    let span = select_span(elems);
1115    let tight = !elems.iter().any(|(c, _)| c.is::<ParbreakElem>());
1116    let styles = elems.iter().filter(|(c, _)| c.is::<T::Item>()).map(|&(_, s)| s);
1117    let trunk = StyleChain::trunk(styles).unwrap();
1118    let trunk_depth = trunk.links().count();
1119    let children = elems
1120        .iter()
1121        .copied()
1122        .filter_map(|(c, s)| {
1123            let item = c.to_packed::<T::Item>()?.clone();
1124            let local = s.suffix(trunk_depth);
1125            Some(T::Item::styled(item, local))
1126        })
1127        .collect();
1128
1129    // Create and visit the list.
1130    let s = grouped.end();
1131    let elem = T::create(children, tight).pack().spanned(span);
1132    visit(s, s.store(elem), trunk)
1133}
1134
1135/// Visit textual elements in `s.sink[start..]` and apply regex show rules to
1136/// them.
1137fn visit_textual(s: &mut State, start: usize) -> SourceResult<bool> {
1138    // Try to find a regex match in the grouped textual elements.
1139    if let Some(m) = find_regex_match_in_elems(s, &s.sink[start..]) {
1140        collapse_spaces(&mut s.sink, start);
1141        let elems = s.store_slice(&s.sink[start..]);
1142        s.sink.truncate(start);
1143        visit_regex_match(s, &elems, m)?;
1144        return Ok(true);
1145    }
1146
1147    Ok(false)
1148}
1149
1150/// Finds the leftmost regex match for this style chain in the given textual
1151/// elements.
1152///
1153/// Collects the element's merged textual representation into the bump arena.
1154/// This merging also takes into account space collapsing so that we don't need
1155/// to call `collapse_spaces` on every textual group, performing yet another
1156/// linear pass. We only collapse the spaces elements themselves on the cold
1157/// path where there is an actual match.
1158fn find_regex_match_in_elems<'a>(
1159    s: &State,
1160    elems: &[Pair<'a>],
1161) -> Option<RegexMatch<'a>> {
1162    let mut buf = BumpString::new_in(&s.arenas.bump);
1163    let mut base = 0;
1164    let mut leftmost = None;
1165    let mut current = StyleChain::default();
1166    let mut space = SpaceState::Destructive;
1167
1168    for &(content, styles) in elems {
1169        if content.is::<TagElem>() {
1170            continue;
1171        }
1172
1173        let linebreak = content.is::<LinebreakElem>();
1174        if linebreak && let SpaceState::Space(_) = space {
1175            buf.pop();
1176        }
1177
1178        if styles != current && !buf.is_empty() {
1179            leftmost = find_regex_match_in_str(&buf, current);
1180            if leftmost.is_some() {
1181                break;
1182            }
1183            base += buf.len();
1184            buf.clear();
1185        }
1186
1187        current = styles;
1188        space = if content.is::<SpaceElem>() {
1189            if space != SpaceState::Supportive {
1190                continue;
1191            }
1192            buf.push(' ');
1193            SpaceState::Space(0)
1194        } else if linebreak {
1195            buf.push('\n');
1196            SpaceState::Destructive
1197        } else if let Some(elem) = content.to_packed::<SmartQuoteElem>() {
1198            buf.push(if elem.double.get(styles) { '"' } else { '\'' });
1199            SpaceState::Supportive
1200        } else if let Some(elem) = content.to_packed::<TextElem>() {
1201            buf.push_str(&elem.text);
1202            SpaceState::Supportive
1203        } else {
1204            panic!("tried to find regex match in non-textual elements");
1205        };
1206    }
1207
1208    if leftmost.is_none() {
1209        leftmost = find_regex_match_in_str(&buf, current);
1210    }
1211
1212    leftmost.map(|m| RegexMatch { offset: base + m.offset, ..m })
1213}
1214
1215/// Finds the leftmost regex match for this style chain in the given text.
1216fn find_regex_match_in_str<'a>(
1217    text: &str,
1218    styles: StyleChain<'a>,
1219) -> Option<RegexMatch<'a>> {
1220    let mut r = 0;
1221    let mut revoked = SmallBitSet::new();
1222    let mut leftmost: Option<(regex::Match, RecipeIndex, &Recipe)> = None;
1223
1224    let depth = LazyCell::new(|| styles.recipes().count());
1225
1226    for entry in styles.entries() {
1227        let recipe = match &**entry {
1228            Style::Recipe(recipe) => recipe,
1229            Style::Property(_) => continue,
1230            Style::Revocation(index) => {
1231                revoked.insert(index.0);
1232                continue;
1233            }
1234        };
1235        r += 1;
1236
1237        let Some(Selector::Regex(regex)) = recipe.selector() else { continue };
1238        let Some(m) = regex.find(text) else { continue };
1239
1240        // Make sure we don't get any empty matches.
1241        if m.range().is_empty() {
1242            continue;
1243        }
1244
1245        // If we already have a match that is equally or more to the left, we're
1246        // not interested in this new match.
1247        if leftmost.is_some_and(|(p, ..)| p.start() <= m.start()) {
1248            continue;
1249        }
1250
1251        // Check whether the rule is already revoked. Do it only now to not
1252        // compute the depth unnecessarily. We subtract 1 from r because we
1253        // already incremented it.
1254        let index = RecipeIndex(*depth - (r - 1));
1255        if revoked.contains(index.0) {
1256            continue;
1257        }
1258
1259        leftmost = Some((m, index, recipe));
1260    }
1261
1262    leftmost.map(|(m, id, recipe)| RegexMatch {
1263        offset: m.start(),
1264        text: m.as_str().into(),
1265        id,
1266        recipe,
1267        styles,
1268    })
1269}
1270
1271/// Visit a match of a regular expression.
1272///
1273/// This first revisits all elements before the match, potentially slicing up
1274/// a text element, then the transformed match, and then the remaining elements
1275/// after the match.
1276fn visit_regex_match<'a>(
1277    s: &mut State<'a, '_, '_, '_>,
1278    elems: &[Pair<'a>],
1279    m: RegexMatch<'a>,
1280) -> SourceResult<()> {
1281    let match_range = m.offset..m.offset + m.text.len();
1282
1283    // Replace with the correct intuitive element kind: if matching against a
1284    // lone symbol, return a `SymbolElem`, otherwise return a newly composed
1285    // `TextElem`. We should only match against a `SymbolElem` during math
1286    // realization (`RealizationKind::Math`).
1287    let piece = match elems {
1288        &[(lone, _)] if lone.is::<SymbolElem>() => lone.clone(),
1289        _ => TextElem::packed(m.text),
1290    };
1291
1292    let context = Context::new(None, Some(m.styles));
1293    let output = m.recipe.apply(s.engine, context.track(), piece)?;
1294
1295    let mut cursor = 0;
1296    let mut output = Some(output);
1297    let mut visit_unconsumed_match = |s: &mut State<'a, '_, '_, '_>| -> SourceResult<()> {
1298        if let Some(output) = output.take() {
1299            let revocation = Style::Revocation(m.id).into();
1300            let outer = s.arenas.bump.alloc(m.styles);
1301            let chained = outer.chain(s.arenas.styles.alloc(revocation));
1302            visit(s, s.store(output), chained)?;
1303        }
1304        Ok(())
1305    };
1306
1307    for &(content, styles) in elems {
1308        // Just forward tags.
1309        if content.is::<TagElem>() {
1310            visit(s, content, styles)?;
1311            continue;
1312        }
1313
1314        // At this point, we can have a `TextElem`, `SymbolElem`, `SpaceElem`,
1315        // `LinebreakElem`, or `SmartQuoteElem`. We now determine the range of
1316        // the element.
1317        let len = if let Some(elem) = content.to_packed::<TextElem>() {
1318            elem.text.len()
1319        } else if let Some(elem) = content.to_packed::<SymbolElem>() {
1320            elem.text.len()
1321        } else {
1322            1 // The rest are Ascii, so just one byte.
1323        };
1324        let elem_range = cursor..cursor + len;
1325
1326        // If the element starts before the start of match, visit it fully or
1327        // sliced.
1328        if elem_range.start < match_range.start {
1329            if elem_range.end <= match_range.start {
1330                visit(s, content, styles)?;
1331            } else {
1332                let mut elem = content.to_packed::<TextElem>().unwrap().clone();
1333                elem.text = elem.text[..match_range.start - elem_range.start].into();
1334                visit(s, s.store(elem.pack()), styles)?;
1335            }
1336        }
1337
1338        // When the match starts before this element ends, visit it.
1339        if match_range.start < elem_range.end {
1340            visit_unconsumed_match(s)?;
1341        }
1342
1343        // If the element ends after the end of the match, visit if fully or
1344        // sliced.
1345        if elem_range.end > match_range.end {
1346            if elem_range.start >= match_range.end {
1347                visit(s, content, styles)?;
1348            } else {
1349                let mut elem = content.to_packed::<TextElem>().unwrap().clone();
1350                elem.text = elem.text[match_range.end - elem_range.start..].into();
1351                visit(s, s.store(elem.pack()), styles)?;
1352            }
1353        }
1354
1355        cursor = elem_range.end;
1356    }
1357
1358    // If the match wasn't consumed yet, visit it. This shouldn't really happen
1359    // in practice (we'd need to have an empty match at the end), but it's an
1360    // extra fail-safe.
1361    visit_unconsumed_match(s)?;
1362
1363    Ok(())
1364}
1365
1366/// Collapses all spaces within `buf[start..]` that are at the edges or in the
1367/// vicinity of destructive elements.
1368fn collapse_spaces(buf: &mut Vec<Pair>, start: usize) {
1369    let mut state = SpaceState::Destructive;
1370    let mut k = start;
1371
1372    // We do one pass over the elements, backshifting everything as necessary
1373    // when a space collapses. The variable `i` is our cursor in the original
1374    // elements. The variable `k` is our cursor in the result. At all times, we
1375    // have `k <= i`, so we can do it in place.
1376    for i in start..buf.len() {
1377        let (content, styles) = buf[i];
1378
1379        // Determine the next state.
1380        if content.is::<TagElem>() {
1381            // Nothing to do.
1382        } else if content.is::<SpaceElem>() {
1383            if state != SpaceState::Supportive {
1384                continue;
1385            }
1386            state = SpaceState::Space(k);
1387        } else if content.is::<LinebreakElem>() {
1388            destruct_space(buf, &mut k, &mut state);
1389        } else if let Some(elem) = content.to_packed::<HElem>() {
1390            if elem.amount.is_fractional() || elem.weak.get(styles) {
1391                destruct_space(buf, &mut k, &mut state);
1392            }
1393        } else {
1394            state = SpaceState::Supportive;
1395        };
1396
1397        // Copy over normal elements (in place).
1398        if k < i {
1399            buf[k] = buf[i];
1400        }
1401        k += 1;
1402    }
1403
1404    destruct_space(buf, &mut k, &mut state);
1405
1406    // Delete all the excess that's left due to the gaps produced by spaces.
1407    buf.truncate(k);
1408}
1409
1410/// Deletes a preceding space if any.
1411fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) {
1412    if let SpaceState::Space(s) = *state {
1413        buf.copy_within(s + 1..*end, s);
1414        *end -= 1;
1415    }
1416    *state = SpaceState::Destructive;
1417}
1418
1419/// Finds the first non-detached span in the list.
1420fn select_span(children: &[Pair]) -> Span {
1421    Span::find(children.iter().map(|(c, _)| c.span()))
1422}
1423
1424/// Turn realized content with styles back into owned content and a trunk style
1425/// chain.
1426fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) {
1427    let trunk = StyleChain::trunk_from_pairs(buf).unwrap_or_default();
1428    let depth = trunk.links().count();
1429
1430    let mut seq = Vec::with_capacity(buf.len());
1431
1432    for (chain, group) in buf.group_by_key(|&(_, s)| s) {
1433        let iter = group.iter().map(|&(c, _)| c.clone());
1434        let suffix = chain.suffix(depth);
1435        if suffix.is_empty() {
1436            seq.extend(iter);
1437        } else if let &[(element, _)] = group {
1438            seq.push(element.clone().styled_with_map(suffix));
1439        } else {
1440            seq.push(Content::sequence(iter).styled_with_map(suffix));
1441        }
1442    }
1443
1444    (Content::sequence(seq), trunk)
1445}