use std::sync::LazyLock;
use icu_properties::CanonicalCombiningClass;
use icu_properties::maps::CodePointMapData;
use icu_provider::AsDeserializingBufferProvider;
use icu_provider_blob::BlobDataProvider;
use crate::foundations::{Content, NativeElement, Str, SymbolElem, cast, elem, func};
use crate::layout::{Length, Rel};
use crate::math::Mathy;
#[elem(Mathy)]
pub struct AccentElem {
#[required]
pub base: Content,
#[required]
pub accent: Accent,
#[default(Rel::one())]
pub size: Rel<Length>,
#[default(true)]
pub dotless: bool,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Accent(pub char);
impl Accent {
pub fn new(c: char) -> Self {
Self(Self::combine(c).unwrap_or(c))
}
pub fn normalize(s: &str) -> Option<Self> {
Self::combining(s).or_else(|| s.parse::<char>().ok().map(Self))
}
pub fn is_bottom(&self) -> bool {
static COMBINING_CLASS_DATA: LazyLock<CodePointMapData<CanonicalCombiningClass>> =
LazyLock::new(|| {
icu_properties::maps::load_canonical_combining_class(
&BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
.unwrap()
.as_deserializing(),
)
.unwrap()
});
matches!(
COMBINING_CLASS_DATA.as_borrowed().get(self.0),
CanonicalCombiningClass::Below
)
}
}
macro_rules! accents {
($($primary:literal, $($option:literal)|* => $name:ident),* $(,)?) => {
impl Accent {
pub fn combine(c: char) -> Option<char> {
Self::combining(c.encode_utf8(&mut [0; 4])).map(|v| v.0)
}
pub fn combining(value: &str) -> Option<Self> {
Some(match value {
$($($option)|* => Accent($primary),)*
_ => return None,
})
}
}
$(
#[func]
pub fn $name(
base: Content,
#[named]
size: Option<Rel<Length>>,
#[named]
dotless: Option<bool>,
) -> Content {
let mut accent = AccentElem::new(base, Accent::new($primary));
if let Some(size) = size {
accent = accent.with_size(size);
}
if let Some(dotless) = dotless {
accent = accent.with_dotless(dotless);
}
accent.pack()
}
)+
};
}
accents! {
'\u{0300}', "\u{0300}" | "`" => grave,
'\u{0301}', "\u{0301}" | "´" => acute,
'\u{0302}', "\u{0302}" | "^" | "ˆ" => hat,
'\u{0303}', "\u{0303}" | "~" | "∼" | "˜" => tilde,
'\u{0304}', "\u{0304}" | "¯" => macron,
'\u{0305}', "\u{0305}" | "-" | "‾" | "−" => dash,
'\u{0306}', "\u{0306}" | "˘" => breve,
'\u{0307}', "\u{0307}" | "." | "˙" | "⋅" => dot,
'\u{0308}', "\u{0308}" | "¨" => dot_double,
'\u{20db}', "\u{20db}" => dot_triple,
'\u{20dc}', "\u{20dc}" => dot_quad,
'\u{030a}', "\u{030a}" | "∘" | "○" => circle,
'\u{030b}', "\u{030b}" | "˝" => acute_double,
'\u{030c}', "\u{030c}" | "ˇ" => caron,
'\u{20d6}', "\u{20d6}" | "←" => arrow_l,
'\u{20d7}', "\u{20d7}" | "→" | "⟶" => arrow,
'\u{20e1}', "\u{20e1}" | "↔" | "↔\u{fe0e}" | "⟷" => arrow_l_r,
'\u{20d0}', "\u{20d0}" | "↼" => harpoon_lt,
'\u{20d1}', "\u{20d1}" | "⇀" => harpoon,
}
cast! {
Accent,
self => self.0.into_value(),
v: Str => Self::normalize(&v).ok_or("expected exactly one character")?,
v: Content => v.to_packed::<SymbolElem>()
.and_then(|elem| Accent::normalize(&elem.text))
.ok_or("expected a single-codepoint symbol")?,
}