Skip to main content

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