Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032//! CSS value parsing: convert raw component values into typed property values.//!//! Provides `CssValue` enum and parsing from `ComponentValue` lists.
use crate::parser::ComponentValue;
// ---------------------------------------------------------------------------// Core value types// ---------------------------------------------------------------------------
/// A fully parsed, typed CSS value.#[derive(Debug, Clone, PartialEq)]pub enum CssValue { /// A length with resolved unit. Length(f64, LengthUnit), /// A percentage value. Percentage(f64), /// A color value (r, g, b, a in 0–255 range, alpha 0–255). Color(Color), /// A numeric value (unitless). Number(f64), /// A string value. String(String), /// A URL value from `url(...)`. Url(String), /// A parsed `linear-gradient(...)` image value. LinearGradient(LinearGradient), /// A parsed `radial-gradient(...)` image value. RadialGradient(RadialGradient), /// A keyword (ident). Keyword(String), /// The `auto` keyword. Auto, /// The `inherit` keyword. Inherit, /// The `initial` keyword. Initial, /// The `unset` keyword. Unset, /// The `currentColor` keyword. CurrentColor, /// The `none` keyword (for display, background, etc.). None, /// The `transparent` keyword. Transparent, /// Zero (unitless). Zero, /// A list of values (for multi-value properties like margin shorthand). List(Vec<CssValue>), /// A CSS math function expression: calc(), min(), max(), clamp(). Math(Box<MathExpr>), /// A parsed CSS transform scale() function. TransformScale(f64, f64), /// A parsed CSS transform translate*() function. TransformTranslate(TransformLength, TransformLength),}
/// One translate component in a CSS transform.#[derive(Debug, Clone, Copy, PartialEq)]pub enum TransformLength { Px(f64), Percent(f64),}
/// Direction of a parsed `linear-gradient(...)`.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum LinearGradientDirection { ToBottom, ToTop, ToRight, ToLeft, ToBottomRight, ToBottomLeft, ToTopRight, ToTopLeft,}
/// One color stop in a parsed `linear-gradient(...)`.#[derive(Debug, Clone, Copy, PartialEq)]pub struct GradientStop { pub color: Color, /// Stop position as a normalized fraction of the gradient line. pub position: Option<f32>,}
/// Minimal linear-gradient representation used by the renderer.#[derive(Debug, Clone, PartialEq)]pub struct LinearGradient { pub direction: LinearGradientDirection, pub stops: Vec<GradientStop>,}
/// Minimal radial-gradient representation used by the renderer.#[derive(Debug, Clone, PartialEq)]pub struct RadialGradient { pub stops: Vec<GradientStop>,}
// ---------------------------------------------------------------------------// CSS math expression AST (calc, min, max, clamp)// ---------------------------------------------------------------------------
/// A node in a CSS math expression tree.#[derive(Debug, Clone, PartialEq)]pub enum MathExpr { /// A literal length value. Length(f64, LengthUnit), /// A literal percentage value. Percentage(f64), /// A literal unitless number. Number(f64), /// Addition: `a + b`. Add(Box<MathExpr>, Box<MathExpr>), /// Subtraction: `a - b`. Sub(Box<MathExpr>, Box<MathExpr>), /// Multiplication: `a * b` (one operand should be a number). Mul(Box<MathExpr>, Box<MathExpr>), /// Division: `a / b` (divisor should be a number). Div(Box<MathExpr>, Box<MathExpr>), /// `min(a, b, ...)` — returns the smallest value. Min(Vec<MathExpr>), /// `max(a, b, ...)` — returns the largest value. Max(Vec<MathExpr>), /// `clamp(min, val, max)` — equivalent to `max(min, min(val, max))`. Clamp(Box<MathExpr>, Box<MathExpr>, Box<MathExpr>),}
/// CSS length unit.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum LengthUnit { // Absolute Px, Pt, Cm, Mm, In, Pc, // Font-relative Em, Rem, // Viewport Vw, Vh, Vmin, Vmax,}
/// A CSS color in RGBA format.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8,}
impl Color { pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } }
pub const fn rgb(r: u8, g: u8, b: u8) -> Self { Self { r, g, b, a: 255 } }}
// ---------------------------------------------------------------------------// Shorthand expansion result// ---------------------------------------------------------------------------
/// A longhand property-value pair produced by shorthand expansion.#[derive(Debug, Clone, PartialEq)]pub struct LonghandDeclaration { pub property: String, pub value: CssValue, pub important: bool,}
// ---------------------------------------------------------------------------// Value parsing// ---------------------------------------------------------------------------
/// Parse a single `CssValue` from a list of component values.pub fn parse_value(values: &[ComponentValue]) -> CssValue { // Filter out whitespace for easier matching let non_ws: Vec<&ComponentValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .collect();
if non_ws.is_empty() { return CssValue::Keyword(String::new()); }
// Single-value case if non_ws.len() == 1 { return parse_single_value(non_ws[0]); }
// Multi-value: parse each non-whitespace value let parsed: Vec<CssValue> = non_ws.iter().copied().map(parse_single_value).collect(); CssValue::List(parsed)}
/// Parse a single component value into a `CssValue`.pub fn parse_single_value(cv: &ComponentValue) -> CssValue { match cv { ComponentValue::Ident(s) => parse_keyword(s), ComponentValue::String(s) => CssValue::String(s.clone()), ComponentValue::Number(n, _) => { if *n == 0.0 { CssValue::Zero } else { CssValue::Number(*n) } } ComponentValue::Percentage(n) => CssValue::Percentage(*n), ComponentValue::Dimension(n, _, unit) => parse_dimension(*n, unit), ComponentValue::Hash(s, _) => parse_hex_color(s), ComponentValue::Function(name, args) => parse_function(name, args), ComponentValue::Comma => CssValue::Keyword(",".to_string()), ComponentValue::Delim(c) => CssValue::Keyword(c.to_string()), ComponentValue::Whitespace => CssValue::Keyword(" ".to_string()), }}
fn parse_keyword(s: &str) -> CssValue { match s.to_ascii_lowercase().as_str() { "auto" => CssValue::Auto, "inherit" => CssValue::Inherit, "initial" => CssValue::Initial, "unset" => CssValue::Unset, "none" => CssValue::None, "transparent" => CssValue::Transparent, "currentcolor" => CssValue::CurrentColor, // Named colors name => { if let Some(color) = named_color(name) { CssValue::Color(color) } else { CssValue::Keyword(s.to_ascii_lowercase()) } } }}
fn parse_dimension(n: f64, unit: &str) -> CssValue { let u = unit.to_ascii_lowercase(); match u.as_str() { "px" => CssValue::Length(n, LengthUnit::Px), "pt" => CssValue::Length(n, LengthUnit::Pt), "cm" => CssValue::Length(n, LengthUnit::Cm), "mm" => CssValue::Length(n, LengthUnit::Mm), "in" => CssValue::Length(n, LengthUnit::In), "pc" => CssValue::Length(n, LengthUnit::Pc), "em" => CssValue::Length(n, LengthUnit::Em), "rem" => CssValue::Length(n, LengthUnit::Rem), "vw" => CssValue::Length(n, LengthUnit::Vw), "vh" => CssValue::Length(n, LengthUnit::Vh), "vmin" => CssValue::Length(n, LengthUnit::Vmin), "vmax" => CssValue::Length(n, LengthUnit::Vmax), _ => CssValue::Keyword(format!("{n}{u}")), }}
// ---------------------------------------------------------------------------// Color parsing// ---------------------------------------------------------------------------
fn parse_hex_color(hex: &str) -> CssValue { let chars: Vec<char> = hex.chars().collect(); match chars.len() { // #rgb 3 => { let r = hex_digit(chars[0]) * 17; let g = hex_digit(chars[1]) * 17; let b = hex_digit(chars[2]) * 17; CssValue::Color(Color::rgb(r, g, b)) } // #rgba 4 => { let r = hex_digit(chars[0]) * 17; let g = hex_digit(chars[1]) * 17; let b = hex_digit(chars[2]) * 17; let a = hex_digit(chars[3]) * 17; CssValue::Color(Color::new(r, g, b, a)) } // #rrggbb 6 => { let r = hex_byte(chars[0], chars[1]); let g = hex_byte(chars[2], chars[3]); let b = hex_byte(chars[4], chars[5]); CssValue::Color(Color::rgb(r, g, b)) } // #rrggbbaa 8 => { let r = hex_byte(chars[0], chars[1]); let g = hex_byte(chars[2], chars[3]); let b = hex_byte(chars[4], chars[5]); let a = hex_byte(chars[6], chars[7]); CssValue::Color(Color::new(r, g, b, a)) } _ => CssValue::Keyword(format!("#{hex}")), }}
fn hex_digit(c: char) -> u8 { match c { '0'..='9' => c as u8 - b'0', 'a'..='f' => c as u8 - b'a' + 10, 'A'..='F' => c as u8 - b'A' + 10, _ => 0, }}
fn hex_byte(hi: char, lo: char) -> u8 { hex_digit(hi) * 16 + hex_digit(lo)}
fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { match name.to_ascii_lowercase().as_str() { "url" => parse_url(args), "rgb" => parse_rgb(args, false), "rgba" => parse_rgb(args, true), "linear-gradient" => parse_linear_gradient(args), "radial-gradient" => parse_radial_gradient(args), "hsl" => parse_hsl(args), "hsla" => parse_hsl(args), "calc" => parse_math_calc(args), "min" => parse_math_min(args), "max" => parse_math_max(args), "clamp" => parse_math_clamp(args), "scale" => parse_transform_scale(args), "translate" => parse_transform_translate(args), "translate3d" => parse_transform_translate3d(args), "translatex" => parse_transform_translate_axis(args, true), "translatey" => parse_transform_translate_axis(args, false), _ => CssValue::Keyword(format!("{name}()")), }}
fn parse_linear_gradient(args: &[ComponentValue]) -> CssValue { let mut groups: Vec<Vec<ComponentValue>> = vec![Vec::new()]; for arg in args { if matches!(arg, ComponentValue::Comma) { groups.push(Vec::new()); } else { groups.last_mut().unwrap().push(arg.clone()); } } groups.retain(|group| { group .iter() .any(|v| !matches!(v, ComponentValue::Whitespace)) });
let mut direction = LinearGradientDirection::ToBottom; if let Some(first) = groups.first() { if let Some(parsed_direction) = parse_linear_gradient_direction(first) { direction = parsed_direction; groups.remove(0); } }
let stops: Vec<GradientStop> = groups .iter() .filter_map(|group| parse_gradient_stop(group)) .collect(); if stops.len() < 2 { return CssValue::Keyword("linear-gradient()".to_string()); }
CssValue::LinearGradient(LinearGradient { direction, stops })}
fn parse_radial_gradient(args: &[ComponentValue]) -> CssValue { let mut groups: Vec<Vec<ComponentValue>> = vec![Vec::new()]; for arg in args { if matches!(arg, ComponentValue::Comma) { groups.push(Vec::new()); } else { groups.last_mut().unwrap().push(arg.clone()); } } groups.retain(|group| { group .iter() .any(|v| !matches!(v, ComponentValue::Whitespace)) });
if groups .first() .is_some_and(|group| parse_radial_gradient_shape(group)) { groups.remove(0); }
let stops: Vec<GradientStop> = groups .iter() .filter_map(|group| parse_gradient_stop(group)) .collect(); if stops.len() < 2 { return CssValue::Keyword("radial-gradient()".to_string()); }
CssValue::RadialGradient(RadialGradient { stops })}
fn parse_radial_gradient_shape(group: &[ComponentValue]) -> bool { let idents: Vec<String> = group .iter() .filter_map(|cv| match cv { ComponentValue::Ident(s) => Some(s.to_ascii_lowercase()), ComponentValue::Whitespace => None, _ => Some(String::new()), }) .collect();
idents.iter().any(|s| { matches!( s.as_str(), "circle" | "ellipse" | "closest-side" | "closest-corner" | "farthest-side" | "farthest-corner" ) })}
fn parse_linear_gradient_direction(group: &[ComponentValue]) -> Option<LinearGradientDirection> { let idents: Vec<String> = group .iter() .filter_map(|cv| match cv { ComponentValue::Ident(s) => Some(s.to_ascii_lowercase()), ComponentValue::Whitespace => None, _ => Some(String::new()), }) .collect(); if idents.first().map(String::as_str) != Some("to") { return None; }
let has_top = idents.iter().any(|s| s == "top"); let has_right = idents.iter().any(|s| s == "right"); let has_bottom = idents.iter().any(|s| s == "bottom"); let has_left = idents.iter().any(|s| s == "left"); match (has_top, has_right, has_bottom, has_left) { (true, true, false, false) => Some(LinearGradientDirection::ToTopRight), (true, false, false, true) => Some(LinearGradientDirection::ToTopLeft), (false, true, true, false) => Some(LinearGradientDirection::ToBottomRight), (false, false, true, true) => Some(LinearGradientDirection::ToBottomLeft), (true, false, false, false) => Some(LinearGradientDirection::ToTop), (false, true, false, false) => Some(LinearGradientDirection::ToRight), (false, false, true, false) => Some(LinearGradientDirection::ToBottom), (false, false, false, true) => Some(LinearGradientDirection::ToLeft), _ => None, }}
fn parse_gradient_stop(group: &[ComponentValue]) -> Option<GradientStop> { let non_ws: Vec<&ComponentValue> = group .iter() .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) .collect(); let first = non_ws.first()?; let color = match parse_single_value(first) { CssValue::Color(c) => c, CssValue::Transparent => Color::new(0, 0, 0, 0), CssValue::CurrentColor => return None, _ => return None, }; let position = non_ws.iter().skip(1).find_map(|cv| match cv { ComponentValue::Percentage(p) => Some((*p as f32) / 100.0), ComponentValue::Number(n, _) => Some(*n as f32), ComponentValue::Dimension(n, _, unit) if unit.eq_ignore_ascii_case("px") => Some(*n as f32), _ => None, });
Some(GradientStop { color, position })}
fn parse_transform_scale(args: &[ComponentValue]) -> CssValue { let values: Vec<f64> = args .iter() .filter_map(|arg| match arg { ComponentValue::Number(n, _) => Some(*n), ComponentValue::Percentage(n) => Some(*n / 100.0), ComponentValue::Whitespace | ComponentValue::Comma => None, _ => None, }) .collect();
match values.as_slice() { [x] => CssValue::TransformScale(*x, *x), [x, y] => CssValue::TransformScale(*x, *y), _ => CssValue::Keyword("scale()".to_string()), }}
fn parse_transform_translate(args: &[ComponentValue]) -> CssValue { let values: Vec<TransformLength> = args.iter().filter_map(parse_transform_length_arg).collect();
match values.as_slice() { [x] => CssValue::TransformTranslate(*x, TransformLength::Px(0.0)), [x, y] => CssValue::TransformTranslate(*x, *y), _ => CssValue::Keyword("translate()".to_string()), }}
fn parse_transform_translate3d(args: &[ComponentValue]) -> CssValue { let values: Vec<TransformLength> = args.iter().filter_map(parse_transform_length_arg).collect();
match values.as_slice() { [x, y, _z] => CssValue::TransformTranslate(*x, *y), _ => CssValue::Keyword("translate3d()".to_string()), }}
fn parse_transform_translate_axis(args: &[ComponentValue], x_axis: bool) -> CssValue { let values: Vec<TransformLength> = args.iter().filter_map(parse_transform_length_arg).collect();
match values.as_slice() { [value] if x_axis => CssValue::TransformTranslate(*value, TransformLength::Px(0.0)), [value] => CssValue::TransformTranslate(TransformLength::Px(0.0), *value), _ => CssValue::Keyword(if x_axis { "translateX()".to_string() } else { "translateY()".to_string() }), }}
fn parse_transform_length_arg(arg: &ComponentValue) -> Option<TransformLength> { match arg { ComponentValue::Number(n, _) if *n == 0.0 => Some(TransformLength::Px(0.0)), ComponentValue::Percentage(n) => Some(TransformLength::Percent(*n)), ComponentValue::Dimension(n, _, unit) if unit.eq_ignore_ascii_case("px") => { Some(TransformLength::Px(*n)) } ComponentValue::Whitespace | ComponentValue::Comma => None, _ => None, }}
fn parse_url(args: &[ComponentValue]) -> CssValue { for arg in args { match arg { ComponentValue::String(s) | ComponentValue::Ident(s) => { return CssValue::Url(s.clone()); } ComponentValue::Whitespace => {} _ => return CssValue::Keyword("url()".to_string()), } } CssValue::Keyword("url()".to_string())}
// ---------------------------------------------------------------------------// CSS math function parsing (calc, min, max, clamp)// ---------------------------------------------------------------------------
fn parse_math_calc(args: &[ComponentValue]) -> CssValue { let tokens = filter_math_tokens(args); match parse_sum(&tokens) { Some((expr, [])) => CssValue::Math(Box::new(expr)), _ => CssValue::Keyword("calc()".to_string()), }}
fn parse_math_min(args: &[ComponentValue]) -> CssValue { let exprs = parse_comma_separated_exprs(args); if exprs.is_empty() { return CssValue::Keyword("min()".to_string()); } CssValue::Math(Box::new(MathExpr::Min(exprs)))}
fn parse_math_max(args: &[ComponentValue]) -> CssValue { let exprs = parse_comma_separated_exprs(args); if exprs.is_empty() { return CssValue::Keyword("max()".to_string()); } CssValue::Math(Box::new(MathExpr::Max(exprs)))}
fn parse_math_clamp(args: &[ComponentValue]) -> CssValue { let exprs = parse_comma_separated_exprs(args); if exprs.len() != 3 { return CssValue::Keyword("clamp()".to_string()); } let mut iter = exprs.into_iter(); let min = iter.next().unwrap(); let val = iter.next().unwrap(); let max = iter.next().unwrap(); CssValue::Math(Box::new(MathExpr::Clamp( Box::new(min), Box::new(val), Box::new(max), )))}
/// Intermediate token for math expression parsing.#[derive(Debug, Clone)]enum MathToken { Number(f64), Dimension(f64, LengthUnit), Percentage(f64), Plus, Minus, Star, Slash, /// A pre-parsed nested math expression (e.g., nested min/max/clamp/calc). Nested(MathExpr),}
/// Convert ComponentValues into MathTokens, filtering whitespace but preserving/// it around +/- operators (whitespace is required around + and - per spec).fn filter_math_tokens(args: &[ComponentValue]) -> Vec<MathToken> { let mut tokens = Vec::new(); let mut i = 0; while i < args.len() { match &args[i] { ComponentValue::Number(n, _) => tokens.push(MathToken::Number(*n)), ComponentValue::Percentage(n) => tokens.push(MathToken::Percentage(*n)), ComponentValue::Dimension(n, _, unit) => { if let Some(lu) = parse_length_unit(unit) { tokens.push(MathToken::Dimension(*n, lu)); } } ComponentValue::Delim('+') => tokens.push(MathToken::Plus), ComponentValue::Delim('-') => tokens.push(MathToken::Minus), ComponentValue::Delim('*') => tokens.push(MathToken::Star), ComponentValue::Delim('/') => tokens.push(MathToken::Slash), ComponentValue::Function(name, inner_args) => { let lower = name.to_ascii_lowercase(); let nested = match lower.as_str() { "calc" => { let inner_tokens = filter_math_tokens(inner_args); parse_sum(&inner_tokens).and_then(|(e, r)| { if r.is_empty() { Some(e) } else { Option::None } }) } "min" => { let exprs = parse_comma_separated_exprs(inner_args); if exprs.is_empty() { Option::None } else { Some(MathExpr::Min(exprs)) } } "max" => { let exprs = parse_comma_separated_exprs(inner_args); if exprs.is_empty() { Option::None } else { Some(MathExpr::Max(exprs)) } } "clamp" => { let exprs = parse_comma_separated_exprs(inner_args); if exprs.len() == 3 { let mut it = exprs.into_iter(); Some(MathExpr::Clamp( Box::new(it.next().unwrap()), Box::new(it.next().unwrap()), Box::new(it.next().unwrap()), )) } else { Option::None } } _ => Option::None, }; if let Some(expr) = nested { tokens.push(MathToken::Nested(expr)); } } ComponentValue::Whitespace | ComponentValue::Comma => {} _ => {} } i += 1; } tokens}
fn parse_length_unit(unit: &str) -> Option<LengthUnit> { match unit.to_ascii_lowercase().as_str() { "px" => Some(LengthUnit::Px), "pt" => Some(LengthUnit::Pt), "cm" => Some(LengthUnit::Cm), "mm" => Some(LengthUnit::Mm), "in" => Some(LengthUnit::In), "pc" => Some(LengthUnit::Pc), "em" => Some(LengthUnit::Em), "rem" => Some(LengthUnit::Rem), "vw" => Some(LengthUnit::Vw), "vh" => Some(LengthUnit::Vh), "vmin" => Some(LengthUnit::Vmin), "vmax" => Some(LengthUnit::Vmax), _ => Option::None, }}
/// Parse comma-separated math expressions (for min, max, clamp arguments).fn parse_comma_separated_exprs(args: &[ComponentValue]) -> Vec<MathExpr> { // Split on commas at the top level, then parse each group as a sum expression. let mut groups: Vec<Vec<&ComponentValue>> = vec![vec![]]; for cv in args { if matches!(cv, ComponentValue::Comma) { groups.push(vec![]); } else { groups.last_mut().unwrap().push(cv); } }
let mut results = Vec::new(); for group in groups { let owned: Vec<ComponentValue> = group.into_iter().cloned().collect(); let tokens = filter_math_tokens(&owned); if let Some((expr, rest)) = parse_sum(&tokens) { if rest.is_empty() { results.push(expr); } } } results}
/// Parse an additive expression: `product (('+' | '-') product)*`./// Per spec, whitespace is required around `+` and `-`.fn parse_sum(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { let (mut left, mut rest) = parse_product(tokens)?; while !rest.is_empty() { match rest.first() { Some(MathToken::Plus) => { let (right, r) = parse_product(&rest[1..])?; left = MathExpr::Add(Box::new(left), Box::new(right)); rest = r; } Some(MathToken::Minus) => { let (right, r) = parse_product(&rest[1..])?; left = MathExpr::Sub(Box::new(left), Box::new(right)); rest = r; } _ => break, } } Some((left, rest))}
/// Parse a multiplicative expression: `atom (('*' | '/') atom)*`.fn parse_product(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { let (mut left, mut rest) = parse_atom(tokens)?; while !rest.is_empty() { match rest.first() { Some(MathToken::Star) => { let (right, r) = parse_atom(&rest[1..])?; left = MathExpr::Mul(Box::new(left), Box::new(right)); rest = r; } Some(MathToken::Slash) => { let (right, r) = parse_atom(&rest[1..])?; left = MathExpr::Div(Box::new(left), Box::new(right)); rest = r; } _ => break, } } Some((left, rest))}
/// Parse an atom: a number, dimension, percentage, parenthesized expression,/// or nested math function.fn parse_atom(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { let first = tokens.first()?; match first { MathToken::Number(n) => Some((MathExpr::Number(*n), &tokens[1..])), MathToken::Dimension(n, unit) => Some((MathExpr::Length(*n, *unit), &tokens[1..])), MathToken::Percentage(n) => Some((MathExpr::Percentage(*n), &tokens[1..])), MathToken::Nested(expr) => Some((expr.clone(), &tokens[1..])), // Handle unary minus: e.g. `-20px` tokenized as Minus followed by value. MathToken::Minus => { let (inner, rest) = parse_atom(&tokens[1..])?; let negated = MathExpr::Mul(Box::new(MathExpr::Number(-1.0)), Box::new(inner)); Some((negated, rest)) } _ => Option::None, }}
fn parse_rgb(args: &[ComponentValue], _has_alpha: bool) -> CssValue { let nums: Vec<f64> = args .iter() .filter_map(|cv| match cv { ComponentValue::Number(n, _) => Some(*n), ComponentValue::Percentage(n) => Some(*n * 2.55), _ => Option::None, }) .collect();
match nums.len() { 3 => CssValue::Color(Color::rgb( clamp_u8(nums[0]), clamp_u8(nums[1]), clamp_u8(nums[2]), )), 4 => { let a = if args .iter() .any(|cv| matches!(cv, ComponentValue::Percentage(_))) { // If any arg is a percentage, treat alpha as 0-1 float clamp_u8(nums[3] / 2.55 * 255.0) } else { // Check if alpha looks like a 0-1 range if nums[3] <= 1.0 { clamp_u8(nums[3] * 255.0) } else { clamp_u8(nums[3]) } }; CssValue::Color(Color::new( clamp_u8(nums[0]), clamp_u8(nums[1]), clamp_u8(nums[2]), a, )) } _ => CssValue::Keyword("rgb()".to_string()), }}
fn parse_hsl(args: &[ComponentValue]) -> CssValue { let mut values = args.iter().filter_map(|cv| match cv { ComponentValue::Number(n, _) => Some(HslComponent::Number(*n)), ComponentValue::Percentage(n) => Some(HslComponent::Percentage(*n)), ComponentValue::Dimension(n, _, unit) => { hue_dimension_to_degrees(*n, unit).map(HslComponent::Number) } _ => None, });
let hue = match values.next() { Some(HslComponent::Number(n)) => n, Some(HslComponent::Percentage(n)) => n * 3.6, None => return CssValue::Keyword("hsl()".to_string()), }; let saturation = match values.next() { Some(HslComponent::Percentage(n)) => (n / 100.0).clamp(0.0, 1.0), Some(HslComponent::Number(n)) => n.clamp(0.0, 1.0), None => return CssValue::Keyword("hsl()".to_string()), }; let lightness = match values.next() { Some(HslComponent::Percentage(n)) => (n / 100.0).clamp(0.0, 1.0), Some(HslComponent::Number(n)) => n.clamp(0.0, 1.0), None => return CssValue::Keyword("hsl()".to_string()), }; let alpha = match values.next() { Some(HslComponent::Percentage(n)) => clamp_u8(n / 100.0 * 255.0), Some(HslComponent::Number(n)) if n <= 1.0 => clamp_u8(n * 255.0), Some(HslComponent::Number(n)) => clamp_u8(n), None => 255, };
let (r, g, b) = hsl_to_rgb(hue, saturation, lightness); CssValue::Color(Color::new(r, g, b, alpha))}
#[derive(Debug, Clone, Copy)]enum HslComponent { Number(f64), Percentage(f64),}
fn hue_dimension_to_degrees(value: f64, unit: &str) -> Option<f64> { match unit.to_ascii_lowercase().as_str() { "deg" => Some(value), "grad" => Some(value * 0.9), "rad" => Some(value * 180.0 / std::f64::consts::PI), "turn" => Some(value * 360.0), _ => None, }}
fn hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> (u8, u8, u8) { let hue = hue.rem_euclid(360.0) / 360.0; if saturation == 0.0 { let gray = clamp_u8(lightness * 255.0); return (gray, gray, gray); }
let q = if lightness < 0.5 { lightness * (1.0 + saturation) } else { lightness + saturation - lightness * saturation }; let p = 2.0 * lightness - q;
( clamp_u8(hue_to_rgb(p, q, hue + 1.0 / 3.0) * 255.0), clamp_u8(hue_to_rgb(p, q, hue) * 255.0), clamp_u8(hue_to_rgb(p, q, hue - 1.0 / 3.0) * 255.0), )}
fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 { if t < 0.0 { t += 1.0; } if t > 1.0 { t -= 1.0; } if t < 1.0 / 6.0 { p + (q - p) * 6.0 * t } else if t < 1.0 / 2.0 { q } else if t < 2.0 / 3.0 { p + (q - p) * (2.0 / 3.0 - t) * 6.0 } else { p }}
fn clamp_u8(n: f64) -> u8 { n.round().clamp(0.0, 255.0) as u8}
// ---------------------------------------------------------------------------// Named colors (CSS Level 1 + transparent)// ---------------------------------------------------------------------------
pub fn named_color(name: &str) -> Option<Color> { Some(match name { "black" => Color::rgb(0, 0, 0), "silver" => Color::rgb(192, 192, 192), "gray" | "grey" => Color::rgb(128, 128, 128), "white" => Color::rgb(255, 255, 255), "maroon" => Color::rgb(128, 0, 0), "red" => Color::rgb(255, 0, 0), "purple" => Color::rgb(128, 0, 128), "fuchsia" | "magenta" => Color::rgb(255, 0, 255), "green" => Color::rgb(0, 128, 0), "lime" => Color::rgb(0, 255, 0), "olive" => Color::rgb(128, 128, 0), "yellow" => Color::rgb(255, 255, 0), "navy" => Color::rgb(0, 0, 128), "blue" => Color::rgb(0, 0, 255), "teal" => Color::rgb(0, 128, 128), "aqua" | "cyan" => Color::rgb(0, 255, 255), "orange" => Color::rgb(255, 165, 0), _ => return Option::None, })}
// ---------------------------------------------------------------------------// Canvas color string parsing// ---------------------------------------------------------------------------
/// Parse a CSS color string as used by the Canvas 2D API (`fillStyle`,/// `strokeStyle`). Supports `#rgb`, `#rrggbb`, `#rgba`, `#rrggbbaa`,/// `rgb()`, `rgba()`, `hsl()`, `hsla()`, named colors, and `transparent`.////// Returns `None` for invalid / unrecognised strings (per spec, the previous/// value should be kept).pub fn parse_color_string(s: &str) -> Option<Color> { let s = s.trim(); if s.is_empty() { return None; }
// "transparent" if s.eq_ignore_ascii_case("transparent") { return Some(Color::new(0, 0, 0, 0)); }
// Hex: #rgb, #rgba, #rrggbb, #rrggbbaa if let Some(hex) = s.strip_prefix('#') { // Validate all characters are hex digits before parsing. if !hex.chars().all(|c| c.is_ascii_hexdigit()) { return None; } return match parse_hex_color(hex) { CssValue::Color(c) => Some(c), _ => None, }; }
// rgb(...) / rgba(...) let lower = s.to_ascii_lowercase(); if lower.starts_with("rgb") { // Extract contents between parens if let Some(start) = s.find('(') { if let Some(end) = s.rfind(')') { if start < end { let inner = &s[start + 1..end]; let args = tokenize_color_args(inner); let result = parse_rgb(&args, lower.starts_with("rgba")); return match result { CssValue::Color(c) => Some(c), _ => None, }; } } } return None; }
// hsl(...) / hsla(...) if lower.starts_with("hsl") { if let Some(start) = s.find('(') { if let Some(end) = s.rfind(')') { if start < end { let inner = &s[start + 1..end]; let args = tokenize_color_args(inner); let result = parse_hsl(&args); return match result { CssValue::Color(c) => Some(c), _ => None, }; } } } return None; }
// Named colors named_color(&lower)}
/// Tokenize the inner arguments of CSS color functions into `ComponentValue`s.fn tokenize_color_args(s: &str) -> Vec<ComponentValue> { use crate::tokenizer::NumericType; let mut result = Vec::new(); for part in s.split(|c: char| c == ',' || c == '/' || c.is_ascii_whitespace()) { let part = part.trim(); if part.is_empty() { continue; } if let Some(pct) = part.strip_suffix('%') { if let Ok(n) = pct.parse::<f64>() { result.push(ComponentValue::Percentage(n)); continue; } } for unit in ["deg", "grad", "rad", "turn"] { if let Some(value) = part.strip_suffix(unit) { if let Ok(n) = value.parse::<f64>() { let nt = if value.contains('.') { NumericType::Number } else { NumericType::Integer }; result.push(ComponentValue::Dimension(n, nt, unit.to_string())); continue; } } } if let Ok(n) = part.parse::<f64>() { let nt = if part.contains('.') { NumericType::Number } else { NumericType::Integer }; result.push(ComponentValue::Number(n, nt)); continue; } } result}
// ---------------------------------------------------------------------------// Shorthand expansion// ---------------------------------------------------------------------------
/// Expand a CSS declaration into longhand declarations./// Returns `None` if the property is not a known shorthand.pub fn expand_shorthand( property: &str, values: &[ComponentValue], important: bool,) -> Option<Vec<LonghandDeclaration>> { match property { "margin" => Some(expand_box_shorthand("margin", values, important)), "padding" => Some(expand_box_shorthand("padding", values, important)), "border" => Some(expand_border(values, important)), "border-top" | "border-right" | "border-bottom" | "border-left" => { Some(expand_border_side(property, values, important)) } "border-width" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-width", d.property); d }) .collect(), ), "border-style" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-style", d.property); d }) .collect(), ), "border-color" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-color", d.property); d }) .collect(), ), "border-radius" => Some(expand_border_radius(values, important)), "background" => Some(expand_background(values, important)), "flex" => Some(expand_flex(values, important)), "flex-flow" => Some(expand_flex_flow(values, important)), "gap" => Some(expand_gap(values, important)), "grid-column" => Some(expand_grid_line("grid-column", values, important)), "grid-row" => Some(expand_grid_line("grid-row", values, important)), "grid-area" => Some(expand_grid_area(values, important)), _ => Option::None, }}
/// Expand the `border-radius` shorthand into the four per-corner longhands.////// Accepts 1–4 length/percentage values with the CSS corner order/// (top-left, top-right, bottom-right, bottom-left). Any elliptical/// `horizontal / vertical` form is reduced to its horizontal radii — we render/// circular corners only — by dropping everything from the `/` onward.fn expand_border_radius(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let horizontal: Vec<&ComponentValue> = values .iter() .take_while(|v| !matches!(v, ComponentValue::Delim('/'))) .collect(); let parsed: Vec<CssValue> = horizontal .into_iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect();
let (tl, tr, br, bl) = match parsed.len() { 1 => ( parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), ), 2 => ( parsed[0].clone(), parsed[1].clone(), parsed[0].clone(), parsed[1].clone(), ), 3 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[1].clone(), ), 4 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[3].clone(), ), _ => { let fallback = parsed.first().cloned().unwrap_or(CssValue::Zero); ( fallback.clone(), fallback.clone(), fallback.clone(), fallback, ) } };
vec![ LonghandDeclaration { property: "border-top-left-radius".to_string(), value: tl, important, }, LonghandDeclaration { property: "border-top-right-radius".to_string(), value: tr, important, }, LonghandDeclaration { property: "border-bottom-right-radius".to_string(), value: br, important, }, LonghandDeclaration { property: "border-bottom-left-radius".to_string(), value: bl, important, }, ]}
/// Expand the `flex` shorthand into `flex-grow`, `flex-shrink`, `flex-basis`.fn expand_flex(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect();
let (grow, shrink, basis) = match parsed.as_slice() { [CssValue::None] => (CssValue::Number(0.0), CssValue::Number(0.0), CssValue::Auto), [CssValue::Auto] => (CssValue::Number(1.0), CssValue::Number(1.0), CssValue::Auto), [CssValue::Number(g)] => (CssValue::Number(*g), CssValue::Number(1.0), CssValue::Zero), [CssValue::Zero] => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Zero), [CssValue::Number(g), CssValue::Number(s)] => { (CssValue::Number(*g), CssValue::Number(*s), CssValue::Zero) } [CssValue::Number(g), CssValue::Number(s), basis] => { (CssValue::Number(*g), CssValue::Number(*s), basis.clone()) } _ => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Auto), };
vec![ LonghandDeclaration { property: "flex-grow".to_string(), value: grow, important, }, LonghandDeclaration { property: "flex-shrink".to_string(), value: shrink, important, }, LonghandDeclaration { property: "flex-basis".to_string(), value: basis, important, }, ]}
/// Expand the `flex-flow` shorthand into `flex-direction` and `flex-wrap`.fn expand_flex_flow(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect();
let mut direction = CssValue::Keyword("row".to_string()); let mut wrap = CssValue::Keyword("nowrap".to_string());
for val in &parsed { if let CssValue::Keyword(k) = val { match k.as_str() { "row" | "row-reverse" | "column" | "column-reverse" => { direction = val.clone(); } "nowrap" | "wrap" | "wrap-reverse" => { wrap = val.clone(); } _ => {} } } }
vec![ LonghandDeclaration { property: "flex-direction".to_string(), value: direction, important, }, LonghandDeclaration { property: "flex-wrap".to_string(), value: wrap, important, }, ]}
/// Expand the `gap` shorthand into `row-gap` and `column-gap`.fn expand_gap(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect();
let (row, col) = match parsed.as_slice() { [single] => (single.clone(), single.clone()), [r, c] => (r.clone(), c.clone()), _ => (CssValue::Zero, CssValue::Zero), };
vec![ LonghandDeclaration { property: "row-gap".to_string(), value: row, important, }, LonghandDeclaration { property: "column-gap".to_string(), value: col, important, }, ]}
/// Expand `grid-column` or `grid-row` shorthand into start/end longhands.////// `grid-column: 1 / span 2` → grid-column-start: 1; grid-column-end: span 2;/// `grid-column: 2` → grid-column-start: 2; grid-column-end: auto;fn expand_grid_line( property: &str, values: &[ComponentValue], important: bool,) -> Vec<LonghandDeclaration> { let (start_prop, end_prop) = if property == "grid-column" { ("grid-column-start", "grid-column-end") } else { ("grid-row-start", "grid-row-end") };
let groups = split_at_slash(values); let start_value = parse_grid_line_group(&groups[0]); let end_value = if groups.len() > 1 { parse_grid_line_group(&groups[1]) } else { CssValue::Auto };
vec![ LonghandDeclaration { property: start_prop.to_string(), value: start_value, important, }, LonghandDeclaration { property: end_prop.to_string(), value: end_value, important, }, ]}
/// Expand `grid-area` shorthand into row-start, column-start, row-end, column-end.////// `grid-area: 1 / 2 / 3 / 4` → all four longhands./// Missing values default to `auto`.fn expand_grid_area(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let groups = split_at_slash(values); let row_start = parse_grid_line_group(&groups[0]); let col_start = if groups.len() > 1 { parse_grid_line_group(&groups[1]) } else { CssValue::Auto }; let row_end = if groups.len() > 2 { parse_grid_line_group(&groups[2]) } else { CssValue::Auto }; let col_end = if groups.len() > 3 { parse_grid_line_group(&groups[3]) } else { CssValue::Auto };
vec![ LonghandDeclaration { property: "grid-row-start".to_string(), value: row_start, important, }, LonghandDeclaration { property: "grid-column-start".to_string(), value: col_start, important, }, LonghandDeclaration { property: "grid-row-end".to_string(), value: row_end, important, }, LonghandDeclaration { property: "grid-column-end".to_string(), value: col_end, important, }, ]}
/// Split component values at `/` delimiters into groups.fn split_at_slash(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { let mut groups: Vec<Vec<&ComponentValue>> = vec![Vec::new()]; for cv in values { if matches!(cv, ComponentValue::Delim('/')) { groups.push(Vec::new()); } else { groups.last_mut().unwrap().push(cv); } } groups}
/// Parse a single grid line group (between slashes) into a CssValue.fn parse_grid_line_group(values: &[&ComponentValue]) -> CssValue { let non_ws: Vec<&&ComponentValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .collect();
if non_ws.is_empty() { return CssValue::Auto; } if non_ws.len() == 1 { return parse_single_value(non_ws[0]); } let parsed: Vec<CssValue> = non_ws.iter().map(|v| parse_single_value(v)).collect(); CssValue::List(parsed)}
/// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern.fn expand_box_shorthand( prefix: &str, values: &[ComponentValue], important: bool,) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect();
let (top, right, bottom, left) = match parsed.len() { 1 => ( parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), ), 2 => ( parsed[0].clone(), parsed[1].clone(), parsed[0].clone(), parsed[1].clone(), ), 3 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[1].clone(), ), 4 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[3].clone(), ), _ => { let fallback = if parsed.is_empty() { CssValue::Zero } else { parsed[0].clone() }; ( fallback.clone(), fallback.clone(), fallback.clone(), fallback, ) } };
vec![ LonghandDeclaration { property: format!("{prefix}-top"), value: top, important, }, LonghandDeclaration { property: format!("{prefix}-right"), value: right, important, }, LonghandDeclaration { property: format!("{prefix}-bottom"), value: bottom, important, }, LonghandDeclaration { property: format!("{prefix}-left"), value: left, important, }, ]}
/// Expand a side-specific border shorthand into width, style, and color.fn expand_border_side( property: &str, values: &[ComponentValue], important: bool,) -> Vec<LonghandDeclaration> { let side = property.strip_prefix("border-").unwrap_or(property); expand_border(values, important) .into_iter() .map(|mut d| { let suffix = d .property .strip_prefix("border-") .unwrap_or(d.property.as_str()); d.property = format!("border-{side}-{suffix}"); d }) .collect()}
/// Expand `border` shorthand into border-width, border-style, border-color.fn expand_border(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .map(parse_single_value) .collect();
let mut width = CssValue::Keyword("medium".to_string()); let mut style = CssValue::None; let mut color = CssValue::CurrentColor;
for val in &parsed { match val { CssValue::Length(_, _) | CssValue::Zero => width = val.clone(), CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { color = val.clone() } CssValue::Keyword(kw) => match kw.as_str() { "thin" | "medium" | "thick" => width = val.clone(), "none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset" => style = val.clone(), _ => { // Could be a named color if let Some(c) = named_color(kw) { color = CssValue::Color(c); } } }, _ => {} } }
vec![ LonghandDeclaration { property: "border-width".to_string(), value: width, important, }, LonghandDeclaration { property: "border-style".to_string(), value: style, important, }, LonghandDeclaration { property: "border-color".to_string(), value: color, important, }, ]}
/// Expand `background` shorthand (basic color and image support).fn expand_background(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { let parsed: Vec<CssValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .map(parse_single_value) .collect();
let mut bg_color = CssValue::Transparent; let mut bg_images = Vec::new();
for val in &parsed { match val { CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { bg_color = val.clone() } CssValue::Url(_) | CssValue::LinearGradient(_) | CssValue::RadialGradient(_) => { bg_images.push(val.clone()) } CssValue::Keyword(kw) => { if let Some(c) = named_color(kw) { bg_color = CssValue::Color(c); } else { match kw.as_str() { "," => {} "none" => bg_images.push(CssValue::None), _ => bg_color = val.clone(), } } } _ => {} } }
let mut declarations = vec![LonghandDeclaration { property: "background-color".to_string(), value: bg_color, important, }]; if !bg_images.is_empty() { let value = if bg_images.len() == 1 { bg_images.remove(0) } else { CssValue::List(bg_images) }; declarations.push(LonghandDeclaration { property: "background-image".to_string(), value, important, }); } declarations}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*; use crate::tokenizer::{HashType, NumericType};
// -- Length tests --------------------------------------------------------
#[test] fn test_parse_px() { let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "px".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(16.0, LengthUnit::Px) ); }
#[test] fn test_parse_em() { let cv = ComponentValue::Dimension(1.5, NumericType::Number, "em".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(1.5, LengthUnit::Em) ); }
#[test] fn test_parse_rem() { let cv = ComponentValue::Dimension(2.0, NumericType::Number, "rem".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(2.0, LengthUnit::Rem) ); }
#[test] fn test_parse_pt() { let cv = ComponentValue::Dimension(12.0, NumericType::Integer, "pt".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(12.0, LengthUnit::Pt) ); }
#[test] fn test_parse_cm() { let cv = ComponentValue::Dimension(2.54, NumericType::Number, "cm".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(2.54, LengthUnit::Cm) ); }
#[test] fn test_parse_mm() { let cv = ComponentValue::Dimension(10.0, NumericType::Integer, "mm".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(10.0, LengthUnit::Mm) ); }
#[test] fn test_parse_in() { let cv = ComponentValue::Dimension(1.0, NumericType::Integer, "in".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(1.0, LengthUnit::In) ); }
#[test] fn test_parse_pc() { let cv = ComponentValue::Dimension(6.0, NumericType::Integer, "pc".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(6.0, LengthUnit::Pc) ); }
#[test] fn test_parse_vw() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vw".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vw) ); }
#[test] fn test_parse_vh() { let cv = ComponentValue::Dimension(100.0, NumericType::Integer, "vh".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(100.0, LengthUnit::Vh) ); }
#[test] fn test_parse_vmin() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmin".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vmin) ); }
#[test] fn test_parse_vmax() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmax".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vmax) ); }
#[test] fn test_parse_percentage() { let cv = ComponentValue::Percentage(50.0); assert_eq!(parse_single_value(&cv), CssValue::Percentage(50.0)); }
#[test] fn test_parse_zero() { let cv = ComponentValue::Number(0.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Zero); }
#[test] fn test_parse_number() { let cv = ComponentValue::Number(42.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Number(42.0)); }
// -- Color tests --------------------------------------------------------
#[test] fn test_hex_color_3() { let cv = ComponentValue::Hash("f00".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); }
#[test] fn test_hex_color_4() { let cv = ComponentValue::Hash("f00a".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 0, 0, 170)) ); }
#[test] fn test_hex_color_6() { let cv = ComponentValue::Hash("ff8800".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 136, 0)) ); }
#[test] fn test_hex_color_8() { let cv = ComponentValue::Hash("ff880080".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 136, 0, 128)) ); }
#[test] fn test_named_color_red() { let cv = ComponentValue::Ident("red".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); }
#[test] fn test_named_color_blue() { let cv = ComponentValue::Ident("blue".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(0, 0, 255)) ); }
#[test] fn test_named_color_black() { let cv = ComponentValue::Ident("black".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(0, 0, 0)) ); }
#[test] fn test_named_color_white() { let cv = ComponentValue::Ident("white".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 255, 255)) ); }
#[test] fn test_transparent() { let cv = ComponentValue::Ident("transparent".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Transparent); }
#[test] fn test_current_color() { let cv = ComponentValue::Ident("currentColor".to_string()); assert_eq!(parse_single_value(&cv), CssValue::CurrentColor); }
#[test] fn test_rgb_function() { let args = vec![ ComponentValue::Number(255.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(128.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ]; let cv = ComponentValue::Function("rgb".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 128, 0)) ); }
#[test] fn test_rgba_function() { let args = vec![ ComponentValue::Number(255.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.5, NumericType::Number), ]; let cv = ComponentValue::Function("rgba".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 0, 0, 128)) ); }
#[test] fn test_hsl_function() { let args = vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Percentage(0.0), ComponentValue::Comma, ComponentValue::Percentage(89.0), ]; let cv = ComponentValue::Function("hsl".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(227, 227, 227)) ); }
#[test] fn test_hsla_function_with_angle_and_alpha() { let args = vec![ ComponentValue::Dimension(0.5, NumericType::Number, "turn".to_string()), ComponentValue::Whitespace, ComponentValue::Percentage(100.0), ComponentValue::Whitespace, ComponentValue::Percentage(50.0), ComponentValue::Whitespace, ComponentValue::Delim('/'), ComponentValue::Whitespace, ComponentValue::Number(0.25, NumericType::Number), ]; let cv = ComponentValue::Function("hsla".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(0, 255, 255, 64)) ); }
// -- Keyword tests ------------------------------------------------------
#[test] fn test_keyword_auto() { let cv = ComponentValue::Ident("auto".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Auto); }
#[test] fn test_keyword_inherit() { let cv = ComponentValue::Ident("inherit".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Inherit); }
#[test] fn test_keyword_initial() { let cv = ComponentValue::Ident("initial".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Initial); }
#[test] fn test_keyword_unset() { let cv = ComponentValue::Ident("unset".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Unset); }
#[test] fn test_keyword_none() { let cv = ComponentValue::Ident("none".to_string()); assert_eq!(parse_single_value(&cv), CssValue::None); }
#[test] fn test_keyword_display_block() { let cv = ComponentValue::Ident("block".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("block".to_string()) ); }
#[test] fn test_keyword_display_inline() { let cv = ComponentValue::Ident("inline".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("inline".to_string()) ); }
#[test] fn test_keyword_display_flex() { let cv = ComponentValue::Ident("flex".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("flex".to_string()) ); }
#[test] fn test_keyword_position() { for kw in &["static", "relative", "absolute", "fixed"] { let cv = ComponentValue::Ident(kw.to_string()); assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); } }
#[test] fn test_keyword_text_align() { for kw in &["left", "center", "right", "justify"] { let cv = ComponentValue::Ident(kw.to_string()); assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); } }
#[test] fn test_keyword_font_weight() { let cv = ComponentValue::Ident("bold".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("bold".to_string()) ); let cv = ComponentValue::Ident("normal".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("normal".to_string()) ); // Numeric font-weight let cv = ComponentValue::Number(700.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Number(700.0)); }
#[test] fn test_keyword_overflow() { for kw in &["visible", "hidden", "scroll", "auto"] { let cv = ComponentValue::Ident(kw.to_string()); let expected = match *kw { "auto" => CssValue::Auto, _ => CssValue::Keyword(kw.to_string()), }; assert_eq!(parse_single_value(&cv), expected); } }
// -- parse_value (multi-value) ------------------------------------------
#[test] fn test_parse_value_single() { let values = vec![ComponentValue::Ident("red".to_string())]; assert_eq!(parse_value(&values), CssValue::Color(Color::rgb(255, 0, 0))); }
#[test] fn test_parse_value_multi() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; assert_eq!( parse_value(&values), CssValue::List(vec![ CssValue::Length(10.0, LengthUnit::Px), CssValue::Length(20.0, LengthUnit::Px), ]) ); }
// -- Shorthand expansion tests ------------------------------------------
#[test] fn test_margin_one_value() { let values = vec![ComponentValue::Dimension( 10.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result.len(), 4); for decl in &result { assert_eq!(decl.value, CssValue::Length(10.0, LengthUnit::Px)); } assert_eq!(result[0].property, "margin-top"); assert_eq!(result[1].property, "margin-right"); assert_eq!(result[2].property, "margin-bottom"); assert_eq!(result[3].property, "margin-left"); }
#[test] fn test_margin_two_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(10.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left }
#[test] fn test_margin_three_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left }
#[test] fn test_margin_four_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(40.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(40.0, LengthUnit::Px)); // left }
#[test] fn test_margin_auto() { let values = vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Whitespace, ComponentValue::Ident("auto".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Zero); // top assert_eq!(result[1].value, CssValue::Auto); // right assert_eq!(result[2].value, CssValue::Zero); // bottom assert_eq!(result[3].value, CssValue::Auto); // left }
#[test] fn test_padding_shorthand() { let values = vec![ComponentValue::Dimension( 5.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("padding", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "padding-top"); assert_eq!(result[1].property, "padding-right"); assert_eq!(result[2].property, "padding-bottom"); assert_eq!(result[3].property, "padding-left"); }
#[test] fn test_border_shorthand() { let values = vec![ ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("solid".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("red".to_string()), ]; let result = expand_shorthand("border", &values, false).unwrap(); assert_eq!(result.len(), 3); assert_eq!(result[0].property, "border-width"); assert_eq!(result[0].value, CssValue::Length(1.0, LengthUnit::Px)); assert_eq!(result[1].property, "border-style"); assert_eq!(result[1].value, CssValue::Keyword("solid".to_string())); assert_eq!(result[2].property, "border-color"); assert_eq!(result[2].value, CssValue::Color(Color::rgb(255, 0, 0))); }
#[test] fn test_border_shorthand_defaults() { // Just a width let values = vec![ComponentValue::Dimension( 2.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("border", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(2.0, LengthUnit::Px)); assert_eq!(result[1].value, CssValue::None); // default style assert_eq!(result[2].value, CssValue::CurrentColor); // default color }
#[test] fn test_border_side_shorthand() { let values = vec![ ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("solid".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("red".to_string()), ]; let result = expand_shorthand("border-bottom", &values, false).unwrap();
assert_eq!(result.len(), 3); assert_eq!(result[0].property, "border-bottom-width"); assert_eq!(result[0].value, CssValue::Length(1.0, LengthUnit::Px)); assert_eq!(result[1].property, "border-bottom-style"); assert_eq!(result[1].value, CssValue::Keyword("solid".to_string())); assert_eq!(result[2].property, "border-bottom-color"); assert_eq!(result[2].value, CssValue::Color(Color::rgb(255, 0, 0))); }
#[test] fn test_background_shorthand_color() { let values = vec![ComponentValue::Hash("ff0000".to_string(), HashType::Id)]; let result = expand_shorthand("background", &values, false).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].property, "background-color"); assert_eq!(result[0].value, CssValue::Color(Color::rgb(255, 0, 0))); }
#[test] fn test_background_shorthand_image() { let values = vec![ComponentValue::Function( "url".to_string(), vec![ComponentValue::String("hero.jpg".to_string())], )]; let result = expand_shorthand("background", &values, false).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].property, "background-color"); assert_eq!(result[1].property, "background-image"); assert_eq!(result[1].value, CssValue::Url("hero.jpg".to_string())); }
#[test] fn test_parse_linear_gradient() { let cv = ComponentValue::Function( "linear-gradient".to_string(), vec![ ComponentValue::Ident("to".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("bottom".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("left".to_string()), ComponentValue::Comma, ComponentValue::Function( "rgba".to_string(), vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.6, NumericType::Number), ], ), ComponentValue::Whitespace, ComponentValue::Percentage(-20.0), ComponentValue::Comma, ComponentValue::Ident("transparent".to_string()), ComponentValue::Whitespace, ComponentValue::Percentage(30.0), ], );
assert_eq!( parse_single_value(&cv), CssValue::LinearGradient(LinearGradient { direction: LinearGradientDirection::ToBottomLeft, stops: vec![ GradientStop { color: Color::new(0, 0, 0, 153), position: Some(-0.2), }, GradientStop { color: Color::new(0, 0, 0, 0), position: Some(0.3), }, ], }) ); }
#[test] fn test_parse_radial_gradient() { let cv = ComponentValue::Function( "radial-gradient".to_string(), vec![ ComponentValue::Ident("circle".to_string()), ComponentValue::Comma, ComponentValue::Function( "rgba".to_string(), vec![ ComponentValue::Number(255.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(105.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(15.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.23, NumericType::Number), ], ), ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Function( "hsla".to_string(), vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Percentage(0.0), ComponentValue::Comma, ComponentValue::Percentage(100.0), ComponentValue::Comma, ComponentValue::Number(0.0, NumericType::Integer), ], ), ComponentValue::Whitespace, ComponentValue::Percentage(47.0), ], );
assert_eq!( parse_single_value(&cv), CssValue::RadialGradient(RadialGradient { stops: vec![ GradientStop { color: Color::new(255, 105, 15, 59), position: Some(0.0), }, GradientStop { color: Color::new(255, 255, 255, 0), position: Some(0.47), }, ], }) ); }
#[test] fn test_background_shorthand_gradient_image() { let values = vec![ComponentValue::Function( "linear-gradient".to_string(), vec![ ComponentValue::Function( "rgba".to_string(), vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Number(0.6, NumericType::Number), ], ), ComponentValue::Comma, ComponentValue::Ident("transparent".to_string()), ], )]; let result = expand_shorthand("background", &values, false).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].property, "background-color"); assert_eq!(result[1].property, "background-image"); assert!(matches!( result[1].value, CssValue::LinearGradient(LinearGradient { direction: LinearGradientDirection::ToBottom, .. }) )); }
#[test] fn test_background_shorthand_radial_gradient_image() { let values = vec![ComponentValue::Function( "radial-gradient".to_string(), vec![ ComponentValue::Ident("circle".to_string()), ComponentValue::Comma, ComponentValue::Ident("red".to_string()), ComponentValue::Comma, ComponentValue::Ident("transparent".to_string()), ], )]; let result = expand_shorthand("background", &values, false).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].property, "background-color"); assert_eq!(result[1].property, "background-image"); assert!(matches!( result[1].value, CssValue::RadialGradient(RadialGradient { .. }) )); }
#[test] fn test_non_shorthand_returns_none() { let values = vec![ComponentValue::Ident("red".to_string())]; assert!(expand_shorthand("color", &values, false).is_none()); }
#[test] fn test_important_propagated() { let values = vec![ComponentValue::Dimension( 10.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("margin", &values, true).unwrap(); for decl in &result { assert!(decl.important); } }
// -- Integration: parse from CSS text -----------------------------------
#[test] fn test_parse_from_css_text() { use crate::parser::Parser;
let ss = Parser::parse("p { color: red; margin: 10px 20px; }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), };
// color: red let color_val = parse_value(&rule.declarations[0].value); assert_eq!(color_val, CssValue::Color(Color::rgb(255, 0, 0)));
// margin: 10px 20px (multi-value) let margin_val = parse_value(&rule.declarations[1].value); assert_eq!( margin_val, CssValue::List(vec![ CssValue::Length(10.0, LengthUnit::Px), CssValue::Length(20.0, LengthUnit::Px), ]) ); }
#[test] fn test_parse_transform_translate3d_from_css_text() { let val = parse_value(&parse_cv("translate3d(-50%, -25%, 0)")); assert_eq!( val, CssValue::TransformTranslate( TransformLength::Percent(-50.0), TransformLength::Percent(-25.0), ) ); }
#[test] fn test_parse_transform_translate_axis_from_css_text() { let val = parse_value(&parse_cv("translateX(12px)")); assert_eq!( val, CssValue::TransformTranslate(TransformLength::Px(12.0), TransformLength::Px(0.0)) ); }
#[test] fn test_shorthand_from_css_text() { use crate::parser::Parser;
let ss = Parser::parse("div { margin: 10px 20px 30px 40px; }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), };
let longhands = expand_shorthand( &rule.declarations[0].property, &rule.declarations[0].value, rule.declarations[0].important, ) .unwrap();
assert_eq!(longhands[0].property, "margin-top"); assert_eq!(longhands[0].value, CssValue::Length(10.0, LengthUnit::Px)); assert_eq!(longhands[1].property, "margin-right"); assert_eq!(longhands[1].value, CssValue::Length(20.0, LengthUnit::Px)); assert_eq!(longhands[2].property, "margin-bottom"); assert_eq!(longhands[2].value, CssValue::Length(30.0, LengthUnit::Px)); assert_eq!(longhands[3].property, "margin-left"); assert_eq!(longhands[3].value, CssValue::Length(40.0, LengthUnit::Px)); }
#[test] fn test_case_insensitive_units() { let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "PX".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(16.0, LengthUnit::Px) ); }
#[test] fn test_case_insensitive_color_name() { let cv = ComponentValue::Ident("RED".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); }
#[test] fn test_case_insensitive_keywords() { let cv = ComponentValue::Ident("AUTO".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Auto);
let cv = ComponentValue::Ident("INHERIT".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Inherit); }
#[test] fn test_named_color_grey_alias() { let cv = ComponentValue::Ident("grey".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(128, 128, 128)) ); }
#[test] fn test_named_color_all_16_plus() { let colors = vec![ ("black", 0, 0, 0), ("silver", 192, 192, 192), ("gray", 128, 128, 128), ("white", 255, 255, 255), ("maroon", 128, 0, 0), ("red", 255, 0, 0), ("purple", 128, 0, 128), ("fuchsia", 255, 0, 255), ("green", 0, 128, 0), ("lime", 0, 255, 0), ("olive", 128, 128, 0), ("yellow", 255, 255, 0), ("navy", 0, 0, 128), ("blue", 0, 0, 255), ("teal", 0, 128, 128), ("aqua", 0, 255, 255), ("orange", 255, 165, 0), ]; for (name, r, g, b) in colors { let cv = ComponentValue::Ident(name.to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(r, g, b)), "failed for {name}" ); } }
#[test] fn test_border_width_shorthand() { let values = vec![ ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(2.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("border-width", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-width"); assert_eq!(result[1].property, "border-right-width"); }
#[test] fn test_border_style_shorthand() { let values = vec![ComponentValue::Ident("solid".to_string())]; let result = expand_shorthand("border-style", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-style"); }
#[test] fn test_border_color_shorthand() { let values = vec![ComponentValue::Ident("red".to_string())]; let result = expand_shorthand("border-color", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-color"); }
#[test] fn test_string_value() { let cv = ComponentValue::String("hello".to_string()); assert_eq!( parse_single_value(&cv), CssValue::String("hello".to_string()) ); }
// -- Math function parsing tests ------------------------------------------
#[test] fn test_calc_simple_px() { let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Delim('+'), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Add( Box::new(MathExpr::Length(10.0, LengthUnit::Px)), Box::new(MathExpr::Length(20.0, LengthUnit::Px)), ))) ); }
#[test] fn test_calc_percentage_minus_px() { let args = vec![ ComponentValue::Percentage(100.0), ComponentValue::Whitespace, ComponentValue::Delim('-'), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Sub( Box::new(MathExpr::Percentage(100.0)), Box::new(MathExpr::Length(20.0, LengthUnit::Px)), ))) ); }
#[test] fn test_calc_multiplication() { // calc(2 * 10px) let args = vec![ ComponentValue::Number(2.0, NumericType::Integer), ComponentValue::Whitespace, ComponentValue::Delim('*'), ComponentValue::Whitespace, ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Mul( Box::new(MathExpr::Number(2.0)), Box::new(MathExpr::Length(10.0, LengthUnit::Px)), ))) ); }
#[test] fn test_calc_division() { // calc(100px / 2) let args = vec![ ComponentValue::Dimension(100.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Delim('/'), ComponentValue::Whitespace, ComponentValue::Number(2.0, NumericType::Integer), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Div( Box::new(MathExpr::Length(100.0, LengthUnit::Px)), Box::new(MathExpr::Number(2.0)), ))) ); }
#[test] fn test_calc_operator_precedence() { // calc(10px + 2 * 5px) should be Add(10px, Mul(2, 5px)) let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Delim('+'), ComponentValue::Whitespace, ComponentValue::Number(2.0, NumericType::Integer), ComponentValue::Whitespace, ComponentValue::Delim('*'), ComponentValue::Whitespace, ComponentValue::Dimension(5.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Add( Box::new(MathExpr::Length(10.0, LengthUnit::Px)), Box::new(MathExpr::Mul( Box::new(MathExpr::Number(2.0)), Box::new(MathExpr::Length(5.0, LengthUnit::Px)), )), ))) ); }
#[test] fn test_min_two_args() { let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), ]; let cv = ComponentValue::Function("min".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Min(vec![ MathExpr::Length(10.0, LengthUnit::Px), MathExpr::Length(5.0, LengthUnit::Vw), ]))) ); }
#[test] fn test_max_two_args() { let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), ]; let cv = ComponentValue::Function("max".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Max(vec![ MathExpr::Length(10.0, LengthUnit::Px), MathExpr::Length(5.0, LengthUnit::Vw), ]))) ); }
#[test] fn test_clamp_three_args() { let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Percentage(50.0), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Dimension(200.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("clamp".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Clamp( Box::new(MathExpr::Length(10.0, LengthUnit::Px)), Box::new(MathExpr::Percentage(50.0)), Box::new(MathExpr::Length(200.0, LengthUnit::Px)), ))) ); }
#[test] fn test_calc_nested_min() { // calc(min(10px, 5vw) + 20px) let args = vec![ ComponentValue::Function( "min".to_string(), vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), ], ), ComponentValue::Whitespace, ComponentValue::Delim('+'), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Add( Box::new(MathExpr::Min(vec![ MathExpr::Length(10.0, LengthUnit::Px), MathExpr::Length(5.0, LengthUnit::Vw), ])), Box::new(MathExpr::Length(20.0, LengthUnit::Px)), ))) ); }
#[test] fn test_calc_mixed_units() { // calc(50% - 2em) let args = vec![ ComponentValue::Percentage(50.0), ComponentValue::Whitespace, ComponentValue::Delim('-'), ComponentValue::Whitespace, ComponentValue::Dimension(2.0, NumericType::Number, "em".to_string()), ]; let cv = ComponentValue::Function("calc".to_string(), args); let result = parse_single_value(&cv); assert_eq!( result, CssValue::Math(Box::new(MathExpr::Sub( Box::new(MathExpr::Percentage(50.0)), Box::new(MathExpr::Length(2.0, LengthUnit::Em)), ))) ); }
#[test] fn test_invalid_calc_empty() { let args = vec![]; let cv = ComponentValue::Function("calc".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Keyword("calc()".to_string()) ); }
#[test] fn test_invalid_clamp_too_few_args() { let args = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Comma, ComponentValue::Dimension(50.0, NumericType::Integer, "px".to_string()), ]; let cv = ComponentValue::Function("clamp".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Keyword("clamp()".to_string()) ); }
#[test] fn test_calc_from_css_text() { use crate::parser::Parser;
let ss = Parser::parse("div { width: calc(100% - 20px); }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; let val = parse_value(&rule.declarations[0].value); assert!(matches!(val, CssValue::Math(_))); }
#[test] fn test_min_from_css_text() { use crate::parser::Parser;
let ss = Parser::parse("div { width: min(10px, 5vw); }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; let val = parse_value(&rule.declarations[0].value); assert!(matches!(val, CssValue::Math(_))); }
#[test] fn test_clamp_from_css_text() { use crate::parser::Parser;
let ss = Parser::parse("div { width: clamp(10px, 50%, 200px); }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; let val = parse_value(&rule.declarations[0].value); assert!(matches!(val, CssValue::Math(_))); }
#[test] fn expand_grid_column_shorthand() { let longhands = expand_shorthand("grid-column", &parse_cv("1 / 3"), false).unwrap(); assert_eq!(longhands.len(), 2); assert_eq!(longhands[0].property, "grid-column-start"); assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 1.0)); assert_eq!(longhands[1].property, "grid-column-end"); assert!(matches!(longhands[1].value, CssValue::Number(n) if n == 3.0)); }
#[test] fn expand_grid_column_single_value() { let longhands = expand_shorthand("grid-column", &parse_cv("2"), false).unwrap(); assert_eq!(longhands[0].property, "grid-column-start"); assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 2.0)); assert_eq!(longhands[1].property, "grid-column-end"); assert_eq!(longhands[1].value, CssValue::Auto); }
#[test] fn expand_grid_row_shorthand() { let longhands = expand_shorthand("grid-row", &parse_cv("1 / 4"), false).unwrap(); assert_eq!(longhands.len(), 2); assert_eq!(longhands[0].property, "grid-row-start"); assert_eq!(longhands[1].property, "grid-row-end"); }
#[test] fn expand_grid_area_shorthand() { let longhands = expand_shorthand("grid-area", &parse_cv("1 / 2 / 3 / 4"), false).unwrap(); assert_eq!(longhands.len(), 4); assert_eq!(longhands[0].property, "grid-row-start"); assert_eq!(longhands[1].property, "grid-column-start"); assert_eq!(longhands[2].property, "grid-row-end"); assert_eq!(longhands[3].property, "grid-column-end"); }
/// Helper: parse a CSS value string into component values using the parser. fn parse_cv(input: &str) -> Vec<ComponentValue> { use crate::parser::Parser; let css = format!("x {{ p: {} }}", input); let ss = Parser::parse(&css); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; rule.declarations[0].value.clone() }
// -- Canvas color string parsing tests ----------------------------------
#[test] fn canvas_color_hex_rgb() { assert_eq!(parse_color_string("#ff0000"), Some(Color::rgb(255, 0, 0))); }
#[test] fn canvas_color_hex_short() { assert_eq!(parse_color_string("#f00"), Some(Color::rgb(255, 0, 0))); }
#[test] fn canvas_color_hex_rgba() { assert_eq!( parse_color_string("#ff000080"), Some(Color::new(255, 0, 0, 128)) ); }
#[test] fn canvas_color_hex_short_rgba() { assert_eq!( parse_color_string("#f008"), Some(Color::new(255, 0, 0, 136)) ); }
#[test] fn canvas_color_named_red() { assert_eq!(parse_color_string("red"), Some(Color::rgb(255, 0, 0))); }
#[test] fn canvas_color_named_blue() { assert_eq!(parse_color_string("blue"), Some(Color::rgb(0, 0, 255))); }
#[test] fn canvas_color_transparent() { assert_eq!( parse_color_string("transparent"), Some(Color::new(0, 0, 0, 0)) ); }
#[test] fn canvas_color_rgb_func() { assert_eq!( parse_color_string("rgb(128, 64, 32)"), Some(Color::rgb(128, 64, 32)) ); }
#[test] fn canvas_color_rgba_func() { let c = parse_color_string("rgba(255, 0, 0, 0.5)").unwrap(); assert_eq!(c.r, 255); assert_eq!(c.g, 0); assert_eq!(c.b, 0); assert_eq!(c.a, 128); }
#[test] fn canvas_color_hsl_func() { assert_eq!( parse_color_string("hsl(0, 0%, 89%)"), Some(Color::rgb(227, 227, 227)) ); }
#[test] fn canvas_color_hsla_func_with_slash_alpha() { assert_eq!( parse_color_string("hsla(0.5turn 100% 50% / 25%)"), Some(Color::new(0, 255, 255, 64)) ); }
#[test] fn canvas_color_invalid_returns_none() { assert_eq!(parse_color_string("notacolor"), None); assert_eq!(parse_color_string(""), None); assert_eq!(parse_color_string("#xyz"), None); }
#[test] fn canvas_color_case_insensitive() { assert_eq!(parse_color_string("RED"), Some(Color::rgb(255, 0, 0))); assert_eq!( parse_color_string("Transparent"), Some(Color::new(0, 0, 0, 0)) ); }
#[test] fn canvas_color_whitespace_trimmed() { assert_eq!(parse_color_string(" red "), Some(Color::rgb(255, 0, 0))); }}