use std::fmt::Write;
use super::ToCss;
use super::properties::FontVariant;
use super::types::ComputedStyle;
macro_rules! emit_if_changed {
($self:expr, $default:expr, $buf:expr, $field:ident, $css_name:expr) => {
if $self.$field != $default.$field {
$buf.push_str($css_name);
$buf.push_str(": ");
$self.$field.to_css($buf);
$buf.push_str("; ");
}
};
}
macro_rules! emit_color_if_some {
($self:expr, $buf:expr, $field:ident, $css_name:expr) => {
if let Some(color) = $self.$field {
$buf.push_str($css_name);
$buf.push_str(": ");
color.to_css($buf);
$buf.push_str("; ");
}
};
}
macro_rules! emit_4sided {
($self:expr, $default:expr, $buf:expr,
$top:ident, $right:ident, $bottom:ident, $left:ident,
$prefix:expr) => {
emit_if_changed!($self, $default, $buf, $top, concat!($prefix, "-top"));
emit_if_changed!($self, $default, $buf, $right, concat!($prefix, "-right"));
emit_if_changed!($self, $default, $buf, $bottom, concat!($prefix, "-bottom"));
emit_if_changed!($self, $default, $buf, $left, concat!($prefix, "-left"));
};
}
macro_rules! emit_4sided_color {
($self:expr, $buf:expr,
$top:ident, $right:ident, $bottom:ident, $left:ident,
$prefix:expr) => {
emit_color_if_some!($self, $buf, $top, concat!($prefix, "-top-color"));
emit_color_if_some!($self, $buf, $right, concat!($prefix, "-right-color"));
emit_color_if_some!($self, $buf, $bottom, concat!($prefix, "-bottom-color"));
emit_color_if_some!($self, $buf, $left, concat!($prefix, "-left-color"));
};
}
macro_rules! emit_4corner {
($self:expr, $default:expr, $buf:expr,
$tl:ident, $tr:ident, $bl:ident, $br:ident) => {
emit_if_changed!($self, $default, $buf, $tl, "border-top-left-radius");
emit_if_changed!($self, $default, $buf, $tr, "border-top-right-radius");
emit_if_changed!($self, $default, $buf, $bl, "border-bottom-left-radius");
emit_if_changed!($self, $default, $buf, $br, "border-bottom-right-radius");
};
}
impl ToCss for ComputedStyle {
fn to_css(&self, buf: &mut String) {
let default = ComputedStyle::default();
if let Some(ref family) = self.font_family {
buf.push_str("font-family: ");
quote_font_family(buf, family);
buf.push_str("; ");
}
emit_if_changed!(self, default, buf, font_size, "font-size");
emit_if_changed!(self, default, buf, font_weight, "font-weight");
emit_if_changed!(self, default, buf, font_style, "font-style");
emit_color_if_some!(self, buf, color, "color");
emit_color_if_some!(self, buf, background_color, "background-color");
emit_if_changed!(self, default, buf, text_align, "text-align");
emit_if_changed!(self, default, buf, text_indent, "text-indent");
emit_if_changed!(self, default, buf, line_height, "line-height");
let mut decorations = Vec::new();
if self.text_decoration_underline {
decorations.push("underline");
}
if self.text_decoration_line_through {
decorations.push("line-through");
}
if !decorations.is_empty() {
write!(buf, "text-decoration: {}; ", decorations.join(" ")).unwrap();
}
emit_if_changed!(self, default, buf, display, "display");
emit_4sided!(
self,
default,
buf,
margin_top,
margin_right,
margin_bottom,
margin_left,
"margin"
);
emit_4sided!(
self,
default,
buf,
padding_top,
padding_right,
padding_bottom,
padding_left,
"padding"
);
emit_if_changed!(self, default, buf, vertical_align, "vertical-align");
emit_if_changed!(self, default, buf, list_style_type, "list-style-type");
if self.font_variant != FontVariant::Normal {
buf.push_str("font-variant: ");
self.font_variant.to_css(buf);
buf.push_str("; ");
}
emit_if_changed!(self, default, buf, letter_spacing, "letter-spacing");
emit_if_changed!(self, default, buf, word_spacing, "word-spacing");
emit_if_changed!(self, default, buf, text_transform, "text-transform");
emit_if_changed!(self, default, buf, hyphens, "hyphens");
emit_if_changed!(self, default, buf, white_space, "white-space");
emit_if_changed!(self, default, buf, underline_style, "text-decoration-style");
if self.overline {
buf.push_str("text-decoration-line: overline; ");
}
emit_color_if_some!(self, buf, underline_color, "text-decoration-color");
emit_if_changed!(self, default, buf, width, "width");
emit_if_changed!(self, default, buf, height, "height");
emit_if_changed!(self, default, buf, max_width, "max-width");
emit_if_changed!(self, default, buf, min_height, "min-height");
emit_if_changed!(self, default, buf, float, "float");
emit_if_changed!(self, default, buf, break_before, "break-before");
emit_if_changed!(self, default, buf, break_after, "break-after");
emit_if_changed!(self, default, buf, break_inside, "break-inside");
emit_4sided!(
self,
default,
buf,
border_style_top,
border_style_right,
border_style_bottom,
border_style_left,
"border-style"
);
emit_4sided!(
self,
default,
buf,
border_width_top,
border_width_right,
border_width_bottom,
border_width_left,
"border-width"
);
emit_4sided_color!(
self,
buf,
border_color_top,
border_color_right,
border_color_bottom,
border_color_left,
"border"
);
emit_4corner!(
self,
default,
buf,
border_radius_top_left,
border_radius_top_right,
border_radius_bottom_left,
border_radius_bottom_right
);
emit_if_changed!(
self,
default,
buf,
list_style_position,
"list-style-position"
);
emit_if_changed!(self, default, buf, visibility, "visibility");
}
}
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 mut style = ComputedStyle::default();
style.font_family = Some("001_cvi_cover-din next lt pro,sans-serif".to_string());
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
);
}
}