use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::{Track, Tracked};
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
};
use crate::layout::{
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
};
use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
use crate::text::{LocalName, SpaceElem, TextElem};
#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
pub struct OutlineElem {
pub title: Smart<Option<Content>>,
#[default(LocatableSelector(HeadingElem::elem().select()))]
#[borrowed]
pub target: LocatableSelector,
pub depth: Option<NonZeroUsize>,
pub indent: Smart<OutlineIndent>,
}
#[scope]
impl OutlineElem {
#[elem]
type OutlineEntry;
}
impl Show for Packed<OutlineElem> {
#[typst_macros::time(name = "outline", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let elems = engine.introspector.query(&self.target(styles).0);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
out.set(ParElem::set_justify(false));
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
out.set(OutlineEntry::set_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", Show)]
pub struct OutlineEntry {
#[required]
pub level: NonZeroUsize,
#[required]
pub element: Content,
#[borrowed]
#[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>>,
}
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let context = Context::new(None, Some(styles));
let context = context.track();
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
}
}
#[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 = Self::parent_in(styles)
.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(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(),
prefix,
HElem::new((hanging_indent - prefix_width).into()).pack(),
inner,
]);
Content::sequence(seq)
} else {
inner
};
let inset = Sides::default().with(
TextElem::dir_in(styles).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 styles = context.styles().at(span)?;
let mut seq = vec![];
let rtl = TextElem::dir_in(styles) == Dir::RTL;
if rtl {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body().at(span)?);
if rtl {
seq.push(TextElem::packed("\u{202C}"));
}
if let Some(filler) = self.fill(styles) {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.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(self.page(engine, context, span)?);
Ok(Content::sequence(seq))
}
#[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 {
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
self.element
.with::<dyn Outlinable>()
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
}
fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element;
elem.location().ok_or_else(|| {
if elem.can::<dyn Locatable>() && 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, Locatable, Show)]
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");
}
}
impl Show for Packed<PrefixInfo> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}