use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::Track;
use crate::diag::{bail, At, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
};
use crate::introspection::{Counter, CounterKey, Locatable};
use crate::layout::{
BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing,
};
use crate::model::{
Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
};
use crate::syntax::Span;
use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
use crate::utils::NonZeroExt;
#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)]
pub struct OutlineElem {
pub title: Smart<Option<Content>>,
#[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
#[borrowed]
pub target: LocatableSelector,
pub depth: Option<NonZeroUsize>,
#[default(None)]
#[borrowed]
pub indent: Option<Smart<OutlineIndent>>,
#[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
pub fill: Option<Content>,
}
#[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 mut seq = vec![ParbreakElem::shared().clone()];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
);
}
let indent = self.indent(styles);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
let mut ancestors: Vec<&Content> = vec![];
let elems = engine.introspector.query(&self.target(styles).0);
for elem in &elems {
let Some(entry) = OutlineEntry::from_outlinable(
engine,
self.span(),
elem.clone(),
self.fill(styles),
styles,
)?
else {
continue;
};
let level = entry.level();
if depth < *level {
continue;
}
while ancestors
.last()
.and_then(|ancestor| ancestor.with::<dyn Outlinable>())
.is_some_and(|last| last.level() >= *level)
{
ancestors.pop();
}
OutlineIndent::apply(
indent,
engine,
&ancestors,
&mut seq,
styles,
self.span(),
)?;
seq.push(entry.pack());
seq.push(LinebreakElem::shared().clone());
ancestors.push(elem);
}
seq.push(ParbreakElem::shared().clone());
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
out
}
}
impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline";
}
pub trait Outlinable: Refable {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>>;
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent {
Bool(bool),
Rel(Rel<Length>),
Func(Func),
}
impl OutlineIndent {
fn apply(
indent: &Option<Smart<Self>>,
engine: &mut Engine,
ancestors: &Vec<&Content>,
seq: &mut Vec<Content>,
styles: StyleChain,
span: Span,
) -> SourceResult<()> {
match indent {
None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {}
Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => {
let mut hidden = Content::empty();
for ancestor in ancestors {
let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
if let Some(numbering) = ancestor_outlinable.numbering() {
let numbers = ancestor_outlinable.counter().display_at_loc(
engine,
ancestor.location().unwrap(),
styles,
numbering,
)?;
hidden += numbers + SpaceElem::shared().clone();
};
}
if !ancestors.is_empty() {
seq.push(HideElem::new(hidden).pack());
seq.push(SpaceElem::shared().clone());
}
}
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
seq.push(
HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()),
);
}
Some(Smart::Custom(OutlineIndent::Func(func))) => {
let depth = ancestors.len();
let LengthOrContent(content) = func
.call(engine, Context::new(None, Some(styles)).track(), [depth])?
.cast()
.at(span)?;
if !content.is_empty() {
seq.push(content);
}
}
};
Ok(())
}
}
cast! {
OutlineIndent,
self => match self {
Self::Bool(v) => v.into_value(),
Self::Rel(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: bool => OutlineIndent::Bool(v),
v: Rel<Length> => OutlineIndent::Rel(v),
v: Func => OutlineIndent::Func(v),
}
struct LengthOrContent(Content);
cast! {
LengthOrContent,
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
v: Content => Self(v),
}
#[elem(name = "entry", title = "Outline Entry", Show)]
pub struct OutlineEntry {
#[required]
pub level: NonZeroUsize,
#[required]
pub element: Content,
#[required]
pub body: Content,
#[required]
pub fill: Option<Content>,
#[required]
pub page: Content,
}
impl OutlineEntry {
fn from_outlinable(
engine: &mut Engine,
span: Span,
elem: Content,
fill: Option<Content>,
styles: StyleChain,
) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let Some(body) = outlinable.outline(engine, styles)? else {
return Ok(None);
};
let location = elem.location().unwrap();
let page_numbering = engine
.introspector
.page_numbering(location)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
let page = Counter::new(CounterKey::Page).display_at_loc(
engine,
location,
styles,
&page_numbering,
)?;
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
}
}
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![];
let elem = self.element();
let Some(location) = elem.location() else {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
bail!(
self.span(), "{} must have a location", elem.func().name();
hint: "try using a query or a show rule to customize the outline.entry instead",
)
} else {
bail!(self.span(), "cannot outline {}", elem.func().name())
}
};
let rtl = TextElem::dir_in(styles) == Dir::RTL;
if rtl {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body().clone().linked(Destination::Location(location)));
if rtl {
seq.push(TextElem::packed("\u{202C}"));
}
if let Some(filler) = self.fill() {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.with_width(Fr::one().into())
.pack()
.spanned(self.span()),
);
seq.push(SpaceElem::shared().clone());
} else {
seq.push(HElem::new(Fr::one().into()).pack());
}
let page = self.page().clone().linked(Destination::Location(location));
seq.push(page);
Ok(Content::sequence(seq))
}
}