use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{warning, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)]
pub struct HeadingElem {
pub level: Smart<NonZeroUsize>,
#[default(NonZeroUsize::ONE)]
pub depth: NonZeroUsize,
#[default(0)]
pub offset: usize,
#[borrowed]
pub numbering: Option<Numbering>,
pub supplement: Smart<Option<Supplement>>,
#[default(true)]
pub outlined: bool,
#[default(Smart::Auto)]
pub bookmarked: Smart<bool>,
#[default(Smart::Auto)]
pub hanging_indent: Smart<Length>,
#[required]
pub body: Content,
}
impl HeadingElem {
pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize {
self.level(styles).unwrap_or_else(|| {
NonZeroUsize::new(self.offset(styles) + self.depth(styles).get())
.expect("overflow to 0 on NoneZeroUsize + usize")
})
}
}
impl Synthesize for Packed<HeadingElem> {
fn synthesize(
&mut self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let supplement = match (**self).supplement(styles) {
Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(supplement)) => {
supplement.resolve(engine, styles, [self.clone().pack()])?
}
};
let elem = self.as_mut();
elem.push_level(Smart::Custom(elem.resolve_level(styles)));
elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
Ok(())
}
}
impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let html = TargetElem::target_in(styles).is_html();
const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = self.span();
let mut realized = self.body.clone();
let hanging_indent = self.hanging_indent(styles);
let mut indent = match hanging_indent {
Smart::Custom(length) => length.resolve(styles),
Smart::Auto => Abs::zero(),
};
if let Some(numbering) = (**self).numbering(styles).as_ref() {
let location = self.location().unwrap();
let numbering = Counter::of(HeadingElem::elem())
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
if hanging_indent.is_auto() && !html {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
let link = LocatorLink::measure(location);
let size = (engine.routines.layout_frame)(
engine,
&numbering,
Locator::link(&link),
styles,
pod,
)?
.size();
indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
}
let spacing = if html {
SpaceElem::shared().clone()
} else {
HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
};
realized = numbering + spacing + realized;
}
Ok(if html {
let level = self.resolve_level(styles).get();
if level >= 6 {
engine.sink.warn(warning!(span,
"heading of level {} was transformed to \
<div role=\"heading\" aria-level=\"{}\">, which is not \
supported by all assistive technology",
level, level + 1;
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
hint: "you may want to restructure your document so that \
it doesn't contain deep headings"));
HtmlElem::new(tag::div)
.with_body(Some(realized))
.with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack()
.spanned(span)
} else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
block.pack().spanned(span)
})
}
}
impl ShowSet for Packed<HeadingElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let level = (**self).resolve_level(styles).get();
let scale = match level {
1 => 1.4,
2 => 1.2,
_ => 1.0,
};
let size = Em::new(scale);
let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
let below = Em::new(0.75) / scale;
let mut out = Styles::new();
out.set(TextElem::set_size(TextSize(size.into())));
out.set(TextElem::set_weight(FontWeight::BOLD));
out.set(BlockElem::set_above(Smart::Custom(above.into())));
out.set(BlockElem::set_below(Smart::Custom(below.into())));
out.set(BlockElem::set_sticky(true));
out
}
}
impl Count for Packed<HeadingElem> {
fn update(&self) -> Option<CounterUpdate> {
(**self)
.numbering(StyleChain::default())
.is_some()
.then(|| CounterUpdate::Step((**self).resolve_level(StyleChain::default())))
}
}
impl Refable for Packed<HeadingElem> {
fn supplement(&self) -> Content {
match (**self).supplement(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
Counter::of(HeadingElem::elem())
}
fn numbering(&self) -> Option<&Numbering> {
(**self).numbering(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<HeadingElem> {
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
}
fn level(&self) -> NonZeroUsize {
(**self).resolve_level(StyleChain::default())
}
fn prefix(&self, numbers: Content) -> Content {
numbers
}
fn body(&self) -> Content {
self.body.clone()
}
}
impl LocalName for Packed<HeadingElem> {
const KEY: &'static str = "heading";
}