use core::fmt;
use crate::to_css::ToCss;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CssProp {
name: &'static str,
value: String,
}
impl CssProp {
pub(crate) fn new(name: &'static str, value: String) -> Self {
Self { name, value }
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn value(&self) -> &str {
&self.value
}
}
impl ToCss for CssProp {
fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
dest.write_str(self.name)?;
dest.write_str(": ")?;
dest.write_str(&self.value)?;
dest.write_char(';')
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct Css {
props: Vec<CssProp>,
}
impl Css {
pub fn new() -> Self {
Self { props: Vec::new() }
}
pub(crate) fn push(mut self, name: &'static str, value: impl ToCss) -> Self {
self.props.push(CssProp::new(name, value.to_css_string()));
self
}
pub(crate) fn push_raw(mut self, name: &'static str, value: impl Into<String>) -> Self {
self.props.push(CssProp::new(name, value.into()));
self
}
pub fn raw(self, name: &'static str, value: impl Into<String>) -> Self {
self.push_raw(name, value)
}
pub fn is_empty(&self) -> bool {
self.props.is_empty()
}
pub fn len(&self) -> usize {
self.props.len()
}
pub fn entries(&self) -> impl Iterator<Item = &CssProp> {
self.props.iter()
}
pub fn resolved(&self) -> Vec<&CssProp> {
let mut seen: std::collections::HashSet<&'static str> = std::collections::HashSet::new();
let mut out: Vec<&CssProp> = Vec::new();
for prop in self.props.iter().rev() {
if seen.insert(prop.name) {
out.push(prop);
}
}
out.reverse();
out
}
pub fn merge(mut self, other: Css) -> Self {
self.props.extend(other.props);
self
}
}
impl ToCss for Css {
fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
let resolved = self.resolved();
for (i, prop) in resolved.iter().enumerate() {
if i > 0 {
dest.write_char(' ')?;
}
prop.to_css(dest)?;
}
Ok(())
}
}
impl fmt::Display for Css {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ToCss::to_css(self, f)
}
}
impl From<Css> for String {
fn from(s: Css) -> Self {
s.to_css_string()
}
}
impl From<&Css> for String {
fn from(s: &Css) -> Self {
s.to_css_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_style_serializes_to_empty_string() {
assert_eq!(Css::new().to_css_string(), "");
assert!(Css::new().is_empty());
}
#[test]
fn raw_appends_a_declaration() {
let s = Css::new().raw("color", "red");
assert_eq!(s.to_css_string(), "color: red;");
assert!(!s.is_empty());
assert_eq!(s.len(), 1);
}
#[test]
fn multiple_distinct_properties_keep_order() {
let s = Css::new()
.raw("color", "red")
.raw("background-color", "blue");
assert_eq!(s.to_css_string(), "color: red; background-color: blue;");
}
#[test]
fn duplicate_property_uses_last_value() {
let s = Css::new()
.raw("color", "red")
.raw("color", "blue")
.raw("color", "green");
assert_eq!(s.to_css_string(), "color: green;");
assert_eq!(s.len(), 3);
assert_eq!(s.resolved().len(), 1);
}
#[test]
fn duplicate_property_preserves_position_of_last() {
let s = Css::new()
.raw("color", "red")
.raw("background-color", "white")
.raw("color", "blue");
assert_eq!(s.to_css_string(), "background-color: white; color: blue;");
}
#[test]
fn entries_iterates_all_in_order() {
let s = Css::new().raw("color", "red").raw("color", "blue");
let names: Vec<&str> = s.entries().map(|p| p.name()).collect();
assert_eq!(names, ["color", "color"]);
}
#[test]
fn merge_lets_other_win() {
let base = Css::new().raw("color", "red");
let overlay = Css::new().raw("color", "blue");
let merged = base.merge(overlay);
assert_eq!(merged.to_css_string(), "color: blue;");
}
#[test]
fn merge_preserves_distinct_props() {
let base = Css::new().raw("color", "red");
let overlay = Css::new().raw("background-color", "yellow");
let merged = base.merge(overlay);
assert_eq!(
merged.to_css_string(),
"color: red; background-color: yellow;"
);
}
#[test]
fn into_string_via_from_owned() {
let s = Css::new().raw("color", "red");
let css: String = s.into();
assert_eq!(css, "color: red;");
}
#[test]
fn into_string_via_from_borrowed() {
let s = Css::new().raw("color", "red");
let css: String = (&s).into();
assert_eq!(css, "color: red;");
}
#[test]
fn display_matches_to_css_string() {
let s = Css::new().raw("color", "red").raw("padding", "8px");
assert_eq!(format!("{s}"), s.to_css_string());
}
#[test]
fn style_prop_accessors() {
let s = Css::new().raw("color", "red");
let prop = s.entries().next().unwrap();
assert_eq!(prop.name(), "color");
assert_eq!(prop.value(), "red");
assert_eq!(prop.to_css_string(), "color: red;");
}
}