Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
at url-parser
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983//! CSS cascade and computed style resolution.//!//! For each DOM element, resolves the final computed value of every CSS property//! by collecting matching rules, applying the cascade (specificity + source order),//! handling property inheritance, and resolving relative values.
use we_css::parser::{Declaration, Stylesheet};use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit};use we_dom::{Document, NodeData, NodeId};
use crate::matching::collect_matching_rules;
// ---------------------------------------------------------------------------// Display// ---------------------------------------------------------------------------
/// CSS `display` property values (subset).#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum Display { Block, #[default] Inline, None,}
// ---------------------------------------------------------------------------// Position// ---------------------------------------------------------------------------
/// CSS `position` property values.#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum Position { #[default] Static, Relative, Absolute, Fixed,}
// ---------------------------------------------------------------------------// FontWeight// ---------------------------------------------------------------------------
/// CSS `font-weight` as a numeric value (100-900).#[derive(Debug, Clone, Copy, PartialEq)]pub struct FontWeight(pub f32);
impl Default for FontWeight { fn default() -> Self { FontWeight(400.0) // normal }}
// ---------------------------------------------------------------------------// FontStyle// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum FontStyle { #[default] Normal, Italic, Oblique,}
// ---------------------------------------------------------------------------// TextAlign// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum TextAlign { #[default] Left, Right, Center, Justify,}
// ---------------------------------------------------------------------------// TextDecoration// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum TextDecoration { #[default] None, Underline, Overline, LineThrough,}
// ---------------------------------------------------------------------------// Overflow// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum Overflow { #[default] Visible, Hidden, Scroll, Auto,}
// ---------------------------------------------------------------------------// Visibility// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum Visibility { #[default] Visible, Hidden, Collapse,}
// ---------------------------------------------------------------------------// BorderStyle// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum BorderStyle { #[default] None, Hidden, Dotted, Dashed, Solid, Double, Groove, Ridge, Inset, Outset,}
// ---------------------------------------------------------------------------// LengthOrAuto// ---------------------------------------------------------------------------
/// A computed length (resolved to px) or `auto`.#[derive(Debug, Clone, Copy, PartialEq, Default)]pub enum LengthOrAuto { Length(f32), #[default] Auto,}
// ---------------------------------------------------------------------------// ComputedStyle// ---------------------------------------------------------------------------
/// The fully resolved computed style for a single element.#[derive(Debug, Clone, PartialEq)]pub struct ComputedStyle { // Display pub display: Display,
// Box model: margin pub margin_top: LengthOrAuto, pub margin_right: LengthOrAuto, pub margin_bottom: LengthOrAuto, pub margin_left: LengthOrAuto,
// Box model: padding (no auto for padding) pub padding_top: f32, pub padding_right: f32, pub padding_bottom: f32, pub padding_left: f32,
// Box model: border width pub border_top_width: f32, pub border_right_width: f32, pub border_bottom_width: f32, pub border_left_width: f32,
// Box model: border style pub border_top_style: BorderStyle, pub border_right_style: BorderStyle, pub border_bottom_style: BorderStyle, pub border_left_style: BorderStyle,
// Box model: border color pub border_top_color: Color, pub border_right_color: Color, pub border_bottom_color: Color, pub border_left_color: Color,
// Box model: dimensions pub width: LengthOrAuto, pub height: LengthOrAuto,
// Text / inherited pub color: Color, pub font_size: f32, pub font_weight: FontWeight, pub font_style: FontStyle, pub font_family: String, pub text_align: TextAlign, pub text_decoration: TextDecoration, pub line_height: f32,
// Background pub background_color: Color,
// Position pub position: Position, pub top: LengthOrAuto, pub right: LengthOrAuto, pub bottom: LengthOrAuto, pub left: LengthOrAuto,
// Overflow pub overflow: Overflow,
// Visibility (inherited) pub visibility: Visibility,}
impl Default for ComputedStyle { fn default() -> Self { ComputedStyle { display: Display::Inline,
margin_top: LengthOrAuto::Length(0.0), margin_right: LengthOrAuto::Length(0.0), margin_bottom: LengthOrAuto::Length(0.0), margin_left: LengthOrAuto::Length(0.0),
padding_top: 0.0, padding_right: 0.0, padding_bottom: 0.0, padding_left: 0.0,
border_top_width: 0.0, border_right_width: 0.0, border_bottom_width: 0.0, border_left_width: 0.0,
border_top_style: BorderStyle::None, border_right_style: BorderStyle::None, border_bottom_style: BorderStyle::None, border_left_style: BorderStyle::None,
border_top_color: Color::rgb(0, 0, 0), border_right_color: Color::rgb(0, 0, 0), border_bottom_color: Color::rgb(0, 0, 0), border_left_color: Color::rgb(0, 0, 0),
width: LengthOrAuto::Auto, height: LengthOrAuto::Auto,
color: Color::rgb(0, 0, 0), font_size: 16.0, font_weight: FontWeight(400.0), font_style: FontStyle::Normal, font_family: String::new(), text_align: TextAlign::Left, text_decoration: TextDecoration::None, line_height: 19.2, // 1.2 * 16
background_color: Color::new(0, 0, 0, 0), // transparent
position: Position::Static, top: LengthOrAuto::Auto, right: LengthOrAuto::Auto, bottom: LengthOrAuto::Auto, left: LengthOrAuto::Auto,
overflow: Overflow::Visible, visibility: Visibility::Visible, } }}
// ---------------------------------------------------------------------------// Property classification: inherited vs non-inherited// ---------------------------------------------------------------------------
fn is_inherited_property(property: &str) -> bool { matches!( property, "color" | "font-size" | "font-weight" | "font-style" | "font-family" | "text-align" | "text-decoration" | "line-height" | "visibility" )}
// ---------------------------------------------------------------------------// User-agent stylesheet// ---------------------------------------------------------------------------
/// Returns the user-agent default stylesheet.pub fn ua_stylesheet() -> Stylesheet { use we_css::parser::Parser; Parser::parse(UA_CSS)}
const UA_CSS: &str = r#"html, body, div, p, pre, h1, h2, h3, h4, h5, h6,ul, ol, li, blockquote, section, article, nav,header, footer, main, hr { display: block;}
span, a, em, strong, b, i, u, code, small, sub, sup, br { display: inline;}
head, title, script, style, link, meta { display: none;}
body { margin: 8px;}
h1 { font-size: 2em; margin-top: 0.67em; margin-bottom: 0.67em; font-weight: bold;}
h2 { font-size: 1.5em; margin-top: 0.83em; margin-bottom: 0.83em; font-weight: bold;}
h3 { font-size: 1.17em; margin-top: 1em; margin-bottom: 1em; font-weight: bold;}
h4 { font-size: 1em; margin-top: 1.33em; margin-bottom: 1.33em; font-weight: bold;}
h5 { font-size: 0.83em; margin-top: 1.67em; margin-bottom: 1.67em; font-weight: bold;}
h6 { font-size: 0.67em; margin-top: 2.33em; margin-bottom: 2.33em; font-weight: bold;}
p { margin-top: 1em; margin-bottom: 1em;}
strong, b { font-weight: bold;}
em, i { font-style: italic;}
a { color: blue; text-decoration: underline;}
u { text-decoration: underline;}"#;
// ---------------------------------------------------------------------------// Resolve a CssValue to f32 px given context// ---------------------------------------------------------------------------
fn resolve_length(value: &CssValue, _parent_font_size: f32, current_font_size: f32) -> Option<f32> { match value { // Em units resolve relative to the element's own computed font-size // (for properties other than font-size, which has its own handling). CssValue::Length(n, unit) => Some(resolve_length_unit(*n, *unit, current_font_size)), CssValue::Percentage(p) => Some((*p / 100.0) as f32 * current_font_size), CssValue::Zero => Some(0.0), CssValue::Number(n) if *n == 0.0 => Some(0.0), _ => None, }}
fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32) -> f32 { let v = value as f32; match unit { LengthUnit::Px => v, LengthUnit::Em => v * em_base, LengthUnit::Rem => v * 16.0, // root font size is always 16px for now LengthUnit::Pt => v * (96.0 / 72.0), LengthUnit::Cm => v * (96.0 / 2.54), LengthUnit::Mm => v * (96.0 / 25.4), LengthUnit::In => v * 96.0, LengthUnit::Pc => v * 16.0, // Viewport units: use a reasonable default (can be parameterized later) LengthUnit::Vw | LengthUnit::Vh | LengthUnit::Vmin | LengthUnit::Vmax => v, }}
fn resolve_length_or_auto( value: &CssValue, parent_font_size: f32, current_font_size: f32,) -> LengthOrAuto { match value { CssValue::Auto => LengthOrAuto::Auto, _ => resolve_length(value, parent_font_size, current_font_size) .map(LengthOrAuto::Length) .unwrap_or(LengthOrAuto::Auto), }}
fn resolve_color(value: &CssValue, current_color: Color) -> Option<Color> { match value { CssValue::Color(c) => Some(*c), CssValue::CurrentColor => Some(current_color), CssValue::Transparent => Some(Color::new(0, 0, 0, 0)), _ => None, }}
// ---------------------------------------------------------------------------// Apply a single property value to a ComputedStyle// ---------------------------------------------------------------------------
fn apply_property( style: &mut ComputedStyle, property: &str, value: &CssValue, parent: &ComputedStyle,) { // Handle inherit/initial/unset match value { CssValue::Inherit => { inherit_property(style, property, parent); return; } CssValue::Initial => { reset_property_to_initial(style, property); return; } CssValue::Unset => { if is_inherited_property(property) { inherit_property(style, property, parent); } else { reset_property_to_initial(style, property); } return; } _ => {} }
let parent_fs = parent.font_size; let current_fs = style.font_size;
match property { "display" => { style.display = match value { CssValue::Keyword(k) => match k.as_str() { "block" => Display::Block, "inline" => Display::Inline, _ => Display::Block, }, CssValue::None => Display::None, _ => style.display, }; }
// Margin "margin-top" => { style.margin_top = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-right" => { style.margin_right = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-bottom" => { style.margin_bottom = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-left" => { style.margin_left = resolve_length_or_auto(value, parent_fs, current_fs); }
// Padding "padding-top" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_top = v; } } "padding-right" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_right = v; } } "padding-bottom" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_bottom = v; } } "padding-left" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_left = v; } }
// Border width "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => { let w = resolve_border_width(value, parent_fs); match property { "border-top-width" => style.border_top_width = w, "border-right-width" => style.border_right_width = w, "border-bottom-width" => style.border_bottom_width = w, "border-left-width" => style.border_left_width = w, _ => {} } }
// Border width shorthand (single value applied to all sides) "border-width" => { let w = resolve_border_width(value, parent_fs); style.border_top_width = w; style.border_right_width = w; style.border_bottom_width = w; style.border_left_width = w; }
// Border style "border-top-style" | "border-right-style" | "border-bottom-style" | "border-left-style" => { let s = parse_border_style(value); match property { "border-top-style" => style.border_top_style = s, "border-right-style" => style.border_right_style = s, "border-bottom-style" => style.border_bottom_style = s, "border-left-style" => style.border_left_style = s, _ => {} } }
"border-style" => { let s = parse_border_style(value); style.border_top_style = s; style.border_right_style = s; style.border_bottom_style = s; style.border_left_style = s; }
// Border color "border-top-color" | "border-right-color" | "border-bottom-color" | "border-left-color" => { if let Some(c) = resolve_color(value, style.color) { match property { "border-top-color" => style.border_top_color = c, "border-right-color" => style.border_right_color = c, "border-bottom-color" => style.border_bottom_color = c, "border-left-color" => style.border_left_color = c, _ => {} } } }
"border-color" => { if let Some(c) = resolve_color(value, style.color) { style.border_top_color = c; style.border_right_color = c; style.border_bottom_color = c; style.border_left_color = c; } }
// Dimensions "width" => { style.width = resolve_length_or_auto(value, parent_fs, current_fs); } "height" => { style.height = resolve_length_or_auto(value, parent_fs, current_fs); }
// Color (inherited) "color" => { if let Some(c) = resolve_color(value, parent.color) { style.color = c; // Update border colors to match (currentColor default) } }
// Font-size (inherited) — special: em units relative to parent "font-size" => { match value { CssValue::Length(n, unit) => { style.font_size = resolve_length_unit(*n, *unit, parent_fs); } CssValue::Percentage(p) => { style.font_size = (*p / 100.0) as f32 * parent_fs; } CssValue::Zero => { style.font_size = 0.0; } CssValue::Keyword(k) => { style.font_size = match k.as_str() { "xx-small" => 9.0, "x-small" => 10.0, "small" => 13.0, "medium" => 16.0, "large" => 18.0, "x-large" => 24.0, "xx-large" => 32.0, "smaller" => parent_fs * 0.833, "larger" => parent_fs * 1.2, _ => style.font_size, }; } _ => {} } // Update line-height when font-size changes style.line_height = style.font_size * 1.2; }
// Font-weight (inherited) "font-weight" => { style.font_weight = match value { CssValue::Keyword(k) => match k.as_str() { "normal" => FontWeight(400.0), "bold" => FontWeight(700.0), "lighter" => FontWeight((parent.font_weight.0 - 100.0).max(100.0)), "bolder" => FontWeight((parent.font_weight.0 + 300.0).min(900.0)), _ => style.font_weight, }, CssValue::Number(n) => FontWeight(*n as f32), _ => style.font_weight, }; }
// Font-style (inherited) "font-style" => { style.font_style = match value { CssValue::Keyword(k) => match k.as_str() { "normal" => FontStyle::Normal, "italic" => FontStyle::Italic, "oblique" => FontStyle::Oblique, _ => style.font_style, }, _ => style.font_style, }; }
// Font-family (inherited) "font-family" => { if let CssValue::String(s) | CssValue::Keyword(s) = value { style.font_family = s.clone(); } }
// Text-align (inherited) "text-align" => { style.text_align = match value { CssValue::Keyword(k) => match k.as_str() { "left" => TextAlign::Left, "right" => TextAlign::Right, "center" => TextAlign::Center, "justify" => TextAlign::Justify, _ => style.text_align, }, _ => style.text_align, }; }
// Text-decoration (inherited) "text-decoration" => { style.text_decoration = match value { CssValue::Keyword(k) => match k.as_str() { "underline" => TextDecoration::Underline, "overline" => TextDecoration::Overline, "line-through" => TextDecoration::LineThrough, _ => style.text_decoration, }, CssValue::None => TextDecoration::None, _ => style.text_decoration, }; }
// Line-height (inherited) "line-height" => match value { CssValue::Keyword(k) if k == "normal" => { style.line_height = style.font_size * 1.2; } CssValue::Number(n) => { style.line_height = *n as f32 * style.font_size; } CssValue::Length(n, unit) => { style.line_height = resolve_length_unit(*n, *unit, style.font_size); } CssValue::Percentage(p) => { style.line_height = (*p / 100.0) as f32 * style.font_size; } _ => {} },
// Background color "background-color" => { if let Some(c) = resolve_color(value, style.color) { style.background_color = c; } }
// Position "position" => { style.position = match value { CssValue::Keyword(k) => match k.as_str() { "static" => Position::Static, "relative" => Position::Relative, "absolute" => Position::Absolute, "fixed" => Position::Fixed, _ => style.position, }, _ => style.position, }; }
// Position offsets "top" => style.top = resolve_length_or_auto(value, parent_fs, current_fs), "right" => style.right = resolve_length_or_auto(value, parent_fs, current_fs), "bottom" => style.bottom = resolve_length_or_auto(value, parent_fs, current_fs), "left" => style.left = resolve_length_or_auto(value, parent_fs, current_fs),
// Overflow "overflow" => { style.overflow = match value { CssValue::Keyword(k) => match k.as_str() { "visible" => Overflow::Visible, "hidden" => Overflow::Hidden, "scroll" => Overflow::Scroll, _ => style.overflow, }, CssValue::Auto => Overflow::Auto, _ => style.overflow, }; }
// Visibility (inherited) "visibility" => { style.visibility = match value { CssValue::Keyword(k) => match k.as_str() { "visible" => Visibility::Visible, "hidden" => Visibility::Hidden, "collapse" => Visibility::Collapse, _ => style.visibility, }, _ => style.visibility, }; }
_ => {} // Unknown property — ignore }}
fn resolve_border_width(value: &CssValue, em_base: f32) -> f32 { match value { CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base), CssValue::Zero => 0.0, CssValue::Number(n) if *n == 0.0 => 0.0, CssValue::Keyword(k) => match k.as_str() { "thin" => 1.0, "medium" => 3.0, "thick" => 5.0, _ => 0.0, }, _ => 0.0, }}
fn parse_border_style(value: &CssValue) -> BorderStyle { match value { CssValue::Keyword(k) => match k.as_str() { "none" => BorderStyle::None, "hidden" => BorderStyle::Hidden, "dotted" => BorderStyle::Dotted, "dashed" => BorderStyle::Dashed, "solid" => BorderStyle::Solid, "double" => BorderStyle::Double, "groove" => BorderStyle::Groove, "ridge" => BorderStyle::Ridge, "inset" => BorderStyle::Inset, "outset" => BorderStyle::Outset, _ => BorderStyle::None, }, CssValue::None => BorderStyle::None, _ => BorderStyle::None, }}
fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &ComputedStyle) { match property { "color" => style.color = parent.color, "font-size" => { style.font_size = parent.font_size; style.line_height = style.font_size * 1.2; } "font-weight" => style.font_weight = parent.font_weight, "font-style" => style.font_style = parent.font_style, "font-family" => style.font_family = parent.font_family.clone(), "text-align" => style.text_align = parent.text_align, "text-decoration" => style.text_decoration = parent.text_decoration, "line-height" => style.line_height = parent.line_height, "visibility" => style.visibility = parent.visibility, // Non-inherited properties: inherit from parent if explicitly requested "display" => style.display = parent.display, "margin-top" => style.margin_top = parent.margin_top, "margin-right" => style.margin_right = parent.margin_right, "margin-bottom" => style.margin_bottom = parent.margin_bottom, "margin-left" => style.margin_left = parent.margin_left, "padding-top" => style.padding_top = parent.padding_top, "padding-right" => style.padding_right = parent.padding_right, "padding-bottom" => style.padding_bottom = parent.padding_bottom, "padding-left" => style.padding_left = parent.padding_left, "width" => style.width = parent.width, "height" => style.height = parent.height, "background-color" => style.background_color = parent.background_color, "position" => style.position = parent.position, "overflow" => style.overflow = parent.overflow, _ => {} }}
fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { let initial = ComputedStyle::default(); match property { "display" => style.display = initial.display, "margin-top" => style.margin_top = initial.margin_top, "margin-right" => style.margin_right = initial.margin_right, "margin-bottom" => style.margin_bottom = initial.margin_bottom, "margin-left" => style.margin_left = initial.margin_left, "padding-top" => style.padding_top = initial.padding_top, "padding-right" => style.padding_right = initial.padding_right, "padding-bottom" => style.padding_bottom = initial.padding_bottom, "padding-left" => style.padding_left = initial.padding_left, "border-top-width" => style.border_top_width = initial.border_top_width, "border-right-width" => style.border_right_width = initial.border_right_width, "border-bottom-width" => style.border_bottom_width = initial.border_bottom_width, "border-left-width" => style.border_left_width = initial.border_left_width, "width" => style.width = initial.width, "height" => style.height = initial.height, "color" => style.color = initial.color, "font-size" => { style.font_size = initial.font_size; style.line_height = initial.line_height; } "font-weight" => style.font_weight = initial.font_weight, "font-style" => style.font_style = initial.font_style, "font-family" => style.font_family = initial.font_family.clone(), "text-align" => style.text_align = initial.text_align, "text-decoration" => style.text_decoration = initial.text_decoration, "line-height" => style.line_height = initial.line_height, "background-color" => style.background_color = initial.background_color, "position" => style.position = initial.position, "top" => style.top = initial.top, "right" => style.right = initial.right, "bottom" => style.bottom = initial.bottom, "left" => style.left = initial.left, "overflow" => style.overflow = initial.overflow, "visibility" => style.visibility = initial.visibility, _ => {} }}
// ---------------------------------------------------------------------------// Styled tree// ---------------------------------------------------------------------------
/// A node in the styled tree: a DOM node paired with its computed style.#[derive(Debug)]pub struct StyledNode { pub node: NodeId, pub style: ComputedStyle, pub children: Vec<StyledNode>,}
/// Extract CSS stylesheets from `<style>` elements in the document.////// Walks the DOM tree, finds all `<style>` elements, extracts their text/// content, and parses each as a CSS stylesheet. Returns stylesheets in/// document order.pub fn extract_stylesheets(doc: &Document) -> Vec<Stylesheet> { let mut stylesheets = Vec::new(); collect_style_elements(doc, doc.root(), &mut stylesheets); stylesheets}
fn collect_style_elements(doc: &Document, node: NodeId, stylesheets: &mut Vec<Stylesheet>) { match doc.node_data(node) { NodeData::Element { tag_name, .. } if tag_name == "style" => { let mut css_text = String::new(); for child in doc.children(node) { if let NodeData::Text { data } = doc.node_data(child) { css_text.push_str(data); } } if !css_text.is_empty() { stylesheets.push(we_css::parser::Parser::parse(&css_text)); } } _ => { for child in doc.children(node) { collect_style_elements(doc, child, stylesheets); } } }}
/// Resolve styles for an entire document tree.////// `stylesheets` is a list of author stylesheets (the UA stylesheet is/// automatically prepended).pub fn resolve_styles(doc: &Document, author_stylesheets: &[Stylesheet]) -> Option<StyledNode> { let ua = ua_stylesheet();
// Combine UA + author stylesheets into a single list for rule collection. // UA rules come first (lower priority), author rules come after. let mut combined = Stylesheet { rules: ua.rules.clone(), }; for ss in author_stylesheets { combined.rules.extend(ss.rules.iter().cloned()); }
let root = doc.root(); resolve_node(doc, root, &combined, &ComputedStyle::default())}
fn resolve_node( doc: &Document, node: NodeId, stylesheet: &Stylesheet, parent_style: &ComputedStyle,) -> Option<StyledNode> { match doc.node_data(node) { NodeData::Document => { // Document node: resolve children, return first element child or wrapper. let mut children = Vec::new(); for child in doc.children(node) { if let Some(styled) = resolve_node(doc, child, stylesheet, parent_style) { children.push(styled); } } if children.len() == 1 { children.into_iter().next() } else if children.is_empty() { None } else { Some(StyledNode { node, style: parent_style.clone(), children, }) } } NodeData::Element { .. } => { let style = compute_style_for_element(doc, node, stylesheet, parent_style);
if style.display == Display::None { return None; }
let mut children = Vec::new(); for child in doc.children(node) { if let Some(styled) = resolve_node(doc, child, stylesheet, &style) { children.push(styled); } }
Some(StyledNode { node, style, children, }) } NodeData::Text { data } => { if data.trim().is_empty() { return None; } // Text nodes inherit all properties from their parent. Some(StyledNode { node, style: parent_style.clone(), children: Vec::new(), }) } NodeData::Comment { .. } => None, }}
/// Compute the style for a single element node.fn compute_style_for_element( doc: &Document, node: NodeId, stylesheet: &Stylesheet, parent_style: &ComputedStyle,) -> ComputedStyle { // Start from initial values, inheriting inherited properties from parent let mut style = ComputedStyle { color: parent_style.color, font_size: parent_style.font_size, font_weight: parent_style.font_weight, font_style: parent_style.font_style, font_family: parent_style.font_family.clone(), text_align: parent_style.text_align, text_decoration: parent_style.text_decoration, line_height: parent_style.line_height, visibility: parent_style.visibility, ..ComputedStyle::default() };
// Step 2: Collect matching rules, sorted by specificity + source order let matched_rules = collect_matching_rules(doc, node, stylesheet);
// Step 3: Separate normal and !important declarations let mut normal_decls: Vec<(String, CssValue)> = Vec::new(); let mut important_decls: Vec<(String, CssValue)> = Vec::new();
for matched in &matched_rules { for decl in &matched.rule.declarations { let property = &decl.property;
// Try shorthand expansion first if let Some(longhands) = expand_shorthand(property, &decl.value, decl.important) { for lh in longhands { if lh.important { important_decls.push((lh.property, lh.value)); } else { normal_decls.push((lh.property, lh.value)); } } } else { // Regular longhand property let value = parse_value(&decl.value); if decl.important { important_decls.push((property.clone(), value)); } else { normal_decls.push((property.clone(), value)); } } } }
// Step 4: Apply inline style declarations (from style attribute). // Inline styles have specificity (1,0,0,0) — higher than any selector. // We apply them after stylesheet rules so they override. let inline_decls = parse_inline_style(doc, node);
// Step 5: Apply normal declarations (already in specificity order) for (prop, value) in &normal_decls { apply_property(&mut style, prop, value, parent_style); }
// Step 6: Apply inline style normal declarations (override stylesheet normals) for decl in &inline_decls { if !decl.important { let property = decl.property.as_str(); if let Some(longhands) = expand_shorthand(property, &decl.value, false) { for lh in &longhands { apply_property(&mut style, &lh.property, &lh.value, parent_style); } } else { let value = parse_value(&decl.value); apply_property(&mut style, property, &value, parent_style); } } }
// Step 7: Apply !important declarations (override everything normal) for (prop, value) in &important_decls { apply_property(&mut style, prop, value, parent_style); }
// Step 8: Apply inline style !important declarations (highest priority) for decl in &inline_decls { if decl.important { let property = decl.property.as_str(); if let Some(longhands) = expand_shorthand(property, &decl.value, true) { for lh in &longhands { apply_property(&mut style, &lh.property, &lh.value, parent_style); } } else { let value = parse_value(&decl.value); apply_property(&mut style, property, &value, parent_style); } } }
style}
/// Parse inline style from the `style` attribute of an element.fn parse_inline_style(doc: &Document, node: NodeId) -> Vec<Declaration> { if let Some(style_attr) = doc.get_attribute(node, "style") { // Wrap in a dummy rule so the CSS parser can parse it let css = format!("x {{ {style_attr} }}"); let ss = we_css::parser::Parser::parse(&css); if let Some(we_css::parser::Rule::Style(rule)) = ss.rules.into_iter().next() { rule.declarations } else { Vec::new() } } else { Vec::new() }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*; use we_css::parser::Parser;
fn make_doc_with_body() -> (Document, NodeId, NodeId, NodeId) { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.append_child(root, html); doc.append_child(html, body); (doc, root, html, body) }
// ----------------------------------------------------------------------- // UA defaults // -----------------------------------------------------------------------
#[test] fn ua_body_has_8px_margin() { let (doc, _, _, _) = make_doc_with_body(); let styled = resolve_styles(&doc, &[]).unwrap(); // styled is <html>, first child is <body> let body = &styled.children[0]; assert_eq!(body.style.margin_top, LengthOrAuto::Length(8.0)); assert_eq!(body.style.margin_right, LengthOrAuto::Length(8.0)); assert_eq!(body.style.margin_bottom, LengthOrAuto::Length(8.0)); assert_eq!(body.style.margin_left, LengthOrAuto::Length(8.0)); }
#[test] fn ua_h1_font_size() { let (mut doc, _, _, body) = make_doc_with_body(); let h1 = doc.create_element("h1"); let text = doc.create_text("Title"); doc.append_child(body, h1); doc.append_child(h1, text);
let styled = resolve_styles(&doc, &[]).unwrap(); let body_node = &styled.children[0]; let h1_node = &body_node.children[0];
// h1 = 2em of parent (16px) = 32px assert_eq!(h1_node.style.font_size, 32.0); assert_eq!(h1_node.style.font_weight, FontWeight(700.0)); }
#[test] fn ua_h2_font_size() { let (mut doc, _, _, body) = make_doc_with_body(); let h2 = doc.create_element("h2"); doc.append_child(body, h2);
let styled = resolve_styles(&doc, &[]).unwrap(); let body_node = &styled.children[0]; let h2_node = &body_node.children[0];
// h2 = 1.5em = 24px assert_eq!(h2_node.style.font_size, 24.0); }
#[test] fn ua_p_has_1em_margins() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("text"); doc.append_child(body, p); doc.append_child(p, text);
let styled = resolve_styles(&doc, &[]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// p margin-top/bottom = 1em = 16px assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(16.0)); assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(16.0)); }
#[test] fn ua_display_block_elements() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); doc.append_child(body, div);
let styled = resolve_styles(&doc, &[]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.display, Display::Block); }
#[test] fn ua_display_inline_elements() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let span = doc.create_element("span"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, span); doc.append_child(span, text);
let styled = resolve_styles(&doc, &[]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; let span_node = &p_node.children[0]; assert_eq!(span_node.style.display, Display::Inline); }
#[test] fn ua_display_none_for_head() { let (mut doc, _, html, _) = make_doc_with_body(); let head = doc.create_element("head"); let title = doc.create_element("title"); doc.append_child(html, head); doc.append_child(head, title);
let styled = resolve_styles(&doc, &[]).unwrap(); // head should not appear in styled tree (display: none) for child in &styled.children { if let NodeData::Element { tag_name, .. } = doc.node_data(child.node) { assert_ne!(tag_name.as_str(), "head"); } } }
// ----------------------------------------------------------------------- // Author stylesheets // -----------------------------------------------------------------------
#[test] fn author_color_override() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("hello"); doc.append_child(body, p); doc.append_child(p, text);
let ss = Parser::parse("p { color: red; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); }
#[test] fn author_background_color() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); doc.append_child(body, div);
let ss = Parser::parse("div { background-color: blue; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.background_color, Color::rgb(0, 0, 255)); }
#[test] fn author_font_size_px() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); doc.append_child(body, p);
let ss = Parser::parse("p { font-size: 24px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.font_size, 24.0); }
#[test] fn author_margin_px() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); doc.append_child(body, div);
let ss = Parser::parse("div { margin-top: 20px; margin-bottom: 10px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0)); assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0)); }
// ----------------------------------------------------------------------- // Cascade: specificity ordering // -----------------------------------------------------------------------
#[test] fn higher_specificity_wins() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); doc.set_attribute(p, "class", "highlight"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
// .highlight (0,1,0) > p (0,0,1) let ss = Parser::parse("p { color: red; } .highlight { color: green; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); }
#[test] fn source_order_tiebreak() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
// Same specificity: later wins let ss = Parser::parse("p { color: red; } p { color: blue; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); }
#[test] fn important_overrides_specificity() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); doc.set_attribute(p, "id", "main"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
// #main (1,0,0) has higher specificity, but p has !important let ss = Parser::parse("#main { color: blue; } p { color: red !important; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); }
// ----------------------------------------------------------------------- // Inheritance // -----------------------------------------------------------------------
#[test] fn color_inherits_to_children() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
let ss = Parser::parse("div { color: green; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.color, Color::rgb(0, 128, 0)); // p inherits color from div assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); }
#[test] fn font_size_inherits() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
let ss = Parser::parse("div { font-size: 20px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.font_size, 20.0); assert_eq!(p_node.style.font_size, 20.0); }
#[test] fn margin_does_not_inherit() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let span = doc.create_element("span"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, span); doc.append_child(span, text);
let ss = Parser::parse("div { margin-top: 50px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let span_node = &div_node.children[0];
assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(50.0)); // span should NOT inherit margin assert_eq!(span_node.style.margin_top, LengthOrAuto::Length(0.0)); }
// ----------------------------------------------------------------------- // inherit / initial / unset keywords // -----------------------------------------------------------------------
#[test] fn inherit_keyword_for_non_inherited() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
let ss = Parser::parse("div { background-color: red; } p { background-color: inherit; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.background_color, Color::rgb(255, 0, 0)); assert_eq!(p_node.style.background_color, Color::rgb(255, 0, 0)); }
#[test] fn initial_keyword_resets() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
// div sets color to red, p resets to initial (black) let ss = Parser::parse("div { color: red; } p { color: initial; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.color, Color::rgb(255, 0, 0)); assert_eq!(p_node.style.color, Color::rgb(0, 0, 0)); // initial }
#[test] fn unset_inherits_for_inherited_property() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
// color is inherited, so unset => inherit let ss = Parser::parse("div { color: green; } p { color: unset; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(p_node.style.color, div_node.style.color); }
#[test] fn unset_resets_for_non_inherited_property() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
// margin is non-inherited, so unset => initial (0) let ss = Parser::parse("p { margin-top: unset; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// UA sets p margin-top to 1em=16px, but unset resets to initial (0) assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0)); }
// ----------------------------------------------------------------------- // Em unit resolution // -----------------------------------------------------------------------
#[test] fn em_margin_relative_to_font_size() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, text);
let ss = Parser::parse("div { font-size: 20px; margin-top: 2em; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.font_size, 20.0); // margin-top 2em resolves relative to the element's own computed font-size (20px) assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(40.0)); }
#[test] fn em_font_size_relative_to_parent() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
let ss = Parser::parse("div { font-size: 20px; } p { font-size: 1.5em; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.font_size, 20.0); // p font-size = 1.5 * parent(20px) = 30px assert_eq!(p_node.style.font_size, 30.0); }
// ----------------------------------------------------------------------- // Inline styles // -----------------------------------------------------------------------
#[test] fn inline_style_overrides_stylesheet() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); doc.set_attribute(p, "style", "color: green;"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
let ss = Parser::parse("p { color: red; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// Inline style wins over author stylesheet assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); }
#[test] fn inline_style_important() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); doc.set_attribute(p, "style", "color: green !important;"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
let ss = Parser::parse("p { color: red !important; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// Inline !important beats stylesheet !important assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); }
// ----------------------------------------------------------------------- // Shorthand expansion // -----------------------------------------------------------------------
#[test] fn margin_shorthand_in_stylesheet() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, text);
let ss = Parser::parse("div { margin: 10px 20px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(10.0)); assert_eq!(div_node.style.margin_right, LengthOrAuto::Length(20.0)); assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0)); assert_eq!(div_node.style.margin_left, LengthOrAuto::Length(20.0)); }
#[test] fn padding_shorthand() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, text);
let ss = Parser::parse("div { padding: 5px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.padding_top, 5.0); assert_eq!(div_node.style.padding_right, 5.0); assert_eq!(div_node.style.padding_bottom, 5.0); assert_eq!(div_node.style.padding_left, 5.0); }
// ----------------------------------------------------------------------- // UA + author cascade // -----------------------------------------------------------------------
#[test] fn author_overrides_ua() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
// UA gives p margin-top=1em=16px. Author overrides to 0. let ss = Parser::parse("p { margin-top: 0; margin-bottom: 0; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0)); assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(0.0)); }
// ----------------------------------------------------------------------- // Text node inherits parent style // -----------------------------------------------------------------------
#[test] fn text_node_inherits_style() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("hello"); doc.append_child(body, p); doc.append_child(p, text);
let ss = Parser::parse("p { color: red; font-size: 20px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; let text_node = &p_node.children[0];
assert_eq!(text_node.style.color, Color::rgb(255, 0, 0)); assert_eq!(text_node.style.font_size, 20.0); }
// ----------------------------------------------------------------------- // Multiple stylesheets // -----------------------------------------------------------------------
#[test] fn multiple_author_stylesheets() { let (mut doc, _, _, body) = make_doc_with_body(); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, p); doc.append_child(p, text);
let ss1 = Parser::parse("p { color: red; }"); let ss2 = Parser::parse("p { color: blue; }"); let styled = resolve_styles(&doc, &[ss1, ss2]).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// Later stylesheet wins (higher source order) assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); }
// ----------------------------------------------------------------------- // Border // -----------------------------------------------------------------------
#[test] fn border_shorthand_all_sides() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, text);
let ss = Parser::parse("div { border: 2px solid red; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.border_top_width, 2.0); assert_eq!(div_node.style.border_top_style, BorderStyle::Solid); assert_eq!(div_node.style.border_top_color, Color::rgb(255, 0, 0)); }
// ----------------------------------------------------------------------- // Position // -----------------------------------------------------------------------
#[test] fn position_property() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, text);
let ss = Parser::parse("div { position: relative; top: 10px; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.position, Position::Relative); assert_eq!(div_node.style.top, LengthOrAuto::Length(10.0)); }
// ----------------------------------------------------------------------- // Display: none removes from tree // -----------------------------------------------------------------------
#[test] fn display_none_removes_element() { let (mut doc, _, _, body) = make_doc_with_body(); let div1 = doc.create_element("div"); let t1 = doc.create_text("visible"); doc.append_child(body, div1); doc.append_child(div1, t1);
let div2 = doc.create_element("div"); doc.set_attribute(div2, "class", "hidden"); let t2 = doc.create_text("hidden"); doc.append_child(body, div2); doc.append_child(div2, t2);
let ss = Parser::parse(".hidden { display: none; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0];
// Only div1 should appear assert_eq!(body_node.children.len(), 1); }
// ----------------------------------------------------------------------- // Visibility // -----------------------------------------------------------------------
#[test] fn visibility_inherited() { let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); let p = doc.create_element("p"); let text = doc.create_text("x"); doc.append_child(body, div); doc.append_child(div, p); doc.append_child(p, text);
let ss = Parser::parse("div { visibility: hidden; }"); let styled = resolve_styles(&doc, &[ss]).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0];
assert_eq!(div_node.style.visibility, Visibility::Hidden); assert_eq!(p_node.style.visibility, Visibility::Hidden); }
// ----------------------------------------------------------------------- // extract_stylesheets // -----------------------------------------------------------------------
#[test] fn extract_single_style_element() { let (mut doc, _, html, _) = make_doc_with_body(); let head = doc.create_element("head"); let style = doc.create_element("style"); let css_text = doc.create_text("p { color: red; }"); doc.append_child(html, head); doc.append_child(head, style); doc.append_child(style, css_text);
let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 1); assert!(!sheets[0].rules.is_empty()); }
#[test] fn extract_multiple_style_elements() { let (mut doc, _, html, _) = make_doc_with_body(); let head = doc.create_element("head"); doc.append_child(html, head);
let style1 = doc.create_element("style"); let css1 = doc.create_text("p { color: red; }"); doc.append_child(head, style1); doc.append_child(style1, css1);
let style2 = doc.create_element("style"); let css2 = doc.create_text("div { margin: 10px; }"); doc.append_child(head, style2); doc.append_child(style2, css2);
let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 2); }
#[test] fn extract_no_style_elements() { let (doc, _, _, _) = make_doc_with_body(); let sheets = extract_stylesheets(&doc); assert!(sheets.is_empty()); }
#[test] fn extract_empty_style_element() { let (mut doc, _, html, _) = make_doc_with_body(); let head = doc.create_element("head"); let style = doc.create_element("style"); doc.append_child(html, head); doc.append_child(head, style); // No text child — empty style element.
let sheets = extract_stylesheets(&doc); assert!(sheets.is_empty()); }
#[test] fn extract_style_in_body() { let (mut doc, _, _, body) = make_doc_with_body(); // Style elements in body should also be extracted. let style = doc.create_element("style"); let css_text = doc.create_text("h1 { font-size: 2em; }"); doc.append_child(body, style); doc.append_child(style, css_text);
let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 1); }
#[test] fn extract_and_resolve_styles_from_dom() { // Build a DOM with <style> and verify resolved styles. let (mut doc, _, html, body) = make_doc_with_body(); let head = doc.create_element("head"); let style = doc.create_element("style"); let css_text = doc.create_text("p { color: red; font-size: 24px; }"); doc.append_child(html, head); doc.append_child(head, style); doc.append_child(style, css_text);
let p = doc.create_element("p"); let text = doc.create_text("hello"); doc.append_child(body, p); doc.append_child(p, text);
let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); assert_eq!(p_node.style.font_size, 24.0); }
#[test] fn style_attribute_works_with_extract() { // Inline style attributes should work even without <style> elements. let (mut doc, _, _, body) = make_doc_with_body(); let div = doc.create_element("div"); doc.set_attribute(div, "style", "color: green; margin-top: 20px;"); let text = doc.create_text("styled"); doc.append_child(body, div); doc.append_child(div, text);
let sheets = extract_stylesheets(&doc); assert!(sheets.is_empty());
let styled = resolve_styles(&doc, &sheets).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.color, Color::rgb(0, 128, 0)); assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0)); }
#[test] fn style_element_and_inline_style_combined() { let (mut doc, _, html, body) = make_doc_with_body(); let head = doc.create_element("head"); let style = doc.create_element("style"); let css_text = doc.create_text("p { color: red; font-size: 20px; }"); doc.append_child(html, head); doc.append_child(head, style); doc.append_child(style, css_text);
let p = doc.create_element("p"); // Inline style overrides stylesheet for color. doc.set_attribute(p, "style", "color: blue;"); let text = doc.create_text("hello"); doc.append_child(body, p); doc.append_child(p, text);
let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// Inline style wins for color. assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); // Font-size comes from stylesheet. assert_eq!(p_node.style.font_size, 20.0); }
#[test] fn multiple_style_elements_cascade_correctly() { let (mut doc, _, html, body) = make_doc_with_body(); let head = doc.create_element("head"); doc.append_child(html, head);
// First stylesheet sets color to red. let style1 = doc.create_element("style"); let css1 = doc.create_text("p { color: red; }"); doc.append_child(head, style1); doc.append_child(style1, css1);
// Second stylesheet sets color to blue (later wins in cascade). let style2 = doc.create_element("style"); let css2 = doc.create_text("p { color: blue; }"); doc.append_child(head, style2); doc.append_child(style2, css2);
let p = doc.create_element("p"); let text = doc.create_text("hello"); doc.append_child(body, p); doc.append_child(p, text);
let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 2);
let styled = resolve_styles(&doc, &sheets).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0];
// Later stylesheet wins. assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); }}