1use 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#[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
78struct State<'a, 'x, 'y, 'z> {
89 kind: RealizationKind<'x>,
91 engine: &'x mut Engine<'y>,
93 locator: &'x mut SplitLocator<'z>,
95 arenas: &'a Arenas,
97 sink: Vec<Pair<'a>>,
99 rules: &'x [&'x GroupingRule],
101 groupings: ArrayVec<Grouping<'x>, MAX_GROUP_NESTING>,
103 outside: bool,
106 may_attach: bool,
108 saw_parbreak: bool,
110}
111
112struct GroupingRule {
114 priority: u8,
117 tags: bool,
121 effect: fn(&Content) -> GroupingEffect,
123 interrupt: fn(Element) -> bool,
125 finish: fn(Grouped) -> SourceResult<()>,
128}
129
130#[derive(Debug, Copy, Clone, Eq, PartialEq)]
132enum GroupingEffect {
133 Trigger,
135 Inner,
139 Neutral,
147 Interrupt,
149}
150
151struct Grouping<'a> {
153 start: usize,
155 interrupted: bool,
159 contains_neutral: bool,
161 rule: &'a GroupingRule,
163}
164
165struct Grouped<'a, 'x, 'y, 'z, 's> {
167 s: &'s mut State<'a, 'x, 'y, 'z>,
169 start: usize,
171}
172
173struct Verdict<'a> {
175 prepared: bool,
178 map: Styles,
180 step: Option<ShowStep<'a>>,
182}
183
184enum ShowStep<'a> {
186 Recipe(&'a Recipe, RecipeIndex),
188 Builtin(NativeShowRule),
190}
191
192struct RegexMatch<'a> {
194 offset: usize,
196 text: EcoString,
198 styles: StyleChain<'a>,
200 id: RecipeIndex,
202 recipe: &'a Recipe,
204}
205
206impl<'a> State<'a, '_, '_, '_> {
207 fn store(&self, content: Content) -> &'a Content {
209 self.arenas.content.alloc(content)
210 }
211
212 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 fn get(&self) -> &[Pair<'a>] {
227 &self.s.sink[self.start..]
228 }
229
230 fn get_mut(&mut self) -> (&mut Vec<Pair<'a>>, usize) {
232 (&mut self.s.sink, self.start)
233 }
234
235 fn end(self) -> &'s mut State<'a, 'x, 'y, 'z> {
238 self.s.sink.truncate(self.start);
239 self.s
240 }
241}
242
243fn visit<'a>(
245 s: &mut State<'a, '_, '_, '_>,
246 content: &'a Content,
247 styles: StyleChain<'a>,
248) -> SourceResult<()> {
249 if content.is::<TagElem>() {
251 s.sink.push((content, styles));
252 return Ok(());
253 }
254
255 if visit_kind_rules(s, content, styles)? {
258 return Ok(());
259 }
260
261 if visit_show_rules(s, content, styles)? {
263 return Ok(());
264 }
265
266 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 if let Some(styled) = content.to_packed::<StyledElem>() {
277 return visit_styled(s, &styled.child, Cow::Borrowed(&styled.styles), styles);
278 }
279
280 if visit_grouping_rules(s, content, styles)? {
283 return Ok(());
284 }
285
286 if visit_filter_rules(s, content, styles)? {
288 return Ok(());
289 }
290
291 s.sink.push((content, styles));
294
295 Ok(())
296}
297
298fn 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 if let Some(elem) = content.to_packed::<EquationElem>() {
312 visit(s, &elem.body, styles)?;
313 return Ok(true);
314 }
315
316 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 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 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
353fn visit_show_rules<'a>(
356 s: &mut State<'a, '_, '_, '_>,
357 content: &'a Content,
358 styles: StyleChain<'a>,
359) -> SourceResult<bool> {
360 let Some(Verdict { prepared, mut map, step }) = verdict(s.engine, content, styles)
362 else {
363 return Ok(false);
364 };
365
366 let mut output = Cow::Borrowed(content);
368
369 let mut tags = None;
372 if !prepared {
373 tags = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
374 }
375
376 if let Some(step) = step {
378 let chained = styles.chain(&map);
379 let result = match step {
380 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 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 output = Cow::Owned(s.engine.delay(result));
405 }
406
407 let realized = match output {
409 Cow::Borrowed(realized) => realized,
410 Cow::Owned(realized) => s.store(realized),
411 };
412
413 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 if let Some(tag) = end {
431 visit(s, s.store(TagElem::packed(tag)), styles)?;
432 }
433
434 Ok(true)
435}
436
437fn 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 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 let depth = LazyCell::new(|| styles.recipes().count());
468
469 for (r, recipe) in styles.recipes().enumerate() {
470 if !recipe
472 .selector()
473 .is_some_and(|selector| selector.matches(elem, Some(styles)))
474 {
475 continue;
476 }
477
478 if let Transformation::Style(transform) = recipe.transform() {
480 if !prepared {
481 map.apply(transform.clone());
482 }
483 continue;
484 }
485
486 if step.is_some() {
488 continue;
489 }
490
491 let index = RecipeIndex(*depth - r);
493 if elem.is_guarded(index) {
494 continue;
495 }
496
497 step = Some(ShowStep::Recipe(recipe, index));
499
500 if prepared {
504 break;
505 }
506 }
507
508 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 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
534fn 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 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 if let Some(show_settable) = elem.with::<dyn ShowSet>() {
563 map.apply(show_settable.show_set(styles));
564 }
565
566 if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() {
570 synthesizable.synthesize(engine, styles.chain(map))?;
571 }
572
573 elem.materialize(styles.chain(map));
576
577 let tags = elem
583 .location()
584 .map(|loc| (Tag::Start(elem.clone(), flags), Tag::End(loc, key, flags)));
585
586 elem.mark_prepared();
589
590 Ok(tags)
591}
592
593fn 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 if local.is_empty() {
602 return visit(s, content, outer);
603 }
604
605 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 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 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 s.outside {
661 local = Cow::Owned(local.into_owned().outside());
662 }
663
664 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 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 if pagebreak {
690 visit(s, PagebreakElem::shared_boundary(), *outer)?;
691 }
692
693 Ok(())
694}
695
696fn 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 let mut i = 0;
710 while let Some(active) = s.groupings.last_mut() {
711 if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
713 break;
714 }
715
716 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 bail!(content.span(), "maximum grouping depth exceeded");
734 }
735 }
736
737 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
753fn 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 return Ok(true);
768 } else if content.is::<ParbreakElem>() {
769 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 return Ok(true);
781 }
782
783 s.may_attach = content.is::<ParElem>();
785
786 Ok(false)
787}
788
789fn finish(s: &mut State) -> SourceResult<()> {
791 finish_grouping_while(s, |s| {
792 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 if matches!(s.kind, RealizationKind::Par | RealizationKind::Math) {
808 collapse_spaces(&mut s.sink, 0);
809 }
810
811 Ok(())
812}
813
814fn 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
835fn finish_grouping_while<F>(s: &mut State, mut f: F) -> SourceResult<()>
837where
838 F: FnMut(&mut State) -> bool,
839{
840 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
854fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
856 let Grouping { start, rule, contains_neutral, .. } = s.groupings.pop().unwrap();
858 if contains_neutral {
859 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 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 !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
898fn finish_grouping(
901 s: &mut State,
902 rule: &GroupingRule,
903 mut start: usize,
904) -> SourceResult<()> {
905 let trimmed = s.sink[start..]
908 .trim_end_matches(|(c, _)| (rule.effect)(c) != GroupingEffect::Trigger);
909 let mut end = start + trimmed.len();
910
911 if rule.tags {
918 if std::ptr::eq(rule, &PAR) {
925 for _ in s.sink.extract_if(end.., |(c, _)| c.is::<SpaceElem>()) {}
926 }
927
928 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 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 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 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 (rule.finish)(Grouped { s, start })?;
976
977 for &(content, styles) in tags.iter().chain(&tail) {
979 visit(s, content, styles)?;
980 }
981
982 Ok(())
983}
984
985fn 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
998fn to_tag<'a>((c, _): &Pair<'a>) -> Option<&'a Packed<TagElem>> {
1000 c.to_packed::<TagElem>()
1001}
1002
1003const MAX_GROUP_NESTING: usize = 3;
1006
1007static BUNDLE_RULES: &[&GroupingRule] = &[];
1009
1010static FLOW_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
1012
1013static PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
1015
1016static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
1018
1019static TEXTUAL: GroupingRule = GroupingRule {
1021 priority: 3,
1022 tags: true,
1023 effect: |content| {
1024 let elem = content.elem();
1025 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 interrupt: |_| true,
1042 finish: finish_textual,
1043};
1044
1045static 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
1075static 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
1095static LIST: GroupingRule = list_like_grouping::<ListElem>();
1097
1098static ENUM: GroupingRule = list_like_grouping::<EnumElem>();
1100
1101static TERMS: GroupingRule = list_like_grouping::<TermsElem>();
1103
1104const 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
1124fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
1133 if visit_textual(s, start)? {
1136 return Ok(());
1137 }
1138
1139 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 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
1165fn 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
1172fn 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
1190fn finish_par(mut grouped: Grouped) -> SourceResult<()> {
1192 let (sink, start) = grouped.get_mut();
1194 collapse_spaces(sink, start);
1195
1196 let elems = grouped.get();
1198 let span = select_span(elems);
1199 let (body, trunk) = repack(elems);
1200
1201 let s = grouped.end();
1203 let elem = ParElem::new(body).pack().spanned(span);
1204 visit(s, s.store(elem), trunk)
1205}
1206
1207fn finish_cites(grouped: Grouped) -> SourceResult<()> {
1209 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 let s = grouped.end();
1217 let elem = CiteGroup::new(children).pack().spanned(span);
1218 visit(s, s.store(elem), trunk)
1219}
1220
1221fn finish_list_like<T: ListLike>(grouped: Grouped) -> SourceResult<()> {
1223 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 let s = grouped.end();
1242 let elem = T::create(children, tight).pack().spanned(span);
1243 visit(s, s.store(elem), trunk)
1244}
1245
1246fn visit_textual(s: &mut State, start: usize) -> SourceResult<bool> {
1249 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
1261fn 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 != 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
1320fn 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 if m.range().is_empty() {
1347 continue;
1348 }
1349
1350 if leftmost.is_some_and(|(p, ..)| p.start() <= m.start()) {
1353 continue;
1354 }
1355
1356 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
1376fn 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 if content.is::<TagElem>() {
1405 visit(s, content, styles)?;
1406 continue;
1407 }
1408
1409 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 };
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 visit(s, content, styles)?;
1426 continue;
1427 }
1428
1429 if elem_range.start < match_range.start {
1430 let end = match_range.start - elem_range.start;
1433 visit(s, s.store(slice_textual(content, ..end)), styles)?;
1434 }
1435
1436 if let Some(RegexMatch { text, styles, id, recipe, offset: _ }) = m.take() {
1440 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 slice_textual(
1449 content,
1450 match_range.start - elem_range.start
1451 ..match_range.end - elem_range.start,
1452 )
1453 } else {
1454 TextElem::packed(text).spanned(content.span())
1462 };
1463
1464 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 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
1485fn slice_textual(elem: &Content, range: impl SliceIndex<str, Output = str>) -> Content {
1488 if let Some(elem) = elem.to_packed::<TextElem>() {
1489 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 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
1508fn select_span(children: &[Pair]) -> Span {
1510 Span::find(children.iter().map(|(c, _)| c.span()))
1511}
1512
1513fn 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}