use std::borrow::Cow;
use std::cell::LazyCell;
use std::slice::SliceIndex;
use arrayvec::ArrayVec;
use bumpalo::Bump;
use bumpalo::collections::{CollectIn, String as BumpString, Vec as BumpVec};
use comemo::Track;
use ecow::EcoString;
use typst_html::HtmlElem;
use typst_library::diag::{At, SourceResult, bail, warning};
use typst_library::engine::Engine;
use typst_library::foundations::{
Content, Context, ContextElem, Element, NativeElement, NativeShowRule, Packed,
Recipe, RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem,
Styles, SymbolElem, Synthesize, Target, TargetElem, Transformation,
};
use typst_library::introspection::{
Locatable, LocationKey, SplitLocator, Tag, TagElem, TagFlags, Tagged,
};
use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
};
use typst_library::math::{EquationElem, Mathy};
use typst_library::model::{
CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike,
ParElem, ParbreakElem, TermsElem,
};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_syntax::Span;
use typst_utils::{ListSet, SliceExt, SmallBitSet};
mod spaces;
use spaces::{SpaceState, collapse_spaces, collapse_state_textual};
#[typst_macros::time(name = "realize")]
pub fn realize<'a>(
kind: RealizationKind,
engine: &mut Engine,
locator: &mut SplitLocator,
arenas: &'a Arenas,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<Vec<Pair<'a>>> {
let mut s = State {
engine,
locator,
arenas,
rules: match kind {
RealizationKind::Bundle => BUNDLE_RULES,
RealizationKind::Document { .. } => FLOW_RULES,
RealizationKind::Fragment { .. } => FLOW_RULES,
RealizationKind::Par => PAR_RULES,
RealizationKind::Math => MATH_RULES,
},
sink: vec![],
groupings: ArrayVec::new(),
outside: matches!(kind, RealizationKind::Document { .. }),
may_attach: false,
saw_parbreak: false,
kind,
};
visit(&mut s, content, styles)?;
finish(&mut s)?;
Ok(s.sink)
}
struct State<'a, 'x, 'y, 'z> {
kind: RealizationKind<'x>,
engine: &'x mut Engine<'y>,
locator: &'x mut SplitLocator<'z>,
arenas: &'a Arenas,
sink: Vec<Pair<'a>>,
rules: &'x [&'x GroupingRule],
groupings: ArrayVec<Grouping<'x>, MAX_GROUP_NESTING>,
outside: bool,
may_attach: bool,
saw_parbreak: bool,
}
struct GroupingRule {
priority: u8,
tags: bool,
effect: fn(&Content) -> GroupingEffect,
interrupt: fn(Element) -> bool,
finish: fn(Grouped) -> SourceResult<()>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum GroupingEffect {
Trigger,
Inner,
Neutral,
Interrupt,
}
struct Grouping<'a> {
start: usize,
interrupted: bool,
contains_neutral: bool,
rule: &'a GroupingRule,
}
struct Grouped<'a, 'x, 'y, 'z, 's> {
s: &'s mut State<'a, 'x, 'y, 'z>,
start: usize,
}
struct Verdict<'a> {
prepared: bool,
map: Styles,
step: Option<ShowStep<'a>>,
}
enum ShowStep<'a> {
Recipe(&'a Recipe, RecipeIndex),
Builtin(NativeShowRule),
}
struct RegexMatch<'a> {
offset: usize,
text: EcoString,
styles: StyleChain<'a>,
id: RecipeIndex,
recipe: &'a Recipe,
}
impl<'a> State<'a, '_, '_, '_> {
fn store(&self, content: Content) -> &'a Content {
self.arenas.content.alloc(content)
}
fn store_slice(&self, pairs: &[Pair<'a>]) -> BumpVec<'a, Pair<'a>> {
let mut vec = BumpVec::new_in(&self.arenas.bump);
vec.extend_from_slice_copy(pairs);
vec
}
}
impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> {
fn get(&self) -> &[Pair<'a>] {
&self.s.sink[self.start..]
}
fn get_mut(&mut self) -> (&mut Vec<Pair<'a>>, usize) {
(&mut self.s.sink, self.start)
}
fn end(self) -> &'s mut State<'a, 'x, 'y, 'z> {
self.s.sink.truncate(self.start);
self.s
}
}
fn visit<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<()> {
if content.is::<TagElem>() {
s.sink.push((content, styles));
return Ok(());
}
if visit_kind_rules(s, content, styles)? {
return Ok(());
}
if visit_show_rules(s, content, styles)? {
return Ok(());
}
if let Some(sequence) = content.to_packed::<SequenceElem>() {
for elem in &sequence.children {
visit(s, elem, styles)?;
}
return Ok(());
}
if let Some(styled) = content.to_packed::<StyledElem>() {
return visit_styled(s, &styled.child, Cow::Borrowed(&styled.styles), styles);
}
if visit_grouping_rules(s, content, styles)? {
return Ok(());
}
if visit_filter_rules(s, content, styles)? {
return Ok(());
}
s.sink.push((content, styles));
Ok(())
}
fn visit_kind_rules<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
if let RealizationKind::Math = s.kind {
if let Some(elem) = content.to_packed::<EquationElem>() {
visit(s, &elem.body, styles)?;
return Ok(true);
}
if let Some(elem) = content.to_packed::<SymbolElem>() {
if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) {
visit_regex_match(s, &[(content, styles)], m)?;
return Ok(true);
}
} else if let Some(elem) = content.to_packed::<TextElem>()
&& let Some(m) = find_regex_match_in_str(&elem.text, styles)
{
visit_regex_match(s, &[(content, styles)], m)?;
return Ok(true);
}
} else {
if content.can::<dyn Mathy>() && !content.is::<EquationElem>() {
let eq = EquationElem::new(content.clone()).pack().spanned(content.span());
visit(s, s.store(eq), styles)?;
return Ok(true);
}
if let Some(elem) = content.to_packed::<SymbolElem>() {
let mut text = TextElem::packed(elem.text.clone()).spanned(elem.span());
if let Some(label) = elem.label() {
text.set_label(label);
}
visit(s, s.store(text), styles)?;
return Ok(true);
}
}
Ok(false)
}
fn visit_show_rules<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
let Some(Verdict { prepared, mut map, step }) = verdict(s.engine, content, styles)
else {
return Ok(false);
};
let mut output = Cow::Borrowed(content);
let mut tags = None;
if !prepared {
tags = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
}
if let Some(step) = step {
let chained = styles.chain(&map);
let result = match step {
ShowStep::Recipe(recipe, guard) => {
let context = Context::new(output.location(), Some(chained));
recipe.apply(
s.engine,
context.track(),
output.into_owned().guarded(guard),
)
}
ShowStep::Builtin(rule) => {
let _scope = typst_timing::TimingScope::new(output.elem().name());
rule.apply(&output, s.engine, chained)
.map(|content| content.spanned(output.span()))
}
};
output = Cow::Owned(s.engine.delay(result));
}
let realized = match output {
Cow::Borrowed(realized) => realized,
Cow::Owned(realized) => s.store(realized),
};
let (start, end) = tags.unzip();
if let Some(tag) = start {
visit(s, s.store(TagElem::packed(tag)), styles)?;
}
let prev_outside = s.outside;
s.outside &= content.is::<ContextElem>();
s.engine.route.increase();
s.engine.route.check_show_depth().at(content.span())?;
visit_styled(s, realized, Cow::Owned(map), styles)?;
s.outside = prev_outside;
s.engine.route.decrease();
if let Some(tag) = end {
visit(s, s.store(TagElem::packed(tag)), styles)?;
}
Ok(true)
}
fn verdict<'a>(
engine: &mut Engine,
elem: &'a Content,
styles: StyleChain<'a>,
) -> Option<Verdict<'a>> {
let prepared = elem.is_prepared();
let mut map = Styles::new();
let mut step = None;
let mut elem = elem;
let mut slot;
if !prepared && elem.can::<dyn Synthesize>() {
slot = elem.clone();
slot.with_mut::<dyn Synthesize>()
.unwrap()
.synthesize(engine, styles)
.ok();
elem = &slot;
}
let depth = LazyCell::new(|| styles.recipes().count());
for (r, recipe) in styles.recipes().enumerate() {
if !recipe
.selector()
.is_some_and(|selector| selector.matches(elem, Some(styles)))
{
continue;
}
if let Transformation::Style(transform) = recipe.transform() {
if !prepared {
map.apply(transform.clone());
}
continue;
}
if step.is_some() {
continue;
}
let index = RecipeIndex(*depth - r);
if elem.is_guarded(index) {
continue;
}
step = Some(ShowStep::Recipe(recipe, index));
if prepared {
break;
}
}
if step.is_none() {
let target = styles.get(TargetElem::target);
if let Some(rule) = engine.library.rules.get(target, elem) {
step = Some(ShowStep::Builtin(rule));
}
}
if step.is_none()
&& map.is_empty()
&& (prepared || {
elem.label().is_none()
&& elem.location().is_none()
&& !elem.can::<dyn ShowSet>()
&& !elem.can::<dyn Locatable>()
&& !elem.can::<dyn Tagged>()
&& !elem.can::<dyn Synthesize>()
})
{
return None;
}
Some(Verdict { prepared, map, step })
}
fn prepare(
engine: &mut Engine,
locator: &mut SplitLocator,
elem: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<(Tag, Tag)>> {
let key = typst_utils::hash128(&elem);
let flags = TagFlags {
introspectable: elem.can::<dyn Locatable>()
|| elem.label().is_some()
|| elem.location().is_some(),
tagged: elem.can::<dyn Tagged>(),
};
if elem.location().is_none() && flags.any() {
let loc = locator.next_location(engine, key, elem.span());
elem.set_location(loc);
}
if let Some(show_settable) = elem.with::<dyn ShowSet>() {
map.apply(show_settable.show_set(styles));
}
if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() {
synthesizable.synthesize(engine, styles.chain(map))?;
}
elem.materialize(styles.chain(map));
let tags = elem
.location()
.map(|loc| (Tag::Start(elem.clone(), flags), Tag::End(loc, key, flags)));
elem.mark_prepared();
Ok(tags)
}
fn visit_styled<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
mut local: Cow<'a, Styles>,
outer: StyleChain<'a>,
) -> SourceResult<()> {
if local.is_empty() {
return visit(s, content, outer);
}
let mut pagebreak = false;
for style in local.iter() {
let Some(elem) = style.element() else { continue };
if elem == DocumentElem::ELEM {
let local = StyleChain::new(&local);
if let RealizationKind::Document { info } = &mut s.kind {
info.populate(local);
} else if !matches!(s.kind, RealizationKind::Bundle) {
bail!(
style.span(),
"document set rules are not allowed inside of containers",
);
}
if local.has(DocumentElem::format)
&& !matches!(s.kind, RealizationKind::Bundle)
{
bail!(
style.span(),
"setting the document format is only supported in the bundle target"
);
}
} else if elem == TextElem::ELEM {
if let RealizationKind::Document { info } = &mut s.kind {
info.populate_locale(StyleChain::new(&local));
}
} else if elem == PageElem::ELEM {
match s.kind {
RealizationKind::Bundle => {}
RealizationKind::Document { .. } => match outer.get(TargetElem::target) {
Target::Paged => {
pagebreak = true;
s.outside = true;
}
Target::Html => {
s.engine.sink.warn(warning!(
style.span(),
"page set rule was ignored during HTML export"
));
}
Target::Bundle => {}
},
_ => bail!(
style.span(),
"page configuration is not allowed inside of containers",
),
}
}
}
if s.outside {
local = Cow::Owned(local.into_owned().outside());
}
let outer = s.arenas.bump.alloc(outer);
let local = match local {
Cow::Borrowed(map) => map,
Cow::Owned(owned) => &*s.arenas.styles.alloc(owned),
};
if pagebreak {
let relevant = local
.as_slice()
.trim_end_matches(|style| style.element() != Some(PageElem::ELEM));
visit(s, PagebreakElem::shared_weak(), outer.chain(relevant))?;
}
finish_interrupted(s, local)?;
visit(s, content, outer.chain(local))?;
finish_interrupted(s, local)?;
if pagebreak {
visit(s, PagebreakElem::shared_boundary(), *outer)?;
}
Ok(())
}
fn visit_grouping_rules<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
let matching = s
.rules
.iter()
.find(|&rule| (rule.effect)(content) == GroupingEffect::Trigger);
let mut i = 0;
while let Some(active) = s.groupings.last_mut() {
if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
break;
}
let effect = (active.rule.effect)(content);
if !active.interrupted && effect != GroupingEffect::Interrupt {
active.contains_neutral |= effect == GroupingEffect::Neutral;
s.sink.push((content, styles));
return Ok(true);
}
finish_innermost_grouping(s)?;
i += 1;
if i > 512 {
bail!(content.span(), "maximum grouping depth exceeded");
}
}
if let Some(rule) = matching {
let start = s.sink.len();
s.groupings.push(Grouping {
start,
rule,
interrupted: false,
contains_neutral: false,
});
s.sink.push((content, styles));
return Ok(true);
}
Ok(false)
}
fn visit_filter_rules<'a>(
s: &mut State<'a, '_, '_, '_>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
if matches!(s.kind, RealizationKind::Par | RealizationKind::Math) {
return Ok(false);
}
if content.is::<SpaceElem>() {
return Ok(true);
} else if content.is::<ParbreakElem>() {
s.may_attach = false;
s.saw_parbreak = true;
return Ok(true);
} else if !s.may_attach
&& content
.to_packed::<VElem>()
.is_some_and(|elem| elem.attach.get(styles))
{
return Ok(true);
}
s.may_attach = content.is::<ParElem>();
Ok(false)
}
fn finish(s: &mut State) -> SourceResult<()> {
finish_grouping_while(s, |s| {
if is_fully_inline_or_neutral(s) {
if let RealizationKind::Fragment { kind } = &mut s.kind {
**kind = FragmentKind::Inline;
}
s.groupings.pop();
collapse_spaces(&mut s.sink, 0);
false
} else {
!s.groupings.is_empty()
}
})?;
if matches!(s.kind, RealizationKind::Par | RealizationKind::Math) {
collapse_spaces(&mut s.sink, 0);
}
Ok(())
}
fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> {
let mut last = None;
for elem in local.iter().filter_map(|style| style.element()) {
if last == Some(elem) {
continue;
}
finish_grouping_while(s, |s| {
s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem))
&& if is_fully_inline_or_neutral(s) {
s.groupings[0].interrupted = true;
false
} else {
true
}
})?;
last = Some(elem);
}
Ok(())
}
fn finish_grouping_while<F>(s: &mut State, mut f: F) -> SourceResult<()>
where
F: FnMut(&mut State) -> bool,
{
let mut i = 0;
while f(s) {
finish_innermost_grouping(s)?;
i += 1;
if i > 512 {
bail!(Span::detached(), "maximum grouping depth exceeded");
}
}
Ok(())
}
fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
let Grouping { start, rule, contains_neutral, .. } = s.groupings.pop().unwrap();
if contains_neutral {
let elems = s.store_slice(&s.sink[start..]);
s.sink.truncate(start);
for (is_neutral, slice) in
elems.group_by_key(|(c, _)| (rule.effect)(c) == GroupingEffect::Neutral)
{
if is_neutral {
for &(content, styles) in slice {
visit(s, content, styles)?;
}
} else {
let trimmed = slice.trim_start_matches(|(c, _)| {
(rule.effect)(c) != GroupingEffect::Trigger
});
let split = slice.len() - trimmed.len();
for &(content, styles) in &slice[..split] {
visit(s, content, styles)?;
}
if !trimmed.is_empty() {
let start = s.sink.len();
s.sink.extend_from_slice(trimmed);
finish_grouping(s, rule, start)?;
}
}
}
Ok(())
} else {
finish_grouping(s, rule, start)
}
}
fn finish_grouping(
s: &mut State,
rule: &GroupingRule,
mut start: usize,
) -> SourceResult<()> {
let trimmed = s.sink[start..]
.trim_end_matches(|(c, _)| (rule.effect)(c) != GroupingEffect::Trigger);
let mut end = start + trimmed.len();
if rule.tags {
if std::ptr::eq(rule, &PAR) {
for _ in s.sink.extract_if(end.., |(c, _)| c.is::<SpaceElem>()) {}
}
let bump = &s.arenas.bump;
let before = tag_set(bump, s.sink[..start].iter().rev().map_while(to_tag));
let within = tag_set(bump, s.sink[start..end].iter().filter_map(to_tag));
let after = tag_set(bump, s.sink[end..].iter().map_while(to_tag));
for (k, (c, _)) in s.sink[..start].iter().enumerate().rev() {
let Some(elem) = c.to_packed::<TagElem>() else { break };
let key = elem.tag.location().into();
if within.contains(&key) || after.contains(&key) {
start = k;
}
}
for (k, (c, _)) in s.sink.iter().enumerate().skip(end) {
let Some(elem) = c.to_packed::<TagElem>() else { break };
let key = elem.tag.location().into();
if within.contains(&key) || before.contains(&key) {
end = k + 1;
}
}
}
let tail = s.store_slice(&s.sink[end..]);
s.sink.truncate(end);
let mut tags = BumpVec::<Pair>::new_in(&s.arenas.bump);
if !rule.tags {
let mut k = start;
for i in start..end {
if s.sink[i].0.is::<TagElem>() {
tags.push(s.sink[i]);
continue;
}
if k < i {
s.sink[k] = s.sink[i];
}
k += 1;
}
s.sink.truncate(k);
}
(rule.finish)(Grouped { s, start })?;
for &(content, styles) in tags.iter().chain(&tail) {
visit(s, content, styles)?;
}
Ok(())
}
fn tag_set<'a>(
bump: &'a Bump,
iter: impl IntoIterator<Item = &'a Packed<TagElem>>,
) -> ListSet<BumpVec<'a, LocationKey>> {
ListSet::new(
iter.into_iter()
.map(|elem| LocationKey::new(elem.tag.location()))
.collect_in::<BumpVec<_>>(bump),
)
}
fn to_tag<'a>((c, _): &Pair<'a>) -> Option<&'a Packed<TagElem>> {
c.to_packed::<TagElem>()
}
const MAX_GROUP_NESTING: usize = 3;
static BUNDLE_RULES: &[&GroupingRule] = &[];
static FLOW_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
static PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
static TEXTUAL: GroupingRule = GroupingRule {
priority: 3,
tags: true,
effect: |content| {
let elem = content.elem();
if elem == TextElem::ELEM
|| elem == LinebreakElem::ELEM
|| elem == SmartQuoteElem::ELEM
{
GroupingEffect::Trigger
} else if elem == SpaceElem::ELEM {
GroupingEffect::Inner
} else {
GroupingEffect::Interrupt
}
},
interrupt: |_| true,
finish: finish_textual,
};
static PAR: GroupingRule = GroupingRule {
priority: 1,
tags: true,
effect: |content| {
let elem = content.elem();
if elem == TextElem::ELEM
|| elem == HElem::ELEM
|| elem == LinebreakElem::ELEM
|| elem == SmartQuoteElem::ELEM
|| elem == InlineElem::ELEM
|| elem == BoxElem::ELEM
{
GroupingEffect::Trigger
} else if elem == SpaceElem::ELEM {
GroupingEffect::Inner
} else if let Some(elem) = content.to_packed::<HtmlElem>() {
if typst_html::tag::should_group_into_pars(elem.tag) {
GroupingEffect::Trigger
} else {
GroupingEffect::Neutral
}
} else {
GroupingEffect::Interrupt
}
},
interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM,
finish: finish_par,
};
static CITES: GroupingRule = GroupingRule {
priority: 2,
tags: false,
effect: |content| {
let elem = content.elem();
if elem == CiteElem::ELEM {
GroupingEffect::Trigger
} else if elem == SpaceElem::ELEM {
GroupingEffect::Inner
} else {
GroupingEffect::Interrupt
}
},
interrupt: |elem| {
elem == CiteGroup::ELEM || elem == ParElem::ELEM || elem == AlignElem::ELEM
},
finish: finish_cites,
};
static LIST: GroupingRule = list_like_grouping::<ListElem>();
static ENUM: GroupingRule = list_like_grouping::<EnumElem>();
static TERMS: GroupingRule = list_like_grouping::<TermsElem>();
const fn list_like_grouping<T: ListLike>() -> GroupingRule {
GroupingRule {
priority: 2,
tags: false,
effect: |content| {
let elem = content.elem();
if elem == T::Item::ELEM {
GroupingEffect::Trigger
} else if elem == SpaceElem::ELEM || elem == ParbreakElem::ELEM {
GroupingEffect::Inner
} else {
GroupingEffect::Interrupt
}
},
interrupt: |elem| elem == T::ELEM || elem == AlignElem::ELEM,
finish: finish_list_like::<T>,
}
}
fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
if visit_textual(s, start)? {
return Ok(());
}
if in_non_par_grouping(s) {
let elems = s.store_slice(&s.sink[start..]);
s.sink.truncate(start);
finish_grouping_while(s, in_non_par_grouping)?;
start = s.sink.len();
s.sink.extend(elems);
}
if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
s.groupings.push(Grouping {
start,
rule: &PAR,
interrupted: false,
contains_neutral: false,
});
}
Ok(())
}
fn in_non_par_grouping(s: &mut State) -> bool {
s.groupings.last().is_some_and(|grouping| {
!std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted
})
}
fn is_fully_inline_or_neutral(s: &State) -> bool {
if let RealizationKind::Fragment { .. } = s.kind
&& !s.saw_parbreak
&& let [grouping] = s.groupings.as_slice()
&& std::ptr::eq(grouping.rule, &PAR)
&& s.sink[..grouping.start].iter().all(|(c, _)| {
c.is::<TagElem>() || (grouping.rule.effect)(c) == GroupingEffect::Neutral
})
{
true
} else {
false
}
}
fn finish_par(mut grouped: Grouped) -> SourceResult<()> {
let (sink, start) = grouped.get_mut();
collapse_spaces(sink, start);
let elems = grouped.get();
let span = select_span(elems);
let (body, trunk) = repack(elems);
let s = grouped.end();
let elem = ParElem::new(body).pack().spanned(span);
visit(s, s.store(elem), trunk)
}
fn finish_cites(grouped: Grouped) -> SourceResult<()> {
let elems = grouped.get();
let span = select_span(elems);
let trunk = elems[0].1;
let children = elems.iter().map(|(c, _)| (**c).clone()).collect();
let s = grouped.end();
let elem = CiteGroup::new(children).pack().spanned(span);
visit(s, s.store(elem), trunk)
}
fn finish_list_like<T: ListLike>(grouped: Grouped) -> SourceResult<()> {
let elems = grouped.get();
let span = select_span(elems);
let tight = !elems.iter().any(|(c, _)| c.is::<ParbreakElem>());
let styles = elems.iter().filter(|(c, _)| c.is::<T::Item>()).map(|&(_, s)| s);
let trunk = StyleChain::trunk(styles).unwrap();
let trunk_depth = trunk.links().count();
let children = elems
.iter()
.copied()
.filter_map(|(c, s)| {
let item = c.to_packed::<T::Item>()?.clone();
let local = s.suffix(trunk_depth);
Some(T::Item::styled(item, local))
})
.collect();
let s = grouped.end();
let elem = T::create(children, tight).pack().spanned(span);
visit(s, s.store(elem), trunk)
}
fn visit_textual(s: &mut State, start: usize) -> SourceResult<bool> {
if let Some(m) = find_regex_match_in_elems(s, &s.sink[start..]) {
collapse_spaces(&mut s.sink, start);
let elems = s.store_slice(&s.sink[start..]);
s.sink.truncate(start);
visit_regex_match(s, &elems, m)?;
return Ok(true);
}
Ok(false)
}
fn find_regex_match_in_elems<'a>(
s: &State,
elems: &[Pair<'a>],
) -> Option<RegexMatch<'a>> {
let mut buf = BumpString::new_in(&s.arenas.bump);
let mut base = 0;
let mut leftmost = None;
let mut current = StyleChain::default();
let mut state = SpaceState::Destructive;
for &(content, styles) in elems {
let (new_state, text) = collapse_state_textual(content, styles);
state = match new_state {
SpaceState::Invisible => continue,
SpaceState::Destructive => {
if state == SpaceState::Space {
buf.pop();
}
SpaceState::Destructive
}
SpaceState::Supportive => SpaceState::Supportive,
SpaceState::Space => {
if state != SpaceState::Supportive {
continue;
}
SpaceState::Space
}
};
if styles != current && !buf.is_empty() {
leftmost = find_regex_match_in_str(&buf, current);
if leftmost.is_some() {
break;
}
base += buf.len();
buf.clear();
}
current = styles;
buf.push_str(text);
}
if leftmost.is_none() {
leftmost = find_regex_match_in_str(&buf, current);
}
leftmost.map(|m| RegexMatch { offset: base + m.offset, ..m })
}
fn find_regex_match_in_str<'a>(
text: &str,
styles: StyleChain<'a>,
) -> Option<RegexMatch<'a>> {
let mut r = 0;
let mut revoked = SmallBitSet::new();
let mut leftmost: Option<(regex::Match, RecipeIndex, &Recipe)> = None;
let depth = LazyCell::new(|| styles.recipes().count());
for entry in styles.entries() {
let recipe = match &**entry {
Style::Recipe(recipe) => recipe,
Style::Property(_) => continue,
Style::Revocation(index) => {
revoked.insert(index.0);
continue;
}
};
r += 1;
let Some(Selector::Regex(regex)) = recipe.selector() else { continue };
let Some(m) = regex.find(text) else { continue };
if m.range().is_empty() {
continue;
}
if leftmost.is_some_and(|(p, ..)| p.start() <= m.start()) {
continue;
}
let index = RecipeIndex(*depth - (r - 1));
if revoked.contains(index.0) {
continue;
}
leftmost = Some((m, index, recipe));
}
leftmost.map(|(m, id, recipe)| RegexMatch {
offset: m.start(),
text: m.as_str().into(),
id,
recipe,
styles,
})
}
fn visit_regex_match<'a>(
s: &mut State<'a, '_, '_, '_>,
elems: &[Pair<'a>],
m: RegexMatch<'a>,
) -> SourceResult<()> {
let match_range = m.offset..m.offset + m.text.len();
let mut cursor = 0;
let mut m = Some(m);
for &(content, styles) in elems {
if content.is::<TagElem>() {
visit(s, content, styles)?;
continue;
}
let len = if let Some(elem) = content.to_packed::<TextElem>() {
elem.text.len()
} else if let Some(elem) = content.to_packed::<SymbolElem>() {
elem.text.len()
} else {
1 };
let elem_range = cursor..cursor + len;
cursor = elem_range.end;
if elem_range.end <= match_range.start || match_range.end <= elem_range.start {
visit(s, content, styles)?;
continue;
}
if elem_range.start < match_range.start {
let end = match_range.start - elem_range.start;
visit(s, s.store(slice_textual(content, ..end)), styles)?;
}
if let Some(RegexMatch { text, styles, id, recipe, offset: _ }) = m.take() {
debug_assert!(elem_range.start <= match_range.start);
let matched_text = if match_range.end <= elem_range.end
&& (content.is::<TextElem>() || content.is::<SymbolElem>())
{
slice_textual(
content,
match_range.start - elem_range.start
..match_range.end - elem_range.start,
)
} else {
TextElem::packed(text).spanned(content.span())
};
let context = Context::new(None, Some(styles));
let output = recipe.apply(s.engine, context.track(), matched_text)?;
let revocation = Style::Revocation(id).into();
let outer = s.arenas.bump.alloc(styles);
let chained = outer.chain(s.arenas.styles.alloc(revocation));
visit(s, s.store(output), chained)?;
}
if elem_range.end > match_range.end {
let start = match_range.end - elem_range.start;
visit(s, s.store(slice_textual(content, start..)), styles)?;
}
}
debug_assert!(m.is_none());
Ok(())
}
fn slice_textual(elem: &Content, range: impl SliceIndex<str, Output = str>) -> Content {
if let Some(elem) = elem.to_packed::<TextElem>() {
let mut elem = elem.clone();
elem.text = elem.text[range].into();
elem.pack()
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
let mut elem = elem.clone();
elem.text = elem.text[range].into();
elem.pack()
} else {
panic!("can only slice text and symbols");
}
}
fn select_span(children: &[Pair]) -> Span {
Span::find(children.iter().map(|(c, _)| c.span()))
}
fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) {
let trunk = StyleChain::trunk_from_pairs(buf).unwrap_or_default();
let depth = trunk.links().count();
let mut seq = Vec::with_capacity(buf.len());
for (chain, group) in buf.group_by_key(|&(_, s)| s) {
let iter = group.iter().map(|&(c, _)| c.clone());
let suffix = chain.suffix(depth);
if suffix.is_empty() {
seq.extend(iter);
} else if let &[(element, _)] = group {
seq.push(element.clone().styled_with_map(suffix));
} else {
seq.push(Content::sequence(iter).styled_with_map(suffix));
}
}
(Content::sequence(seq), trunk)
}