use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::Tracked;
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{At, HintedStrResult, SourceResult, StrResult, bail, error};
use crate::engine::Engine;
use crate::foundations::{
Args, Construct, Content, Context, Func, LocatableSelector, NativeElement, Packed,
Resolve, ShowSet, Smart, StyleChain, Styles, cast, elem, func, scope, select_where,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, Tagged,
Unqueriable,
};
use crate::layout::{
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
};
use crate::model::{HeadingElem, NumberingPattern, ParElem, Refable};
use crate::pdf::PdfMarkerTag;
use crate::text::{LocalName, SpaceElem, TextElem};
#[elem(scope, keywords = ["Table of Contents", "toc"], ShowSet, LocalName, Locatable, Tagged)]
pub struct OutlineElem {
pub title: Smart<Option<Content>>,
#[default(LocatableSelector(HeadingElem::ELEM.select()))]
pub target: LocatableSelector,
pub depth: Option<NonZeroUsize>,
pub indent: Smart<OutlineIndent>,
}
#[scope]
impl OutlineElem {
#[elem]
type OutlineEntry;
}
impl Packed<OutlineElem> {
pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
let span = self.span();
self.title
.get_cloned(styles)
.unwrap_or_else(|| {
Some(
TextElem::packed(Packed::<OutlineElem>::local_name_in(styles))
.spanned(span),
)
})
.map(|title| {
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span)
})
}
pub fn realize_flat(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Vec<Packed<OutlineEntry>>> {
let mut entries = vec![];
for result in self.realize_iter(engine, styles) {
let (entry, _, included) = result?;
if included {
entries.push(entry);
}
}
Ok(entries)
}
pub fn realize_tree(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Vec<OutlineNode>> {
let flat = self.realize_iter(engine, styles).collect::<SourceResult<Vec<_>>>()?;
Ok(OutlineNode::build_tree(flat))
}
fn realize_iter(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> impl Iterator<Item = SourceResult<(Packed<OutlineEntry>, NonZeroUsize, bool)>>
{
let span = self.span();
let elems = engine.introspector.query(&self.target.get_ref(styles).0);
let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX);
elems.into_iter().map(move |elem| {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(self.span(), "cannot outline {}", elem.func().name());
};
let level = outlinable.level();
let include = outlinable.outlined() && level <= depth;
let entry = Packed::new(OutlineEntry::new(level, elem)).spanned(span);
Ok((entry, level, include))
})
}
}
#[derive(Debug)]
pub struct OutlineNode<T = Packed<OutlineEntry>> {
pub entry: T,
pub level: NonZeroUsize,
pub children: Vec<OutlineNode<T>>,
}
impl<T> OutlineNode<T> {
pub fn build_tree(
flat: impl IntoIterator<Item = (T, NonZeroUsize, bool)>,
) -> Vec<Self> {
let mut last_skipped_level = None;
let mut tree: Vec<OutlineNode<T>> = vec![];
for (entry, level, include) in flat {
if include {
let mut children = &mut tree;
while children.last().is_some_and(|last| {
last_skipped_level.is_none_or(|l| last.level < l)
&& last.level < level
}) {
children = &mut children.last_mut().unwrap().children;
}
last_skipped_level = None;
children.push(OutlineNode { entry, level, children: vec![] });
} else if last_skipped_level.is_none_or(|l| level < l) {
last_skipped_level = Some(level);
}
}
tree
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::outlined, false);
out.set(HeadingElem::numbering, None);
out.set(ParElem::justify, false);
out.set(BlockElem::above, Smart::Custom(styles.get(ParElem::leading).into()));
out.set(OutlineEntry::parent, Some(self.clone()));
out
}
}
impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline";
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent {
Rel(Rel),
Func(Func),
}
impl OutlineIndent {
fn resolve(
&self,
engine: &mut Engine,
context: Tracked<Context>,
level: NonZeroUsize,
span: Span,
) -> SourceResult<Rel> {
let depth = level.get() - 1;
match self {
Self::Rel(length) => Ok(*length * depth as f64),
Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
}
}
}
cast! {
OutlineIndent,
self => match self {
Self::Rel(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: Rel<Length> => Self::Rel(v),
v: Func => Self::Func(v),
}
pub trait Outlinable: Refable {
fn outlined(&self) -> bool;
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
fn prefix(&self, numbers: Content) -> Content;
fn body(&self) -> Content;
}
#[elem(scope, name = "entry", title = "Outline Entry", Locatable, Tagged)]
pub struct OutlineEntry {
#[required]
pub level: NonZeroUsize,
#[required]
pub element: Content,
#[default(Some(
RepeatElem::new(TextElem::packed("."))
.with_gap(Em::new(0.15).into())
.pack()
))]
pub fill: Option<Content>,
#[ghost]
#[internal]
pub parent: Option<Packed<OutlineElem>>,
}
#[scope]
impl OutlineEntry {
#[func(contextual)]
pub fn indented(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
prefix: Option<Content>,
inner: Content,
#[named]
#[default(Em::new(0.5).into())]
gap: Length,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let outline = styles
.get_ref(Self::parent)
.as_ref()
.ok_or("must be called within the context of an outline")
.at(span)?;
let outline_loc = outline.location().unwrap();
let prefix_width = prefix
.as_ref()
.map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
.transpose()?;
let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
let indent = outline.indent.get_ref(styles);
let (base_indent, hanging_indent) = match &indent {
Smart::Auto => compute_auto_indents(
engine.introspector,
outline_loc,
styles,
self.level,
prefix_inset,
),
Smart::Custom(amount) => {
let base = amount.resolve(engine, context, self.level, span)?;
(base, prefix_inset)
}
};
let body = if let (
Some(prefix),
Some(prefix_width),
Some(prefix_inset),
Some(hanging_indent),
) = (prefix, prefix_width, prefix_inset, hanging_indent)
{
let mut seq = Vec::with_capacity(5);
if indent.is_auto() {
seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
}
seq.extend([
HElem::new((-hanging_indent).into()).pack(),
PdfMarkerTag::Label(prefix),
HElem::new((hanging_indent - prefix_width).into()).pack(),
inner,
]);
Content::sequence(seq)
} else {
inner
};
let inset = Sides::default().with(
styles.resolve(TextElem::dir).start(),
Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
);
Ok(BlockElem::new()
.with_inset(inset)
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span))
}
#[func(contextual)]
pub fn prefix(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Option<Content>> {
let outlinable = self.outlinable().at(span)?;
let Some(numbering) = outlinable.numbering() else { return Ok(None) };
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbers =
outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
Ok(Some(outlinable.prefix(numbers)))
}
#[func(contextual)]
pub fn inner(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let body = self.body().at(span)?;
let page = self.page(engine, context, span)?;
self.build_inner(context, span, body, page)
}
#[func]
pub fn body(&self) -> StrResult<Content> {
Ok(self.outlinable()?.body())
}
#[func(contextual)]
pub fn page(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbering = engine
.introspector
.page_numbering(loc)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
}
}
impl OutlineEntry {
pub fn build_inner(
&self,
context: Tracked<Context>,
span: Span,
body: Content,
page: Content,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let mut seq = vec![];
let rtl = styles.resolve(TextElem::dir) == Dir::RTL;
if rtl {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(body);
if rtl {
seq.push(TextElem::packed("\u{202C}"));
}
if let Some(filler) = self.fill.get_cloned(styles) {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler))
.with_width(Fr::one().into())
.pack()
.spanned(span),
);
seq.push(SpaceElem::shared().clone());
} else {
seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
}
seq.push(TextElem::packed("\u{2060}"));
seq.push(page);
Ok(Content::sequence(seq))
}
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
self.element
.with::<dyn Outlinable>()
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
}
pub fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element;
elem.location().ok_or_else(|| {
if elem.can::<dyn Outlinable>() {
error!(
"{} must have a location", elem.func().name();
hint: "try using a show rule to customize the outline.entry instead",
)
} else {
error!("cannot outline {}", elem.func().name())
}
})
}
}
cast! {
OutlineEntry,
v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
}
fn measure_prefix(
engine: &mut Engine,
prefix: &Content,
loc: Location,
styles: StyleChain,
) -> SourceResult<Abs> {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
let link = LocatorLink::measure(loc);
Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
.width())
}
fn compute_auto_indents(
introspector: Tracked<Introspector>,
outline_loc: Location,
styles: StyleChain,
level: NonZeroUsize,
prefix_inset: Option<Abs>,
) -> (Rel, Option<Abs>) {
let indents = query_prefix_widths(introspector, outline_loc);
let fallback = Em::new(1.2).resolve(styles);
let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
let last = level.get() - 1;
let base: Abs = (0..last).map(get).sum();
let hang = prefix_inset.map(|p| p.max(get(last)));
(base.into(), hang)
}
#[comemo::memoize]
fn query_prefix_widths(
introspector: Tracked<Introspector>,
outline_loc: Location,
) -> SmallVec<[Option<Abs>; 4]> {
let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
let elems = introspector.query(&select_where!(PrefixInfo, key => outline_loc));
for elem in &elems {
let info = elem.to_packed::<PrefixInfo>().unwrap();
let level = info.level.get();
if widths.len() < level {
widths.resize(level, None);
}
widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
}
widths
}
#[elem(Construct, Unqueriable, Locatable)]
pub(crate) struct PrefixInfo {
#[required]
key: Location,
#[required]
#[internal]
level: NonZeroUsize,
#[required]
#[internal]
inset: Abs,
}
impl Construct for PrefixInfo {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}