Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296//! 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, Flex, InlineFlex, 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,}
// ---------------------------------------------------------------------------// BoxSizing// ---------------------------------------------------------------------------
/// CSS `box-sizing` property values.#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum BoxSizing { #[default] ContentBox, BorderBox,}
// ---------------------------------------------------------------------------// 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,}
// ---------------------------------------------------------------------------// Flex enums// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum FlexDirection { #[default] Row, RowReverse, Column, ColumnReverse,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum FlexWrap { #[default] Nowrap, Wrap, WrapReverse,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum JustifyContent { #[default] FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum AlignItems { #[default] Stretch, FlexStart, FlexEnd, Center, Baseline,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum AlignContent { #[default] Stretch, FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum AlignSelf { #[default] Auto, FlexStart, FlexEnd, Center, Baseline, Stretch,}
// ---------------------------------------------------------------------------// 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), a percentage (unresolved), or `auto`.#[derive(Debug, Clone, Copy, PartialEq, Default)]pub enum LengthOrAuto { Length(f32), /// Percentage value (0.0–100.0), resolved during layout against the containing block. Percentage(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 (percentages resolve against containing block width) pub padding_top: LengthOrAuto, pub padding_right: LengthOrAuto, pub padding_bottom: LengthOrAuto, pub padding_left: LengthOrAuto,
// 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,
// Box model: sizing pub box_sizing: BoxSizing,
// 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,
// Flex container properties pub flex_direction: FlexDirection, pub flex_wrap: FlexWrap, pub justify_content: JustifyContent, pub align_items: AlignItems, pub align_content: AlignContent, pub row_gap: f32, pub column_gap: f32,
// Flex item properties pub flex_grow: f32, pub flex_shrink: f32, pub flex_basis: LengthOrAuto, pub align_self: AlignSelf, pub order: i32,}
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: LengthOrAuto::Length(0.0), padding_right: LengthOrAuto::Length(0.0), padding_bottom: LengthOrAuto::Length(0.0), padding_left: LengthOrAuto::Length(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,
box_sizing: BoxSizing::ContentBox,
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,
flex_direction: FlexDirection::Row, flex_wrap: FlexWrap::Nowrap, justify_content: JustifyContent::FlexStart, align_items: AlignItems::Stretch, align_content: AlignContent::Stretch, row_gap: 0.0, column_gap: 0.0,
flex_grow: 0.0, flex_shrink: 1.0, flex_basis: LengthOrAuto::Auto, align_self: AlignSelf::Auto, order: 0, } }}
// ---------------------------------------------------------------------------// 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_unit(value: f64, unit: LengthUnit, em_base: f32, viewport: (f32, f32)) -> f32 { let v = value as f32; let (vw, vh) = viewport; 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, LengthUnit::Vw => v * vw / 100.0, LengthUnit::Vh => v * vh / 100.0, LengthUnit::Vmin => v * vw.min(vh) / 100.0, LengthUnit::Vmax => v * vw.max(vh) / 100.0, }}
/// Resolve a CSS value to `LengthOrAuto` for layout properties (width, height,/// margin, padding, top/right/bottom/left). Percentages are preserved as/// `LengthOrAuto::Percentage` for later resolution during layout against the/// containing block.fn resolve_layout_length_or_auto( value: &CssValue, current_font_size: f32, viewport: (f32, f32),) -> LengthOrAuto { match value { CssValue::Auto => LengthOrAuto::Auto, CssValue::Percentage(p) => LengthOrAuto::Percentage(*p as f32), CssValue::Length(n, unit) => { LengthOrAuto::Length(resolve_length_unit(*n, *unit, current_font_size, viewport)) } CssValue::Zero => LengthOrAuto::Length(0.0), CssValue::Number(n) if *n == 0.0 => LengthOrAuto::Length(0.0), _ => 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, viewport: (f32, f32),) { // 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, "flex" => Display::Flex, "inline-flex" => Display::InlineFlex, _ => Display::Block, }, CssValue::None => Display::None, _ => style.display, }; }
// Margin (percentages preserved for layout resolution) "margin-top" => { style.margin_top = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-right" => { style.margin_right = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-bottom" => { style.margin_bottom = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-left" => { style.margin_left = resolve_layout_length_or_auto(value, current_fs, viewport); }
// Padding (percentages preserved for layout resolution) "padding-top" => { style.padding_top = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-right" => { style.padding_right = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-bottom" => { style.padding_bottom = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-left" => { style.padding_left = resolve_layout_length_or_auto(value, current_fs, viewport); }
// Border width "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => { let w = resolve_border_width(value, parent_fs, viewport); 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, viewport); 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 (percentages preserved for layout resolution) "width" => { style.width = resolve_layout_length_or_auto(value, current_fs, viewport); } "height" => { style.height = resolve_layout_length_or_auto(value, current_fs, viewport); }
// Box sizing "box-sizing" => { style.box_sizing = match value { CssValue::Keyword(k) => match k.as_str() { "content-box" => BoxSizing::ContentBox, "border-box" => BoxSizing::BorderBox, _ => style.box_sizing, }, _ => style.box_sizing, }; }
// 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, viewport); } 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, viewport); } 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 (percentages preserved for layout resolution) "top" => style.top = resolve_layout_length_or_auto(value, current_fs, viewport), "right" => style.right = resolve_layout_length_or_auto(value, current_fs, viewport), "bottom" => style.bottom = resolve_layout_length_or_auto(value, current_fs, viewport), "left" => style.left = resolve_layout_length_or_auto(value, current_fs, viewport),
// 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, }; }
// Flex container properties "flex-direction" => { style.flex_direction = match value { CssValue::Keyword(k) => match k.as_str() { "row" => FlexDirection::Row, "row-reverse" => FlexDirection::RowReverse, "column" => FlexDirection::Column, "column-reverse" => FlexDirection::ColumnReverse, _ => style.flex_direction, }, _ => style.flex_direction, }; } "flex-wrap" => { style.flex_wrap = match value { CssValue::Keyword(k) => match k.as_str() { "nowrap" => FlexWrap::Nowrap, "wrap" => FlexWrap::Wrap, "wrap-reverse" => FlexWrap::WrapReverse, _ => style.flex_wrap, }, _ => style.flex_wrap, }; } "justify-content" => { style.justify_content = match value { CssValue::Keyword(k) => match k.as_str() { "flex-start" => JustifyContent::FlexStart, "flex-end" => JustifyContent::FlexEnd, "center" => JustifyContent::Center, "space-between" => JustifyContent::SpaceBetween, "space-around" => JustifyContent::SpaceAround, "space-evenly" => JustifyContent::SpaceEvenly, _ => style.justify_content, }, _ => style.justify_content, }; } "align-items" => { style.align_items = match value { CssValue::Keyword(k) => match k.as_str() { "stretch" => AlignItems::Stretch, "flex-start" => AlignItems::FlexStart, "flex-end" => AlignItems::FlexEnd, "center" => AlignItems::Center, "baseline" => AlignItems::Baseline, _ => style.align_items, }, _ => style.align_items, }; } "align-content" => { style.align_content = match value { CssValue::Keyword(k) => match k.as_str() { "stretch" => AlignContent::Stretch, "flex-start" => AlignContent::FlexStart, "flex-end" => AlignContent::FlexEnd, "center" => AlignContent::Center, "space-between" => AlignContent::SpaceBetween, "space-around" => AlignContent::SpaceAround, _ => style.align_content, }, _ => style.align_content, }; } "row-gap" => { if let CssValue::Length(n, unit) = value { style.row_gap = resolve_length_unit(*n, *unit, current_fs, viewport); } else if let CssValue::Zero = value { style.row_gap = 0.0; } } "column-gap" => { if let CssValue::Length(n, unit) = value { style.column_gap = resolve_length_unit(*n, *unit, current_fs, viewport); } else if let CssValue::Zero = value { style.column_gap = 0.0; } }
// Flex item properties "flex-grow" => { if let CssValue::Number(n) = value { style.flex_grow = *n as f32; } else if let CssValue::Zero = value { style.flex_grow = 0.0; } } "flex-shrink" => { if let CssValue::Number(n) = value { style.flex_shrink = *n as f32; } else if let CssValue::Zero = value { style.flex_shrink = 0.0; } } "flex-basis" => { style.flex_basis = resolve_layout_length_or_auto(value, current_fs, viewport); } "align-self" => { style.align_self = match value { CssValue::Keyword(k) => match k.as_str() { "flex-start" => AlignSelf::FlexStart, "flex-end" => AlignSelf::FlexEnd, "center" => AlignSelf::Center, "baseline" => AlignSelf::Baseline, "stretch" => AlignSelf::Stretch, _ => style.align_self, }, CssValue::Auto => AlignSelf::Auto, _ => style.align_self, }; } "order" => { if let CssValue::Number(n) = value { style.order = *n as i32; } else if let CssValue::Zero = value { style.order = 0; } }
_ => {} // Unknown property — ignore }}
fn resolve_border_width(value: &CssValue, em_base: f32, viewport: (f32, f32)) -> f32 { match value { CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base, viewport), 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, "box-sizing" => style.box_sizing = parent.box_sizing, "background-color" => style.background_color = parent.background_color, "position" => style.position = parent.position, "overflow" => style.overflow = parent.overflow, "flex-direction" => style.flex_direction = parent.flex_direction, "flex-wrap" => style.flex_wrap = parent.flex_wrap, "justify-content" => style.justify_content = parent.justify_content, "align-items" => style.align_items = parent.align_items, "align-content" => style.align_content = parent.align_content, "row-gap" => style.row_gap = parent.row_gap, "column-gap" => style.column_gap = parent.column_gap, "flex-grow" => style.flex_grow = parent.flex_grow, "flex-shrink" => style.flex_shrink = parent.flex_shrink, "flex-basis" => style.flex_basis = parent.flex_basis, "align-self" => style.align_self = parent.align_self, "order" => style.order = parent.order, _ => {} }}
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, "box-sizing" => style.box_sizing = initial.box_sizing, "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, "flex-direction" => style.flex_direction = initial.flex_direction, "flex-wrap" => style.flex_wrap = initial.flex_wrap, "justify-content" => style.justify_content = initial.justify_content, "align-items" => style.align_items = initial.align_items, "align-content" => style.align_content = initial.align_content, "row-gap" => style.row_gap = initial.row_gap, "column-gap" => style.column_gap = initial.column_gap, "flex-grow" => style.flex_grow = initial.flex_grow, "flex-shrink" => style.flex_shrink = initial.flex_shrink, "flex-basis" => style.flex_basis = initial.flex_basis, "align-self" => style.align_self = initial.align_self, "order" => style.order = initial.order, _ => {} }}
// ---------------------------------------------------------------------------// 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], viewport: (f32, f32),) -> 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(), viewport)}
fn resolve_node( doc: &Document, node: NodeId, stylesheet: &Stylesheet, parent_style: &ComputedStyle, viewport: (f32, f32),) -> 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, viewport) { 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, viewport);
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, viewport) { 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, viewport: (f32, f32),) -> 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, viewport); }
// 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, viewport); } } else { let value = parse_value(&decl.value); apply_property(&mut style, property, &value, parent_style, viewport); } } }
// Step 7: Apply !important declarations (override everything normal) for (prop, value) in &important_decls { apply_property(&mut style, prop, value, parent_style, viewport); }
// 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, viewport); } } else { let value = parse_value(&decl.value); apply_property(&mut style, property, &value, parent_style, viewport); } } }
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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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, &[], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0];
assert_eq!(div_node.style.padding_top, LengthOrAuto::Length(5.0)); assert_eq!(div_node.style.padding_right, LengthOrAuto::Length(5.0)); assert_eq!(div_node.style.padding_bottom, LengthOrAuto::Length(5.0)); assert_eq!(div_node.style.padding_left, LengthOrAuto::Length(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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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], (800.0, 600.0)).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, (800.0, 600.0)).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, (800.0, 600.0)).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, (800.0, 600.0)).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, (800.0, 600.0)).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)); }
#[test] fn box_sizing_content_box_default() { let html_str = r#"<!DOCTYPE html><html><head></head><body><div>Test</div></body></html>"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let div = &body.children[0]; assert_eq!(div.style.box_sizing, BoxSizing::ContentBox); }
#[test] fn box_sizing_border_box_parsed() { let html_str = r#"<!DOCTYPE html><html><head><style>div { box-sizing: border-box; }</style></head><body><div>Test</div></body></html>"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let div = &body.children[0]; assert_eq!(div.style.box_sizing, BoxSizing::BorderBox); }
#[test] fn box_sizing_not_inherited() { let html_str = r#"<!DOCTYPE html><html><head><style>.parent { box-sizing: border-box; }</style></head><body><div class="parent"><p>Child</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let parent = &body.children[0]; let child = &parent.children[0]; assert_eq!(parent.style.box_sizing, BoxSizing::BorderBox); assert_eq!(child.style.box_sizing, BoxSizing::ContentBox); }}