use std::fmt::{Display, Write};
use std::ops::Deref;
use ecow::{EcoString, EcoVec, eco_format};
use typst_library::diag::WarningSink;
use typst_library::layout::{Abs, Angle, Em, Length, Ratio, Rel};
use typst_library::visualize::{
Color, Hsl, LinearRgb, Oklab, Oklch, Paint, ProcessColor, Rgb,
};
use typst_utils::Numeric;
use crate::property;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Properties(EcoVec<Property>);
impl Properties {
pub fn new() -> Self {
Self::default()
}
pub fn build<S: WarningSink>(sink: S) -> PropertiesBuilder<S> {
PropertiesBuilder::new(sink)
}
pub fn push(&mut self, property: &'static str, value: impl Into<EcoString>) {
let property = Property::new(property, value.into());
let res = self.0.binary_search_by_key(&property.name, |p| p.name);
match res {
Ok(idx) => self.0.make_mut()[idx] = property,
Err(idx) => self.0.insert(idx, property),
}
}
pub fn remove(&mut self, property: &'static str) {
if let Ok(i) = self.0.binary_search_by_key(&property, |p| p.name) {
self.0.remove(i);
}
}
pub fn with(mut self, property: &'static str, value: impl Into<EcoString>) -> Self {
self.push(property, value);
self
}
pub fn to_inline(&self) -> impl Display + use<'_> {
typst_utils::display(move |f| {
for (i, Property { name, value }) in self.iter().enumerate() {
if i > 0 {
f.write_str("; ")?;
}
write!(f, "{name}: {value}")?;
}
Ok(())
})
}
}
impl Deref for Properties {
type Target = [Property];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct PropertiesBuilder<S> {
sink: S,
props: Properties,
}
impl<S: WarningSink> PropertiesBuilder<S> {
pub fn new(sink: S) -> Self {
Self { sink, props: Properties::default() }
}
pub fn push(&mut self, property: &'static str, value: impl ToCss) {
let mut writer = CssWriter::new(&mut self.sink);
writer.emit(value);
if !writer.error {
self.props.push(property, writer.buf);
}
}
pub fn with(mut self, property: &'static str, value: impl ToCss) -> Self {
self.push(property, value);
self
}
pub fn finish(self) -> Properties {
self.props
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Property {
pub name: &'static str,
pub value: EcoString,
}
impl Property {
pub fn new(name: &'static str, value: EcoString) -> Self {
Self { name, value }
}
}
pub struct CssWriter<'a> {
sink: &'a mut dyn WarningSink,
buf: EcoString,
error: bool,
}
impl<'a> CssWriter<'a> {
fn new(sink: &'a mut dyn WarningSink) -> Self {
Self { sink, buf: EcoString::new(), error: false }
}
fn call<'b>(&'b mut self, name: &str, separator: Separator) -> CallWriter<'a, 'b> {
CallWriter::start(self, name, separator)
}
fn calc<'b>(&'b mut self) -> CalcWriter<'a, 'b> {
CalcWriter::start(self)
}
fn emit(&mut self, value: impl ToCss) {
value.emit(self)
}
fn write(&mut self, value: &str) {
self.buf.push_str(value);
}
fn write_fmt(&mut self, value: impl Display) {
write!(&mut self.buf, "{value}").unwrap();
}
fn ignored(&mut self, what: &str) {
self.sink
.emit(eco_format!("{what} was ignored during HTML export").into());
}
fn fail(&mut self, what: &str) {
self.ignored(what);
self.error = true;
}
}
struct CallWriter<'a, 'b> {
w: &'b mut CssWriter<'a>,
count: usize,
separator: Separator,
}
impl<'a, 'b> CallWriter<'a, 'b> {
fn start(w: &'b mut CssWriter<'a>, name: &str, separator: Separator) -> Self {
w.write(name);
w.write("(");
Self { w, count: 0, separator }
}
fn arg(&mut self, value: impl ToCss) -> &mut Self {
self.arg_with(value, self.separator)
}
fn arg_with(&mut self, value: impl ToCss, separator: Separator) -> &mut Self {
if self.count > 0 {
self.w.write(match separator {
Separator::Space => " ",
Separator::Slash => " / ",
});
}
self.w.emit(value);
self.count += 1;
self
}
}
impl Drop for CallWriter<'_, '_> {
fn drop(&mut self) {
self.w.write(")");
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Separator {
Space,
Slash,
}
struct CalcWriter<'a, 'b> {
w: &'b mut CssWriter<'a>,
start_idx: usize,
count: usize,
}
impl<'a, 'b> CalcWriter<'a, 'b> {
fn start(w: &'b mut CssWriter<'a>) -> Self {
let start_idx = w.buf.len();
Self { w, start_idx, count: 0 }
}
fn sum<T>(&mut self, value: T) -> &mut Self
where
T: ToCss + Numeric + Ord,
{
if value == T::zero() {
return self;
}
if self.count == 0 {
self.w.emit(value);
} else {
if value < T::zero() {
self.w.write(" - ");
self.w.emit(value.neg());
} else {
self.w.write(" + ");
self.w.emit(value);
}
}
self.count += 1;
self
}
}
impl Drop for CalcWriter<'_, '_> {
fn drop(&mut self) {
match self.count {
0 => {
self.w.write("0");
}
1 => (),
2.. => {
let mut buf = EcoString::with_capacity(self.w.buf.len() + 6);
buf.push_str(&self.w.buf[..self.start_idx]);
buf.push_str("calc(");
buf.push_str(&self.w.buf[self.start_idx..]);
buf.push_str(")");
self.w.buf = buf;
}
}
}
}
pub trait ToCss {
fn emit(&self, w: &mut CssWriter);
fn to_css(&self, mut sink: impl WarningSink) -> EcoString {
let mut w = CssWriter::new(&mut sink);
self.emit(&mut w);
w.buf
}
}
impl<T: ToCss + ?Sized> ToCss for &T {
fn emit(&self, w: &mut CssWriter) {
(**self).emit(w);
}
}
impl ToCss for str {
fn emit(&self, w: &mut CssWriter) {
w.write(self);
}
}
struct Number<T: Into<f64>>(T);
impl<T: Into<f64> + Copy> ToCss for Number<T> {
fn emit(&self, w: &mut CssWriter) {
w.emit(NumberWithPrecision(self.0, 4));
}
}
struct NumberWithPrecision<T: Into<f64>>(T, i16);
impl<T: Into<f64> + Copy> ToCss for NumberWithPrecision<T> {
fn emit(&self, w: &mut CssWriter) {
w.write_fmt(typst_utils::round_with_precision(self.0.into(), self.1));
}
}
impl ToCss for Abs {
fn emit(&self, w: &mut CssWriter) {
w.emit(Number(self.to_pt()));
w.write("pt");
}
}
impl ToCss for Em {
fn emit(&self, w: &mut CssWriter) {
w.emit(Number(self.get()));
w.write("em");
}
}
impl ToCss for Length {
fn emit(&self, w: &mut CssWriter) {
w.calc().sum(self.em).sum(self.abs);
}
}
impl ToCss for Angle {
fn emit(&self, w: &mut CssWriter) {
w.emit(Number(self.to_deg()));
w.write("deg");
}
}
impl ToCss for Ratio {
fn emit(&self, w: &mut CssWriter) {
w.emit(NumberWithPrecision(self.get() * 100.0, 2));
w.write("%");
}
}
impl ToCss for Rel {
fn emit(&self, w: &mut CssWriter) {
w.calc().sum(self.rel).sum(self.abs.em).sum(self.abs.abs);
}
}
impl ToCss for Paint {
fn emit(&self, w: &mut CssWriter) {
match self {
Self::Solid(color) => w.emit(color),
Self::Gradient(_) => w.fail("gradient"),
Self::Tiling(_) => w.fail("tiling"),
}
}
}
impl ToCss for Color {
fn emit(&self, w: &mut CssWriter) {
let process = self.to_process();
match process {
ProcessColor::Rgb(_) | ProcessColor::Cmyk(_) | ProcessColor::Luma(_) => {
w.emit(process.to_rgb())
}
ProcessColor::Oklab(v) => w.emit(v),
ProcessColor::Oklch(v) => w.emit(v),
ProcessColor::LinearRgb(v) => w.emit(v),
ProcessColor::Hsl(_) | ProcessColor::Hsv(_) => w.emit(process.to_hsl()),
}
}
}
impl ToCss for Rgb {
fn emit(&self, w: &mut CssWriter) {
let low = self.into_format::<u8, u8>();
let high = low.into_format::<f32, f32>();
if is_very_close(self.red, high.red)
&& is_very_close(self.blue, high.blue)
&& is_very_close(self.green, high.green)
&& is_very_close(self.alpha, high.alpha)
{
let (r, g, b, a) = low.into_components();
w.write_fmt(format_args!("#{r:02x}{g:02x}{b:02x}"));
if a != u8::MAX {
w.write_fmt(format_args!("{a:02x}"));
}
} else {
w.call("rgb", Separator::Space)
.arg(to_ratio(self.red))
.arg(to_ratio(self.green))
.arg(to_ratio(self.blue))
.maybe_alpha_arg(self.alpha);
}
}
}
impl ToCss for Oklab {
fn emit(&self, w: &mut CssWriter) {
w.call("oklab", Separator::Space)
.arg(to_ratio(self.l))
.arg(Number(self.a))
.arg(Number(self.b))
.maybe_alpha_arg(self.alpha);
}
}
impl ToCss for Oklch {
fn emit(&self, w: &mut CssWriter) {
w.call("oklch", Separator::Space)
.arg(to_ratio(self.l))
.arg(Number(self.chroma))
.arg(to_angle(self.hue.into_degrees()))
.maybe_alpha_arg(self.alpha);
}
}
impl ToCss for LinearRgb {
fn emit(&self, w: &mut CssWriter) {
w.call("color", Separator::Space)
.arg("srgb-linear")
.arg(to_ratio(self.red))
.arg(to_ratio(self.green))
.arg(to_ratio(self.blue))
.maybe_alpha_arg(self.alpha);
}
}
impl ToCss for Hsl {
fn emit(&self, w: &mut CssWriter) {
w.call("hsl", Separator::Space)
.arg(to_angle(self.hue.into_degrees()))
.arg(to_ratio(self.saturation))
.arg(to_ratio(self.lightness))
.maybe_alpha_arg(self.alpha);
}
}
impl ToCss for property::Display {
fn emit(&self, w: &mut CssWriter) {
w.write(self.as_str());
}
}
trait MaybeAlpha {
fn maybe_alpha_arg(&mut self, value: f32);
}
impl MaybeAlpha for CallWriter<'_, '_> {
fn maybe_alpha_arg(&mut self, value: f32) {
if !is_very_close(value, 1.0) {
self.arg_with(to_ratio(value), Separator::Slash);
}
}
}
fn to_angle(degrees: impl Into<f64>) -> Angle {
Angle::deg(degrees.into())
}
fn to_ratio(v: impl Into<f64>) -> Ratio {
Ratio::new(v.into())
}
fn is_very_close(a: impl Into<f64>, b: impl Into<f64>) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f64 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f64;
(a.into() - b.into()).abs() < EPS
}