use std::fmt::Write;
use super::ToCss;
use super::types::ComputedStyle;
type EmitFn = fn(&ComputedStyle, &ComputedStyle, &mut String) -> bool;
struct CssProperty {
name: &'static str,
in_blob: bool,
emit: EmitFn,
}
macro_rules! prop {
($name:expr, $field:ident) => {
prop!($name, $field, in_blob: true)
};
($name:expr, $field:ident, in_blob: $in_blob:expr) => {
CssProperty {
name: $name,
in_blob: $in_blob,
emit: |s, d, out| {
if s.$field == d.$field {
false
} else {
s.$field.to_css(out);
true
}
},
}
};
}
macro_rules! color_prop {
($name:expr, $field:ident) => {
CssProperty {
name: $name,
in_blob: true,
emit: |s, _d, out| match s.$field {
Some(color) => {
color.to_css(out);
true
}
None => false,
},
}
};
}
macro_rules! count_prop {
($name:expr, $field:ident) => {
CssProperty {
name: $name,
in_blob: false,
emit: |s, d, out| {
if s.$field == d.$field {
false
} else {
write!(out, "{}", s.$field).unwrap();
true
}
},
}
};
}
const PROPERTIES: &[CssProperty] = &[
CssProperty {
name: "font-family",
in_blob: true,
emit: |s, _d, out| match &s.font_family {
Some(family) => {
quote_font_family(out, family);
true
}
None => false,
},
},
prop!("font-size", font_size),
prop!("font-weight", font_weight),
prop!("font-style", font_style),
color_prop!("color", color),
color_prop!("background-color", background_color),
prop!("text-align", text_align),
prop!("text-indent", text_indent),
prop!("line-height", line_height),
CssProperty {
name: "text-decoration",
in_blob: true,
emit: |s, _d, out| {
match (s.text_decoration_underline, s.text_decoration_line_through) {
(true, true) => out.push_str("underline line-through"),
(true, false) => out.push_str("underline"),
(false, true) => out.push_str("line-through"),
(false, false) => return false,
}
true
},
},
prop!("display", display),
prop!("margin-top", margin_top),
prop!("margin-right", margin_right),
prop!("margin-bottom", margin_bottom),
prop!("margin-left", margin_left),
prop!("padding-top", padding_top),
prop!("padding-right", padding_right),
prop!("padding-bottom", padding_bottom),
prop!("padding-left", padding_left),
prop!("vertical-align", vertical_align),
prop!("list-style-type", list_style_type),
prop!("font-variant", font_variant),
prop!("letter-spacing", letter_spacing),
prop!("word-spacing", word_spacing),
prop!("text-transform", text_transform),
prop!("hyphens", hyphens),
prop!("white-space", white_space),
prop!("text-decoration-style", underline_style),
CssProperty {
name: "text-decoration-line",
in_blob: true,
emit: |s, _d, out| {
if s.overline {
out.push_str("overline");
true
} else {
false
}
},
},
color_prop!("text-decoration-color", underline_color),
prop!("width", width),
prop!("height", height),
prop!("max-width", max_width),
prop!("min-height", min_height),
prop!("float", float),
prop!("break-before", break_before),
prop!("break-after", break_after),
prop!("break-inside", break_inside),
prop!("border-style-top", border_style_top),
prop!("border-style-right", border_style_right),
prop!("border-style-bottom", border_style_bottom),
prop!("border-style-left", border_style_left),
prop!("border-width-top", border_width_top),
prop!("border-width-right", border_width_right),
prop!("border-width-bottom", border_width_bottom),
prop!("border-width-left", border_width_left),
color_prop!("border-top-color", border_color_top),
color_prop!("border-right-color", border_color_right),
color_prop!("border-bottom-color", border_color_bottom),
color_prop!("border-left-color", border_color_left),
prop!("border-top-left-radius", border_radius_top_left),
prop!("border-top-right-radius", border_radius_top_right),
prop!("border-bottom-left-radius", border_radius_bottom_left),
prop!("border-bottom-right-radius", border_radius_bottom_right),
prop!("list-style-position", list_style_position),
prop!("visibility", visibility),
prop!("min-width", min_width, in_blob: false),
prop!("max-height", max_height, in_blob: false),
prop!("clear", clear, in_blob: false),
count_prop!("orphans", orphans),
count_prop!("widows", widows),
prop!("word-break", word_break, in_blob: false),
prop!("border-collapse", border_collapse, in_blob: false),
prop!("border-spacing", border_spacing, in_blob: false),
];
static DEFAULT_STYLE: std::sync::LazyLock<ComputedStyle> =
std::sync::LazyLock::new(ComputedStyle::default);
static PROPERTY_INDEX: std::sync::LazyLock<rustc_hash::FxHashMap<&'static str, usize>> =
std::sync::LazyLock::new(|| {
PROPERTIES
.iter()
.enumerate()
.map(|(i, p)| (p.name, i))
.collect()
});
pub fn for_each_changed_property(style: &ComputedStyle, f: &mut dyn FnMut(&str, &str)) {
let default = &*DEFAULT_STYLE;
let mut value = String::new();
for prop in PROPERTIES {
if !prop.in_blob {
continue;
}
value.clear();
if (prop.emit)(style, default, &mut value) {
f(prop.name, &value);
}
}
}
pub fn changed_property_value(style: &ComputedStyle, name: &str) -> Option<String> {
let idx = *PROPERTY_INDEX
.get(name)
.unwrap_or_else(|| panic!("unknown CSS property name: {name}"));
let prop = &PROPERTIES[idx];
let mut value = String::new();
(prop.emit)(style, &DEFAULT_STYLE, &mut value).then_some(value)
}
impl ToCss for ComputedStyle {
fn to_css(&self, buf: &mut String) {
for_each_changed_property(self, &mut |name, value| {
buf.push_str(name);
buf.push_str(": ");
buf.push_str(value);
buf.push_str("; ");
});
}
}
const GENERIC_FAMILIES: &[&str] = &[
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
"math",
"emoji",
"fangsong",
];
fn quote_font_family(buf: &mut String, family: &str) {
for (i, part) in family.split(',').enumerate() {
if i > 0 {
buf.push(',');
}
let trimmed = part.trim();
let is_generic = GENERIC_FAMILIES
.iter()
.any(|g| g.eq_ignore_ascii_case(trimmed));
let needs_quoting = !is_generic
&& (trimmed.contains(' ')
|| trimmed.starts_with(|c: char| c.is_ascii_digit())
|| trimmed.contains('"')
|| trimmed.is_empty());
if needs_quoting {
buf.push('"');
buf.push_str(trimmed);
buf.push('"');
} else {
buf.push_str(trimmed);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quoted(input: &str) -> String {
let mut buf = String::new();
quote_font_family(&mut buf, input);
buf
}
#[test]
fn test_font_family_quoting_spaces_and_digit_prefix() {
assert_eq!(
quoted("001_cvi_cover-din next lt pro,sans-serif"),
r#""001_cvi_cover-din next lt pro",sans-serif"#
);
}
#[test]
fn test_font_family_quoting_spaces() {
assert_eq!(
quoted("DIN Next LT Pro,sans-serif"),
r#""DIN Next LT Pro",sans-serif"#
);
}
#[test]
fn test_font_family_no_quoting_single_word() {
assert_eq!(quoted("Helvetica"), "Helvetica");
}
#[test]
fn test_font_family_generic_not_quoted() {
assert_eq!(quoted("serif"), "serif");
assert_eq!(quoted("sans-serif"), "sans-serif");
assert_eq!(quoted("monospace"), "monospace");
}
#[test]
fn test_font_family_generic_case_insensitive() {
assert_eq!(quoted("Sans-Serif"), "Sans-Serif");
}
#[test]
fn test_font_family_full_stack() {
assert_eq!(
quoted("031_next-reads-shift light,palatino,palatino linotype,georgia,serif"),
r#""031_next-reads-shift light",palatino,"palatino linotype",georgia,serif"#
);
}
#[test]
fn test_font_family_leading_digit() {
assert_eq!(quoted("123font"), r#""123font""#);
}
#[test]
fn test_computed_style_font_family_quoted() {
let style = ComputedStyle {
font_family: Some("001_cvi_cover-din next lt pro,sans-serif".to_string()),
..Default::default()
};
let mut css = String::new();
style.to_css(&mut css);
assert!(
css.contains(r#"font-family: "001_cvi_cover-din next lt pro",sans-serif;"#),
"Expected quoted font-family in CSS output, got: {}",
css
);
}
#[test]
fn test_property_names_unique() {
for (i, a) in PROPERTIES.iter().enumerate() {
for b in &PROPERTIES[i + 1..] {
assert_ne!(a.name, b.name, "duplicate property name in table");
}
}
}
#[test]
fn test_changed_property_value_matches_blob() {
let style = ComputedStyle {
font_weight: crate::style::FontWeight::BOLD,
..Default::default()
};
assert_eq!(
changed_property_value(&style, "font-weight"),
Some("bold".to_string())
);
assert_eq!(changed_property_value(&style, "font-style"), None);
}
#[test]
#[should_panic(expected = "unknown CSS property name")]
fn test_changed_property_value_unknown_name_panics() {
changed_property_value(&ComputedStyle::default(), "not-a-property");
}
}