1use 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#[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
74struct State<'a, 'x, 'y, 'z> {
85 kind: RealizationKind<'x>,
87 engine: &'x mut Engine<'y>,
89 locator: &'x mut SplitLocator<'z>,
91 arenas: &'a Arenas,
93 sink: Vec<Pair<'a>>,
95 rules: &'x [&'x GroupingRule],
97 groupings: ArrayVec<Grouping<'x>, MAX_GROUP_NESTING>,
99 outside: bool,
102 may_attach: bool,
104 saw_parbreak: bool,
106}
107
108struct GroupingRule {
110 priority: u8,
113 tags: bool,
117 trigger: fn(&Content, &State) -> bool,
119 inner: fn(&Content) -> bool,
122 interrupt: fn(Element) -> bool,
124 finish: fn(Grouped) -> SourceResult<()>,
127}
128
129struct Grouping<'a> {
131 start: usize,
133 interrupted: bool,
137 rule: &'a GroupingRule,
139}
140
141struct Grouped<'a, 'x, 'y, 'z, 's> {
143 s: &'s mut State<'a, 'x, 'y, 'z>,
145 start: usize,
147}
148
149struct Verdict<'a> {
151 prepared: bool,
154 map: Styles,
156 step: Option<ShowStep<'a>>,
158}
159
160enum ShowStep<'a> {
162 Recipe(&'a Recipe, RecipeIndex),
164 Builtin(NativeShowRule),
166}
167
168struct RegexMatch<'a> {
170 offset: usize,
172 text: EcoString,
174 styles: StyleChain<'a>,
176 id: RecipeIndex,
178 recipe: &'a Recipe,
180}
181
182#[derive(Debug, Copy, Clone, Eq, PartialEq)]
184enum SpaceState {
185 Destructive,
187 Supportive,
189 Space(usize),
191}
192
193impl<'a> State<'a, '_, '_, '_> {
194 fn store(&self, content: Content) -> &'a Content {
196 self.arenas.content.alloc(content)
197 }
198
199 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 fn get(&self) -> &[Pair<'a>] {
214 &self.s.sink[self.start..]
215 }
216
217 fn get_mut(&mut self) -> (&mut Vec<Pair<'a>>, usize) {
219 (&mut self.s.sink, self.start)
220 }
221
222 fn end(self) -> &'s mut State<'a, 'x, 'y, 'z> {
225 self.s.sink.truncate(self.start);
226 self.s
227 }
228}
229
230fn visit<'a>(
232 s: &mut State<'a, '_, '_, '_>,
233 content: &'a Content,
234 styles: StyleChain<'a>,
235) -> SourceResult<()> {
236 if content.is::<TagElem>() {
238 s.sink.push((content, styles));
239 return Ok(());
240 }
241
242 if visit_kind_rules(s, content, styles)? {
245 return Ok(());
246 }
247
248 if visit_show_rules(s, content, styles)? {
250 return Ok(());
251 }
252
253 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 if let Some(styled) = content.to_packed::<StyledElem>() {
264 return visit_styled(s, &styled.child, Cow::Borrowed(&styled.styles), styles);
265 }
266
267 if visit_grouping_rules(s, content, styles)? {
270 return Ok(());
271 }
272
273 if visit_filter_rules(s, content, styles)? {
275 return Ok(());
276 }
277
278 s.sink.push((content, styles));
281
282 Ok(())
283}
284
285fn 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 if let Some(elem) = content.to_packed::<EquationElem>() {
299 visit(s, &elem.body, styles)?;
300 return Ok(true);
301 }
302
303 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 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 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
340fn visit_show_rules<'a>(
343 s: &mut State<'a, '_, '_, '_>,
344 content: &'a Content,
345 styles: StyleChain<'a>,
346) -> SourceResult<bool> {
347 let Some(Verdict { prepared, mut map, step }) = verdict(s.engine, content, styles)
349 else {
350 return Ok(false);
351 };
352
353 let mut output = Cow::Borrowed(content);
355
356 let mut tags = None;
359 if !prepared {
360 tags = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
361 }
362
363 if let Some(step) = step {
365 let chained = styles.chain(&map);
366 let result = match step {
367 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 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 output = Cow::Owned(s.engine.delay(result));
392 }
393
394 let realized = match output {
396 Cow::Borrowed(realized) => realized,
397 Cow::Owned(realized) => s.store(realized),
398 };
399
400 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 if let Some(tag) = end {
418 visit(s, s.store(TagElem::packed(tag)), styles)?;
419 }
420
421 Ok(true)
422}
423
424fn 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 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 let depth = LazyCell::new(|| styles.recipes().count());
455
456 for (r, recipe) in styles.recipes().enumerate() {
457 if !recipe
459 .selector()
460 .is_some_and(|selector| selector.matches(elem, Some(styles)))
461 {
462 continue;
463 }
464
465 if let Transformation::Style(transform) = recipe.transform() {
467 if !prepared {
468 map.apply(transform.clone());
469 }
470 continue;
471 }
472
473 if step.is_some() {
475 continue;
476 }
477
478 let index = RecipeIndex(*depth - r);
480 if elem.is_guarded(index) {
481 continue;
482 }
483
484 step = Some(ShowStep::Recipe(recipe, index));
486
487 if prepared {
491 break;
492 }
493 }
494
495 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 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
521fn 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 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 if let Some(show_settable) = elem.with::<dyn ShowSet>() {
550 map.apply(show_settable.show_set(styles));
551 }
552
553 if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() {
557 synthesizable.synthesize(engine, styles.chain(map))?;
558 }
559
560 elem.materialize(styles.chain(map));
563
564 let tags = elem
570 .location()
571 .map(|loc| (Tag::Start(elem.clone(), flags), Tag::End(loc, key, flags)));
572
573 elem.mark_prepared();
576
577 Ok(tags)
578}
579
580fn 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 if local.is_empty() {
589 return visit(s, content, outer);
590 }
591
592 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 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 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 s.outside {
633 local = Cow::Owned(local.into_owned().outside());
634 }
635
636 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 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 if pagebreak {
662 visit(s, PagebreakElem::shared_boundary(), *outer)?;
663 }
664
665 Ok(())
666}
667
668fn 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 let mut i = 0;
679 while let Some(active) = s.groupings.last() {
680 if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
682 break;
683 }
684
685 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 bail!(content.span(), "maximum grouping depth exceeded");
703 }
704 }
705
706 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
717fn 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 return Ok(true);
732 } else if content.is::<ParbreakElem>() {
733 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 return Ok(true);
745 }
746
747 s.may_attach = content.is::<ParElem>();
749
750 Ok(false)
751}
752
753fn finish(s: &mut State) -> SourceResult<()> {
755 finish_grouping_while(s, |s| {
756 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 if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) {
770 collapse_spaces(&mut s.sink, 0);
771 }
772
773 Ok(())
774}
775
776fn 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
797fn finish_grouping_while<F>(s: &mut State, mut f: F) -> SourceResult<()>
799where
800 F: FnMut(&mut State) -> bool,
801{
802 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
816fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
818 let Grouping { mut start, rule, .. } = s.groupings.pop().unwrap();
820
821 let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s));
824 let mut end = start + trimmed.len();
825
826 if rule.tags {
833 if std::ptr::eq(rule, &PAR) {
840 for _ in s.sink.extract_if(end.., |(c, _)| c.is::<SpaceElem>()) {}
841 }
842
843 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 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 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 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 (rule.finish)(Grouped { s, start })?;
891
892 for &(content, styles) in tags.iter().chain(&tail) {
894 visit(s, content, styles)?;
895 }
896
897 Ok(())
898}
899
900fn 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
913fn to_tag<'a>((c, _): &Pair<'a>) -> Option<&'a Packed<TagElem>> {
915 c.to_packed::<TagElem>()
916}
917
918const MAX_GROUP_NESTING: usize = 3;
921
922static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
924
925static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
927
928static HTML_DOCUMENT_RULES: &[&GroupingRule] =
930 &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
931
932static HTML_FRAGMENT_RULES: &[&GroupingRule] =
934 &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
935
936static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
938
939static TEXTUAL: GroupingRule = GroupingRule {
941 priority: 3,
942 tags: true,
943 trigger: |content, _| {
944 let elem = content.elem();
945 elem == TextElem::ELEM
949 || elem == LinebreakElem::ELEM
950 || elem == SmartQuoteElem::ELEM
951 },
952 inner: |content| content.elem() == SpaceElem::ELEM,
953 interrupt: |_| true,
956 finish: finish_textual,
957};
958
959static 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
982static 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
994static LIST: GroupingRule = list_like_grouping::<ListElem>();
996
997static ENUM: GroupingRule = list_like_grouping::<EnumElem>();
999
1000static TERMS: GroupingRule = list_like_grouping::<TermsElem>();
1002
1003const 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
1018fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
1027 if visit_textual(s, start)? {
1030 return Ok(());
1031 }
1032
1033 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 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
1054fn 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
1061fn 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
1075fn finish_par(mut grouped: Grouped) -> SourceResult<()> {
1077 let (sink, start) = grouped.get_mut();
1079 collapse_spaces(sink, start);
1080
1081 let elems = grouped.get();
1083 let span = select_span(elems);
1084 let (body, trunk) = repack(elems);
1085
1086 let s = grouped.end();
1088 let elem = ParElem::new(body).pack().spanned(span);
1089 visit(s, s.store(elem), trunk)
1090}
1091
1092fn finish_cites(grouped: Grouped) -> SourceResult<()> {
1094 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 let s = grouped.end();
1106 let elem = CiteGroup::new(children).pack().spanned(span);
1107 visit(s, s.store(elem), trunk)
1108}
1109
1110fn finish_list_like<T: ListLike>(grouped: Grouped) -> SourceResult<()> {
1112 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 let s = grouped.end();
1131 let elem = T::create(children, tight).pack().spanned(span);
1132 visit(s, s.store(elem), trunk)
1133}
1134
1135fn visit_textual(s: &mut State, start: usize) -> SourceResult<bool> {
1138 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
1150fn 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
1215fn 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 if m.range().is_empty() {
1242 continue;
1243 }
1244
1245 if leftmost.is_some_and(|(p, ..)| p.start() <= m.start()) {
1248 continue;
1249 }
1250
1251 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
1271fn 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 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 if content.is::<TagElem>() {
1310 visit(s, content, styles)?;
1311 continue;
1312 }
1313
1314 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 };
1324 let elem_range = cursor..cursor + len;
1325
1326 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 if match_range.start < elem_range.end {
1340 visit_unconsumed_match(s)?;
1341 }
1342
1343 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 visit_unconsumed_match(s)?;
1362
1363 Ok(())
1364}
1365
1366fn collapse_spaces(buf: &mut Vec<Pair>, start: usize) {
1369 let mut state = SpaceState::Destructive;
1370 let mut k = start;
1371
1372 for i in start..buf.len() {
1377 let (content, styles) = buf[i];
1378
1379 if content.is::<TagElem>() {
1381 } 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 if k < i {
1399 buf[k] = buf[i];
1400 }
1401 k += 1;
1402 }
1403
1404 destruct_space(buf, &mut k, &mut state);
1405
1406 buf.truncate(k);
1408}
1409
1410fn 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
1419fn select_span(children: &[Pair]) -> Span {
1421 Span::find(children.iter().map(|(c, _)| c.span()))
1422}
1423
1424fn 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}