use std::num::NonZeroUsize;
use std::str::FromStr;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location};
use crate::layout::{Abs, Em, HElem, Length, Ratio};
use crate::model::{Destination, Numbering, NumberingPattern, ParElem};
use crate::text::{SuperElem, TextElem, TextSize};
use crate::utils::NonZeroExt;
use crate::visualize::{LineElem, Stroke};
#[elem(scope, Locatable, Show, Count)]
pub struct FootnoteElem {
#[borrowed]
#[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
pub numbering: Numbering,
#[required]
pub body: FootnoteBody,
}
#[scope]
impl FootnoteElem {
#[elem]
type FootnoteEntry;
}
impl FootnoteElem {
pub fn with_content(content: Content) -> Self {
Self::new(FootnoteBody::Content(content))
}
pub fn with_label(label: Label) -> Self {
Self::new(FootnoteBody::Reference(label))
}
pub fn into_ref(&self, label: Label) -> Self {
Self {
body: FootnoteBody::Reference(label),
..self.clone()
}
}
pub fn is_ref(&self) -> bool {
matches!(self.body(), FootnoteBody::Reference(_))
}
pub fn body_content(&self) -> Option<&Content> {
match self.body() {
FootnoteBody::Content(content) => Some(content),
_ => None,
}
}
}
impl Packed<FootnoteElem> {
pub fn declaration_location(&self, engine: &Engine) -> StrResult<Location> {
match self.body() {
FootnoteBody::Reference(label) => {
let element = engine.introspector.query_label(*label)?;
let footnote = element
.to_packed::<FootnoteElem>()
.ok_or("referenced element should be a footnote")?;
if self.location() == footnote.location() {
bail!("footnote cannot reference itself");
}
footnote.declaration_location(engine)
}
_ => Ok(self.location().unwrap()),
}
}
}
impl Show for Packed<FootnoteElem> {
#[typst_macros::time(name = "footnote", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let loc = self.declaration_location(engine).at(span)?;
let numbering = self.numbering(styles);
let counter = Counter::of(FootnoteElem::elem());
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1);
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
}
}
impl Count for Packed<FootnoteElem> {
fn update(&self) -> Option<CounterUpdate> {
(!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum FootnoteBody {
Content(Content),
Reference(Label),
}
cast! {
FootnoteBody,
self => match self {
Self::Content(v) => v.into_value(),
Self::Reference(v) => v.into_value(),
},
v: Content => Self::Content(v),
v: Label => Self::Reference(v),
}
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
pub struct FootnoteEntry {
#[required]
pub note: Packed<FootnoteElem>,
#[default(
LineElem::new()
.with_length(Ratio::new(0.3).into())
.with_stroke(Stroke {
thickness: Smart::Custom(Abs::pt(0.5).into()),
..Default::default()
})
.pack()
)]
pub separator: Content,
#[default(Em::new(1.0).into())]
#[resolve]
pub clearance: Length,
#[default(Em::new(0.5).into())]
#[resolve]
pub gap: Length,
#[default(Em::new(1.0).into())]
pub indent: Length,
}
impl Show for Packed<FootnoteEntry> {
#[typst_macros::time(name = "footnote.entry", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let note = self.note();
let number_gap = Em::new(0.05);
let default = StyleChain::default();
let numbering = note.numbering(default);
let counter = Counter::of(FootnoteElem::elem());
let Some(loc) = note.location() else {
bail!(
span, "footnote entry must have a location";
hint: "try using a query or a show rule to customize the footnote instead"
);
};
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num)
.pack()
.spanned(span)
.linked(Destination::Location(loc))
.located(loc.variant(1));
Ok(Content::sequence([
HElem::new(self.indent(styles).into()).pack(),
sup,
HElem::new(number_gap.into()).with_weak(true).pack(),
note.body_content().unwrap().clone(),
]))
}
}
impl ShowSet for Packed<FootnoteEntry> {
fn show_set(&self, _: StyleChain) -> Styles {
let text_size = Em::new(0.85);
let leading = Em::new(0.5);
let mut out = Styles::new();
out.set(ParElem::set_leading(leading.into()));
out.set(TextElem::set_size(TextSize(text_size.into())));
out
}
}
cast! {
FootnoteElem,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::with_content)
}