use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use crate::color::Rgba;
use crate::model::{Edge, MAX_MAGNITUDE};
const BOOLEAN_KEYWORDS: [&str; 2] = ["false", "true"];
const FILL_PATTERN_KIND: StyleValueKind =
StyleValueKind::Keyword(&["dots", "grain", "lines", "none", "paper"]);
const FONT_KIND: StyleValueKind = StyleValueKind::Keyword(&["mono"]);
const TEXT_TRANSFORM_KIND: StyleValueKind =
StyleValueKind::Keyword(&["capitalize", "lowercase", "none", "uppercase"]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StyleKey<V> {
name: &'static str,
value_type: PhantomData<V>,
}
pub type ColorKey = StyleKey<ColorValue>;
pub type NumberKey = StyleKey<NumberValue>;
pub trait StyleValue {
const KIND: StyleValueKind;
type Parsed;
fn parse(text: &str) -> Option<Self::Parsed>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColorValue;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NumberValue;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AnyStyleKey {
name: &'static str,
value_kind: StyleValueKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StyleValueKind {
Color,
Number,
Boolean,
Keyword(&'static [&'static str]),
}
impl ColorKey {
pub const FILL: ColorKey = StyleKey::new("fill");
pub const STROKE: ColorKey = StyleKey::new("stroke");
}
impl NumberKey {
pub const STROKE_DASH: NumberKey = StyleKey::new("stroke-dash");
}
impl<V: StyleValue> StyleKey<V> {
const fn new(name: &'static str) -> Self {
Self {
name,
value_type: PhantomData,
}
}
pub const fn erased(self) -> AnyStyleKey {
AnyStyleKey::new(self.name, V::KIND)
}
pub fn read_from(self, style: &BTreeMap<String, String>) -> Option<V::Parsed> {
let value = style.get(self.name)?;
V::parse(value)
}
pub const fn name(&self) -> &'static str {
self.name
}
}
impl StyleValue for ColorValue {
const KIND: StyleValueKind = StyleValueKind::Color;
type Parsed = Rgba;
fn parse(text: &str) -> Option<Rgba> {
Rgba::parse(text)
}
}
impl StyleValue for NumberValue {
const KIND: StyleValueKind = StyleValueKind::Number;
type Parsed = f64;
fn parse(text: &str) -> Option<f64> {
let number: f64 = text.parse().ok()?;
let is_plain_number = Self::is_plain_number(text);
let is_bounded = number <= MAX_MAGNITUDE;
let is_bounded_plain_number = is_plain_number && is_bounded;
is_bounded_plain_number.then_some(number)
}
}
impl NumberValue {
fn is_plain_number(text: &str) -> bool {
let Some((integer_digits, fraction_digits)) = text.split_once('.') else {
return Self::is_ascii_digit_run(text);
};
let is_integer_part_valid = Self::is_ascii_digit_run(integer_digits);
let is_fraction_part_valid = Self::is_ascii_digit_run(fraction_digits);
is_integer_part_valid && is_fraction_part_valid
}
fn is_ascii_digit_run(text: &str) -> bool {
let has_digits = !text.is_empty();
let is_all_digits = text.bytes().all(|digit| digit.is_ascii_digit());
has_digits && is_all_digits
}
}
impl AnyStyleKey {
const fn new(name: &'static str, value_kind: StyleValueKind) -> Self {
Self { name, value_kind }
}
pub const ALL: [AnyStyleKey; 20] = [
AnyStyleKey::new("3d", StyleValueKind::Boolean),
AnyStyleKey::new("animated", StyleValueKind::Boolean),
AnyStyleKey::new("bold", StyleValueKind::Boolean),
AnyStyleKey::new("border-radius", StyleValueKind::Number),
AnyStyleKey::new("double-border", StyleValueKind::Boolean),
ColorKey::FILL.erased(),
AnyStyleKey::new("fill-pattern", FILL_PATTERN_KIND),
AnyStyleKey::new("filled", StyleValueKind::Boolean),
AnyStyleKey::new("font", FONT_KIND),
AnyStyleKey::new("font-color", StyleValueKind::Color),
AnyStyleKey::new("font-size", StyleValueKind::Number),
AnyStyleKey::new("italic", StyleValueKind::Boolean),
AnyStyleKey::new("multiple", StyleValueKind::Boolean),
AnyStyleKey::new("opacity", StyleValueKind::Number),
AnyStyleKey::new("shadow", StyleValueKind::Boolean),
ColorKey::STROKE.erased(),
NumberKey::STROKE_DASH.erased(),
AnyStyleKey::new("stroke-width", StyleValueKind::Number),
AnyStyleKey::new("text-transform", TEXT_TRANSFORM_KIND),
AnyStyleKey::new("underline", StyleValueKind::Boolean),
];
pub fn find(name: &str) -> Option<AnyStyleKey> {
let mut known_keys = Self::ALL.iter();
let known_key = known_keys.find(|known_key| known_key.name == name);
known_key.copied()
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn value_kind(&self) -> StyleValueKind {
self.value_kind
}
}
impl fmt::Display for AnyStyleKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.name)
}
}
impl Edge {
pub const DEFAULT_STROKE_DASH: f64 = 5.0;
pub fn stroke_dash(&self) -> f64 {
let style = self.style();
let authored_dash = NumberKey::STROKE_DASH.read_from(style);
authored_dash.unwrap_or(Self::DEFAULT_STROKE_DASH)
}
}
impl StyleValueKind {
pub fn accepts(&self, value: &str) -> bool {
let lowercase_value = value.to_ascii_lowercase();
let text = lowercase_value.as_str();
match self {
Self::Color => Rgba::parse(text).is_some(),
Self::Number => NumberValue::parse(text).is_some(),
Self::Boolean => BOOLEAN_KEYWORDS.contains(&text),
Self::Keyword(keywords) => keywords.contains(&text),
}
}
}
impl fmt::Display for StyleValueKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Color => formatter.write_str("a color (hex, CSS name or `transparent`)"),
Self::Number => write!(
formatter,
"a non-negative plain number no greater than {MAX_MAGNITUDE}"
),
Self::Boolean => formatter.write_str("a boolean"),
Self::Keyword(keywords) => {
let quoted_keywords: Vec<String> = keywords
.iter()
.map(|keyword| format!("`{keyword}`"))
.collect();
let keyword_list = quoted_keywords.join(", ");
write!(formatter, "one of {keyword_list}")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Element;
#[test]
fn style_keys_are_sorted_and_unique() {
let is_sorted = AnyStyleKey::ALL
.windows(2)
.all(|pair| pair[0].name < pair[1].name);
assert!(is_sorted);
}
#[test]
fn typed_keys_read_their_values_and_leave_unset_keys_none() {
let element = Element::new("api")
.with_style("fill", "RED")
.with_style("stroke-dash", "5")
.with_style("stroke-width", "2");
let red = Rgba::from_packed(0xff0000ff);
let element_style = element.style();
assert_eq!(ColorKey::FILL.read_from(element_style), Some(red));
assert_eq!(ColorKey::STROKE.read_from(element_style), None);
assert_eq!(NumberKey::STROKE_DASH.read_from(element_style), Some(5.0));
let edge = Edge::new().with_style("stroke", "#14f195");
let green = Rgba::from_packed(0x14f195ff);
let edge_style = edge.style();
assert_eq!(ColorKey::STROKE.read_from(edge_style), Some(green));
}
#[test]
fn each_value_kind_accepts_only_its_own_values() {
let overflowing_number = "9".repeat(400);
let cases = [
(StyleValueKind::Color, "#3478F6", true),
(StyleValueKind::Color, "RebeccaPurple", true),
(StyleValueKind::Color, "transparent", true),
(StyleValueKind::Color, "12", false),
(StyleValueKind::Color, "#12345", false),
(StyleValueKind::Number, "1.5", true),
(StyleValueKind::Number, "-1.5", false),
(StyleValueKind::Number, "12px", false),
(StyleValueKind::Number, ".5", false),
(StyleValueKind::Number, overflowing_number.as_str(), false),
(StyleValueKind::Number, "1000000", true),
(StyleValueKind::Number, "1000000.5", false),
(StyleValueKind::Number, "10000000000000", false),
(StyleValueKind::Boolean, "TRUE", true),
(StyleValueKind::Boolean, "yes", false),
(FONT_KIND, "mono", true),
(FONT_KIND, "serif", false),
(TEXT_TRANSFORM_KIND, "Capitalize", true),
(TEXT_TRANSFORM_KIND, "title", false),
];
for (value_kind, value, is_expected_accepted) in cases {
let is_accepted = value_kind.accepts(value);
assert_eq!(is_accepted, is_expected_accepted, "{value_kind}: {value:?}");
}
}
}