use ecow::eco_format;
use typst_utils::singleton;
use crate::diag::{HintedStrResult, SourceResult, StrResult, bail};
use crate::engine::Engine;
use crate::foundations::{
AlternativeFold, Args, Cast, CastInfo, Construct, Content, Dict, Fold, FromValue,
IntoValue, NativeElement, Packed, Reflect, Smart, Unlabellable, Value, cast, dict,
elem, scope,
};
use crate::introspection::{Count, CounterUpdate, Locatable, Tagged, Unqueriable};
use crate::layout::{Abs, Em, HAlignment, Length, OuterHAlignment, Ratio, Rel};
use crate::model::Numbering;
#[elem(scope, title = "Paragraph", Locatable, Tagged)]
pub struct ParElem {
#[default(Em::new(0.65).into())]
pub leading: Length,
#[default(Em::new(1.2).into())]
pub spacing: Length,
#[default(false)]
pub justify: bool,
#[fold]
pub justification_limits: JustificationLimits,
pub linebreaks: Smart<Linebreaks>,
pub first_line_indent: FirstLineIndent,
pub hanging_indent: Length,
#[required]
pub body: Content,
}
#[scope]
impl ParElem {
#[elem]
type ParLine;
}
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
pub struct JustificationLimits {
spacing: Option<Limits<Rel>>,
tracking: Option<Limits<Length>>,
}
impl JustificationLimits {
pub fn spacing(&self) -> &Limits<Rel> {
self.spacing.as_ref().unwrap_or(&Limits::SPACING_DEFAULT)
}
pub fn tracking(&self) -> &Limits<Length> {
self.tracking.as_ref().unwrap_or(&Limits::TRACKING_DEFAULT)
}
}
cast! {
JustificationLimits,
self => {
let mut dict = Dict::new();
if let Some(spacing) = &self.spacing {
dict.insert("spacing".into(), spacing.into_value());
}
if let Some(tracking) = &self.tracking {
dict.insert("tracking".into(), tracking.into_value());
}
Value::Dict(dict)
},
mut dict: Dict => {
let spacing = dict
.take("spacing")
.ok()
.map(|v| Limits::cast(v, "spacing"))
.transpose()?;
let tracking = dict
.take("tracking")
.ok()
.map(|v| Limits::cast(v, "tracking"))
.transpose()?;
dict.finish(&["spacing", "tracking"])?;
Self { spacing, tracking }
},
}
impl Fold for JustificationLimits {
fn fold(self, outer: Self) -> Self {
Self {
spacing: self.spacing.fold_or(outer.spacing),
tracking: self.tracking.fold_or(outer.tracking),
}
}
}
impl Default for JustificationLimits {
fn default() -> Self {
Self {
spacing: Some(Limits::SPACING_DEFAULT),
tracking: Some(Limits::TRACKING_DEFAULT),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
pub struct Limits<T> {
pub min: T,
pub max: T,
}
impl Limits<Rel> {
const SPACING_DEFAULT: Self = Self {
min: Rel::new(Ratio::new(2.0 / 3.0), Length::zero()),
max: Rel::new(Ratio::new(1.5), Length::zero()),
};
}
impl Limits<Length> {
const TRACKING_DEFAULT: Self = Self { min: Length::zero(), max: Length::zero() };
}
impl<T: Reflect> Reflect for Limits<T> {
fn input() -> CastInfo {
Dict::input()
}
fn output() -> CastInfo {
Dict::output()
}
fn castable(value: &Value) -> bool {
Dict::castable(value)
}
}
impl<T: IntoValue> IntoValue for Limits<T> {
fn into_value(self) -> Value {
Value::Dict(dict! {
"min" => self.min,
"max" => self.max,
})
}
}
impl<T> Limits<T> {
fn cast(value: Value, field: &str) -> HintedStrResult<Self>
where
T: FromValue + Limit,
{
let mut dict: Dict = value.cast()?;
let mut take = |key, check: fn(T) -> StrResult<T>| {
dict.take(key)?
.cast::<T>()
.map_err(|hinted| hinted.message().clone())
.and_then(check)
.map_err(|err| {
eco_format!("`{key}` value of `{field}` is invalid ({err})")
})
};
let min = take("min", Limit::checked_min)?;
let max = take("max", Limit::checked_max)?;
dict.finish(&["min", "max"])?;
Ok(Self { min, max })
}
}
impl<T> Fold for Limits<T> {
fn fold(self, _: Self) -> Self {
self
}
}
trait Limit: Sized {
fn checked_min(self) -> StrResult<Self>;
fn checked_max(self) -> StrResult<Self>;
}
impl Limit for Length {
fn checked_min(self) -> StrResult<Self> {
if self.abs > Abs::zero() || self.em > Em::zero() {
bail!("length must be negative or zero");
}
Ok(self)
}
fn checked_max(self) -> StrResult<Self> {
if self.abs < Abs::zero() || self.em < Em::zero() {
bail!("length must be positive or zero");
}
Ok(self)
}
}
impl Limit for Rel<Length> {
fn checked_min(self) -> StrResult<Self> {
if self.rel <= Ratio::zero() {
bail!("ratio must be positive");
}
self.abs.checked_min()?;
Ok(self)
}
fn checked_max(self) -> StrResult<Self> {
if self.rel <= Ratio::zero() {
bail!("ratio must be positive");
}
self.abs.checked_max()?;
Ok(self)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Linebreaks {
Simple,
Optimized,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)]
pub struct FirstLineIndent {
pub amount: Length,
pub all: bool,
}
cast! {
FirstLineIndent,
self => Value::Dict(self.into()),
amount: Length => Self { amount, all: false },
mut dict: Dict => {
let amount = dict.take("amount")?.cast()?;
let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false);
dict.finish(&["amount", "all"])?;
Self { amount, all }
},
}
impl From<FirstLineIndent> for Dict {
fn from(indent: FirstLineIndent) -> Self {
dict! {
"amount" => indent.amount,
"all" => indent.all,
}
}
}
#[elem(title = "Paragraph Break", Unlabellable)]
pub struct ParbreakElem {}
impl ParbreakElem {
pub fn shared() -> &'static Content {
singleton!(Content, ParbreakElem::new().pack())
}
}
impl Unlabellable for Packed<ParbreakElem> {}
#[elem(name = "line", title = "Paragraph Line", keywords = ["line numbering"], Construct, Locatable)]
pub struct ParLine {
#[ghost]
pub numbering: Option<Numbering>,
#[ghost]
pub number_align: Smart<HAlignment>,
#[ghost]
#[default(OuterHAlignment::Start)]
pub number_margin: OuterHAlignment,
#[ghost]
#[default]
pub number_clearance: Smart<Length>,
#[ghost]
#[default(LineNumberingScope::Document)]
pub numbering_scope: LineNumberingScope,
}
impl Construct for ParLine {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum LineNumberingScope {
Document,
Page,
}
#[elem(Construct, Unqueriable, Locatable, Count)]
pub struct ParLineMarker {
#[internal]
#[required]
pub numbering: Numbering,
#[internal]
#[required]
pub number_align: Smart<HAlignment>,
#[internal]
#[required]
pub number_margin: OuterHAlignment,
#[internal]
#[required]
pub number_clearance: Smart<Length>,
}
impl Construct for ParLineMarker {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl Count for Packed<ParLineMarker> {
fn update(&self) -> Option<CounterUpdate> {
None
}
}