use std::borrow::Cow;
use std::num::NonZeroUsize;
use std::str::FromStr;
use ecow::EcoString;
use typst_utils::NonZeroExt;
use crate::diag::{SourceResult, bail};
use crate::engine::Engine;
use crate::foundations::{
Content, Element, NativeElement, Packed, Selector, ShowSet, Smart, StyleChain,
Styles, Synthesize, cast, elem, scope, select_where,
};
use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location, Tagged,
};
use crate::layout::{
AlignElem, Alignment, BlockElem, Em, Length, OuterVAlignment, PlacementScope,
VAlignment,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Locale, TextElem};
use crate::visualize::ImageElem;
#[elem(scope, Locatable, Tagged, Synthesize, Count, ShowSet, Refable, Outlinable)]
pub struct FigureElem {
#[required]
pub body: Content,
pub alt: Option<EcoString>,
pub placement: Option<Smart<VAlignment>>,
pub scope: PlacementScope,
pub caption: Option<Packed<FigureCaption>>,
pub kind: Smart<FigureKind>,
pub supplement: Smart<Option<Supplement>>,
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
pub numbering: Option<Numbering>,
#[default(Em::new(0.65).into())]
pub gap: Length,
#[default(true)]
pub outlined: bool,
#[synthesized]
pub counter: Option<Counter>,
#[internal]
#[synthesized]
pub locale: Locale,
}
#[scope]
impl FigureElem {
#[elem]
type FigureCaption;
}
impl FigureElem {
pub fn resolve_separator(&self, styles: StyleChain) -> Content {
match self.caption.get_ref(styles) {
Some(caption) => caption.resolve_separator(styles),
None => FigureCaption::local_separator_in(styles),
}
}
}
impl Synthesize for Packed<FigureElem> {
fn synthesize(
&mut self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let span = self.span();
let location = self.location();
let elem = self.as_mut();
let numbering = elem.numbering.get_ref(styles);
let kind = elem.kind.get_cloned(styles).unwrap_or_else(|| {
elem.body
.query_first_naive(&Selector::can::<dyn Figurable>())
.map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::ELEM))
});
let supplement = match elem.supplement.get_ref(styles).as_ref() {
Smart::Auto => {
let name = match &kind {
FigureKind::Elem(func) => func
.local_name(
styles.get(TextElem::lang),
styles.get(TextElem::region),
)
.map(TextElem::packed),
FigureKind::Name(_) => None,
};
if numbering.is_some() && name.is_none() {
bail!(span, "please specify the figure's supplement")
}
Some(name.unwrap_or_default())
}
Smart::Custom(None) => None,
Smart::Custom(Some(supplement)) => {
let descendant = match kind {
FigureKind::Elem(func) => elem
.body
.query_first_naive(&Selector::Elem(func, None))
.map(Cow::Owned),
FigureKind::Name(_) => None,
};
let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body));
Some(supplement.resolve(engine, styles, [target])?)
}
};
let counter = Counter::new(CounterKey::Selector(
select_where!(FigureElem, kind => kind.clone()),
));
let mut caption = elem.caption.get_cloned(styles);
if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.kind = Some(kind.clone());
caption.supplement = Some(supplement.clone());
caption.numbering = Some(numbering.clone());
caption.counter = Some(Some(counter.clone()));
caption.figure_location = Some(location);
}
elem.kind.set(Smart::Custom(kind));
elem.supplement
.set(Smart::Custom(supplement.map(Supplement::Content)));
elem.counter = Some(Some(counter));
elem.caption.set(caption);
elem.locale = Some(Locale::get_in(styles));
Ok(())
}
}
impl ShowSet for Packed<FigureElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut map = Styles::new();
map.set(BlockElem::breakable, false);
map.set(AlignElem::alignment, Alignment::CENTER);
map
}
}
impl Count for Packed<FigureElem> {
fn update(&self) -> Option<CounterUpdate> {
self.numbering()
.is_some()
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
}
}
impl Refable for Packed<FigureElem> {
fn supplement(&self) -> Content {
match self.supplement.get_cloned(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
self.counter
.clone()
.flatten()
.unwrap_or_else(|| Counter::of(FigureElem::ELEM))
}
fn numbering(&self) -> Option<&Numbering> {
self.numbering.get_ref(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<FigureElem> {
fn outlined(&self) -> bool {
self.outlined.get(StyleChain::default())
&& (self.caption.get_ref(StyleChain::default()).is_some()
|| self.numbering().is_some())
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
fn body(&self) -> Content {
self.caption
.get_ref(StyleChain::default())
.as_ref()
.map(|caption| caption.body.clone())
.unwrap_or_default()
}
}
#[elem(name = "caption", Locatable, Tagged, Synthesize)]
pub struct FigureCaption {
#[default(OuterVAlignment::Bottom)]
pub position: OuterVAlignment,
pub separator: Smart<Content>,
#[required]
pub body: Content,
#[synthesized]
pub kind: FigureKind,
#[synthesized]
pub supplement: Option<Content>,
#[synthesized]
pub numbering: Option<Numbering>,
#[synthesized]
pub counter: Option<Counter>,
#[internal]
#[synthesized]
pub figure_location: Option<Location>,
}
impl FigureCaption {
pub fn realize(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content> {
let mut realized = self.body.clone();
if let (
Some(Some(mut supplement)),
Some(Some(numbering)),
Some(Some(counter)),
Some(Some(location)),
) = (
self.supplement.clone(),
&self.numbering,
&self.counter,
&self.figure_location,
) {
let numbers = counter.display_at_loc(engine, *location, styles, numbering)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
realized = supplement + numbers + self.resolve_separator(styles) + realized;
}
Ok(realized)
}
fn resolve_separator(&self, styles: StyleChain) -> Content {
self.separator
.get_cloned(styles)
.unwrap_or_else(|| Self::local_separator_in(styles))
}
fn local_separator_in(styles: StyleChain) -> Content {
styles.get_cloned(Self::separator).unwrap_or_else(|| {
TextElem::packed(match styles.get(TextElem::lang) {
Lang::CHINESE => "\u{2003}",
Lang::FRENCH => ".\u{a0}– ",
Lang::RUSSIAN => ". ",
Lang::ENGLISH | _ => ": ",
})
})
}
}
impl Synthesize for Packed<FigureCaption> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.separator.set(Smart::Custom(elem.resolve_separator(styles)));
Ok(())
}
}
cast! {
FigureCaption,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum FigureKind {
Elem(Element),
Name(EcoString),
}
cast! {
FigureKind,
self => match self {
Self::Elem(v) => v.into_value(),
Self::Name(v) => v.into_value(),
},
v: Element => Self::Elem(v),
v: EcoString => Self::Name(v),
}
pub trait Figurable {}