use std::collections::HashMap;
use crate::parser::css::{CssValue, SelectorContext, StyleMap};
use crate::parser::dom::HtmlTag;
use crate::types::Color;
const CSS_PIXEL_IN_POINTS: f32 = 0.75;
const LEGACY_TABLE_BORDER_COLOR: Color = Color {
r: 238.0,
g: 238.0,
b: 238.0,
a: 255.0,
};
#[derive(Debug, Default)]
pub(crate) struct HtmlCascadeLayers {
pub(crate) ua: StyleMap,
pub(crate) presentational_hints: StyleMap,
}
#[derive(Debug, Clone, Copy)]
struct LegacyTableBorder {
width: f32,
enables_ua_border: bool,
}
fn parse_non_negative_integer(source: &str) -> Option<u32> {
let mut bytes = source
.bytes()
.skip_while(|byte| byte.is_ascii_whitespace())
.peekable();
if bytes.peek() == Some(&b'+') {
bytes.next();
}
if !bytes.peek().is_some_and(u8::is_ascii_digit) {
return None;
}
let mut value = 0_u32;
while let Some(digit) = bytes.next_if(u8::is_ascii_digit) {
value = value
.saturating_mul(10)
.saturating_add(u32::from(digit - b'0'));
}
Some(value)
}
fn pixel_length_attribute(attributes: &HashMap<String, String>, name: &str) -> Option<f32> {
parse_non_negative_integer(attributes.get(name)?)
.map(|pixels| pixels as f32 * CSS_PIXEL_IN_POINTS)
}
fn legacy_table_border(attributes: &HashMap<String, String>) -> Option<LegacyTableBorder> {
let source = attributes.get("border")?;
let parsed = parse_non_negative_integer(source);
let pixels = parsed.unwrap_or(1);
Some(LegacyTableBorder {
width: pixels as f32 * CSS_PIXEL_IN_POINTS,
enables_ua_border: parsed.is_none_or(|value| value != 0),
})
}
fn set_physical_edges(style: &mut StyleMap, property: &str, value: CssValue) {
for side in ["top", "right", "bottom", "left"] {
style.set(&format!("border-{side}-{property}"), value.clone());
}
}
fn set_legacy_border_ua(style: &mut StyleMap, border_style: &str, width: Option<f32>) {
if let Some(width) = width {
set_physical_edges(style, "width", CssValue::Length(width));
}
set_physical_edges(style, "style", CssValue::Keyword(border_style.to_string()));
set_physical_edges(
style,
"color",
CssValue::Color(LEGACY_TABLE_BORDER_COLOR.into()),
);
}
fn nearest_table_attributes<'a>(
selector_context: &'a SelectorContext<'a>,
) -> Option<&'a HashMap<String, String>> {
selector_context
.ancestors
.iter()
.rev()
.find(|ancestor| ancestor.element.tag == HtmlTag::Table)
.map(|ancestor| &ancestor.element.attributes)
}
pub(crate) fn html_cascade_layers(
tag: HtmlTag,
attributes: &HashMap<String, String>,
selector_context: &SelectorContext<'_>,
) -> HtmlCascadeLayers {
let mut layers = HtmlCascadeLayers::default();
if tag == HtmlTag::Table {
if let Some(border) = legacy_table_border(attributes) {
set_physical_edges(
&mut layers.presentational_hints,
"width",
CssValue::Length(border.width),
);
if border.enables_ua_border {
set_legacy_border_ua(&mut layers.ua, "outset", None);
}
}
if let Some(spacing) = pixel_length_attribute(attributes, "cellspacing") {
layers
.presentational_hints
.set("border-spacing", CssValue::Length(spacing));
}
}
if matches!(tag, HtmlTag::Td | HtmlTag::Th)
&& let Some(table_attributes) = nearest_table_attributes(selector_context)
{
if let Some(padding) = pixel_length_attribute(table_attributes, "cellpadding") {
for side in ["top", "right", "bottom", "left"] {
layers
.presentational_hints
.set(&format!("padding-{side}"), CssValue::Length(padding));
}
}
if legacy_table_border(table_attributes).is_some_and(|border| border.enables_ua_border) {
set_legacy_border_ua(&mut layers.ua, "inset", Some(CSS_PIXEL_IN_POINTS));
}
}
layers
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn html_integer_parser_accepts_the_standard_prefix_and_rejects_sign_errors() {
assert_eq!(parse_non_negative_integer(" +14px"), Some(14));
assert_eq!(parse_non_negative_integer("-1"), None);
assert_eq!(parse_non_negative_integer("px"), None);
}
#[test]
fn invalid_table_border_uses_the_html_default_pixel() {
let attributes = HashMap::from([("border".to_string(), "invalid".to_string())]);
assert_eq!(
legacy_table_border(&attributes).map(|border| border.width),
Some(CSS_PIXEL_IN_POINTS)
);
}
}