use crate::css_native::ComponentValue;
use crate::css_queries::{
Comparison as MediaFeatureComparison, ContainerCondition as ContainerConditionExpr,
ContainerFeature as ContainerSizeFeatureExpr, ContainerFeatureName as ContainerSizeFeatureName,
ContainerValue as ContainerFeatureValue,
};
use crate::debug::{DebugLogger, json_escape};
use crate::flowable::FontRelativeLength;
use crate::flowable::{
BackgroundAttachment, BackgroundBox, BackgroundClipBox, BackgroundPaint,
BackgroundPositionComponent, BackgroundPositionSpec, BackgroundRepeatMode,
BackgroundRepeatSpec, BackgroundSizeMode, BackgroundSizeSpec, BorderCollapseMode,
BorderImageOutsetValue, BorderImageRepeatMode, BorderImageSliceValue, BorderImageSpec,
BorderImageWidthValue, BorderRadiiSpec, BorderRadiusSpec, BorderSpacingSpec,
BoxDecorationBreak, BoxShadowSpec, BreakAfter, BreakBefore, BreakInside, ClipPathCircleSpec,
ClipPathEllipseSpec, ClipPathInsetSpec, ClipPathPathCommand, ClipPathPathSpec,
ClipPathPolygonSpec, ClipPathRadiusSpec, ClipPathRectSpec, ClipPathReferenceBox,
ClipPathShapeControlAnchor, ClipPathShapeFunctionCommand, ClipPathShapeFunctionSpec,
ClipPathShapeRadius, ClipPathShapeSpec, ClipPathXywhSpec, CssTransformOp, CssTransformOrigin,
EdgeSizes, FilterDropShadowSpec, GradientPosition, GradientRepeat, GradientStopPosition,
GridTrackBreadth, GridTrackSize, LengthSpec, MaskComposite, MaskMode, MaskSpec,
OutlineLineStyle, Pagination, PaintFilterOperation, PaintFilterSpec, RadialGradientShape,
RadialGradientSize, TabSizeSpec, TableLayoutMode, TextStyle, TransformBoxMode,
TransformStyleMode,
};
use crate::flowable::{CalcLength, ClampedLength};
use crate::font::FontRegistry;
use crate::svg;
use crate::types::{
BoxSizingMode, Color, I32F32, Margins, MixBlendMode, PageMarks, PageOrientation,
PagePresentation, Pt, ShadingStop, Size,
};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Specificity(u16, u16, u16);
#[derive(Debug, Clone)]
struct SimpleSelector {
tag: Option<String>,
id: Option<String>,
classes: Vec<String>,
attrs: Vec<AttrSelector>,
pseudos: Vec<PseudoClass>,
}
impl SimpleSelector {
fn matches_with_pseudo(&self, element: &ElementInfo, pseudo: PseudoTarget) -> bool {
if let Some(tag) = &self.tag {
if tag != "*" && tag != &element.tag {
return false;
}
}
if let Some(id) = &self.id {
if element.id.as_deref() != Some(id.as_str()) {
return false;
}
}
for class in &self.classes {
if !element.classes.iter().any(|c| c == class) {
return false;
}
}
for attr in &self.attrs {
if !attr.matches(element) {
return false;
}
}
for pseudo_class in &self.pseudos {
if !pseudo_class.matches_with_pseudo(element, pseudo) {
return false;
}
}
true
}
fn specificity(&self) -> Specificity {
let mut id_count = self.id.as_ref().map(|_| 1).unwrap_or(0);
let mut class_count = (self.classes.len() + self.attrs.len()) as u16;
let mut tag_count = self
.tag
.as_ref()
.filter(|tag| tag.as_str() != "*")
.map(|_| 1)
.unwrap_or(0);
for pseudo in &self.pseudos {
match pseudo {
PseudoClass::Not(inner) | PseudoClass::Is(inner) => {
let inner_spec = inner
.iter()
.map(SimpleSelector::specificity)
.max()
.unwrap_or(Specificity(0, 0, 0));
id_count += inner_spec.0;
class_count += inner_spec.1;
tag_count += inner_spec.2;
}
PseudoClass::Has(relative) => {
let inner_spec = relative
.iter()
.map(|selector| selector.selector.specificity())
.max()
.unwrap_or(Specificity(0, 0, 0));
id_count += inner_spec.0;
class_count += inner_spec.1;
tag_count += inner_spec.2;
}
PseudoClass::NthChildOf { selectors, .. } => {
class_count += 1;
let inner_spec = selectors
.iter()
.map(SimpleSelector::specificity)
.max()
.unwrap_or(Specificity(0, 0, 0));
id_count += inner_spec.0;
class_count += inner_spec.1;
tag_count += inner_spec.2;
}
// Selectors inside :where() intentionally contribute no specificity.
PseudoClass::Where(_) => {}
_ => {
class_count += 1;
}
}
}
Specificity(id_count, class_count, tag_count)
}
}
#[derive(Debug, Clone)]
enum PseudoClass {
Root,
Scope,
Empty,
Defined,
Direction(String),
Language(String),
FirstChild,
LastChild,
OnlyChild,
FirstOfType,
LastOfType,
OnlyOfType,
NthChildEven,
NthChildOdd,
NthChild(usize),
NthChildFormula {
a: i32,
b: i32,
},
NthChildOf {
a: i32,
b: i32,
selectors: Vec<SimpleSelector>,
},
NthLastChildFormula {
a: i32,
b: i32,
},
NthOfTypeEven,
NthOfTypeOdd,
NthOfType(usize),
NthOfTypeFormula {
a: i32,
b: i32,
},
NthLastOfTypeFormula {
a: i32,
b: i32,
},
Hover,
Before,
After,
Marker,
FootnoteCall,
FootnoteMarker,
FirstLetter,
FirstLine,
Unsupported,
Not(Vec<SimpleSelector>),
Is(Vec<SimpleSelector>),
Where(Vec<SimpleSelector>),
Has(Vec<RelativeSelector>),
}
#[derive(Debug, Clone)]
struct RelativeSelector {
combinator: Combinator,
selector: SimpleSelector,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PseudoTarget {
None,
Before,
After,
Marker,
FootnoteCall,
FootnoteMarker,
FirstLetter,
FirstLine,
}
impl PseudoClass {
fn is_positional(&self) -> bool {
matches!(
self,
PseudoClass::FirstChild
| PseudoClass::LastChild
| PseudoClass::OnlyChild
| PseudoClass::FirstOfType
| PseudoClass::LastOfType
| PseudoClass::OnlyOfType
| PseudoClass::NthChildEven
| PseudoClass::NthChildOdd
| PseudoClass::NthChild(_)
| PseudoClass::NthChildFormula { .. }
| PseudoClass::NthChildOf { .. }
| PseudoClass::NthLastChildFormula { .. }
| PseudoClass::NthOfTypeEven
| PseudoClass::NthOfTypeOdd
| PseudoClass::NthOfType(_)
| PseudoClass::NthOfTypeFormula { .. }
| PseudoClass::NthLastOfTypeFormula { .. }
)
}
fn matches_with_pseudo(&self, element: &ElementInfo, pseudo: PseudoTarget) -> bool {
match self {
PseudoClass::Root => element.is_root,
PseudoClass::Scope => element.is_root,
PseudoClass::Empty => element.is_empty,
PseudoClass::Defined => element.is_defined,
PseudoClass::Direction(direction) => element
.direction
.as_deref()
.is_some_and(|value| value.eq_ignore_ascii_case(direction)),
PseudoClass::Language(language) => element.language.as_deref().is_some_and(|value| {
value.eq_ignore_ascii_case(language)
|| value
.get(language.len()..)
.is_some_and(|suffix| suffix.starts_with('-'))
&& value
.get(..language.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(language))
}),
PseudoClass::FirstChild => element.child_index == 1,
PseudoClass::LastChild => element.child_index == element.child_count,
PseudoClass::OnlyChild => element.child_count == 1,
PseudoClass::FirstOfType => element.type_index == 1,
PseudoClass::LastOfType => element.type_index == element.type_count,
PseudoClass::OnlyOfType => element.type_count == 1,
PseudoClass::NthChildEven => element.child_index % 2 == 0,
PseudoClass::NthChildOdd => element.child_index % 2 == 1,
PseudoClass::NthChild(n) => element.child_index == *n,
PseudoClass::NthChildFormula { a, b } => nth_index_matches(element.child_index, *a, *b),
PseudoClass::NthChildOf { a, b, selectors } => {
if !selectors
.iter()
.any(|selector| selector.matches_with_pseudo(element, PseudoTarget::None))
{
false
} else {
let index = 1 + element
.prev_siblings
.iter()
.filter(|sibling| {
selectors.iter().any(|selector| {
selector.matches_with_pseudo(sibling, PseudoTarget::None)
})
})
.count();
nth_index_matches(index, *a, *b)
}
}
PseudoClass::NthLastChildFormula { a, b } => nth_index_matches(
element
.child_count
.saturating_add(1)
.saturating_sub(element.child_index),
*a,
*b,
),
PseudoClass::NthOfTypeEven => element.type_index % 2 == 0,
PseudoClass::NthOfTypeOdd => element.type_index % 2 == 1,
PseudoClass::NthOfType(n) => element.type_index == *n,
PseudoClass::NthOfTypeFormula { a, b } => nth_index_matches(element.type_index, *a, *b),
PseudoClass::NthLastOfTypeFormula { a, b } => nth_index_matches(
element
.type_count
.saturating_add(1)
.saturating_sub(element.type_index),
*a,
*b,
),
PseudoClass::Hover => false,
PseudoClass::Before => matches!(pseudo, PseudoTarget::Before),
PseudoClass::After => matches!(pseudo, PseudoTarget::After),
PseudoClass::Marker => matches!(pseudo, PseudoTarget::Marker),
PseudoClass::FootnoteCall => matches!(pseudo, PseudoTarget::FootnoteCall),
PseudoClass::FootnoteMarker => matches!(pseudo, PseudoTarget::FootnoteMarker),
PseudoClass::FirstLetter => matches!(pseudo, PseudoTarget::FirstLetter),
PseudoClass::FirstLine => matches!(pseudo, PseudoTarget::FirstLine),
PseudoClass::Unsupported => false,
PseudoClass::Not(selectors) => !selectors
.iter()
.any(|selector| selector.matches_with_pseudo(element, pseudo)),
PseudoClass::Is(selectors) | PseudoClass::Where(selectors) => selectors
.iter()
.any(|selector| selector.matches_with_pseudo(element, pseudo)),
PseudoClass::Has(selectors) => {
selectors.iter().any(|selector| selector.matches(element))
}
}
}
fn has_positional_pseudo(&self) -> bool {
if self.is_positional() {
return true;
}
match self {
PseudoClass::Not(selectors)
| PseudoClass::Is(selectors)
| PseudoClass::Where(selectors) => selectors.iter().any(|selector| {
selector
.pseudos
.iter()
.any(PseudoClass::has_positional_pseudo)
}),
_ => false,
}
}
fn has_relational_pseudo(&self) -> bool {
match self {
PseudoClass::Has(_) | PseudoClass::NthChildOf { .. } => true,
PseudoClass::Not(selectors)
| PseudoClass::Is(selectors)
| PseudoClass::Where(selectors) => selectors.iter().any(|selector| {
selector
.pseudos
.iter()
.any(PseudoClass::has_relational_pseudo)
}),
_ => false,
}
}
fn pseudo_target(&self) -> Option<PseudoTarget> {
match self {
PseudoClass::Before => Some(PseudoTarget::Before),
PseudoClass::After => Some(PseudoTarget::After),
PseudoClass::Marker => Some(PseudoTarget::Marker),
PseudoClass::FootnoteCall => Some(PseudoTarget::FootnoteCall),
PseudoClass::FootnoteMarker => Some(PseudoTarget::FootnoteMarker),
PseudoClass::FirstLetter => Some(PseudoTarget::FirstLetter),
PseudoClass::FirstLine => Some(PseudoTarget::FirstLine),
PseudoClass::Not(selectors)
| PseudoClass::Is(selectors)
| PseudoClass::Where(selectors) => selectors
.iter()
.find_map(|selector| selector.pseudos.iter().find_map(PseudoClass::pseudo_target)),
_ => None,
}
}
}
fn nth_index_matches(index: usize, a: i32, b: i32) -> bool {
let index = index as i32;
if a == 0 {
return index == b;
}
if a > 0 {
index >= b && (index - b) % a == 0
} else {
index <= b && (b - index) % (-a) == 0
}
}
#[derive(Debug, Clone)]
struct AttrSelector {
name: String,
op: AttrOp,
value: Option<String>,
case_insensitive: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AttrOp {
Exists,
Equals,
Includes,
DashMatch,
Prefix,
Suffix,
Substring,
}
impl AttrSelector {
fn matches(&self, element: &ElementInfo) -> bool {
let Some(value) = element.attrs.get(&self.name) else {
return false;
};
let matches_value = |candidate: &str, expected: &str| {
if self.case_insensitive {
candidate.eq_ignore_ascii_case(expected)
} else {
candidate == expected
}
};
match self.op {
AttrOp::Exists => true,
AttrOp::Equals => self
.value
.as_deref()
.map(|expected| matches_value(value, expected))
.unwrap_or(false),
AttrOp::Includes => self
.value
.as_deref()
.map(|expected| {
value
.split_whitespace()
.any(|part| matches_value(part, expected))
})
.unwrap_or(false),
AttrOp::DashMatch => self
.value
.as_deref()
.map(|expected| {
matches_value(value, expected)
|| value
.get(..expected.len())
.is_some_and(|prefix| matches_value(prefix, expected))
&& value
.get(expected.len()..)
.is_some_and(|suffix| suffix.starts_with('-'))
})
.unwrap_or(false),
AttrOp::Prefix => self
.value
.as_deref()
.map(|expected| {
value
.get(..expected.len())
.is_some_and(|prefix| matches_value(prefix, expected))
})
.unwrap_or(false),
AttrOp::Suffix => self
.value
.as_deref()
.map(|expected| {
value
.get(value.len().saturating_sub(expected.len())..)
.is_some_and(|suffix| matches_value(suffix, expected))
})
.unwrap_or(false),
AttrOp::Substring => self
.value
.as_deref()
.map(|expected| {
if self.case_insensitive {
value
.to_ascii_lowercase()
.contains(&expected.to_ascii_lowercase())
} else {
value.contains(expected)
}
})
.unwrap_or(false),
}
}
}
#[derive(Debug, Clone)]
enum Combinator {
Descendant,
Child,
AdjacentSibling,
GeneralSibling,
}
impl RelativeSelector {
fn matches(&self, element: &ElementInfo) -> bool {
match self.combinator {
Combinator::Descendant => element.children.iter().any(|child| {
self.selector.matches_with_pseudo(child, PseudoTarget::None) || self.matches(child)
}),
Combinator::Child => element
.children
.iter()
.any(|child| self.selector.matches_with_pseudo(child, PseudoTarget::None)),
Combinator::AdjacentSibling => element.next_siblings.first().is_some_and(|sibling| {
self.selector
.matches_with_pseudo(sibling, PseudoTarget::None)
}),
Combinator::GeneralSibling => element.next_siblings.iter().any(|sibling| {
self.selector
.matches_with_pseudo(sibling, PseudoTarget::None)
}),
}
}
}
#[derive(Debug, Clone)]
struct SelectorPattern {
parts: Vec<SimpleSelector>,
combinators: Vec<Combinator>,
}
impl SelectorPattern {
fn has_positional_pseudos(&self) -> bool {
self.parts
.iter()
.any(|part| part.pseudos.iter().any(PseudoClass::has_positional_pseudo))
}
fn has_sibling_combinators(&self) -> bool {
self.combinators.iter().any(|comb| {
matches!(
comb,
Combinator::AdjacentSibling | Combinator::GeneralSibling
)
})
}
fn has_relational_pseudos(&self) -> bool {
self.parts
.iter()
.any(|part| part.pseudos.iter().any(PseudoClass::has_relational_pseudo))
}
fn pseudo_target(&self) -> Option<PseudoTarget> {
for part in &self.parts {
for pseudo in &part.pseudos {
if let Some(target) = pseudo.pseudo_target() {
return Some(target);
}
}
}
None
}
fn matches(&self, element: &ElementInfo, ancestors: &[ElementInfo]) -> bool {
self.matches_with_pseudo(element, ancestors, PseudoTarget::None)
}
fn matches_with_pseudo(
&self,
element: &ElementInfo,
ancestors: &[ElementInfo],
pseudo: PseudoTarget,
) -> bool {
if self.parts.is_empty() {
return false;
}
let last = self.parts.last().expect("non-empty");
if !last.matches_with_pseudo(element, pseudo) {
return false;
}
if self.parts.len() == 1 {
return true;
}
let mut current: &ElementInfo = element;
let mut current_index = ancestors.len();
for (idx, part) in self.parts[..self.parts.len() - 1].iter().rev().enumerate() {
let comb_index = self.combinators.len().saturating_sub(1 + idx);
let combinator = self
.combinators
.get(comb_index)
.unwrap_or(&Combinator::Descendant);
match combinator {
Combinator::Child => {
if current_index == 0 {
return false;
}
current_index -= 1;
current = &ancestors[current_index];
if !part.matches_with_pseudo(current, pseudo) {
return false;
}
}
Combinator::Descendant => {
let mut found = false;
while current_index > 0 {
current_index -= 1;
let ancestor = &ancestors[current_index];
if part.matches_with_pseudo(ancestor, pseudo) {
current = ancestor;
found = true;
break;
}
}
if !found {
return false;
}
}
Combinator::AdjacentSibling => {
let Some(prev) = current.prev_siblings.last() else {
return false;
};
current = prev;
if !part.matches_with_pseudo(current, pseudo) {
return false;
}
}
Combinator::GeneralSibling => {
let mut found = false;
for prev in current.prev_siblings.iter().rev() {
if part.matches_with_pseudo(prev, pseudo) {
current = prev;
found = true;
break;
}
}
if !found {
return false;
}
}
}
}
true
}
fn specificity(&self) -> Specificity {
let mut spec = Specificity(0, 0, 0);
for part in &self.parts {
let part_spec = part.specificity();
spec.0 += part_spec.0;
spec.1 += part_spec.1;
spec.2 += part_spec.2;
}
spec
}
}
#[derive(Debug, Clone)]
struct RuleEntry {
selector: SelectorPattern,
specificity: Specificity,
order: usize,
layer_order: Option<usize>,
container_conditions: Vec<ContainerRuleCondition>,
scope_conditions: Vec<ScopeRuleCondition>,
delta: StyleDelta,
selector_text: String,
}
impl RuleEntry {
fn normal_layer_rank(&self) -> usize {
self.layer_order.unwrap_or(usize::MAX)
}
fn important_layer_rank(&self, layer_count: usize) -> usize {
match self.layer_order {
Some(order) => layer_count.saturating_sub(order),
None => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainerQueryType {
Normal,
InlineSize,
Size,
ScrollState,
}
#[derive(Debug, Clone, PartialEq)]
struct ContainerRuleCondition {
name: Option<String>,
condition: Option<ContainerConditionExpr>,
}
#[derive(Debug, Clone)]
struct ScopeRuleCondition {
starts: Vec<SelectorPattern>,
ends: Vec<SelectorPattern>,
}
#[derive(Debug, Clone, Copy)]
struct FontCalcLength {
abs: Pt,
em: I32F32,
rem: I32F32,
vw: I32F32,
vh: I32F32,
vmin: I32F32,
vmax: I32F32,
}
impl FontCalcLength {
fn resolve(self, parent_font_size: Pt, root_font_size: Pt, viewport: Size) -> Pt {
let vw = viewport.width.mul_fixed(self.vw);
let vh = viewport.height.mul_fixed(self.vh);
let vmin = viewport.width.min(viewport.height).mul_fixed(self.vmin);
let vmax = viewport.width.max(viewport.height).mul_fixed(self.vmax);
self.abs
+ parent_font_size.mul_fixed(self.em)
+ root_font_size.mul_fixed(self.rem)
+ vw
+ vh
+ vmin
+ vmax
}
}
#[derive(Debug, Clone)]
enum FontSizeSpec {
AbsolutePt(Pt),
RelativeScale(I32F32),
Calc(FontCalcLength),
FontRelative(FontRelativeLength),
Inherit,
Initial,
}
#[derive(Debug, Clone)]
enum LineHeightSpec {
Normal,
Number(f32),
AbsolutePt(Pt),
RelativeScale(I32F32),
Calc(FontCalcLength),
Inherit,
Initial,
}
impl LineHeightSpec {
fn to_line_height(&self, font_size: Pt) -> Pt {
let value = match self {
LineHeightSpec::Normal => font_size.mul_ratio(6, 5),
LineHeightSpec::Number(scale) => font_size * *scale,
LineHeightSpec::AbsolutePt(value) => *value,
LineHeightSpec::RelativeScale(scale) => font_size.mul_fixed(*scale),
LineHeightSpec::Calc(calc) => calc.resolve(
font_size,
font_size,
Size {
width: Pt::ZERO,
height: Pt::ZERO,
},
),
LineHeightSpec::Inherit | LineHeightSpec::Initial => font_size.mul_ratio(6, 5),
};
value
}
}
#[derive(Debug, Clone)]
enum ColorSpec {
Value(Color),
AlphaValue(Color, f32),
Inherit,
Initial,
CurrentColor,
}
#[derive(Debug, Clone)]
enum BackgroundSpec {
Value(Color, f32),
Transparent,
Inherit,
Initial,
CurrentColor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum FontSpec {
Value(Vec<Arc<str>>),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FontWeightSpec {
Value(u16),
Inherit,
Initial,
Bolder,
Lighter,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum FontSizeAdjustSpec {
None,
Number(I32F32),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FontFeatureToggleSpec {
Value(bool),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FontStretchSpec {
Value(u16),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WhiteSpaceMode {
Normal,
Pre,
NoWrap,
PreWrap,
BreakSpaces,
PreLine,
PreserveSpaces,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextWrapMode {
Wrap,
NoWrap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextWrapStyleMode {
Auto,
Balance,
Stable,
Pretty,
AvoidOrphans,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlignMode {
Start,
End,
MatchParent,
Left,
Center,
Right,
Justify,
JustifyAll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlignLastMode {
Auto,
Start,
End,
Left,
Center,
Right,
Justify,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextJustifyMode {
Auto,
None,
InterWord,
InterCharacter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HyphensMode {
None,
Manual,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextAlignLastSpec {
Value(TextAlignLastMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextJustifySpec {
Value(TextJustifyMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextTransformSpec {
Value(TextTransformMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HyphensSpec {
Value(HyphensMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HyphenateCharacterSpec {
Auto,
Value(String),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectionMode {
Ltr,
Rtl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnicodeBidiMode {
Normal,
Embed,
Isolate,
BidiOverride,
IsolateOverride,
Plaintext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UnicodeBidiSpec {
Value(UnicodeBidiMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WritingModeMode {
HorizontalTb,
VerticalRl,
VerticalLr,
SidewaysRl,
SidewaysLr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextOrientationMode {
Mixed,
Upright,
Sideways,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VerticalAlignMode {
Baseline,
Sub,
Super,
TextTop,
Top,
Middle,
TextBottom,
Bottom,
Length(LengthSpec),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontStyleMode {
Normal,
Italic,
Oblique(i16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextTransformMode {
None,
Uppercase,
Lowercase,
Capitalize,
}
#[derive(Debug, Clone)]
enum ContentSpec {
Normal,
None,
Text(String),
Parts(Vec<ContentPart>),
Inherit,
Initial,
}
#[derive(Debug, Clone)]
enum ContentPart {
Text(String),
Attr(String),
Image(String),
Leader(String),
Counter(GeneratedCounterContent),
Counters(GeneratedCountersContent),
TargetTextAttr(String),
TargetCounterAttr {
attribute: String,
name: String,
style: GeneratedCounterStyle,
},
OpenQuote,
CloseQuote,
NoOpenQuote,
NoCloseQuote,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedCounterStyle {
pub list_style_type: ListStyleTypeMode,
pub marker: Option<String>,
pub symbols: Option<AnonymousListStyleSymbols>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedCounterContent {
pub name: String,
pub style: GeneratedCounterStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedCountersContent {
pub name: String,
pub separator: String,
pub style: GeneratedCounterStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedTargetCounterContent {
pub target: String,
pub name: String,
pub style: GeneratedCounterStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneratedContentPart {
Text(String),
Image(String),
Leader(String),
Counter(GeneratedCounterContent),
Counters(GeneratedCountersContent),
TargetText(String),
TargetCounter(GeneratedTargetCounterContent),
OpenQuote,
CloseQuote,
NoOpenQuote,
NoCloseQuote,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CounterMutation {
pub name: String,
pub value: i32,
pub reversed: bool,
pub auto_reversed_initial: bool,
}
#[derive(Debug, Clone)]
enum QuotesSpec {
Value(Vec<(String, String)>),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TextDecorationMode {
pub underline: bool,
pub overline: bool,
pub line_through: bool,
}
impl TextDecorationMode {
pub fn is_none(&self) -> bool {
!self.underline && !self.overline && !self.line_through
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextDecorationThicknessMode {
Auto,
FromFont,
Length(LengthSpec),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDecorationStyleMode {
Solid,
Double,
Dotted,
Dashed,
Wavy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDecorationSkipInkMode {
Auto,
None,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextEmphasisStyleMode {
None,
FilledDot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextEmphasisPositionMode {
Over,
Under,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InitialLetterValue {
pub size: f32,
pub sink: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextUnderlineOffsetMode {
Auto,
Length(LengthSpec),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextUnderlinePositionLine {
Auto,
FromFont,
Under,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextUnderlinePositionSide {
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextUnderlinePositionMode {
pub line: TextUnderlinePositionLine,
pub side: Option<TextUnderlinePositionSide>,
}
impl TextUnderlinePositionMode {
pub fn auto() -> Self {
Self {
line: TextUnderlinePositionLine::Auto,
side: None,
}
}
pub fn is_under(self) -> bool {
self.line == TextUnderlinePositionLine::Under
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordBreakMode {
Normal,
BreakWord,
BreakAll,
KeepAll,
Anywhere,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WordBreakSpec {
Value(WordBreakMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowWrapMode {
Normal,
BreakWord,
Anywhere,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OverflowWrapSpec {
Value(OverflowWrapMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineBreakMode {
Auto,
Loose,
Normal,
Strict,
Anywhere,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayMode {
Block,
FlowRoot,
Inline,
InlineBlock,
ListItem,
InlineListItem,
Table,
InlineTable,
TableRowGroup,
TableHeaderGroup,
TableFooterGroup,
TableRow,
TableCell,
TableColumnGroup,
TableColumn,
TableCaption,
Flex,
InlineFlex,
Grid,
InlineGrid,
None,
Contents,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnFillMode {
Balance,
Auto,
BalanceAll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnSpanMode {
None,
All,
Inherit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VisibilityMode {
Visible,
Hidden,
Collapse,
}
impl VisibilityMode {
pub fn paints(self) -> bool {
matches!(self, VisibilityMode::Visible)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PositionMode {
Static,
Relative,
Sticky,
Absolute,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ZIndexSpec {
Auto,
Integer(i32),
Inherit,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum PerspectiveSpec {
None,
Length(LengthSpec),
Inherit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NamedStringSource {
Content,
Attribute(String),
Text(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NamedStringAssignment {
pub name: String,
pub source: NamedStringSource,
}
#[derive(Debug, Clone)]
enum NamedStringSetSpec {
Value(Vec<NamedStringAssignment>),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FloatMode {
None,
Left,
Right,
Footnote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FootnotePolicy {
Auto,
Line,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FootnoteDisplay {
Block,
Inline,
Compact,
}
#[derive(Debug, Clone, Copy)]
enum FootnotePolicySpec {
Value(FootnotePolicy),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy)]
enum FootnoteDisplaySpec {
Value(FootnoteDisplay),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClearMode {
None,
Left,
Right,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexDirectionMode {
Row,
RowReverse,
Column,
ColumnReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexWrapMode {
NoWrap,
Wrap,
WrapReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JustifyContentMode {
FlexStart,
FlexEnd,
Center,
SafeCenter,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignItemsMode {
FlexStart,
FlexEnd,
Center,
Stretch,
FirstBaseline,
LastBaseline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignSelfMode {
Auto,
FlexStart,
FlexEnd,
Center,
Stretch,
FirstBaseline,
LastBaseline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignContentMode {
FlexStart,
FlexEnd,
Center,
Stretch,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridAutoFlowMode {
Row,
Column,
RowDense,
ColumnDense,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridAutoRepeatMode {
Fill,
Fit,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GridAutoRepeatSpec {
pub mode: GridAutoRepeatMode,
pub tracks: Vec<GridTrackSize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum GridLineSpec {
#[default]
Auto,
Line(i32),
Named(String),
NamedOccurrence {
occurrence: i32,
name: String,
},
Span(usize),
SpanNamed {
count: usize,
name: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WhiteSpaceSpec {
Value(WhiteSpaceMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextWrapModeSpec {
Value(TextWrapMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextWrapStyleSpec {
Value(TextWrapStyleMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextDecorationSpec {
Value(TextDecorationMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum TextDecorationThicknessSpec {
Value(TextDecorationThicknessMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextDecorationStyleSpec {
Value(TextDecorationStyleMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextDecorationSkipInkSpec {
Value(TextDecorationSkipInkMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextEmphasisStyleSpec {
Value(TextEmphasisStyleMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextEmphasisPositionSpec {
Value(TextEmphasisPositionMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum InitialLetterSpec {
Value(InitialLetterValue),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum TextUnderlineOffsetSpec {
Value(TextUnderlineOffsetMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextUnderlinePositionSpec {
Value(TextUnderlinePositionMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, PartialEq)]
enum TextShadowSpec {
Value(Vec<BoxShadowSpec>),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LineBreakSpec {
Value(LineBreakMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DisplaySpec {
Value(DisplayMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VisibilitySpec {
Value(VisibilityMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PaginationLineCountSpec {
Value(usize),
Inherit,
Initial,
}
#[derive(Debug, Clone, Default)]
struct PaginationDelta {
break_before: Option<BreakBefore>,
break_after: Option<BreakAfter>,
break_inside: Option<BreakInside>,
orphans: Option<PaginationLineCountSpec>,
widows: Option<PaginationLineCountSpec>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowMode {
Visible,
Hidden,
Clip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarGutterMode {
Auto,
Stable,
StableBothEdges,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectFitMode {
Fill,
Contain,
Cover,
None,
ScaleDown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageRenderingMode {
Auto,
Pixelated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptionSideMode {
Top,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextOverflowMode {
Clip,
Ellipsis,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextOverflowSpec {
Value(TextOverflowMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct TextIndentValue {
value: LengthSpec,
hanging: bool,
each_line: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ObjectFitSpec {
Value(ObjectFitMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ImageRenderingSpec {
Value(ImageRenderingMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ObjectPositionSpec {
Value(BackgroundPositionSpec),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DirectionSpec {
Value(DirectionMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WritingModeSpec {
Value(WritingModeMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextOrientationSpec {
Value(TextOrientationMode),
Inherit,
Initial,
}
#[derive(Debug, Clone)]
enum TransformSpec {
Value(Vec<CssTransformOp>),
Inherit,
Initial,
}
#[derive(Debug, Clone)]
enum ClipPathSpec {
Shape {
shape: ClipPathShapeSpec,
reference_box: ClipPathReferenceBox,
},
None,
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy)]
enum LegacyClipSpec {
Rect(ClipPathRectSpec),
None,
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum TransformOriginSpec {
Value(CssTransformOrigin),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransformBoxSpec {
Value(TransformBoxMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransformStyleSpec {
Value(TransformStyleMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListStyleTypeMode {
Auto,
None,
Disc,
Circle,
Square,
Decimal,
DecimalLeadingZero,
ArabicIndic,
Armenian,
Bengali,
Cambodian,
CjkDecimal,
CjkEarthlyBranch,
CjkHeavenlyStem,
Devanagari,
EthiopicNumeric,
Georgian,
Gujarati,
Gurmukhi,
Hebrew,
JapaneseInformal,
JapaneseFormal,
Kannada,
KoreanHangulFormal,
KoreanHanjaInformal,
KoreanHanjaFormal,
Lao,
LowerArmenian,
Malayalam,
Mongolian,
Myanmar,
Oriya,
Persian,
SimpChineseInformal,
SimpChineseFormal,
Tamil,
Telugu,
Thai,
Tibetan,
TradChineseInformal,
TradChineseFormal,
LowerRoman,
UpperRoman,
LowerAlpha,
UpperAlpha,
LowerGreek,
Hiragana,
HiraganaIroha,
Katakana,
KatakanaIroha,
DisclosureOpen,
DisclosureClosed,
CustomString,
CustomCounterStyleName,
AnonymousSymbols,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnonymousListStyleSymbolsSystem {
Cyclic,
Numeric,
Alphabetic,
Symbolic,
Fixed,
ExtendsDecimal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnonymousListStyleSymbols {
pub system: AnonymousListStyleSymbolsSystem,
pub symbols: Vec<String>,
pub prefix: String,
pub suffix: String,
pub negative_prefix: String,
pub negative_suffix: String,
pub pad_width: usize,
pub pad_symbol: String,
pub fixed_start: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListStylePositionMode {
Outside,
Inside,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ListStylePositionSpec {
Value(ListStylePositionMode),
Inherit,
Initial,
}
#[derive(Debug, Clone, PartialEq)]
enum ListStyleImageSpec {
Url(String),
Image {
css: String,
paint: Option<BackgroundPaint>,
},
None,
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IsolationSpec {
Value(bool),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum OpacitySpec {
Value(f32),
Inherit,
Initial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ContainerNamesSpec {
Value(Vec<String>),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContainerQueryTypeSpec {
Value(ContainerQueryType),
Inherit,
Initial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum AspectRatioSpec {
Auto,
Ratio(f32),
Inherit,
Initial,
}
#[derive(Debug, Clone, Default)]
struct BorderImageDelta {
reset: bool,
source: Option<Option<BackgroundPaint>>,
slice: Option<[BorderImageSliceValue; 4]>,
fill: Option<bool>,
width: Option<[BorderImageWidthValue; 4]>,
outset: Option<[BorderImageOutsetValue; 4]>,
repeat: Option<(BorderImageRepeatMode, BorderImageRepeatMode)>,
}
impl BorderImageDelta {
fn is_empty(&self) -> bool {
!self.reset
&& self.source.is_none()
&& self.slice.is_none()
&& self.fill.is_none()
&& self.width.is_none()
&& self.outset.is_none()
&& self.repeat.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CascadeWideKeyword {
Inherit,
Initial,
Unset,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PhysicalCorner {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BorderRadiusCornerTarget {
Physical(PhysicalCorner),
StartStart,
StartEnd,
EndStart,
EndEnd,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct BorderRadiusCornerValue {
horizontal: LengthSpec,
vertical: LengthSpec,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum BorderRadiusOp {
Shorthand(BorderRadiiSpec),
Corner(BorderRadiusCornerTarget, BorderRadiusCornerValue),
}
#[derive(Debug, Clone)]
enum BorderCascadeOp {
Width(BorderColorTarget, NativeLengthComponent),
Style(BorderColorTarget, BorderLineStyle),
Color(BorderColorTarget, NativeBorderColorComponent),
}
#[derive(Debug, Clone, Default)]
struct StyleDelta {
all: Option<CascadeWideKeyword>,
background_reset: Option<CascadeWideKeyword>,
revert_layer: RevertLayerDelta,
font_size: Option<FontSizeSpec>,
font_size_var: Option<String>,
font_size_adjust: Option<FontSizeAdjustSpec>,
line_height: Option<LineHeightSpec>,
line_height_var: Option<String>,
color: Option<ColorSpec>,
background_color: Option<BackgroundSpec>,
color_var: Option<String>,
background_color_var: Option<String>,
border_color_var: Option<String>,
border_top_color_var: Option<String>,
border_right_color_var: Option<String>,
border_bottom_color_var: Option<String>,
border_left_color_var: Option<String>,
width: Option<LengthSpec>,
height: Option<LengthSpec>,
width_var: Option<String>,
height_var: Option<String>,
max_width: Option<LengthSpec>,
min_width: Option<LengthSpec>,
min_height: Option<LengthSpec>,
max_height: Option<LengthSpec>,
aspect_ratio: Option<AspectRatioSpec>,
min_width_var: Option<String>,
min_height_var: Option<String>,
max_height_var: Option<String>,
max_width_var: Option<String>,
text_align: Option<TextAlignMode>,
text_align_last: Option<TextAlignLastSpec>,
text_justify: Option<TextJustifySpec>,
text_justify_var: Option<String>,
hyphens: Option<HyphensSpec>,
hyphens_var: Option<String>,
hyphenate_character: Option<HyphenateCharacterSpec>,
hyphenate_character_var: Option<String>,
text_wrap_mode: Option<TextWrapModeSpec>,
text_wrap_mode_var: Option<String>,
text_wrap_style: Option<TextWrapStyleSpec>,
text_wrap_style_var: Option<String>,
direction: Option<DirectionSpec>,
unicode_bidi: Option<UnicodeBidiSpec>,
writing_mode: Option<WritingModeSpec>,
text_orientation: Option<TextOrientationSpec>,
vertical_align: Option<VerticalAlignMode>,
font_weight: Option<FontWeightSpec>,
font_weight_var: Option<String>,
font_kerning: Option<FontFeatureToggleSpec>,
common_ligatures: Option<FontFeatureToggleSpec>,
small_caps: Option<FontFeatureToggleSpec>,
font_stretch: Option<FontStretchSpec>,
font_synthesis_weight: Option<bool>,
font_style: Option<FontStyleMode>,
font_style_var: Option<String>,
text_transform: Option<TextTransformSpec>,
transform: Option<TransformSpec>,
transform_var: Option<String>,
translate: Option<TransformSpec>,
translate_var: Option<String>,
rotate: Option<TransformSpec>,
rotate_var: Option<String>,
scale: Option<TransformSpec>,
scale_var: Option<String>,
transform_origin: Option<TransformOriginSpec>,
transform_box: Option<TransformBoxSpec>,
transform_style: Option<TransformStyleSpec>,
perspective_origin: Option<TransformOriginSpec>,
text_decoration: Option<TextDecorationSpec>,
text_decoration_color: Option<ColorSpec>,
text_decoration_color_var: Option<String>,
text_decoration_thickness: Option<TextDecorationThicknessSpec>,
text_decoration_thickness_var: Option<String>,
text_decoration_style: Option<TextDecorationStyleSpec>,
text_decoration_skip_ink: Option<TextDecorationSkipInkSpec>,
text_emphasis_style: Option<TextEmphasisStyleSpec>,
text_emphasis_color: Option<ColorSpec>,
text_emphasis_position: Option<TextEmphasisPositionSpec>,
initial_letter: Option<InitialLetterSpec>,
text_underline_offset: Option<TextUnderlineOffsetSpec>,
text_underline_offset_var: Option<String>,
text_underline_position: Option<TextUnderlinePositionSpec>,
text_underline_position_var: Option<String>,
text_shadow: Option<TextShadowSpec>,
text_shadow_var: Option<String>,
text_overflow: Option<TextOverflowSpec>,
text_indent: Option<TextIndentValue>,
content: Option<ContentSpec>,
quotes: Option<QuotesSpec>,
counter_reset: Option<Vec<CounterMutation>>,
counter_increment: Option<Vec<CounterMutation>>,
counter_set: Option<Vec<CounterMutation>>,
word_break: Option<WordBreakSpec>,
overflow_wrap: Option<OverflowWrapSpec>,
line_break: Option<LineBreakSpec>,
line_break_var: Option<String>,
list_style_type: Option<(
ListStyleTypeMode,
Option<String>,
Option<AnonymousListStyleSymbols>,
)>,
list_style_image: Option<ListStyleImageSpec>,
list_style_position: Option<ListStylePositionSpec>,
letter_spacing: Option<LengthSpec>,
word_spacing: Option<LengthSpec>,
tab_size: Option<TabSizeSpec>,
border_width: EdgeDelta,
border_inline_start_width: Option<LengthSpec>,
border_inline_end_width: Option<LengthSpec>,
border_block_start_width: Option<LengthSpec>,
border_block_end_width: Option<LengthSpec>,
border_inline_start_width_var: Option<LengthVarExpr>,
border_inline_end_width_var: Option<LengthVarExpr>,
border_block_start_width_var: Option<LengthVarExpr>,
border_block_end_width_var: Option<LengthVarExpr>,
border_color: Option<ColorSpec>,
border_top_color: Option<ColorSpec>,
border_right_color: Option<ColorSpec>,
border_bottom_color: Option<ColorSpec>,
border_left_color: Option<ColorSpec>,
border_inline_start_color: Option<ColorSpec>,
border_inline_end_color: Option<ColorSpec>,
border_block_start_color: Option<ColorSpec>,
border_block_end_color: Option<ColorSpec>,
border_inline_start_color_var: Option<String>,
border_inline_end_color_var: Option<String>,
border_block_start_color_var: Option<String>,
border_block_end_color_var: Option<String>,
border_style: BorderStyleDelta,
border_inline_start_style: Option<BorderLineStyle>,
border_inline_end_style: Option<BorderLineStyle>,
border_block_start_style: Option<BorderLineStyle>,
border_block_end_style: Option<BorderLineStyle>,
border_cascade_ops: Vec<BorderCascadeOp>,
border_collapse: Option<BorderCollapseMode>,
caption_side: Option<CaptionSideMode>,
border_spacing: Option<BorderSpacingSpec>,
table_layout: Option<TableLayoutMode>,
empty_cells_hide: Option<bool>,
visibility: Option<VisibilitySpec>,
border_radius: Option<BorderRadiiSpec>,
border_radius_var: Option<String>,
border_radius_ops: Vec<BorderRadiusOp>,
box_decoration_break: Option<BoxDecorationBreak>,
border_image: BorderImageDelta,
outline_width: Option<LengthSpec>,
outline_offset: Option<LengthSpec>,
outline_offset_var: Option<String>,
outline_style: Option<OutlineLineStyle>,
outline_color: Option<ColorSpec>,
outline_visible: Option<bool>,
font_name: Option<FontSpec>,
font_name_var: Option<String>,
box_shadow: Option<BoxShadowSpec>,
box_shadows: Option<Vec<BoxShadowSpec>>,
paint_filter: Option<PaintFilterSpec>,
paint_filter_raw: Option<String>,
backdrop_filter: Option<PaintFilterSpec>,
backdrop_filter_raw: Option<String>,
will_change_backdrop_root: Option<bool>,
mask_backdrop_root: Option<bool>,
mix_blend_mode: Option<MixBlendMode>,
isolation: Option<IsolationSpec>,
opacity: Option<OpacitySpec>,
opacity_var: Option<String>,
container_names: Option<ContainerNamesSpec>,
container_type: Option<ContainerQueryTypeSpec>,
background_paint: Option<BackgroundPaint>,
background_paints: Option<Vec<BackgroundPaint>>,
background_sizes: Option<Vec<BackgroundSizeSpec>>,
background_positions: Option<Vec<BackgroundPositionSpec>>,
background_position_x: Option<Vec<BackgroundPositionComponent>>,
background_position_y: Option<Vec<BackgroundPositionComponent>>,
background_repeats: Option<Vec<BackgroundRepeatSpec>>,
background_attachments: Option<Vec<BackgroundAttachment>>,
background_blend_modes: Option<Vec<MixBlendMode>>,
background_origins: Option<Vec<BackgroundBox>>,
background_clips: Option<Vec<BackgroundClipBox>>,
mask_paints: Option<Vec<BackgroundPaint>>,
mask_sizes: Option<Vec<BackgroundSizeSpec>>,
mask_positions: Option<Vec<BackgroundPositionSpec>>,
mask_repeats: Option<Vec<BackgroundRepeatSpec>>,
mask_origins: Option<Vec<BackgroundBox>>,
mask_clips: Option<Vec<BackgroundClipBox>>,
mask_modes: Option<Vec<MaskMode>>,
mask_composites: Option<Vec<MaskComposite>>,
mask_border: BorderImageDelta,
clip_path: Option<ClipPathSpec>,
legacy_clip: Option<LegacyClipSpec>,
pagination: PaginationDelta,
page_name: Option<Option<u64>>,
margin: EdgeDelta,
margin_inline_start: Option<LengthSpec>,
margin_inline_end: Option<LengthSpec>,
margin_block_start: Option<LengthSpec>,
margin_block_end: Option<LengthSpec>,
margin_inline_start_var: Option<LengthVarExpr>,
margin_inline_end_var: Option<LengthVarExpr>,
margin_block_start_var: Option<LengthVarExpr>,
margin_block_end_var: Option<LengthVarExpr>,
padding: EdgeDelta,
padding_inline_start: Option<LengthSpec>,
padding_inline_end: Option<LengthSpec>,
padding_block_start: Option<LengthSpec>,
padding_block_end: Option<LengthSpec>,
padding_inline_start_var: Option<LengthVarExpr>,
padding_inline_end_var: Option<LengthVarExpr>,
padding_block_start_var: Option<LengthVarExpr>,
padding_block_end_var: Option<LengthVarExpr>,
inline_size: Option<LengthSpec>,
block_size: Option<LengthSpec>,
min_inline_size: Option<LengthSpec>,
max_inline_size: Option<LengthSpec>,
min_block_size: Option<LengthSpec>,
max_block_size: Option<LengthSpec>,
inline_size_var: Option<String>,
block_size_var: Option<String>,
min_inline_size_var: Option<String>,
max_inline_size_var: Option<String>,
min_block_size_var: Option<String>,
max_block_size_var: Option<String>,
white_space: Option<WhiteSpaceSpec>,
white_space_var: Option<String>,
display: Option<DisplaySpec>,
position: Option<PositionMode>,
running_name: Option<Option<Arc<str>>>,
string_set: Option<NamedStringSetSpec>,
float_mode: Option<FloatMode>,
footnote_policy: Option<FootnotePolicySpec>,
footnote_display: Option<FootnoteDisplaySpec>,
clear_mode: Option<ClearMode>,
vertical_align_var: Option<String>,
z_index: Option<ZIndexSpec>,
perspective: Option<PerspectiveSpec>,
inset_left: Option<LengthSpec>,
inset_top: Option<LengthSpec>,
inset_right: Option<LengthSpec>,
inset_bottom: Option<LengthSpec>,
inset_inline_start: Option<LengthSpec>,
inset_inline_end: Option<LengthSpec>,
inset_block_start: Option<LengthSpec>,
inset_block_end: Option<LengthSpec>,
inset_inline_start_var: Option<String>,
inset_inline_end_var: Option<String>,
inset_block_start_var: Option<String>,
inset_block_end_var: Option<String>,
inset_left_var: Option<String>,
inset_top_var: Option<String>,
inset_right_var: Option<String>,
inset_bottom_var: Option<String>,
box_sizing: Option<BoxSizingMode>,
flex_direction: Option<FlexDirectionMode>,
flex_wrap: Option<FlexWrapMode>,
flex_basis: Option<LengthSpec>,
flex_basis_var: Option<String>,
order: Option<i32>,
justify_content: Option<JustifyContentMode>,
align_items: Option<AlignItemsMode>,
align_self: Option<AlignSelfMode>,
justify_items: Option<AlignItemsMode>,
justify_self: Option<AlignSelfMode>,
align_content: Option<AlignContentMode>,
column_count: Option<usize>,
column_count_auto: Option<bool>,
column_width: Option<LengthSpec>,
column_fill: Option<ColumnFillMode>,
column_span: Option<ColumnSpanMode>,
column_rule_width: Option<LengthSpec>,
column_rule_style: Option<BorderLineStyle>,
column_rule_color: Option<ColorSpec>,
grid_columns: Option<usize>,
grid_rows: Option<usize>,
grid_column_tracks: Option<Vec<GridTrackSize>>,
grid_row_tracks: Option<Vec<GridTrackSize>>,
grid_subgrid_columns: Option<bool>,
grid_subgrid_rows: Option<bool>,
grid_column_tracks_var: Option<Option<String>>,
grid_row_tracks_var: Option<Option<String>>,
grid_auto_column_tracks: Option<Vec<GridTrackSize>>,
grid_auto_row_tracks: Option<Vec<GridTrackSize>>,
grid_auto_flow: Option<GridAutoFlowMode>,
grid_column_auto_repeat: Option<Option<GridAutoRepeatSpec>>,
grid_row_auto_repeat: Option<Option<GridAutoRepeatSpec>>,
grid_template_areas: Option<Vec<Vec<Option<String>>>>,
grid_column_line_names: Option<Vec<Vec<String>>>,
grid_row_line_names: Option<Vec<Vec<String>>>,
grid_column_line_start: Option<GridLineSpec>,
grid_column_line_end: Option<GridLineSpec>,
grid_row_line_start: Option<GridLineSpec>,
grid_row_line_end: Option<GridLineSpec>,
grid_area_name: Option<Option<String>>,
grid_column_start: Option<usize>,
grid_row_start: Option<usize>,
row_gap: Option<LengthSpec>,
row_gap_var: Option<String>,
gap: Option<LengthSpec>,
gap_var: Option<String>,
column_gap_normal: Option<bool>,
flex_grow: Option<f32>,
flex_shrink: Option<f32>,
overflow_x: Option<OverflowMode>,
overflow_y: Option<OverflowMode>,
overflow_inline: Option<OverflowMode>,
overflow_block: Option<OverflowMode>,
overflow_clip_margin: Option<LengthSpec>,
scrollbar_gutter: Option<ScrollbarGutterMode>,
line_clamp: Option<Option<usize>>,
object_fit: Option<ObjectFitSpec>,
image_rendering: Option<ImageRenderingSpec>,
object_position: Option<ObjectPositionSpec>,
custom_lengths: HashMap<String, LengthSpec>,
custom_colors: HashMap<String, Color>,
custom_color_alpha: HashMap<String, f32>,
custom_color_refs: HashMap<String, String>,
custom_raw_values: HashMap<String, String>,
custom_font_stacks: HashMap<String, Vec<Arc<str>>>,
}
fn style_delta_authors_font_weight(delta: &StyleDelta) -> bool {
delta.font_weight.is_some() || delta.font_weight_var.is_some() || delta.all.is_some()
}
#[derive(Debug, Clone, Default)]
struct RevertLayerDelta {
color: bool,
background_color: bool,
width: bool,
height: bool,
box_sizing: bool,
display: bool,
visibility: bool,
position: bool,
float_mode: bool,
footnote_policy: bool,
footnote_display: bool,
clear_mode: bool,
z_index: bool,
perspective: bool,
direction: bool,
unicode_bidi: bool,
writing_mode: bool,
text_orientation: bool,
opacity: bool,
paint_filter: bool,
backdrop_filter: bool,
will_change_backdrop_root: bool,
mask_backdrop_root: bool,
clip_path: bool,
legacy_clip: bool,
box_shadow: bool,
text_shadow: bool,
text_decoration_line: bool,
text_decoration_color: bool,
text_decoration_thickness: bool,
text_decoration_style: bool,
text_decoration_skip_ink: bool,
text_emphasis_style: bool,
text_emphasis_color: bool,
text_emphasis_position: bool,
text_underline_offset: bool,
text_underline_position: bool,
text_overflow: bool,
text_indent: bool,
letter_spacing: bool,
word_spacing: bool,
tab_size: bool,
text_align: bool,
text_align_last: bool,
text_justify: bool,
text_transform: bool,
white_space: bool,
text_wrap_mode: bool,
text_wrap_style: bool,
vertical_align: bool,
list_style_type: bool,
list_style_image: bool,
list_style_position: bool,
word_break: bool,
overflow_wrap: bool,
line_break: bool,
hyphens: bool,
hyphenate_character: bool,
mix_blend_mode: bool,
isolation: bool,
overflow_x: bool,
overflow_y: bool,
overflow_inline: bool,
overflow_block: bool,
outline_width: bool,
outline_style: bool,
outline_color: bool,
outline_offset: bool,
margin: EdgeRevertDelta,
padding: EdgeRevertDelta,
border_color: EdgeRevertDelta,
border_width: EdgeRevertDelta,
border_style: EdgeRevertDelta,
inset: EdgeRevertDelta,
margin_logical: LogicalEdgeRevertDelta,
padding_logical: LogicalEdgeRevertDelta,
border_color_logical: LogicalEdgeRevertDelta,
border_width_logical: LogicalEdgeRevertDelta,
border_style_logical: LogicalEdgeRevertDelta,
inset_logical: LogicalEdgeRevertDelta,
}
impl RevertLayerDelta {
fn is_empty(&self) -> bool {
!self.color
&& !self.background_color
&& !self.width
&& !self.height
&& !self.box_sizing
&& !self.display
&& !self.visibility
&& !self.position
&& !self.float_mode
&& !self.footnote_policy
&& !self.footnote_display
&& !self.clear_mode
&& !self.z_index
&& !self.perspective
&& !self.direction
&& !self.writing_mode
&& !self.text_orientation
&& !self.opacity
&& !self.paint_filter
&& !self.backdrop_filter
&& !self.will_change_backdrop_root
&& !self.mask_backdrop_root
&& !self.clip_path
&& !self.box_shadow
&& !self.text_shadow
&& !self.text_decoration_line
&& !self.text_decoration_color
&& !self.text_decoration_thickness
&& !self.text_decoration_style
&& !self.text_decoration_skip_ink
&& !self.text_emphasis_style
&& !self.text_emphasis_color
&& !self.text_emphasis_position
&& !self.text_underline_offset
&& !self.text_underline_position
&& !self.text_overflow
&& !self.text_indent
&& !self.letter_spacing
&& !self.word_spacing
&& !self.tab_size
&& !self.text_align
&& !self.text_align_last
&& !self.text_justify
&& !self.text_transform
&& !self.white_space
&& !self.text_wrap_mode
&& !self.text_wrap_style
&& !self.vertical_align
&& !self.list_style_type
&& !self.list_style_image
&& !self.list_style_position
&& !self.word_break
&& !self.overflow_wrap
&& !self.line_break
&& !self.hyphens
&& !self.hyphenate_character
&& !self.mix_blend_mode
&& !self.isolation
&& !self.overflow_x
&& !self.overflow_y
&& !self.overflow_inline
&& !self.overflow_block
&& !self.outline_width
&& !self.outline_style
&& !self.outline_color
&& !self.outline_offset
&& self.margin.is_empty()
&& self.padding.is_empty()
&& self.border_color.is_empty()
&& self.border_width.is_empty()
&& self.border_style.is_empty()
&& self.inset.is_empty()
&& self.margin_logical.is_empty()
&& self.padding_logical.is_empty()
&& self.border_color_logical.is_empty()
&& self.border_width_logical.is_empty()
&& self.border_style_logical.is_empty()
&& self.inset_logical.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BoxEdgeTarget {
All,
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LogicalBoxEdgeTarget {
Inline,
InlineStart,
InlineEnd,
Block,
BlockStart,
BlockEnd,
}
#[derive(Debug, Clone, Default)]
struct LogicalEdgeRevertDelta {
inline_start: bool,
inline_end: bool,
block_start: bool,
block_end: bool,
}
impl LogicalEdgeRevertDelta {
fn is_empty(&self) -> bool {
!self.inline_start && !self.inline_end && !self.block_start && !self.block_end
}
fn set(&mut self, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
self.inline_start = true;
self.inline_end = true;
}
LogicalBoxEdgeTarget::InlineStart => self.inline_start = true,
LogicalBoxEdgeTarget::InlineEnd => self.inline_end = true,
LogicalBoxEdgeTarget::Block => {
self.block_start = true;
self.block_end = true;
}
LogicalBoxEdgeTarget::BlockStart => self.block_start = true,
LogicalBoxEdgeTarget::BlockEnd => self.block_end = true,
}
}
fn clear(&mut self, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
self.inline_start = false;
self.inline_end = false;
}
LogicalBoxEdgeTarget::InlineStart => self.inline_start = false,
LogicalBoxEdgeTarget::InlineEnd => self.inline_end = false,
LogicalBoxEdgeTarget::Block => {
self.block_start = false;
self.block_end = false;
}
LogicalBoxEdgeTarget::BlockStart => self.block_start = false,
LogicalBoxEdgeTarget::BlockEnd => self.block_end = false,
}
}
}
#[derive(Debug, Clone, Default)]
struct EdgeRevertDelta {
top: bool,
right: bool,
bottom: bool,
left: bool,
}
impl EdgeRevertDelta {
fn is_empty(&self) -> bool {
!self.top && !self.right && !self.bottom && !self.left
}
fn set(&mut self, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
self.top = true;
self.right = true;
self.bottom = true;
self.left = true;
}
BoxEdgeTarget::Top => self.top = true,
BoxEdgeTarget::Right => self.right = true,
BoxEdgeTarget::Bottom => self.bottom = true,
BoxEdgeTarget::Left => self.left = true,
}
}
fn clear(&mut self, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
self.top = false;
self.right = false;
self.bottom = false;
self.left = false;
}
BoxEdgeTarget::Top => self.top = false,
BoxEdgeTarget::Right => self.right = false,
BoxEdgeTarget::Bottom => self.bottom = false,
BoxEdgeTarget::Left => self.left = false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedBorderColors {
pub top: Color,
pub right: Color,
pub bottom: Color,
pub left: Color,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedBorderOpacities {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComputedBorderStyles {
pub top: OutlineLineStyle,
pub right: OutlineLineStyle,
pub bottom: OutlineLineStyle,
pub left: OutlineLineStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComputedBorderHiddenSides {
pub top: bool,
pub right: bool,
pub bottom: bool,
pub left: bool,
}
#[derive(Debug, Clone)]
pub struct ComputedStyle {
pub font_size: Pt,
pending_font_size_var: Option<String>,
font_size_adjust: Option<I32F32>,
font_size_adjust_scale: I32F32,
line_height: LineHeightSpec,
pending_line_height_var: Option<String>,
pub color: Color,
pub background_color: Option<Color>,
pub background_source_color: Option<Color>,
pub background_alpha: f32,
pub background_paint: Option<BackgroundPaint>,
pub background_paints: Vec<BackgroundPaint>,
pub background_sizes: Vec<BackgroundSizeSpec>,
pub background_positions: Vec<BackgroundPositionSpec>,
pub background_repeats: Vec<BackgroundRepeatSpec>,
pub background_attachments: Vec<BackgroundAttachment>,
pub background_blend_modes: Vec<MixBlendMode>,
pub background_origins: Vec<BackgroundBox>,
pub background_clips: Vec<BackgroundClipBox>,
pub mask: MaskSpec,
pending_color_var: Option<String>,
pending_background_color_var: Option<String>,
pending_border_color_var: Option<String>,
pending_border_top_color_var: Option<String>,
pending_border_right_color_var: Option<String>,
pending_border_bottom_color_var: Option<String>,
pending_border_left_color_var: Option<String>,
pending_text_decoration_color_var: Option<String>,
pending_text_decoration_thickness_var: Option<String>,
pending_text_underline_offset_var: Option<String>,
pending_text_underline_position_var: Option<String>,
pending_text_shadow_var: Option<String>,
pending_line_break_var: Option<String>,
pending_font_name_var: Option<String>,
pending_font_weight_var: Option<String>,
pending_font_style_var: Option<String>,
pub pagination: Pagination,
pub margin: EdgeSizes,
pub padding: EdgeSizes,
pub width: LengthSpec,
pub height: LengthSpec,
pending_width_var: Option<String>,
pending_height_var: Option<String>,
pub max_width: LengthSpec,
pub min_width: LengthSpec,
pub min_height: LengthSpec,
pub max_height: LengthSpec,
pub aspect_ratio: Option<f32>,
pending_min_width_var: Option<String>,
pending_min_height_var: Option<String>,
pending_max_height_var: Option<String>,
pending_max_width_var: Option<String>,
pub text_align: TextAlignMode,
pub text_align_last: TextAlignLastMode,
pub text_justify: TextJustifyMode,
pending_text_justify_var: Option<String>,
pub hyphens: HyphensMode,
pending_hyphens_var: Option<String>,
pub hyphenate_character: Option<String>,
pending_hyphenate_character_var: Option<String>,
pub text_wrap_mode: TextWrapMode,
pending_text_wrap_mode_var: Option<String>,
pub text_wrap_style: TextWrapStyleMode,
pending_text_wrap_style_var: Option<String>,
pending_white_space_var: Option<String>,
pub direction: DirectionMode,
pub unicode_bidi: UnicodeBidiMode,
pub writing_mode: WritingModeMode,
pub text_orientation: TextOrientationMode,
pending_vertical_align_var: Option<String>,
vertical_align_inherit_base: VerticalAlignMode,
pub vertical_align: VerticalAlignMode,
pub font_weight: u16,
pub(crate) font_weight_authored: bool,
pub font_kerning: bool,
pub common_ligatures: bool,
pub small_caps: bool,
pub font_stretch: u16,
pub font_synthesis_weight: bool,
pub font_style: FontStyleMode,
pub text_transform: TextTransformMode,
transform_list: Vec<CssTransformOp>,
translate: Vec<CssTransformOp>,
rotate: Vec<CssTransformOp>,
scale: Vec<CssTransformOp>,
pending_translate_var: Option<String>,
pending_rotate_var: Option<String>,
pending_scale_var: Option<String>,
pending_transform_var: Option<String>,
pub transform: Vec<CssTransformOp>,
pub transform_origin: CssTransformOrigin,
pub transform_box: TransformBoxMode,
pub transform_style: TransformStyleMode,
pub perspective_origin: CssTransformOrigin,
pub text_decoration: TextDecorationMode,
pub(crate) text_decoration_propagated: bool,
pub text_decoration_color: Color,
text_decoration_color_tracks_current: bool,
pub text_decoration_thickness: TextDecorationThicknessMode,
pub text_decoration_style: TextDecorationStyleMode,
pub text_decoration_skip_ink: TextDecorationSkipInkMode,
pub text_emphasis_style: TextEmphasisStyleMode,
pub text_emphasis_color: Color,
text_emphasis_color_tracks_current: bool,
pub text_emphasis_position: TextEmphasisPositionMode,
pub initial_letter: Option<InitialLetterValue>,
pub text_underline_offset: TextUnderlineOffsetMode,
pub text_underline_position: TextUnderlinePositionMode,
pub text_shadows: Vec<BoxShadowSpec>,
pub text_overflow: TextOverflowMode,
pub text_indent: LengthSpec,
pub text_indent_hanging: bool,
pub text_indent_each_line: bool,
pub content: Option<String>,
pub generated_content: Option<Vec<GeneratedContentPart>>,
pub(crate) marker_content_overridden: bool,
pub quotes: Vec<(String, String)>,
pub counter_reset: Vec<CounterMutation>,
pub counter_increment: Vec<CounterMutation>,
pub counter_set: Vec<CounterMutation>,
pub word_break: WordBreakMode,
pub overflow_wrap: OverflowWrapMode,
pub line_break: LineBreakMode,
pub list_style_type: ListStyleTypeMode,
pub list_style_marker: Option<String>,
pub list_style_symbols: Option<AnonymousListStyleSymbols>,
pub list_style_image: Option<String>,
pub list_style_image_paint: Option<BackgroundPaint>,
pub list_style_position: ListStylePositionMode,
pub letter_spacing: Pt,
pub word_spacing: Pt,
pub tab_size: TabSizeSpec,
pub border_width: EdgeSizes,
pub border_color: Option<Color>,
pub border_top_color: Option<Color>,
pub border_right_color: Option<Color>,
pub border_bottom_color: Option<Color>,
pub border_left_color: Option<Color>,
pub border_top_alpha: f32,
pub border_right_alpha: f32,
pub border_bottom_alpha: f32,
pub border_left_alpha: f32,
border_style: BorderStyleState,
pub border_collapse: BorderCollapseMode,
pub caption_side: CaptionSideMode,
pub border_spacing: BorderSpacingSpec,
pub table_layout: TableLayoutMode,
pub empty_cells_hide: bool,
pub border_radius: BorderRadiiSpec,
pub box_decoration_break: BoxDecorationBreak,
pub border_image: BorderImageSpec,
pending_border_radius_var: Option<String>,
pub outline_width: LengthSpec,
pub outline_offset: LengthSpec,
pub outline_style: OutlineLineStyle,
pub outline_color: Option<Color>,
pub outline_visible: bool,
pub box_shadow: Option<BoxShadowSpec>,
pub box_shadows: Vec<BoxShadowSpec>,
pub paint_filter: Option<PaintFilterSpec>,
pub(crate) paint_filter_stacking_context: bool,
pub backdrop_filter: Option<PaintFilterSpec>,
pub will_change_backdrop_root: bool,
pub mask_backdrop_root: bool,
pub mix_blend_mode: MixBlendMode,
pub isolation: bool,
pub opacity: f32,
pending_opacity_var: Option<String>,
pub container_names: Vec<String>,
pub container_type: ContainerQueryType,
pub clip_path: Option<ClipPathShapeSpec>,
pub clip_path_reference_box: ClipPathReferenceBox,
pub clip_path_inset: Option<ClipPathInsetSpec>,
pub legacy_clip: Option<ClipPathRectSpec>,
pub root_font_size: Pt,
root_line_height: Pt,
pub white_space: WhiteSpaceMode,
pub display: DisplayMode,
pub visibility: VisibilityMode,
pub font_stack: Vec<Arc<str>>,
pub font_name: Arc<str>,
font_unicode_ranges: Vec<Option<Arc<[(u32, u32)]>>>,
font_face_family: Option<Arc<str>>,
font_face_source_count: usize,
font_face_size_adjust: I32F32,
font_face_satisfies_weight: bool,
font_face_satisfies_style: bool,
pub position: PositionMode,
pub(crate) running_name: Option<Arc<str>>,
pub(crate) string_set: Vec<NamedStringAssignment>,
pub float_mode: FloatMode,
pub footnote_policy: FootnotePolicy,
pub footnote_display: FootnoteDisplay,
pub clear_mode: ClearMode,
pub z_index: i32,
pub z_index_auto: bool,
pub perspective: Option<LengthSpec>,
pub inset_left: LengthSpec,
pub inset_top: LengthSpec,
pub inset_right: LengthSpec,
pub inset_bottom: LengthSpec,
pending_outline_offset_var: Option<String>,
pending_inset_left_var: Option<String>,
pending_inset_top_var: Option<String>,
pending_inset_right_var: Option<String>,
pending_inset_bottom_var: Option<String>,
pub box_sizing: BoxSizingMode,
pub flex_direction: FlexDirectionMode,
pub flex_wrap: FlexWrapMode,
pub flex_basis: LengthSpec,
pending_flex_basis_var: Option<String>,
pub order: i32,
pub justify_content: JustifyContentMode,
pub align_items: AlignItemsMode,
pub align_self: AlignSelfMode,
pub justify_items: AlignItemsMode,
pub justify_self: AlignSelfMode,
pub align_content: AlignContentMode,
pub column_count: usize,
pub column_count_auto: bool,
pub column_width: LengthSpec,
pub column_fill: ColumnFillMode,
pub column_span: ColumnSpanMode,
pub column_rule_width: LengthSpec,
pub column_rule_style: OutlineLineStyle,
pub column_rule_color: Option<Color>,
pub column_rule_visible: bool,
pub grid_columns: Option<usize>,
pub grid_rows: Option<usize>,
pub grid_column_tracks: Vec<GridTrackSize>,
pub grid_row_tracks: Vec<GridTrackSize>,
pub grid_subgrid_columns: bool,
pub grid_subgrid_rows: bool,
pending_grid_column_tracks_var: Option<String>,
pending_grid_row_tracks_var: Option<String>,
pub grid_auto_column_tracks: Vec<GridTrackSize>,
pub grid_auto_row_tracks: Vec<GridTrackSize>,
pub grid_auto_flow: GridAutoFlowMode,
pub grid_column_auto_repeat: Option<GridAutoRepeatSpec>,
pub grid_row_auto_repeat: Option<GridAutoRepeatSpec>,
pub grid_template_areas: Vec<Vec<Option<String>>>,
pub grid_column_line_names: Vec<Vec<String>>,
pub grid_row_line_names: Vec<Vec<String>>,
pub grid_column_line_start: GridLineSpec,
pub grid_column_line_end: GridLineSpec,
pub grid_row_line_start: GridLineSpec,
pub grid_row_line_end: GridLineSpec,
pub grid_area_name: Option<String>,
pub grid_column_start: Option<usize>,
pub grid_row_start: Option<usize>,
pub row_gap: LengthSpec,
pending_row_gap_var: Option<String>,
/// Column-axis gap. Kept as `gap` for API compatibility.
pub gap: LengthSpec,
pending_gap_var: Option<String>,
pub column_gap_normal: bool,
pub flex_grow: f32,
pub flex_shrink: f32,
pub(crate) overflow_x: OverflowMode,
pub(crate) overflow_y: OverflowMode,
pub overflow: OverflowMode,
pub overflow_clip_margin: LengthSpec,
pub scrollbar_gutter: ScrollbarGutterMode,
pub line_clamp: Option<usize>,
pub object_fit: ObjectFitMode,
pub image_rendering: ImageRenderingMode,
pub object_position: BackgroundPositionSpec,
pub custom_lengths: HashMap<String, LengthSpec>,
pub custom_colors: HashMap<String, Color>,
pub custom_color_alpha: HashMap<String, f32>,
pub custom_color_refs: HashMap<String, String>,
pub custom_raw_values: HashMap<String, String>,
pub custom_font_stacks: HashMap<String, Vec<Arc<str>>>,
}
fn inherited_pagination(parent: &ComputedStyle) -> Pagination {
Pagination {
page_name: parent.pagination.page_name,
orphans: parent.pagination.orphans,
widows: parent.pagination.widows,
..Pagination::default()
}
}
fn resolve_pagination_line_count(
value: PaginationLineCountSpec,
inherited: usize,
initial: usize,
) -> usize {
match value {
PaginationLineCountSpec::Value(value) => value,
PaginationLineCountSpec::Inherit => inherited,
PaginationLineCountSpec::Initial => initial,
}
}
impl ComputedStyle {
pub(crate) fn page_context_line_height(&self) -> Option<CssPageLineHeight> {
match self.line_height {
LineHeightSpec::Normal | LineHeightSpec::Inherit | LineHeightSpec::Initial => None,
LineHeightSpec::Number(value) => Some(CssPageLineHeight::Number(value)),
LineHeightSpec::AbsolutePt(value) => Some(CssPageLineHeight::Absolute(value)),
LineHeightSpec::RelativeScale(scale) => {
Some(CssPageLineHeight::Absolute(self.font_size.mul_fixed(scale)))
}
LineHeightSpec::Calc(calc) => Some(CssPageLineHeight::Absolute(calc.resolve(
self.font_size,
self.root_font_size,
Size {
width: Pt::ZERO,
height: Pt::ZERO,
},
))),
}
}
pub fn to_text_style(&self) -> TextStyle {
let (line_height, is_auto) = match self.line_height {
LineHeightSpec::Normal => (self.font_size.mul_ratio(6, 5), true),
LineHeightSpec::Number(scale) => (self.font_size * scale, false),
LineHeightSpec::AbsolutePt(value) => (value, false),
LineHeightSpec::RelativeScale(scale) => (self.font_size.mul_fixed(scale), false),
LineHeightSpec::Calc(calc) => (
calc.resolve(
self.font_size,
self.root_font_size,
Size {
width: Pt::ZERO,
height: Pt::ZERO,
},
),
false,
),
LineHeightSpec::Inherit | LineHeightSpec::Initial => {
(self.font_size.mul_ratio(6, 5), true)
}
};
let mut fallbacks = self.font_stack.clone();
let primary = if fallbacks.is_empty() {
Arc::<str>::from("Helvetica")
} else {
fallbacks.remove(0)
};
TextStyle {
font_size: self
.font_size
.mul_fixed(self.font_face_size_adjust)
.mul_fixed(self.font_size_adjust_scale),
line_height,
line_height_is_auto: is_auto,
color: self.color,
font_name: primary,
font_fallbacks: fallbacks,
font_unicode_ranges: self.font_unicode_ranges.clone(),
font_weight: self.font_weight,
font_weight_authored: self.font_weight_authored,
font_kerning: self.font_kerning,
common_ligatures: self.common_ligatures,
small_caps: self.small_caps,
font_synthesis_weight: self.font_synthesis_weight,
font_style: self.font_style,
font_face_satisfies_weight: self.font_face_satisfies_weight,
font_face_satisfies_style: self.font_face_satisfies_style,
text_decoration: self.text_decoration,
text_decoration_propagated: self.text_decoration_propagated,
text_decoration_color: self.text_decoration_color,
text_decoration_thickness: self.text_decoration_thickness,
text_decoration_style: self.text_decoration_style,
text_decoration_skip_ink: self.text_decoration_skip_ink,
text_emphasis_style: self.text_emphasis_style,
text_emphasis_color: self.text_emphasis_color,
text_emphasis_position: self.text_emphasis_position,
text_underline_offset: self.text_underline_offset,
text_underline_position: self.text_underline_position,
text_shadows: self.text_shadows.clone(),
text_overflow: self.text_overflow,
line_clamp: self.line_clamp,
text_indent: self.text_indent,
text_indent_hanging: self.text_indent_hanging,
text_indent_each_line: self.text_indent_each_line,
direction: self.direction,
unicode_bidi: self.unicode_bidi,
text_align_mode: self.text_align,
word_break: self.word_break,
overflow_wrap: self.overflow_wrap,
line_break: self.line_break,
text_justify: self.text_justify,
hyphens: self.hyphens,
hyphenate_character: self.hyphenate_character.clone(),
writing_mode: self.writing_mode,
text_orientation: self.text_orientation,
letter_spacing: self.letter_spacing,
word_spacing: self.word_spacing,
tab_size: self.tab_size,
root_font_size: self.root_font_size,
visible: self.visibility.paints(),
css_pixel_snap_metrics: true,
}
}
pub fn resolved_border_colors(&self, fallback: Color) -> ComputedBorderColors {
let uniform = self.border_color.unwrap_or(fallback);
ComputedBorderColors {
top: self.border_top_color.unwrap_or(uniform),
right: self.border_right_color.unwrap_or(uniform),
bottom: self.border_bottom_color.unwrap_or(uniform),
left: self.border_left_color.unwrap_or(uniform),
}
}
pub fn resolved_border_opacities(&self) -> ComputedBorderOpacities {
ComputedBorderOpacities {
top: self.border_top_alpha.clamp(0.0, 1.0),
right: self.border_right_alpha.clamp(0.0, 1.0),
bottom: self.border_bottom_alpha.clamp(0.0, 1.0),
left: self.border_left_alpha.clamp(0.0, 1.0),
}
}
pub fn resolved_border_styles(&self) -> ComputedBorderStyles {
ComputedBorderStyles {
top: border_line_paint_style(self.border_style.top),
right: border_line_paint_style(self.border_style.right),
bottom: border_line_paint_style(self.border_style.bottom),
left: border_line_paint_style(self.border_style.left),
}
}
pub fn border_hidden_sides(&self) -> ComputedBorderHiddenSides {
ComputedBorderHiddenSides {
top: matches!(self.border_style.top, BorderLineStyle::Hidden),
right: matches!(self.border_style.right, BorderLineStyle::Hidden),
bottom: matches!(self.border_style.bottom, BorderLineStyle::Hidden),
left: matches!(self.border_style.left, BorderLineStyle::Hidden),
}
}
pub fn resolved_outline_color(&self) -> Color {
self.outline_color.unwrap_or(self.color)
}
pub fn resolved_column_rule_color(&self) -> Color {
self.column_rule_color.unwrap_or(self.color)
}
fn has_custom_property_value(&self, name: &str) -> bool {
self.custom_lengths.contains_key(name)
|| self.custom_colors.contains_key(name)
|| self.custom_color_refs.contains_key(name)
|| self.custom_raw_values.contains_key(name)
|| self.custom_font_stacks.contains_key(name)
}
fn clear_custom_property_value(&mut self, name: &str) {
self.custom_lengths.remove(name);
self.custom_colors.remove(name);
self.custom_color_alpha.remove(name);
self.custom_color_refs.remove(name);
self.custom_raw_values.remove(name);
self.custom_font_stacks.remove(name);
}
fn set_custom_property_raw_value(&mut self, name: &str, raw: &str) {
let raw_preserved = raw.trim();
self.clear_custom_property_value(name);
if raw_preserved.is_empty() {
return;
}
let raw_lower = raw_preserved.to_ascii_lowercase();
self.custom_raw_values
.insert(name.to_string(), raw_preserved.to_string());
if let Some((color, alpha)) = parse_color_string(raw_preserved) {
let blended = if alpha < 1.0 {
Color::rgb(
color.r * alpha + (1.0 - alpha),
color.g * alpha + (1.0 - alpha),
color.b * alpha + (1.0 - alpha),
)
} else {
color
};
self.custom_colors.insert(name.to_string(), blended);
self.custom_color_alpha
.insert(name.to_string(), alpha.clamp(0.0, 1.0));
} else if let Some(color) = parse_rgb_triplet_string(raw_preserved) {
self.custom_colors.insert(name.to_string(), color);
self.custom_color_alpha.insert(name.to_string(), 1.0);
} else if raw_lower.contains("var(") {
self.custom_color_refs.insert(name.to_string(), raw_lower);
} else if let Some(spec) = length_spec_from_string(raw_preserved) {
self.custom_lengths.insert(name.to_string(), spec);
} else if let Some(stack) = font_stack_from_string(raw_preserved) {
self.custom_font_stacks.insert(name.to_string(), stack);
} else {
self.custom_color_refs.insert(name.to_string(), raw_lower);
}
}
}
fn apply_custom_property_registrations(
computed: &mut ComputedStyle,
registrations: &HashMap<String, RegisteredCustomProperty>,
) {
for (name, registration) in registrations {
if registration.inherits && computed.has_custom_property_value(name) {
continue;
}
if !registration.inherits {
computed.clear_custom_property_value(name);
}
if let Some(initial_value) = registration.initial_value.as_deref() {
computed.set_custom_property_raw_value(name, initial_value);
}
}
}
fn registered_custom_property_value_is_valid(
registration: &RegisteredCustomProperty,
raw: &str,
) -> bool {
let raw = raw.trim();
if raw.is_empty() {
return true;
}
let lower = raw.to_ascii_lowercase();
if matches!(
lower.as_str(),
"inherit" | "initial" | "unset" | "revert" | "revert-layer"
) || lower.contains("var(")
{
return true;
}
let syntax = normalize_registered_property_syntax(®istration.syntax);
if syntax == "*" {
return true;
}
let mut saw_supported_branch = false;
for branch in syntax.split('|') {
match registered_custom_property_branch_accepts(branch.trim(), raw) {
Some(true) => return true,
Some(false) => saw_supported_branch = true,
None => {}
}
}
!saw_supported_branch
}
fn normalize_registered_property_syntax(raw: &str) -> String {
raw.trim()
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_ascii_lowercase()
}
fn registered_custom_property_branch_accepts(syntax: &str, raw: &str) -> Option<bool> {
let syntax = syntax.trim().trim_end_matches(['+', '#']).trim();
match syntax {
"<color>" => Some(parse_color_string(raw).is_some()),
"<length>" => Some(
length_spec_from_string(raw)
.map(length_spec_is_registered_length)
.unwrap_or(false),
),
"<percentage>" => Some(
length_spec_from_string(raw)
.map(length_spec_is_registered_percentage)
.unwrap_or(false),
),
"<length-percentage>" => Some(length_spec_from_string(raw).is_some()),
"<number>" => Some(raw.trim().parse::<f32>().is_ok()),
"<integer>" => Some(raw.trim().parse::<i32>().is_ok()),
_ => None,
}
}
fn length_spec_is_registered_length(spec: LengthSpec) -> bool {
match spec {
LengthSpec::Percent(_) => false,
LengthSpec::Calc(calc) => calc.percent.abs() <= f32::EPSILON,
_ => true,
}
}
fn length_spec_is_registered_percentage(spec: LengthSpec) -> bool {
match spec {
LengthSpec::Percent(_) => true,
LengthSpec::Calc(calc) => {
calc.abs == Pt::ZERO
&& calc.em.abs() <= f32::EPSILON
&& calc.rem.abs() <= f32::EPSILON
&& calc.percent.abs() > f32::EPSILON
}
_ => false,
}
}
fn apply_registered_custom_property_initial(
computed: &mut ComputedStyle,
name: &str,
registration: &RegisteredCustomProperty,
) {
computed.clear_custom_property_value(name);
if let Some(initial_value) = registration.initial_value.as_deref() {
computed.set_custom_property_raw_value(name, initial_value);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BorderLineStyle {
None,
Hidden,
Style(OutlineLineStyle),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BorderStyleState {
top: BorderLineStyle,
right: BorderLineStyle,
bottom: BorderLineStyle,
left: BorderLineStyle,
}
impl BorderStyleState {
fn none() -> Self {
Self {
top: BorderLineStyle::None,
right: BorderLineStyle::None,
bottom: BorderLineStyle::None,
left: BorderLineStyle::None,
}
}
}
#[derive(Debug, Clone, Default)]
struct BorderStyleDelta {
top: Option<BorderLineStyle>,
right: Option<BorderLineStyle>,
bottom: Option<BorderLineStyle>,
left: Option<BorderLineStyle>,
}
impl BorderStyleDelta {
fn clear(&mut self, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
self.top = None;
self.right = None;
self.bottom = None;
self.left = None;
}
BoxEdgeTarget::Top => self.top = None,
BoxEdgeTarget::Right => self.right = None,
BoxEdgeTarget::Bottom => self.bottom = None,
BoxEdgeTarget::Left => self.left = None,
}
}
}
#[derive(Debug, Clone, Default)]
struct EdgeDelta {
top: Option<LengthSpec>,
right: Option<LengthSpec>,
bottom: Option<LengthSpec>,
left: Option<LengthSpec>,
top_var: Option<LengthVarExpr>,
right_var: Option<LengthVarExpr>,
bottom_var: Option<LengthVarExpr>,
left_var: Option<LengthVarExpr>,
}
impl EdgeDelta {
fn clear(&mut self, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
self.top = None;
self.right = None;
self.bottom = None;
self.left = None;
self.top_var = None;
self.right_var = None;
self.bottom_var = None;
self.left_var = None;
}
BoxEdgeTarget::Top => {
self.top = None;
self.top_var = None;
}
BoxEdgeTarget::Right => {
self.right = None;
self.right_var = None;
}
BoxEdgeTarget::Bottom => {
self.bottom = None;
self.bottom_var = None;
}
BoxEdgeTarget::Left => {
self.left = None;
self.left_var = None;
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BorderColorTarget {
All,
Top,
Right,
Bottom,
Left,
InlineStart,
InlineEnd,
BlockStart,
BlockEnd,
}
fn set_delta_border_color_spec(delta: &mut StyleDelta, target: BorderColorTarget, spec: ColorSpec) {
delta.border_cascade_ops.push(BorderCascadeOp::Color(
target,
NativeBorderColorComponent::Spec(spec.clone()),
));
clear_delta_border_color(delta, target);
clear_revert_layer_border_color(delta, target);
match target {
BorderColorTarget::All => {
delta.border_color = Some(spec.clone());
delta.border_top_color = Some(spec.clone());
delta.border_right_color = Some(spec.clone());
delta.border_bottom_color = Some(spec.clone());
delta.border_left_color = Some(spec);
delta.border_color_var = None;
delta.border_top_color_var = None;
delta.border_right_color_var = None;
delta.border_bottom_color_var = None;
delta.border_left_color_var = None;
}
BorderColorTarget::Top => {
delta.border_top_color = Some(spec);
delta.border_top_color_var = None;
}
BorderColorTarget::Right => {
delta.border_right_color = Some(spec);
delta.border_right_color_var = None;
}
BorderColorTarget::Bottom => {
delta.border_bottom_color = Some(spec);
delta.border_bottom_color_var = None;
}
BorderColorTarget::Left => {
delta.border_left_color = Some(spec);
delta.border_left_color_var = None;
}
BorderColorTarget::InlineStart => {
delta.border_inline_start_color = Some(spec);
delta.border_inline_start_color_var = None;
}
BorderColorTarget::InlineEnd => {
delta.border_inline_end_color = Some(spec);
delta.border_inline_end_color_var = None;
}
BorderColorTarget::BlockStart => {
delta.border_block_start_color = Some(spec);
delta.border_block_start_color_var = None;
}
BorderColorTarget::BlockEnd => {
delta.border_block_end_color = Some(spec);
delta.border_block_end_color_var = None;
}
}
}
fn set_delta_border_color_var(delta: &mut StyleDelta, target: BorderColorTarget, var: String) {
delta.border_cascade_ops.push(BorderCascadeOp::Color(
target,
NativeBorderColorComponent::Var(var.clone()),
));
clear_delta_border_color(delta, target);
clear_revert_layer_border_color(delta, target);
match target {
BorderColorTarget::All => {
delta.border_color_var = Some(var.clone());
delta.border_top_color_var = Some(var.clone());
delta.border_right_color_var = Some(var.clone());
delta.border_bottom_color_var = Some(var.clone());
delta.border_left_color_var = Some(var);
delta.border_color = None;
delta.border_top_color = None;
delta.border_right_color = None;
delta.border_bottom_color = None;
delta.border_left_color = None;
}
BorderColorTarget::Top => {
delta.border_top_color_var = Some(var);
delta.border_top_color = None;
}
BorderColorTarget::Right => {
delta.border_right_color_var = Some(var);
delta.border_right_color = None;
}
BorderColorTarget::Bottom => {
delta.border_bottom_color_var = Some(var);
delta.border_bottom_color = None;
}
BorderColorTarget::Left => {
delta.border_left_color_var = Some(var);
delta.border_left_color = None;
}
BorderColorTarget::InlineStart => {
delta.border_inline_start_color_var = Some(var);
delta.border_inline_start_color = None;
}
BorderColorTarget::InlineEnd => {
delta.border_inline_end_color_var = Some(var);
delta.border_inline_end_color = None;
}
BorderColorTarget::BlockStart => {
delta.border_block_start_color_var = Some(var);
delta.border_block_start_color = None;
}
BorderColorTarget::BlockEnd => {
delta.border_block_end_color_var = Some(var);
delta.border_block_end_color = None;
}
}
}
fn set_delta_border_width_spec(
delta: &mut StyleDelta,
target: BorderColorTarget,
spec: LengthSpec,
) {
delta.border_cascade_ops.push(BorderCascadeOp::Width(
target,
NativeLengthComponent::Spec(spec),
));
clear_delta_border_width(delta, target);
clear_revert_layer_border_width(delta, target);
match target {
BorderColorTarget::All => {
delta.border_width.top = Some(spec);
delta.border_width.right = Some(spec);
delta.border_width.bottom = Some(spec);
delta.border_width.left = Some(spec);
}
BorderColorTarget::Top => delta.border_width.top = Some(spec),
BorderColorTarget::Right => delta.border_width.right = Some(spec),
BorderColorTarget::Bottom => delta.border_width.bottom = Some(spec),
BorderColorTarget::Left => delta.border_width.left = Some(spec),
BorderColorTarget::InlineStart => delta.border_inline_start_width = Some(spec),
BorderColorTarget::InlineEnd => delta.border_inline_end_width = Some(spec),
BorderColorTarget::BlockStart => delta.border_block_start_width = Some(spec),
BorderColorTarget::BlockEnd => delta.border_block_end_width = Some(spec),
}
}
fn set_delta_border_width_var(
delta: &mut StyleDelta,
target: BorderColorTarget,
expr: LengthVarExpr,
) {
delta.border_cascade_ops.push(BorderCascadeOp::Width(
target,
NativeLengthComponent::Var(expr.name.clone()),
));
clear_delta_border_width(delta, target);
clear_revert_layer_border_width(delta, target);
match target {
BorderColorTarget::All => {
delta.border_width.top_var = Some(expr.clone());
delta.border_width.right_var = Some(expr.clone());
delta.border_width.bottom_var = Some(expr.clone());
delta.border_width.left_var = Some(expr);
}
BorderColorTarget::Top => delta.border_width.top_var = Some(expr),
BorderColorTarget::Right => delta.border_width.right_var = Some(expr),
BorderColorTarget::Bottom => delta.border_width.bottom_var = Some(expr),
BorderColorTarget::Left => delta.border_width.left_var = Some(expr),
BorderColorTarget::InlineStart => delta.border_inline_start_width_var = Some(expr),
BorderColorTarget::InlineEnd => delta.border_inline_end_width_var = Some(expr),
BorderColorTarget::BlockStart => delta.border_block_start_width_var = Some(expr),
BorderColorTarget::BlockEnd => delta.border_block_end_width_var = Some(expr),
}
}
fn set_delta_border_style(
delta: &mut StyleDelta,
target: BorderColorTarget,
style: BorderLineStyle,
) {
delta
.border_cascade_ops
.push(BorderCascadeOp::Style(target, style));
clear_delta_border_style(delta, target);
clear_revert_layer_border_style(delta, target);
match target {
BorderColorTarget::All => {
delta.border_style.top = Some(style);
delta.border_style.right = Some(style);
delta.border_style.bottom = Some(style);
delta.border_style.left = Some(style);
}
BorderColorTarget::Top => delta.border_style.top = Some(style),
BorderColorTarget::Right => delta.border_style.right = Some(style),
BorderColorTarget::Bottom => delta.border_style.bottom = Some(style),
BorderColorTarget::Left => delta.border_style.left = Some(style),
BorderColorTarget::InlineStart => delta.border_inline_start_style = Some(style),
BorderColorTarget::InlineEnd => delta.border_inline_end_style = Some(style),
BorderColorTarget::BlockStart => delta.border_block_start_style = Some(style),
BorderColorTarget::BlockEnd => delta.border_block_end_style = Some(style),
}
}
fn set_delta_outline_width(delta: &mut StyleDelta, value: LengthSpec) {
delta.outline_width = Some(value);
delta.revert_layer.outline_width = false;
}
fn set_delta_outline_offset(delta: &mut StyleDelta, value: LengthSpec) {
delta.outline_offset = Some(value);
delta.outline_offset_var = None;
delta.revert_layer.outline_offset = false;
}
fn set_delta_outline_offset_var(delta: &mut StyleDelta, value: String) {
delta.outline_offset = None;
delta.outline_offset_var = Some(value);
delta.revert_layer.outline_offset = false;
}
fn set_delta_outline_color(delta: &mut StyleDelta, value: ColorSpec) {
delta.outline_color = Some(value);
delta.revert_layer.outline_color = false;
}
fn set_delta_outline_style(delta: &mut StyleDelta, value: Option<OutlineLineStyle>) {
delta.outline_style = value;
delta.outline_visible = Some(value.is_some());
delta.revert_layer.outline_style = false;
}
fn border_color_target_from_property_name(property_name: &str) -> BorderColorTarget {
match property_name {
"border-top" | "border-top-color" | "border-top-width" | "border-top-style" => {
BorderColorTarget::Top
}
"border-block-start"
| "border-block-start-color"
| "border-block-start-width"
| "border-block-start-style" => BorderColorTarget::BlockStart,
"border-right" | "border-right-color" | "border-right-width" | "border-right-style" => {
BorderColorTarget::Right
}
"border-inline-end"
| "border-inline-end-color"
| "border-inline-end-width"
| "border-inline-end-style" => BorderColorTarget::InlineEnd,
"border-bottom" | "border-bottom-color" | "border-bottom-width" | "border-bottom-style" => {
BorderColorTarget::Bottom
}
"border-block-end"
| "border-block-end-color"
| "border-block-end-width"
| "border-block-end-style" => BorderColorTarget::BlockEnd,
"border-left" | "border-left-color" | "border-left-width" | "border-left-style" => {
BorderColorTarget::Left
}
"border-inline-start"
| "border-inline-start-color"
| "border-inline-start-width"
| "border-inline-start-style" => BorderColorTarget::InlineStart,
_ => BorderColorTarget::All,
}
}
#[derive(Debug, Clone)]
struct LengthVarExpr {
name: String,
scale: f32,
}
#[derive(Debug, Clone)]
pub struct ElementInfo {
pub tag: String,
pub id: Option<String>,
pub classes: Vec<String>,
pub attrs: HashMap<String, String>,
pub container_names: Vec<String>,
pub container_type: ContainerQueryType,
pub container_width: Option<Pt>,
pub container_height: Option<Pt>,
pub container_inline_size: Option<Pt>,
pub container_block_size: Option<Pt>,
pub is_root: bool,
pub is_empty: bool,
pub is_defined: bool,
pub language: Option<String>,
pub direction: Option<String>,
pub child_index: usize,
pub child_count: usize,
pub type_index: usize,
pub type_count: usize,
pub prev_siblings: Vec<ElementInfo>,
pub next_siblings: Vec<ElementInfo>,
pub children: Vec<ElementInfo>,
}
impl ElementInfo {
fn synthetic_root(tag: &str) -> Self {
Self {
tag: tag.to_string(),
id: None,
classes: Vec::new(),
attrs: HashMap::new(),
container_names: Vec::new(),
container_type: ContainerQueryType::Normal,
container_width: None,
container_height: None,
container_inline_size: None,
container_block_size: None,
is_root: true,
is_empty: false,
is_defined: true,
language: None,
direction: None,
child_index: 1,
child_count: 1,
type_index: 1,
type_count: 1,
prev_siblings: Vec::new(),
next_siblings: Vec::new(),
children: Vec::new(),
}
}
pub fn apply_computed_container_style(&mut self, style: &ComputedStyle) {
self.container_names = style.container_names.clone();
self.container_type = style.container_type;
self.container_width = explicit_container_length_to_pt(style.width, style);
self.container_height = explicit_container_length_to_pt(style.height, style);
if is_vertical_writing_mode(style.writing_mode) {
self.container_inline_size = self.container_height;
self.container_block_size = self.container_width;
} else {
self.container_inline_size = self.container_width;
self.container_block_size = self.container_height;
}
}
}
fn explicit_container_length_to_pt(spec: LengthSpec, style: &ComputedStyle) -> Option<Pt> {
match spec {
LengthSpec::Absolute(value) => Some(value),
LengthSpec::Em(value) => Some(style.font_size * value),
LengthSpec::Rem(value) => Some(style.root_font_size * value),
LengthSpec::Calc(calc) if calc.percent.abs() <= f32::EPSILON => {
Some(calc.resolve(Pt::ZERO, style.font_size, style.root_font_size))
}
_ => None,
}
}
pub struct StyleResolver {
normal_rules: Vec<RuleEntry>,
important_rules: Vec<RuleEntry>,
normal_index: RuleIndex,
important_index: RuleIndex,
root_font_size: Pt,
viewport: Size,
font_registry: Option<Arc<FontRegistry>>,
author_rule_order_start: usize,
debug: Option<Arc<DebugLogger>>,
root_normal: Vec<StyleDelta>,
root_important: Vec<StyleDelta>,
counter_styles: HashMap<String, AnonymousListStyleSymbols>,
custom_property_registrations: HashMap<String, RegisteredCustomProperty>,
font_face_aliases: HashMap<String, Vec<CompiledFontFace>>,
layer_count: usize,
has_positional_selectors: bool,
has_sibling_selectors: bool,
has_target_counter_content: bool,
page_footnote_area: CssPageFootnoteArea,
}
#[derive(Debug, Clone)]
struct RegisteredCustomProperty {
syntax: String,
inherits: bool,
initial_value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FontFaceStyleDescriptor {
Normal,
Italic,
Oblique,
}
#[derive(Debug, Clone)]
struct CompiledFontFace {
sources: Vec<Arc<str>>,
weight_min: u16,
weight_max: u16,
stretch_min: u16,
stretch_max: u16,
style: FontFaceStyleDescriptor,
size_adjust: I32F32,
unicode_ranges: Option<Arc<[(u32, u32)]>>,
}
#[derive(Debug, Clone)]
struct ResolvedFontFace {
font_name: Arc<str>,
size_adjust: I32F32,
unicode_ranges: Option<Arc<[(u32, u32)]>>,
satisfies_weight: bool,
satisfies_style: bool,
}
impl CompiledFontFace {
fn match_score(&self, weight: u16, stretch: u16, style: FontStyleMode) -> (u16, u8, u16) {
let requested_style = match style {
FontStyleMode::Normal => FontFaceStyleDescriptor::Normal,
FontStyleMode::Italic => FontFaceStyleDescriptor::Italic,
FontStyleMode::Oblique(_) => FontFaceStyleDescriptor::Oblique,
};
let style_distance = if self.style == requested_style {
0
} else if !matches!(self.style, FontFaceStyleDescriptor::Normal)
&& !matches!(requested_style, FontFaceStyleDescriptor::Normal)
{
1
} else {
2
};
let weight_distance = if weight < self.weight_min {
self.weight_min - weight
} else if weight > self.weight_max {
weight - self.weight_max
} else {
0
};
let stretch_distance = if stretch < self.stretch_min {
self.stretch_min - stretch
} else if stretch > self.stretch_max {
stretch - self.stretch_max
} else {
0
};
(stretch_distance, style_distance, weight_distance)
}
fn satisfies_weight(&self, weight: u16) -> bool {
(self.weight_min..=self.weight_max).contains(&weight)
}
fn satisfies_style(&self, style: FontStyleMode) -> bool {
match (self.style, style) {
(FontFaceStyleDescriptor::Normal, FontStyleMode::Normal) => true,
(
FontFaceStyleDescriptor::Italic | FontFaceStyleDescriptor::Oblique,
FontStyleMode::Italic | FontStyleMode::Oblique(_),
) => true,
_ => false,
}
}
}
fn normalized_font_face_name(name: &str) -> String {
name.trim()
.trim_matches('"')
.trim_matches('\'')
.to_ascii_lowercase()
}
fn native_font_face_family_name(raw: &str) -> Option<String> {
let tokens = crate::css_native::tokenize_component_values(raw)?;
let name = match tokens.as_slice() {
[ComponentValue::String(name)] => name.clone(),
values
if values
.iter()
.all(|value| matches!(value, ComponentValue::Ident(_))) =>
{
values
.iter()
.filter_map(|value| match value {
ComponentValue::Ident(name) => Some(name.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(" ")
}
_ => return None,
};
(!name.trim().is_empty()).then_some(name)
}
fn native_font_face_function_argument(raw: &str) -> Option<String> {
if let Some(tokens) = crate::css_native::tokenize_component_values(raw) {
match tokens.as_slice() {
[ComponentValue::String(value)] | [ComponentValue::Ident(value)] => {
return Some(value.clone());
}
values
if values
.iter()
.all(|value| matches!(value, ComponentValue::Ident(_))) =>
{
let value = values
.iter()
.filter_map(|value| match value {
ComponentValue::Ident(name) => Some(name.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(" ");
if !value.is_empty() {
return Some(value);
}
}
_ => {}
}
}
let value = raw.trim().trim_matches('"').trim_matches('\'').trim();
(!value.is_empty()).then(|| value.to_string())
}
fn native_font_face_url_stem(raw: &str) -> Option<String> {
let value = native_font_face_function_argument(raw)?;
if value.trim_start().to_ascii_lowercase().starts_with("data:") {
return None;
}
let end = value
.char_indices()
.filter(|(_, ch)| matches!(ch, '?' | '#'))
.map(|(index, _)| index)
.min()
.unwrap_or(value.len());
let normalized = value[..end].replace('\\', "/");
let file_name = normalized.rsplit('/').next()?.trim();
if file_name.is_empty() {
return None;
}
let stem = file_name
.rsplit_once('.')
.map(|(stem, _)| stem)
.unwrap_or(file_name)
.trim();
(!stem.is_empty()).then(|| stem.to_string())
}
fn native_font_face_weight_descriptor(raw: Option<&str>) -> (u16, u16) {
let Some(raw) = raw else {
return (400, 400);
};
let weights: Vec<u16> = raw
.split_whitespace()
.filter_map(|part| match part.to_ascii_lowercase().as_str() {
"normal" => Some(400),
"bold" => Some(700),
_ => part.parse::<u16>().ok().map(|weight| weight.clamp(1, 1000)),
})
.collect();
match weights.as_slice() {
[weight] => (*weight, *weight),
[first, second, ..] => ((*first).min(*second), (*first).max(*second)),
_ => (400, 400),
}
}
fn native_font_stretch_value(raw: &str) -> Option<u16> {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"ultra-condensed" => 500,
"extra-condensed" => 625,
"condensed" => 750,
"semi-condensed" => 875,
"normal" => 1000,
"semi-expanded" => 1125,
"expanded" => 1250,
"extra-expanded" => 1500,
"ultra-expanded" => 2000,
_ => {
let percent = lowered.strip_suffix('%')?.trim().parse::<f32>().ok()?;
if !percent.is_finite() || percent <= 0.0 {
return None;
}
(percent * 10.0).round().clamp(1.0, 10_000.0) as u16
}
};
Some(value)
}
fn native_font_face_stretch_descriptor(raw: Option<&str>) -> (u16, u16) {
let Some(raw) = raw else {
return (1000, 1000);
};
let values: Vec<u16> = raw
.split_whitespace()
.filter_map(native_font_stretch_value)
.collect();
match values.as_slice() {
[value] => (*value, *value),
[first, second, ..] => ((*first).min(*second), (*first).max(*second)),
_ => (1000, 1000),
}
}
fn native_font_face_style_descriptor(raw: Option<&str>) -> FontFaceStyleDescriptor {
let Some(raw) = raw else {
return FontFaceStyleDescriptor::Normal;
};
match raw
.split_whitespace()
.next()
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
"italic" => FontFaceStyleDescriptor::Italic,
"oblique" => FontFaceStyleDescriptor::Oblique,
_ => FontFaceStyleDescriptor::Normal,
}
}
fn native_font_face_size_adjust_descriptor(raw: Option<&str>) -> I32F32 {
raw.and_then(parse_percent_scalar)
.filter(|value| value.is_finite() && *value >= 0.0)
.map(I32F32::from_num)
.unwrap_or_else(|| I32F32::from_num(1.0))
}
fn native_font_face_unicode_range_descriptor(raw: Option<&str>) -> Option<Arc<[(u32, u32)]>> {
let raw = raw?;
let mut ranges = Vec::new();
for part in raw.split(',') {
let part = part.trim();
let Some(hex) = part
.get(..2)
.filter(|prefix| prefix.eq_ignore_ascii_case("u+"))
.and_then(|_| part.get(2..))
.map(str::trim)
else {
continue;
};
let parsed = if hex.contains('?') {
if hex.len() > 6
|| hex.is_empty()
|| !hex.chars().all(|ch| ch == '?' || ch.is_ascii_hexdigit())
{
None
} else {
let start = hex.replace('?', "0");
let end = hex.replace('?', "F");
u32::from_str_radix(&start, 16)
.ok()
.zip(u32::from_str_radix(&end, 16).ok())
}
} else if let Some((start, end)) = hex.split_once('-') {
let valid = !start.is_empty()
&& !end.is_empty()
&& start.len() <= 6
&& end.len() <= 6
&& start.chars().all(|ch| ch.is_ascii_hexdigit())
&& end.chars().all(|ch| ch.is_ascii_hexdigit());
valid.then(|| {
u32::from_str_radix(start, 16)
.ok()
.zip(u32::from_str_radix(end, 16).ok())
})?
} else if !hex.is_empty() && hex.len() <= 6 && hex.chars().all(|ch| ch.is_ascii_hexdigit())
{
u32::from_str_radix(hex, 16)
.ok()
.map(|value| (value, value))
} else {
None
};
if let Some((start, end)) = parsed {
if start <= end && start <= 0x10_FFFF {
ranges.push((start, end.min(0x10_FFFF)));
}
}
}
(!ranges.is_empty()).then(|| Arc::from(ranges.into_boxed_slice()))
}
fn native_font_face_alias(
block: &crate::css_native::DeclarationBlock,
) -> Option<(String, CompiledFontFace)> {
let descriptor = |expected: &str| {
block
.declarations
.iter()
.filter(|item| item.name_eq(expected))
.map(|item| item.value.trim())
.last()
};
let family = native_font_face_family_name(descriptor("font-family")?)?;
let family = normalized_font_face_name(&family);
if family.is_empty() {
return None;
}
let raw_sources = descriptor("src")?;
let mut sources = Vec::new();
let source_entries = crate::css_native::split_top_level(raw_sources, ',')
.unwrap_or_else(|_| vec![raw_sources.to_string()]);
for source_entry in source_entries {
let Some(tokens) = crate::css_native::tokenize_component_values(&source_entry) else {
continue;
};
let source = tokens.into_iter().find_map(|token| match token {
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("local") => {
native_font_face_function_argument(&arguments)
}
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("url") => {
native_font_face_url_stem(&arguments)
}
_ => None,
});
let Some(source) = source.filter(|source| !source.trim().is_empty()) else {
continue;
};
if !sources
.iter()
.any(|existing: &Arc<str>| existing.eq_ignore_ascii_case(&source))
{
sources.push(Arc::<str>::from(source));
}
}
let (weight_min, weight_max) = native_font_face_weight_descriptor(descriptor("font-weight"));
let (stretch_min, stretch_max) =
native_font_face_stretch_descriptor(descriptor("font-stretch"));
(!sources.is_empty()).then_some((
family,
CompiledFontFace {
sources,
weight_min,
weight_max,
stretch_min,
stretch_max,
style: native_font_face_style_descriptor(descriptor("font-style")),
size_adjust: native_font_face_size_adjust_descriptor(descriptor("size-adjust")),
unicode_ranges: native_font_face_unicode_range_descriptor(descriptor("unicode-range")),
},
))
}
pub(crate) fn css_page_name_id(name: &str) -> u64 {
// Stable FNV-1a keeps pagination metadata Copy-sized and independent of
// process-local string interning while still letting the style and page
// template compilers rendezvous on the same named-page selector.
let mut hash = 0xcbf29ce484222325u64;
for byte in name.trim().bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CssPageMarginBoxKind {
TopLeftCorner,
TopLeft,
TopCenter,
TopRight,
TopRightCorner,
BottomLeftCorner,
BottomLeft,
BottomCenter,
BottomRight,
BottomRightCorner,
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CssPageContentPosition {
Start,
First,
Last,
FirstExcept,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CssPageMarginContentPart {
Text(String),
PageCounter,
PagesCounter,
RunningElement {
name: String,
position: CssPageContentPosition,
},
NamedString {
name: String,
position: CssPageContentPosition,
},
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CssPageMarginBox {
pub kind: Option<CssPageMarginBoxKind>,
/// `None` means the declaration did not specify `content`; an empty vector
/// means it explicitly suppresses generated margin-box content.
pub content: Option<Vec<CssPageMarginContentPart>>,
pub color: Option<Color>,
pub background: Option<(Color, f32)>,
pub width: Option<Pt>,
pub font_name: Option<String>,
pub font_size: Option<Pt>,
pub font_weight: Option<u16>,
pub line_height: Option<CssPageLineHeight>,
}
impl CssPageMarginBox {
fn cascaded_with(&self, declarations: &Self) -> Self {
Self {
kind: declarations.kind.or(self.kind),
content: declarations
.content
.clone()
.or_else(|| self.content.clone()),
color: declarations.color.or(self.color),
background: declarations.background.or(self.background),
width: declarations.width.or(self.width),
font_name: declarations
.font_name
.clone()
.or_else(|| self.font_name.clone()),
font_size: declarations.font_size.or(self.font_size),
font_weight: declarations.font_weight.or(self.font_weight),
line_height: declarations.line_height.or(self.line_height),
}
}
}
/// Cascaded declarations from the GCPM `@footnote` page area. Optional
/// fields preserve whether a nested page rule actually authored a value, so
/// named/pseudo page setups can layer over the base setup without resetting
/// unrelated geometry.
#[derive(Debug, Clone, Default)]
pub(crate) struct CssPageFootnoteArea {
pub border_top_width: Option<Pt>,
pub border_top_color: Option<Color>,
pub border_top_visible: Option<bool>,
pub padding_top: Option<Pt>,
pub max_height: Option<Pt>,
}
impl CssPageFootnoteArea {
fn cascaded_with(&self, declarations: &Self) -> Self {
Self {
border_top_width: declarations.border_top_width.or(self.border_top_width),
border_top_color: declarations.border_top_color.or(self.border_top_color),
border_top_visible: declarations.border_top_visible.or(self.border_top_visible),
padding_top: declarations.padding_top.or(self.padding_top),
max_height: declarations.max_height.or(self.max_height),
}
}
fn has_override(&self) -> bool {
self.border_top_width.is_some()
|| self.border_top_color.is_some()
|| self.border_top_visible.is_some()
|| self.padding_top.is_some()
|| self.max_height.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum CssPageLineHeight {
Absolute(Pt),
Number(f32),
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CssPageSetup {
pub size: Option<Size>,
pub bleed: Option<Pt>,
pub marks: Option<PageMarks>,
pub page_orientation: Option<PageOrientation>,
pub margin_top: Option<Pt>,
pub margin_right: Option<Pt>,
pub margin_bottom: Option<Pt>,
pub margin_left: Option<Pt>,
pub margin_top_percent: Option<f32>,
pub margin_right_percent: Option<f32>,
pub margin_bottom_percent: Option<f32>,
pub margin_left_percent: Option<f32>,
pub background: Option<(Color, f32)>,
pub color: Option<Color>,
pub font_name: Option<String>,
pub font_size: Option<Pt>,
pub font_weight: Option<u16>,
pub line_height: Option<CssPageLineHeight>,
pub page_counter_reset: Option<i32>,
pub page_counter_increment: Option<i32>,
pub margin_boxes: Vec<CssPageMarginBox>,
pub footnote_area: CssPageFootnoteArea,
}
impl CssPageSetup {
pub fn page_presentation(&self) -> PagePresentation {
PagePresentation {
bleed: self.bleed.unwrap_or(Pt::ZERO).max(Pt::ZERO),
marks: self.marks.unwrap_or_default(),
orientation: self.page_orientation.unwrap_or_default(),
}
}
fn has_page_presentation_override(&self) -> bool {
self.bleed.is_some() || self.marks.is_some() || self.page_orientation.is_some()
}
pub fn has_margin_override(&self) -> bool {
self.margin_top.is_some()
|| self.margin_right.is_some()
|| self.margin_bottom.is_some()
|| self.margin_left.is_some()
|| self.margin_top_percent.is_some()
|| self.margin_right_percent.is_some()
|| self.margin_bottom_percent.is_some()
|| self.margin_left_percent.is_some()
}
pub fn resolve_margins(&self, base: Margins, page_size: Size) -> Option<Margins> {
if !self.has_margin_override() {
return None;
}
Some(Margins {
top: self
.margin_top
.or_else(|| {
self.margin_top_percent
.map(|value| page_size.height * value)
})
.unwrap_or(base.top),
right: self
.margin_right
.or_else(|| {
self.margin_right_percent
.map(|value| page_size.width * value)
})
.unwrap_or(base.right),
bottom: self
.margin_bottom
.or_else(|| {
self.margin_bottom_percent
.map(|value| page_size.height * value)
})
.unwrap_or(base.bottom),
left: self
.margin_left
.or_else(|| {
self.margin_left_percent
.map(|value| page_size.width * value)
})
.unwrap_or(base.left),
})
}
pub fn cascaded_with(&self, declarations: &Self) -> Self {
let cascade_margin = |base_absolute: Option<Pt>,
base_percent: Option<f32>,
absolute: Option<Pt>,
percent: Option<f32>| {
if absolute.is_some() || percent.is_some() {
(absolute, percent)
} else {
(base_absolute, base_percent)
}
};
let (margin_top, margin_top_percent) = cascade_margin(
self.margin_top,
self.margin_top_percent,
declarations.margin_top,
declarations.margin_top_percent,
);
let (margin_right, margin_right_percent) = cascade_margin(
self.margin_right,
self.margin_right_percent,
declarations.margin_right,
declarations.margin_right_percent,
);
let (margin_bottom, margin_bottom_percent) = cascade_margin(
self.margin_bottom,
self.margin_bottom_percent,
declarations.margin_bottom,
declarations.margin_bottom_percent,
);
let (margin_left, margin_left_percent) = cascade_margin(
self.margin_left,
self.margin_left_percent,
declarations.margin_left,
declarations.margin_left_percent,
);
let mut margin_boxes = self.margin_boxes.clone();
for declarations in &declarations.margin_boxes {
let Some(kind) = declarations.kind else {
continue;
};
if let Some(existing) = margin_boxes
.iter_mut()
.find(|candidate| candidate.kind == Some(kind))
{
*existing = existing.cascaded_with(declarations);
} else {
margin_boxes.push(declarations.clone());
}
}
Self {
size: declarations.size.or(self.size),
bleed: declarations.bleed.or(self.bleed),
marks: declarations.marks.or(self.marks),
page_orientation: declarations.page_orientation.or(self.page_orientation),
margin_top,
margin_right,
margin_bottom,
margin_left,
margin_top_percent,
margin_right_percent,
margin_bottom_percent,
margin_left_percent,
background: declarations.background.or(self.background),
color: declarations.color.or(self.color),
font_name: declarations
.font_name
.clone()
.or_else(|| self.font_name.clone()),
font_size: declarations.font_size.or(self.font_size),
font_weight: declarations.font_weight.or(self.font_weight),
line_height: declarations.line_height.or(self.line_height),
page_counter_reset: declarations.page_counter_reset.or(self.page_counter_reset),
page_counter_increment: declarations
.page_counter_increment
.or(self.page_counter_increment),
margin_boxes,
footnote_area: self
.footnote_area
.cascaded_with(&declarations.footnote_area),
}
}
fn has_margin_box_override(&self) -> bool {
!self.margin_boxes.is_empty()
}
fn has_page_context_override(&self) -> bool {
self.color.is_some()
|| self.font_name.is_some()
|| self.font_size.is_some()
|| self.font_weight.is_some()
|| self.line_height.is_some()
|| self.page_counter_reset.is_some()
|| self.page_counter_increment.is_some()
|| self.footnote_area.has_override()
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CssNamedPageStyles {
pub id: u64,
pub name: String,
pub base: CssPageSetup,
pub first: CssPageSetup,
pub left: CssPageSetup,
pub right: CssPageSetup,
pub blank: CssPageSetup,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CssPageStyles {
pub base: CssPageSetup,
pub first: CssPageSetup,
pub left: CssPageSetup,
pub right: CssPageSetup,
pub blank: CssPageSetup,
pub named: Vec<CssNamedPageStyles>,
}
impl CssPageStyles {
pub fn has_pseudo_rules(&self) -> bool {
self.base.has_margin_box_override()
|| self.base.has_page_presentation_override()
|| !self.named.is_empty()
|| self.first.size.is_some()
|| self.left.size.is_some()
|| self.right.size.is_some()
|| self.blank.size.is_some()
|| self.first.has_margin_override()
|| self.left.has_margin_override()
|| self.right.has_margin_override()
|| self.blank.has_margin_override()
|| self.first.background.is_some()
|| self.left.background.is_some()
|| self.right.background.is_some()
|| self.blank.background.is_some()
|| self.first.has_margin_box_override()
|| self.left.has_margin_box_override()
|| self.right.has_margin_box_override()
|| self.blank.has_margin_box_override()
|| self.first.has_page_context_override()
|| self.left.has_page_context_override()
|| self.right.has_page_context_override()
|| self.blank.has_page_context_override()
}
fn named_mut(&mut self, name: &str) -> &mut CssNamedPageStyles {
let id = css_page_name_id(name);
if let Some(index) = self.named.iter().position(|page| page.id == id) {
return &mut self.named[index];
}
self.named.push(CssNamedPageStyles {
id,
name: name.trim().to_string(),
..CssNamedPageStyles::default()
});
self.named.last_mut().expect("named page was appended")
}
}
#[derive(Default)]
struct RuleIndex {
by_tag: HashMap<String, Vec<usize>>,
by_id: HashMap<String, Vec<usize>>,
by_class: HashMap<String, Vec<usize>>,
universal: Vec<usize>,
}
impl RuleIndex {
fn new(rules: &[RuleEntry]) -> Self {
let mut index = Self::default();
for (i, rule) in rules.iter().enumerate() {
let Some(last) = rule.selector.parts.last() else {
continue;
};
let mut indexed = false;
if let Some(id) = &last.id {
index.by_id.entry(id.clone()).or_default().push(i);
indexed = true;
}
for class in &last.classes {
index.by_class.entry(class.clone()).or_default().push(i);
indexed = true;
}
if let Some(tag) = &last.tag {
if tag != "*" {
index.by_tag.entry(tag.clone()).or_default().push(i);
indexed = true;
}
}
if !indexed {
index.universal.push(i);
}
}
index
}
fn candidate_indices(&self, element: &ElementInfo) -> Vec<usize> {
let mut out: Vec<usize> = Vec::new();
if let Some(id) = &element.id {
if let Some(v) = self.by_id.get(id) {
out.extend(v);
}
}
for class in &element.classes {
if let Some(v) = self.by_class.get(class) {
out.extend(v);
}
}
if let Some(v) = self.by_tag.get(&element.tag) {
out.extend(v);
}
out.extend(&self.universal);
out.sort_unstable();
out.dedup();
out
}
}
impl StyleResolver {
#[cfg(test)]
pub fn new(css: &str) -> Self {
Self::new_with_debug(css, None)
}
#[allow(dead_code)]
pub fn new_with_debug(css: &str, debug: Option<Arc<DebugLogger>>) -> Self {
Self::new_with_debug_and_viewport(css, debug, None)
}
pub fn new_with_debug_and_viewport(
css: &str,
debug: Option<Arc<DebugLogger>>,
viewport: Option<Size>,
) -> Self {
Self::new_with_debug_viewport_and_fonts(css, debug, viewport, None)
}
pub(crate) fn new_with_debug_viewport_and_fonts(
css: &str,
debug: Option<Arc<DebugLogger>>,
viewport: Option<Size>,
font_registry: Option<Arc<FontRegistry>>,
) -> Self {
let has_target_counter_content = css.to_ascii_lowercase().contains("target-counter(");
let viewport = viewport.unwrap_or(Size {
width: Pt::ZERO,
height: Pt::ZERO,
});
let mut normal_rules = Vec::new();
let mut important_rules = Vec::new();
let mut order = 0usize;
let mut root_normal = Vec::new();
let mut root_important = Vec::new();
let mut counter_styles = HashMap::new();
let mut custom_property_registrations = HashMap::new();
let mut font_face_aliases = HashMap::new();
let mut layer_orders = HashMap::new();
let mut next_layer_order = 0usize;
let mut has_positional_selectors = false;
let mut has_sibling_selectors = false;
fn scoped_layer_name(parent: Option<&str>, name: &str) -> String {
if let Some(parent) = parent.filter(|value| !value.is_empty()) {
format!("{parent}.{name}")
} else {
name.to_string()
}
}
fn register_layer(
name: String,
layer_orders: &mut HashMap<String, usize>,
next_layer_order: &mut usize,
) -> usize {
if let Some(order) = layer_orders.get(&name) {
return *order;
}
let order = *next_layer_order;
*next_layer_order = next_layer_order.saturating_add(1);
layer_orders.insert(name, order);
order
}
fn append_native_rule_list(
rules: &[crate::css_native::Rule],
normal_rules: &mut Vec<RuleEntry>,
important_rules: &mut Vec<RuleEntry>,
order: &mut usize,
root_normal: &mut Vec<StyleDelta>,
root_important: &mut Vec<StyleDelta>,
counter_styles: &mut HashMap<String, AnonymousListStyleSymbols>,
custom_property_registrations: &mut HashMap<String, RegisteredCustomProperty>,
font_face_aliases: &mut HashMap<String, Vec<CompiledFontFace>>,
layer_orders: &mut HashMap<String, usize>,
next_layer_order: &mut usize,
current_layer_order: Option<usize>,
current_layer_name: Option<String>,
container_conditions: &[ContainerRuleCondition],
scope_conditions: &[ScopeRuleCondition],
debug: Option<&DebugLogger>,
has_positional_selectors: &mut bool,
has_sibling_selectors: &mut bool,
viewport: Size,
prefer_print: bool,
) {
for rule in rules {
match rule {
crate::css_native::Rule::Style(style) => {
let (normal_delta, important_delta) =
style_from_native_declarations(&style.declarations, viewport);
let selectors = crate::css_native::split_top_level(&style.selectors, ',')
.unwrap_or_else(|_| vec![style.selectors.clone()]);
if let Some(logger) = debug {
log_native_declaration_no_effects(
&style.declarations,
&style.selectors,
logger,
);
for (name, token_debug) in
collect_native_custom_properties(&style.declarations)
{
let json = format!(
"{{\"type\":\"css.custom\",\"name\":{},\"value\":{},\"tokens\":{}}}",
json_string(&name),
json_string(&token_debug),
json_string(&token_debug)
);
logger.log_json(&json);
}
for selector in &selectors {
let parsed = parse_selector_pattern(selector).is_some();
let json = format!(
"{{\"type\":\"css.rule\",\"selector\":{},\"parsed\":{}}}",
json_string(selector.trim()),
if parsed { "true" } else { "false" }
);
logger.log_json(&json);
if !parsed {
logger.increment("css.selector_unparsed", 1);
}
}
}
for selector in selectors {
let selector_trimmed = selector.trim();
let is_root_selector =
selector_trimmed.to_ascii_lowercase().contains(":root");
if let Some(pattern) = parse_selector_pattern(selector_trimmed) {
if pattern.has_positional_pseudos() {
*has_positional_selectors = true;
}
if pattern.has_sibling_combinators()
|| pattern.has_relational_pseudos()
{
*has_sibling_selectors = true;
}
if !normal_delta.is_empty() {
normal_rules.push(RuleEntry {
specificity: pattern.specificity(),
selector: pattern.clone(),
order: *order,
layer_order: current_layer_order,
container_conditions: container_conditions.to_vec(),
scope_conditions: scope_conditions.to_vec(),
delta: normal_delta.clone(),
selector_text: selector_trimmed.to_string(),
});
}
if !important_delta.is_empty() {
important_rules.push(RuleEntry {
specificity: pattern.specificity(),
selector: pattern.clone(),
order: *order,
layer_order: current_layer_order,
container_conditions: container_conditions.to_vec(),
scope_conditions: scope_conditions.to_vec(),
delta: important_delta.clone(),
selector_text: selector_trimmed.to_string(),
});
}
} else if is_root_selector
&& container_conditions.is_empty()
&& scope_conditions.is_empty()
{
if !normal_delta.is_empty() {
root_normal.push(normal_delta.clone());
}
if !important_delta.is_empty() {
root_important.push(important_delta.clone());
}
}
}
*order = order.saturating_add(1);
}
crate::css_native::Rule::At(at_rule) => match at_rule.name.as_str() {
"counter-style" => {
if let Some(crate::css_native::AtRuleBlock::Declarations(block)) =
&at_rule.block
{
if let Some((name, symbols)) =
native_counter_style_symbols(&at_rule.prelude, block)
{
counter_styles.insert(name, symbols);
}
}
}
"property" => {
if let Some(crate::css_native::AtRuleBlock::Declarations(block)) =
&at_rule.block
{
let name = at_rule.prelude.trim().to_ascii_lowercase();
if name.starts_with("--") {
let descriptor = |expected: &str| {
block
.declarations
.iter()
.filter(|item| item.name_eq(expected))
.map(|item| item.value.trim().to_string())
.last()
};
let syntax = descriptor("syntax").unwrap_or_default();
let inherits = descriptor("inherits")
.is_some_and(|value| value.eq_ignore_ascii_case("true"));
let initial_value = descriptor("initial-value")
.filter(|value| !value.is_empty());
custom_property_registrations.insert(
name,
RegisteredCustomProperty {
syntax,
inherits,
initial_value: initial_value.clone(),
},
);
if let Some(logger) = debug {
logger.increment("css.property.rules", 1);
logger.increment("css.property.rules_registered", 1);
if initial_value.is_some() {
logger.increment("css.property.initial_values", 1);
}
}
}
}
}
"font-face" => {
if let Some(crate::css_native::AtRuleBlock::Declarations(block)) =
&at_rule.block
{
if let Some((family, face)) = native_font_face_alias(block) {
font_face_aliases.entry(family).or_default().push(face);
if let Some(logger) = debug {
logger.increment("css.font_face.rules", 1);
logger.increment("css.font_face.aliases_compiled", 1);
}
} else if let Some(logger) = debug {
logger.increment("css.font_face.rules", 1);
logger.increment("css.font_face.aliases_skipped", 1);
}
}
}
"media" => {
let matched = native_media_prelude_matches(
&at_rule.prelude,
viewport,
prefer_print,
debug,
);
if let Some(logger) = debug {
logger.increment("css.media.rules", 1);
logger.increment(
if matched {
"css.media.rules_matched"
} else {
"css.media.rules_skipped"
},
1,
);
}
if matched {
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) =
&at_rule.block
{
append_native_rule_list(
nested,
normal_rules,
important_rules,
order,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_orders,
next_layer_order,
current_layer_order,
current_layer_name.clone(),
container_conditions,
scope_conditions,
debug,
has_positional_selectors,
has_sibling_selectors,
viewport,
prefer_print,
);
}
}
}
"supports" => {
let matched = native_supports_prelude_matches(&at_rule.prelude);
if let Some(logger) = debug {
logger.increment("css.supports.rules", 1);
logger.increment(
if matched {
"css.supports.rules_matched"
} else {
"css.supports.rules_skipped"
},
1,
);
}
if matched {
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) =
&at_rule.block
{
append_native_rule_list(
nested,
normal_rules,
important_rules,
order,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_orders,
next_layer_order,
current_layer_order,
current_layer_name.clone(),
container_conditions,
scope_conditions,
debug,
has_positional_selectors,
has_sibling_selectors,
viewport,
prefer_print,
);
}
}
}
"layer" => {
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) =
&at_rule.block
{
let raw_name = at_rule.prelude.trim();
let (registered_order, layer_name) = if raw_name.is_empty() {
let value = *next_layer_order;
*next_layer_order = next_layer_order.saturating_add(1);
(value, current_layer_name.clone())
} else {
let name =
scoped_layer_name(current_layer_name.as_deref(), raw_name);
let value = register_layer(
name.clone(),
layer_orders,
next_layer_order,
);
(value, Some(name))
};
append_native_rule_list(
nested,
normal_rules,
important_rules,
order,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_orders,
next_layer_order,
Some(registered_order),
layer_name,
container_conditions,
scope_conditions,
debug,
has_positional_selectors,
has_sibling_selectors,
viewport,
prefer_print,
);
} else if let Ok(names) =
crate::css_native::split_top_level(&at_rule.prelude, ',')
{
for name in names {
let name = name.trim();
if !name.is_empty() {
let scoped =
scoped_layer_name(current_layer_name.as_deref(), name);
register_layer(scoped, layer_orders, next_layer_order);
}
}
}
}
"container" => {
if let Some(logger) = debug {
logger.increment("css.container.rules", 1);
}
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) =
&at_rule.block
{
let mut conditions = container_conditions.to_vec();
conditions.push(native_container_rule_condition(&at_rule.prelude));
append_native_rule_list(
nested,
normal_rules,
important_rules,
order,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_orders,
next_layer_order,
current_layer_order,
current_layer_name.clone(),
&conditions,
scope_conditions,
debug,
has_positional_selectors,
has_sibling_selectors,
viewport,
prefer_print,
);
}
}
"scope" => {
if let Some(logger) = debug {
logger.increment("css.scope.rules", 1);
}
if let (
Some(condition),
Some(crate::css_native::AtRuleBlock::Rules(nested)),
) = (
native_scope_rule_condition(&at_rule.prelude),
&at_rule.block,
) {
let mut conditions = scope_conditions.to_vec();
conditions.push(condition);
append_native_rule_list(
nested,
normal_rules,
important_rules,
order,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_orders,
next_layer_order,
current_layer_order,
current_layer_name.clone(),
container_conditions,
&conditions,
debug,
has_positional_selectors,
has_sibling_selectors,
viewport,
prefer_print,
);
} else if let Some(logger) = debug {
logger.increment("css.scope.rules_skipped", 1);
}
}
"starting-style" => {
if let Some(logger) = debug {
logger.increment("css.starting_style.rules", 1);
logger.increment("css.starting_style.rules_skipped", 1);
}
}
"view-transition" => {
if let Some(logger) = debug {
logger.increment("css.view_transition.rules", 1);
logger.increment("css.view_transition.rules_skipped", 1);
let count = match &at_rule.block {
Some(crate::css_native::AtRuleBlock::Declarations(block)) => {
block.declarations.len() as u64
}
_ => 0,
};
logger.increment("css.view_transition.descriptors_skipped", count);
}
}
name if name.ends_with("keyframes") => {
if let Some(logger) = debug {
logger.increment("css.keyframes.rules", 1);
logger.increment("css.keyframes.rules_skipped", 1);
let count = match &at_rule.block {
Some(crate::css_native::AtRuleBlock::Raw(raw)) => {
count_native_keyframes(raw)
}
_ => 0,
};
logger.increment("css.keyframes.keyframes_skipped", count);
}
}
_ => {}
},
}
}
}
let ua_sheet = crate::css_native::parse_stylesheet(default_ua_css()).ok();
let user_sheet = if css.trim().is_empty() {
None
} else {
crate::css_native::parse_stylesheet(css).ok()
};
let prefer_print = ua_sheet
.as_ref()
.map(|sheet| native_rule_list_has_print_media(&sheet.rules))
.unwrap_or(false)
|| user_sheet
.as_ref()
.map(|sheet| native_rule_list_has_print_media(&sheet.rules))
.unwrap_or(false);
if let Some(sheet) = ua_sheet {
append_native_rule_list(
&sheet.rules,
&mut normal_rules,
&mut important_rules,
&mut order,
&mut root_normal,
&mut root_important,
&mut counter_styles,
&mut custom_property_registrations,
&mut font_face_aliases,
&mut layer_orders,
&mut next_layer_order,
None,
None,
&[],
&[],
debug.as_deref(),
&mut has_positional_selectors,
&mut has_sibling_selectors,
viewport,
prefer_print,
);
}
let author_rule_order_start = order;
if let Some(sheet) = user_sheet {
append_native_rule_list(
&sheet.rules,
&mut normal_rules,
&mut important_rules,
&mut order,
&mut root_normal,
&mut root_important,
&mut counter_styles,
&mut custom_property_registrations,
&mut font_face_aliases,
&mut layer_orders,
&mut next_layer_order,
None,
None,
&[],
&[],
debug.as_deref(),
&mut has_positional_selectors,
&mut has_sibling_selectors,
viewport,
prefer_print,
);
}
let normal_index = RuleIndex::new(&normal_rules);
let important_index = RuleIndex::new(&important_rules);
if let Some(logger) = debug.as_deref() {
let json = format!(
"{{\"type\":\"css.flags\",\"has_positional_selectors\":{},\"has_sibling_selectors\":{}}}",
if has_positional_selectors {
"true"
} else {
"false"
},
if has_sibling_selectors {
"true"
} else {
"false"
}
);
logger.log_json(&json);
}
if let Some(logger) = debug.as_deref() {
let json = format!(
"{{\"type\":\"css.media.prefer_print\",\"enabled\":{}}}",
if prefer_print { "true" } else { "false" }
);
logger.log_json(&json);
}
let page_footnote_area = extract_css_page_styles(css, debug.as_deref(), Some(viewport))
.base
.footnote_area;
Self {
normal_rules,
important_rules,
normal_index,
important_index,
root_font_size: TextStyle::default().font_size,
viewport,
font_registry,
author_rule_order_start,
debug,
root_normal,
root_important,
counter_styles,
custom_property_registrations,
font_face_aliases,
layer_count: next_layer_order,
has_positional_selectors,
has_sibling_selectors,
has_target_counter_content,
page_footnote_area,
}
}
pub fn has_positional_selectors(&self) -> bool {
self.has_positional_selectors
}
pub fn has_sibling_selectors(&self) -> bool {
self.has_sibling_selectors
}
pub(crate) fn has_target_counter_content(&self) -> bool {
self.has_target_counter_content
}
pub(crate) fn page_footnote_area(&self) -> &CssPageFootnoteArea {
&self.page_footnote_area
}
fn resolve_font_face_aliases(&self, computed: &mut ComputedStyle) {
if self.font_face_aliases.is_empty() {
return;
}
let requested_weight = computed.font_weight;
let requested_stretch = computed.font_stretch;
let requested_style = computed.font_style;
let select_face = |key: &str| {
let faces = self.font_face_aliases.get(key)?;
let mut best_score = None;
let mut selected = Vec::new();
for face in faces {
let resolved = if let Some(registry) = self.font_registry.as_deref() {
face.sources.iter().find_map(|source| {
registry
.resolve(source)
.map(|font| Arc::<str>::from(font.name.clone()))
})
} else {
face.sources.first().cloned()
};
let Some(font_name) = resolved else {
continue;
};
let score = face.match_score(requested_weight, requested_stretch, requested_style);
match best_score {
Some(best) if score > best => continue,
Some(best) if score < best => selected.clear(),
None => {}
Some(_) => {}
}
best_score = Some(score);
selected.push(ResolvedFontFace {
font_name,
size_adjust: face.size_adjust,
unicode_ranges: face.unicode_ranges.clone(),
satisfies_weight: face.satisfies_weight(requested_weight),
satisfies_style: face.satisfies_style(requested_style),
});
}
(!selected.is_empty()).then_some(selected)
};
let previous_family = computed.font_face_family.clone();
let previous_source_count = computed.font_face_source_count;
computed.font_face_satisfies_weight = false;
computed.font_face_satisfies_style = false;
computed.font_face_size_adjust = I32F32::from_num(1.0);
let primary_key = previous_family.as_deref().map(str::to_string).or_else(|| {
computed.font_stack.first().and_then(|family| {
let key = normalized_font_face_name(family);
self.font_face_aliases.contains_key(&key).then_some(key)
})
});
if let Some(key) = primary_key {
if let Some(selected) = select_face(&key) {
let old_stack = std::mem::take(&mut computed.font_stack);
let mut old_ranges = std::mem::take(&mut computed.font_unicode_ranges);
old_ranges.resize(old_stack.len(), None);
let fallback_start = if previous_family.is_some() {
previous_source_count.max(1).min(old_stack.len())
} else {
1.min(old_stack.len())
};
computed.font_stack = Vec::with_capacity(
selected.len() + old_stack.len().saturating_sub(fallback_start),
);
computed.font_unicode_ranges = Vec::with_capacity(computed.font_stack.capacity());
for face in &selected {
computed.font_stack.push(face.font_name.clone());
computed
.font_unicode_ranges
.push(face.unicode_ranges.clone());
}
computed
.font_stack
.extend(old_stack.into_iter().skip(fallback_start));
computed
.font_unicode_ranges
.extend(old_ranges.into_iter().skip(fallback_start));
computed.font_face_family = Some(Arc::<str>::from(key));
computed.font_face_source_count = selected.len();
computed.font_face_size_adjust = selected[0].size_adjust;
computed.font_face_satisfies_weight =
selected.iter().all(|face| face.satisfies_weight);
computed.font_face_satisfies_style =
selected.iter().all(|face| face.satisfies_style);
}
}
let fallback_start = computed.font_face_source_count.max(1);
for (index, family) in computed
.font_stack
.iter_mut()
.enumerate()
.skip(fallback_start)
{
let key = normalized_font_face_name(family);
if let Some(selected) = select_face(&key) {
*family = selected[0].font_name.clone();
if let Some(range) = computed.font_unicode_ranges.get_mut(index) {
*range = selected[0].unicode_ranges.clone();
}
}
}
computed
.font_unicode_ranges
.resize(computed.font_stack.len(), None);
if let Some(primary) = computed.font_stack.first() {
computed.font_name = primary.clone();
}
}
#[allow(clippy::too_many_arguments)]
fn apply_style_delta(
&self,
computed: &mut ComputedStyle,
delta: &StyleDelta,
parent: &ComputedStyle,
unset_base: &ComputedStyle,
parent_font_size: Pt,
parent_line_height: LineHeightSpec,
root_font_size: Pt,
element: Option<&ElementInfo>,
) {
if let Some(keyword) = delta.all {
match keyword {
CascadeWideKeyword::Inherit => replace_computed_style_for_all(computed, parent),
CascadeWideKeyword::Initial => {
let initial = self.default_style();
replace_computed_style_for_all(computed, &initial);
}
CascadeWideKeyword::Unset => replace_computed_style_for_all(computed, unset_base),
}
}
apply_delta(
computed,
delta,
&self.custom_property_registrations,
parent,
self.font_registry.as_deref(),
parent_font_size,
parent_line_height,
root_font_size,
element,
self.viewport,
);
}
pub fn debug_logger(&self) -> Option<Arc<DebugLogger>> {
self.debug.clone()
}
pub fn default_style(&self) -> ComputedStyle {
let mut style = ComputedStyle {
font_size: self.root_font_size,
pending_font_size_var: None,
font_size_adjust: None,
font_size_adjust_scale: I32F32::from_num(1.0),
line_height: LineHeightSpec::Normal,
pending_line_height_var: None,
color: Color::BLACK,
background_color: None,
background_source_color: None,
background_alpha: 1.0,
background_paint: None,
background_paints: Vec::new(),
background_sizes: Vec::new(),
background_positions: Vec::new(),
background_repeats: Vec::new(),
background_attachments: Vec::new(),
background_blend_modes: Vec::new(),
background_origins: Vec::new(),
background_clips: Vec::new(),
mask: MaskSpec::default(),
pending_color_var: None,
pending_background_color_var: None,
pending_border_color_var: None,
pending_border_top_color_var: None,
pending_border_right_color_var: None,
pending_border_bottom_color_var: None,
pending_border_left_color_var: None,
pending_text_decoration_color_var: None,
pending_text_decoration_thickness_var: None,
pending_text_underline_offset_var: None,
pending_text_underline_position_var: None,
pending_text_shadow_var: None,
pending_line_break_var: None,
pending_font_name_var: None,
pending_font_weight_var: None,
pending_font_style_var: None,
pagination: Pagination::default(),
margin: EdgeSizes::zero(),
padding: EdgeSizes::zero(),
width: LengthSpec::Auto,
height: LengthSpec::Auto,
pending_width_var: None,
pending_height_var: None,
max_width: LengthSpec::Auto,
min_width: LengthSpec::Auto,
min_height: LengthSpec::Auto,
max_height: LengthSpec::Auto,
aspect_ratio: None,
pending_min_width_var: None,
pending_min_height_var: None,
pending_max_height_var: None,
pending_max_width_var: None,
text_align: TextAlignMode::Start,
text_align_last: TextAlignLastMode::Auto,
text_justify: TextJustifyMode::Auto,
pending_text_justify_var: None,
hyphens: HyphensMode::Manual,
pending_hyphens_var: None,
hyphenate_character: None,
pending_hyphenate_character_var: None,
text_wrap_mode: TextWrapMode::Wrap,
pending_text_wrap_mode_var: None,
text_wrap_style: TextWrapStyleMode::Auto,
pending_text_wrap_style_var: None,
pending_white_space_var: None,
direction: DirectionMode::Ltr,
unicode_bidi: UnicodeBidiMode::Normal,
writing_mode: WritingModeMode::HorizontalTb,
text_orientation: TextOrientationMode::Mixed,
pending_vertical_align_var: None,
vertical_align_inherit_base: VerticalAlignMode::Baseline,
vertical_align: VerticalAlignMode::Baseline,
font_weight: 400,
font_weight_authored: false,
font_kerning: true,
common_ligatures: true,
small_caps: false,
font_stretch: 1000,
font_synthesis_weight: true,
font_style: FontStyleMode::Normal,
text_transform: TextTransformMode::None,
transform_list: Vec::new(),
translate: Vec::new(),
rotate: Vec::new(),
scale: Vec::new(),
pending_translate_var: None,
pending_rotate_var: None,
pending_scale_var: None,
pending_transform_var: None,
transform: Vec::new(),
transform_origin: CssTransformOrigin::center(),
transform_box: TransformBoxMode::BorderBox,
transform_style: TransformStyleMode::Flat,
perspective_origin: CssTransformOrigin::center(),
text_decoration: TextDecorationMode::default(),
text_decoration_propagated: false,
text_decoration_color: Color::BLACK,
text_decoration_color_tracks_current: true,
text_decoration_thickness: TextDecorationThicknessMode::Auto,
text_decoration_style: TextDecorationStyleMode::Solid,
text_decoration_skip_ink: TextDecorationSkipInkMode::Auto,
text_emphasis_style: TextEmphasisStyleMode::None,
text_emphasis_color: Color::BLACK,
text_emphasis_color_tracks_current: true,
text_emphasis_position: TextEmphasisPositionMode::Over,
initial_letter: None,
text_underline_offset: TextUnderlineOffsetMode::Auto,
text_underline_position: TextUnderlinePositionMode::auto(),
text_shadows: Vec::new(),
text_overflow: TextOverflowMode::Clip,
text_indent: LengthSpec::Absolute(Pt::ZERO),
text_indent_hanging: false,
text_indent_each_line: false,
content: None,
generated_content: None,
marker_content_overridden: false,
quotes: default_quotes(),
counter_reset: Vec::new(),
counter_increment: Vec::new(),
counter_set: Vec::new(),
word_break: WordBreakMode::Normal,
overflow_wrap: OverflowWrapMode::Normal,
line_break: LineBreakMode::Auto,
list_style_type: ListStyleTypeMode::Auto,
list_style_marker: None,
list_style_symbols: None,
list_style_image: None,
list_style_image_paint: None,
list_style_position: ListStylePositionMode::Outside,
letter_spacing: Pt::ZERO,
word_spacing: Pt::ZERO,
tab_size: TabSizeSpec::initial(),
border_width: EdgeSizes::zero(),
border_color: None,
border_top_color: None,
border_right_color: None,
border_bottom_color: None,
border_left_color: None,
border_top_alpha: 1.0,
border_right_alpha: 1.0,
border_bottom_alpha: 1.0,
border_left_alpha: 1.0,
border_style: BorderStyleState::none(),
border_collapse: BorderCollapseMode::Separate,
caption_side: CaptionSideMode::Top,
border_spacing: BorderSpacingSpec::zero(),
table_layout: TableLayoutMode::Auto,
empty_cells_hide: false,
border_radius: BorderRadiiSpec::zero(),
box_decoration_break: BoxDecorationBreak::Slice,
border_image: BorderImageSpec::default(),
pending_border_radius_var: None,
outline_width: LengthSpec::Absolute(Pt::from_f32(1.5)),
outline_offset: LengthSpec::Absolute(Pt::ZERO),
outline_style: OutlineLineStyle::Solid,
outline_color: None,
outline_visible: false,
box_shadow: None,
box_shadows: Vec::new(),
paint_filter: None,
paint_filter_stacking_context: false,
backdrop_filter: None,
will_change_backdrop_root: false,
mask_backdrop_root: false,
mix_blend_mode: MixBlendMode::Normal,
isolation: false,
opacity: 1.0,
pending_opacity_var: None,
container_names: Vec::new(),
container_type: ContainerQueryType::Normal,
clip_path: None,
clip_path_reference_box: ClipPathReferenceBox::Border,
clip_path_inset: None,
legacy_clip: None,
root_font_size: self.root_font_size,
root_line_height: self.root_font_size.mul_ratio(6, 5),
white_space: WhiteSpaceMode::Normal,
display: DisplayMode::Inline,
visibility: VisibilityMode::Visible,
font_stack: vec![Arc::<str>::from("Helvetica")],
font_name: Arc::<str>::from("Helvetica"),
font_unicode_ranges: vec![None],
font_face_family: None,
font_face_source_count: 0,
font_face_size_adjust: I32F32::from_num(1.0),
font_face_satisfies_weight: false,
font_face_satisfies_style: false,
position: PositionMode::Static,
running_name: None,
string_set: Vec::new(),
float_mode: FloatMode::None,
footnote_policy: FootnotePolicy::Auto,
footnote_display: FootnoteDisplay::Block,
clear_mode: ClearMode::None,
z_index: 0,
z_index_auto: true,
perspective: None,
inset_left: LengthSpec::Auto,
inset_top: LengthSpec::Auto,
inset_right: LengthSpec::Auto,
inset_bottom: LengthSpec::Auto,
pending_outline_offset_var: None,
pending_inset_left_var: None,
pending_inset_top_var: None,
pending_inset_right_var: None,
pending_inset_bottom_var: None,
box_sizing: BoxSizingMode::ContentBox,
flex_direction: FlexDirectionMode::Row,
flex_wrap: FlexWrapMode::NoWrap,
flex_basis: LengthSpec::Auto,
pending_flex_basis_var: None,
order: 0,
justify_content: JustifyContentMode::FlexStart,
align_items: AlignItemsMode::Stretch,
align_self: AlignSelfMode::Auto,
justify_items: AlignItemsMode::Stretch,
justify_self: AlignSelfMode::Auto,
align_content: AlignContentMode::Stretch,
column_count: 1,
column_count_auto: true,
column_width: LengthSpec::Auto,
column_fill: ColumnFillMode::Balance,
column_span: ColumnSpanMode::None,
column_rule_width: LengthSpec::Absolute(Pt::from_f32(2.25)),
column_rule_style: OutlineLineStyle::Solid,
column_rule_color: None,
column_rule_visible: false,
grid_columns: None,
grid_rows: None,
grid_column_tracks: Vec::new(),
grid_row_tracks: Vec::new(),
grid_subgrid_columns: false,
grid_subgrid_rows: false,
pending_grid_column_tracks_var: None,
pending_grid_row_tracks_var: None,
grid_auto_column_tracks: Vec::new(),
grid_auto_row_tracks: Vec::new(),
grid_auto_flow: GridAutoFlowMode::Row,
grid_column_auto_repeat: None,
grid_row_auto_repeat: None,
grid_template_areas: Vec::new(),
grid_column_line_names: Vec::new(),
grid_row_line_names: Vec::new(),
grid_column_line_start: GridLineSpec::Auto,
grid_column_line_end: GridLineSpec::Auto,
grid_row_line_start: GridLineSpec::Auto,
grid_row_line_end: GridLineSpec::Auto,
grid_area_name: None,
grid_column_start: None,
grid_row_start: None,
row_gap: LengthSpec::Absolute(Pt::ZERO),
pending_row_gap_var: None,
gap: LengthSpec::Absolute(Pt::ZERO),
pending_gap_var: None,
column_gap_normal: true,
flex_grow: 0.0,
flex_shrink: 1.0,
overflow_x: OverflowMode::Visible,
overflow_y: OverflowMode::Visible,
overflow: OverflowMode::Visible,
overflow_clip_margin: LengthSpec::Absolute(Pt::ZERO),
scrollbar_gutter: ScrollbarGutterMode::Auto,
line_clamp: None,
object_fit: ObjectFitMode::Fill,
image_rendering: ImageRenderingMode::Auto,
object_position: BackgroundPositionSpec::center(),
custom_lengths: HashMap::new(),
custom_colors: HashMap::new(),
custom_color_alpha: HashMap::new(),
custom_color_refs: HashMap::new(),
custom_raw_values: HashMap::new(),
custom_font_stacks: HashMap::new(),
};
apply_custom_property_registrations(&mut style, &self.custom_property_registrations);
style
}
pub(crate) fn computed_root_element_style(&self) -> ComputedStyle {
let initial = self.default_style();
self.compute_style(&ElementInfo::synthetic_root("html"), &initial, None, &[])
}
pub fn compute_style(
&self,
element: &ElementInfo,
parent: &ComputedStyle,
inline_style: Option<&str>,
ancestors: &[ElementInfo],
) -> ComputedStyle {
let debug = self.debug.as_ref();
let debug_node = debug.map(|_| format_element_path(element, ancestors));
let propagates_text_decoration = !parent.text_decoration.is_none();
let mut computed = ComputedStyle {
font_size: parent.font_size,
pending_font_size_var: None,
font_size_adjust: parent.font_size_adjust,
font_size_adjust_scale: I32F32::from_num(1.0),
line_height: parent.line_height.clone(),
pending_line_height_var: None,
color: parent.color,
background_color: None,
background_source_color: None,
background_alpha: 1.0,
background_paint: None,
background_paints: Vec::new(),
background_sizes: Vec::new(),
background_positions: Vec::new(),
background_repeats: Vec::new(),
background_attachments: Vec::new(),
background_blend_modes: Vec::new(),
background_origins: Vec::new(),
background_clips: Vec::new(),
mask: MaskSpec::default(),
pending_color_var: None,
pending_background_color_var: None,
pending_border_color_var: None,
pending_border_top_color_var: None,
pending_border_right_color_var: None,
pending_border_bottom_color_var: None,
pending_border_left_color_var: None,
pending_text_decoration_color_var: None,
pending_text_decoration_thickness_var: None,
pending_text_underline_offset_var: None,
pending_text_underline_position_var: None,
pending_text_shadow_var: None,
pending_line_break_var: None,
pending_font_name_var: None,
pending_font_weight_var: None,
pending_font_style_var: None,
pagination: inherited_pagination(parent),
margin: EdgeSizes::zero(),
padding: EdgeSizes::zero(),
width: LengthSpec::Auto,
height: LengthSpec::Auto,
pending_width_var: None,
pending_height_var: None,
max_width: LengthSpec::Auto,
min_width: LengthSpec::Auto,
min_height: LengthSpec::Auto,
max_height: LengthSpec::Auto,
aspect_ratio: None,
pending_min_width_var: None,
pending_min_height_var: None,
pending_max_height_var: None,
pending_max_width_var: None,
text_align: parent.text_align,
text_align_last: parent.text_align_last,
text_justify: parent.text_justify,
pending_text_justify_var: None,
hyphens: parent.hyphens,
pending_hyphens_var: None,
hyphenate_character: parent.hyphenate_character.clone(),
pending_hyphenate_character_var: None,
text_wrap_mode: parent.text_wrap_mode,
pending_text_wrap_mode_var: None,
text_wrap_style: parent.text_wrap_style,
pending_text_wrap_style_var: None,
pending_white_space_var: None,
direction: parent.direction,
unicode_bidi: UnicodeBidiMode::Normal,
writing_mode: parent.writing_mode,
text_orientation: parent.text_orientation,
pending_vertical_align_var: None,
vertical_align_inherit_base: parent.vertical_align,
vertical_align: VerticalAlignMode::Baseline,
font_weight: parent.font_weight,
font_weight_authored: parent.font_weight_authored,
font_kerning: parent.font_kerning,
common_ligatures: parent.common_ligatures,
small_caps: parent.small_caps,
font_stretch: parent.font_stretch,
font_synthesis_weight: parent.font_synthesis_weight,
font_style: parent.font_style,
text_transform: parent.text_transform,
transform_list: Vec::new(),
translate: Vec::new(),
rotate: Vec::new(),
scale: Vec::new(),
pending_translate_var: None,
pending_rotate_var: None,
pending_scale_var: None,
pending_transform_var: None,
transform: Vec::new(),
transform_origin: CssTransformOrigin::center(),
transform_box: TransformBoxMode::BorderBox,
transform_style: TransformStyleMode::Flat,
perspective_origin: CssTransformOrigin::center(),
text_decoration: parent.text_decoration,
text_decoration_propagated: propagates_text_decoration,
text_decoration_color: if propagates_text_decoration {
parent.text_decoration_color
} else {
parent.color
},
text_decoration_color_tracks_current: if propagates_text_decoration {
parent.text_decoration_color_tracks_current
} else {
true
},
text_decoration_thickness: if propagates_text_decoration {
parent.text_decoration_thickness
} else {
TextDecorationThicknessMode::Auto
},
text_decoration_style: if propagates_text_decoration {
parent.text_decoration_style
} else {
TextDecorationStyleMode::Solid
},
text_decoration_skip_ink: parent.text_decoration_skip_ink,
text_emphasis_style: parent.text_emphasis_style,
text_emphasis_color: parent.text_emphasis_color,
text_emphasis_color_tracks_current: parent.text_emphasis_color_tracks_current,
text_emphasis_position: parent.text_emphasis_position,
initial_letter: None,
text_underline_offset: parent.text_underline_offset,
text_underline_position: parent.text_underline_position,
text_shadows: parent.text_shadows.clone(),
text_overflow: TextOverflowMode::Clip,
text_indent: parent.text_indent,
text_indent_hanging: parent.text_indent_hanging,
text_indent_each_line: parent.text_indent_each_line,
content: None,
generated_content: None,
marker_content_overridden: false,
quotes: parent.quotes.clone(),
counter_reset: Vec::new(),
counter_increment: Vec::new(),
counter_set: Vec::new(),
word_break: parent.word_break,
overflow_wrap: parent.overflow_wrap,
line_break: parent.line_break,
list_style_type: parent.list_style_type,
list_style_marker: parent.list_style_marker.clone(),
list_style_symbols: parent.list_style_symbols.clone(),
list_style_image: parent.list_style_image.clone(),
list_style_image_paint: parent.list_style_image_paint.clone(),
list_style_position: parent.list_style_position,
letter_spacing: parent.letter_spacing,
word_spacing: parent.word_spacing,
tab_size: parent.tab_size,
border_width: EdgeSizes::zero(),
border_color: None,
border_top_color: None,
border_right_color: None,
border_bottom_color: None,
border_left_color: None,
border_top_alpha: 1.0,
border_right_alpha: 1.0,
border_bottom_alpha: 1.0,
border_left_alpha: 1.0,
border_style: BorderStyleState::none(),
border_collapse: parent.border_collapse,
caption_side: parent.caption_side,
border_spacing: parent.border_spacing,
table_layout: TableLayoutMode::Auto,
empty_cells_hide: parent.empty_cells_hide,
border_radius: BorderRadiiSpec::zero(),
box_decoration_break: BoxDecorationBreak::Slice,
border_image: BorderImageSpec::default(),
pending_border_radius_var: None,
outline_width: LengthSpec::Absolute(Pt::from_f32(1.5)),
outline_offset: LengthSpec::Absolute(Pt::ZERO),
outline_style: OutlineLineStyle::Solid,
outline_color: None,
outline_visible: false,
box_shadow: None,
box_shadows: Vec::new(),
paint_filter: None,
paint_filter_stacking_context: false,
backdrop_filter: None,
will_change_backdrop_root: false,
mask_backdrop_root: false,
mix_blend_mode: MixBlendMode::Normal,
isolation: false,
opacity: 1.0,
pending_opacity_var: None,
container_names: Vec::new(),
container_type: ContainerQueryType::Normal,
clip_path: None,
clip_path_reference_box: ClipPathReferenceBox::Border,
clip_path_inset: None,
legacy_clip: None,
root_font_size: parent.root_font_size,
root_line_height: parent.root_line_height,
white_space: parent.white_space,
display: DisplayMode::Inline,
visibility: parent.visibility,
font_stack: parent.font_stack.clone(),
font_name: parent.font_name.clone(),
font_unicode_ranges: parent.font_unicode_ranges.clone(),
font_face_family: parent.font_face_family.clone(),
font_face_source_count: parent.font_face_source_count,
font_face_size_adjust: parent.font_face_size_adjust,
font_face_satisfies_weight: parent.font_face_satisfies_weight,
font_face_satisfies_style: parent.font_face_satisfies_style,
position: PositionMode::Static,
running_name: None,
string_set: Vec::new(),
float_mode: FloatMode::None,
footnote_policy: FootnotePolicy::Auto,
footnote_display: FootnoteDisplay::Block,
clear_mode: ClearMode::None,
z_index: 0,
z_index_auto: true,
perspective: None,
inset_left: LengthSpec::Auto,
inset_top: LengthSpec::Auto,
inset_right: LengthSpec::Auto,
inset_bottom: LengthSpec::Auto,
pending_outline_offset_var: None,
pending_inset_left_var: None,
pending_inset_top_var: None,
pending_inset_right_var: None,
pending_inset_bottom_var: None,
box_sizing: BoxSizingMode::ContentBox,
flex_direction: FlexDirectionMode::Row,
flex_wrap: FlexWrapMode::NoWrap,
flex_basis: LengthSpec::Auto,
pending_flex_basis_var: None,
order: 0,
justify_content: JustifyContentMode::FlexStart,
align_items: AlignItemsMode::Stretch,
align_self: AlignSelfMode::Auto,
justify_items: AlignItemsMode::Stretch,
justify_self: AlignSelfMode::Auto,
align_content: AlignContentMode::Stretch,
column_count: 1,
column_count_auto: true,
column_width: LengthSpec::Auto,
column_fill: ColumnFillMode::Balance,
column_span: ColumnSpanMode::None,
column_rule_width: LengthSpec::Absolute(Pt::from_f32(2.25)),
column_rule_style: OutlineLineStyle::Solid,
column_rule_color: None,
column_rule_visible: false,
grid_columns: None,
grid_rows: None,
grid_column_tracks: Vec::new(),
grid_row_tracks: Vec::new(),
grid_subgrid_columns: false,
grid_subgrid_rows: false,
pending_grid_column_tracks_var: None,
pending_grid_row_tracks_var: None,
grid_auto_column_tracks: Vec::new(),
grid_auto_row_tracks: Vec::new(),
grid_auto_flow: GridAutoFlowMode::Row,
grid_column_auto_repeat: None,
grid_row_auto_repeat: None,
grid_template_areas: Vec::new(),
grid_column_line_names: Vec::new(),
grid_row_line_names: Vec::new(),
grid_column_line_start: GridLineSpec::Auto,
grid_column_line_end: GridLineSpec::Auto,
grid_row_line_start: GridLineSpec::Auto,
grid_row_line_end: GridLineSpec::Auto,
grid_area_name: None,
grid_column_start: None,
grid_row_start: None,
row_gap: LengthSpec::Absolute(Pt::ZERO),
pending_row_gap_var: None,
gap: LengthSpec::Absolute(Pt::ZERO),
pending_gap_var: None,
column_gap_normal: true,
flex_grow: 0.0,
flex_shrink: 1.0,
overflow_x: OverflowMode::Visible,
overflow_y: OverflowMode::Visible,
overflow: OverflowMode::Visible,
overflow_clip_margin: LengthSpec::Absolute(Pt::ZERO),
scrollbar_gutter: ScrollbarGutterMode::Auto,
line_clamp: None,
object_fit: ObjectFitMode::Fill,
image_rendering: parent.image_rendering,
object_position: parent.object_position,
custom_lengths: parent.custom_lengths.clone(),
custom_colors: parent.custom_colors.clone(),
custom_color_alpha: parent.custom_color_alpha.clone(),
custom_color_refs: parent.custom_color_refs.clone(),
custom_raw_values: parent.custom_raw_values.clone(),
custom_font_stacks: parent.custom_font_stacks.clone(),
};
apply_custom_property_registrations(&mut computed, &self.custom_property_registrations);
let unset_base = computed.clone();
let parent_font_size = parent.font_size;
let parent_line_height = parent.line_height.clone();
let root_font_size = parent.root_font_size;
if element.is_root {
for delta in &self.root_normal {
self.apply_style_delta(
&mut computed,
delta,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
}
}
let mut matches: Vec<&RuleEntry> = self
.normal_index
.candidate_indices(element)
.into_iter()
.filter_map(|idx| self.normal_rules.get(idx))
.filter(|rule| rule.selector.matches(element, ancestors))
.filter(|rule| container_conditions_match(&rule.container_conditions, ancestors))
.filter(|rule| scope_conditions_match(&rule.scope_conditions, element, ancestors))
.collect();
matches.sort_by(|a, b| {
a.normal_layer_rank()
.cmp(&b.normal_layer_rank())
.then_with(|| {
a.specificity
.cmp(&b.specificity)
.then_with(|| a.order.cmp(&b.order))
})
});
if let (Some(logger), Some(node)) = (debug, debug_node.as_ref()) {
let selectors: Vec<String> = matches
.iter()
.map(|rule| rule.selector_text.clone())
.collect();
if !selectors.is_empty() {
let list = json_array(&selectors);
let json = format!(
"{{\"type\":\"css.match\",\"node\":{},\"selectors\":{}}}",
json_string(node),
list
);
logger.log_json(&json);
}
}
let mut current_normal_layer_rank = None;
let mut normal_layer_base = computed.clone();
for rule in matches {
let layer_rank = rule.normal_layer_rank();
if current_normal_layer_rank != Some(layer_rank) {
current_normal_layer_rank = Some(layer_rank);
normal_layer_base = computed.clone();
}
if style_delta_authors_font_weight(&rule.delta) {
computed.font_weight_authored = rule.order >= self.author_rule_order_start;
}
self.apply_style_delta(
&mut computed,
&rule.delta,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
apply_revert_layer_delta(&mut computed, &rule.delta.revert_layer, &normal_layer_base);
}
let mut inline_normal = StyleDelta::default();
let mut inline_important = StyleDelta::default();
if let Some(inline) = inline_style {
if let Ok(declarations) = crate::css_native::parse_declaration_block(inline) {
if let Some(logger) = debug {
let selector = debug_node
.as_deref()
.map(|node| format!("@inline {node}"))
.unwrap_or_else(|| "@inline".to_string());
log_native_declaration_no_effects(&declarations, &selector, logger);
}
let (normal_delta, important_delta) =
style_from_native_declarations(&declarations, self.viewport);
inline_normal = normal_delta;
inline_important = important_delta;
}
}
if style_delta_authors_font_weight(&inline_normal) {
computed.font_weight_authored = true;
}
self.apply_style_delta(
&mut computed,
&inline_normal,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
if element.is_root {
for delta in &self.root_important {
self.apply_style_delta(
&mut computed,
delta,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
}
}
let mut important_matches: Vec<&RuleEntry> = self
.important_index
.candidate_indices(element)
.into_iter()
.filter_map(|idx| self.important_rules.get(idx))
.filter(|rule| rule.selector.matches(element, ancestors))
.filter(|rule| container_conditions_match(&rule.container_conditions, ancestors))
.filter(|rule| scope_conditions_match(&rule.scope_conditions, element, ancestors))
.collect();
important_matches.sort_by(|a, b| {
a.important_layer_rank(self.layer_count)
.cmp(&b.important_layer_rank(self.layer_count))
.then_with(|| {
a.specificity
.cmp(&b.specificity)
.then_with(|| a.order.cmp(&b.order))
})
});
let mut current_important_layer_rank = None;
let mut important_layer_base = computed.clone();
for rule in important_matches {
let layer_rank = rule.important_layer_rank(self.layer_count);
if current_important_layer_rank != Some(layer_rank) {
current_important_layer_rank = Some(layer_rank);
important_layer_base = computed.clone();
}
if style_delta_authors_font_weight(&rule.delta) {
computed.font_weight_authored = rule.order >= self.author_rule_order_start;
}
self.apply_style_delta(
&mut computed,
&rule.delta,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
apply_revert_layer_delta(
&mut computed,
&rule.delta.revert_layer,
&important_layer_base,
);
}
if style_delta_authors_font_weight(&inline_important) {
computed.font_weight_authored = true;
}
self.apply_style_delta(
&mut computed,
&inline_important,
parent,
&unset_base,
parent_font_size,
parent_line_height,
root_font_size,
Some(element),
);
resolve_pending_font_size_var(&mut computed, self.viewport);
resolve_pending_line_height_var(&mut computed, self.viewport);
resolve_computed_line_height(&mut computed, self.viewport);
let mut unresolved = Vec::new();
if let Some(name) = computed.pending_font_size_var.clone() {
if resolve_font_size_expr(&computed, &name, self.viewport).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_line_height_var.clone() {
if resolve_line_height_expr(&computed, &name, self.viewport).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_width_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_max_width_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_height_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_min_width_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_min_height_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_max_height_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_inset_left_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_inset_top_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_inset_right_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_inset_bottom_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_flex_basis_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_gap_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_row_gap_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_outline_offset_var.clone() {
if resolve_custom_length_from_maps(
&computed.custom_lengths,
&computed.custom_color_refs,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_radius_var.clone() {
if resolve_custom_border_radius_from_maps(
&computed.custom_color_refs,
&computed.custom_raw_values,
&name,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_translate_var.clone() {
if resolve_custom_transform_ops_from_maps(
&computed.custom_color_refs,
&computed.custom_raw_values,
&name,
transform_ops_from_translate_string,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_rotate_var.clone() {
if resolve_custom_transform_ops_from_maps(
&computed.custom_color_refs,
&computed.custom_raw_values,
&name,
transform_ops_from_rotate_string,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_scale_var.clone() {
if resolve_custom_transform_ops_from_maps(
&computed.custom_color_refs,
&computed.custom_raw_values,
&name,
transform_ops_from_scale_string,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_transform_var.clone() {
if resolve_custom_transform_ops_from_maps(
&computed.custom_color_refs,
&computed.custom_raw_values,
&name,
transform_ops_from_string,
)
.is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_background_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none()
&& parse_background_paint_with_style(&computed, &name).is_none()
{
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_top_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_right_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_bottom_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_border_left_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_decoration_color_var.clone() {
if resolve_custom_color(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_decoration_thickness_var.clone() {
if resolve_text_decoration_thickness_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_underline_offset_var.clone() {
if resolve_text_underline_offset_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_underline_position_var.clone() {
if resolve_text_underline_position_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_shadow_var.clone() {
if resolve_text_shadow_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_line_break_var.clone() {
if resolve_line_break_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_justify_var.clone() {
if resolve_text_justify_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_hyphens_var.clone() {
if resolve_hyphens_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_hyphenate_character_var.clone() {
if resolve_hyphenate_character_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_wrap_mode_var.clone() {
if resolve_text_wrap_mode_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_text_wrap_style_var.clone() {
if resolve_text_wrap_style_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_white_space_var.clone() {
if resolve_white_space_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_vertical_align_var.clone() {
if resolve_vertical_align_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_font_weight_var.clone() {
if resolve_font_weight_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_font_style_var.clone() {
if resolve_font_style_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
if let Some(name) = computed.pending_font_name_var.clone() {
if resolve_font_name_expr(&computed, &name).is_none() {
unresolved.push(name);
}
}
resolve_pending_vars(&mut computed);
self.resolve_font_face_aliases(&mut computed);
resolve_font_size_adjust_scale(&mut computed, self.font_registry.as_deref());
if element.is_root {
computed.root_font_size = computed.font_size;
computed.root_line_height = computed.line_height.to_line_height(computed.font_size);
}
resolve_font_relative_lengths(&mut computed, self.font_registry.as_deref());
apply_border_style_mask(&mut computed);
resolve_named_counter_style(&mut computed, &self.counter_styles);
if let (Some(logger), Some(node)) = (debug, debug_node.as_ref()) {
let style_json = debug_style_json(&computed);
let unresolved_json = json_array(&unresolved);
let json = format!(
"{{\"type\":\"css.computed\",\"node\":{},\"style\":{},\"vars_unresolved\":{}}}",
json_string(node),
style_json,
unresolved_json
);
logger.log_json(&json);
}
computed
}
pub fn compute_pseudo_style(
&self,
element: &ElementInfo,
parent: &ComputedStyle,
ancestors: &[ElementInfo],
pseudo: PseudoTarget,
) -> Option<ComputedStyle> {
self.compute_pseudo_style_impl(element, parent, ancestors, pseudo, false)
}
pub(crate) fn compute_marker_style(
&self,
element: &ElementInfo,
parent: &ComputedStyle,
ancestors: &[ElementInfo],
) -> Option<ComputedStyle> {
self.compute_pseudo_style_impl(element, parent, ancestors, PseudoTarget::Marker, true)
}
fn compute_pseudo_style_impl(
&self,
element: &ElementInfo,
parent: &ComputedStyle,
ancestors: &[ElementInfo],
pseudo: PseudoTarget,
include_marker_presentation: bool,
) -> Option<ComputedStyle> {
if matches!(pseudo, PseudoTarget::None) {
return None;
}
if matches!(pseudo, PseudoTarget::Marker)
&& !matches!(
parent.display,
DisplayMode::ListItem | DisplayMode::InlineListItem
)
&& !element.tag.eq_ignore_ascii_case("li")
{
return None;
}
let propagates_text_decoration = !parent.text_decoration.is_none();
let mut computed = ComputedStyle {
font_size: parent.font_size,
pending_font_size_var: None,
font_size_adjust: parent.font_size_adjust,
font_size_adjust_scale: I32F32::from_num(1.0),
line_height: parent.line_height.clone(),
pending_line_height_var: None,
color: parent.color,
background_color: None,
background_source_color: None,
background_alpha: 1.0,
background_paint: None,
background_paints: Vec::new(),
background_sizes: Vec::new(),
background_positions: Vec::new(),
background_repeats: Vec::new(),
background_attachments: Vec::new(),
background_blend_modes: Vec::new(),
background_origins: Vec::new(),
background_clips: Vec::new(),
mask: MaskSpec::default(),
pending_color_var: None,
pending_background_color_var: None,
pending_border_color_var: None,
pending_border_top_color_var: None,
pending_border_right_color_var: None,
pending_border_bottom_color_var: None,
pending_border_left_color_var: None,
pending_text_decoration_color_var: None,
pending_text_decoration_thickness_var: None,
pending_text_underline_offset_var: None,
pending_text_underline_position_var: None,
pending_text_shadow_var: None,
pending_line_break_var: None,
pending_font_name_var: None,
pending_font_weight_var: None,
pending_font_style_var: None,
pagination: inherited_pagination(parent),
margin: EdgeSizes::zero(),
padding: EdgeSizes::zero(),
width: LengthSpec::Auto,
height: LengthSpec::Auto,
pending_width_var: None,
pending_height_var: None,
max_width: LengthSpec::Auto,
min_width: LengthSpec::Auto,
min_height: LengthSpec::Auto,
max_height: LengthSpec::Auto,
aspect_ratio: None,
pending_min_width_var: None,
pending_min_height_var: None,
pending_max_height_var: None,
pending_max_width_var: None,
text_align: parent.text_align,
text_align_last: parent.text_align_last,
text_justify: parent.text_justify,
pending_text_justify_var: None,
hyphens: parent.hyphens,
pending_hyphens_var: None,
hyphenate_character: parent.hyphenate_character.clone(),
pending_hyphenate_character_var: None,
text_wrap_mode: parent.text_wrap_mode,
pending_text_wrap_mode_var: None,
text_wrap_style: parent.text_wrap_style,
pending_text_wrap_style_var: None,
pending_white_space_var: None,
direction: parent.direction,
unicode_bidi: UnicodeBidiMode::Normal,
writing_mode: parent.writing_mode,
text_orientation: parent.text_orientation,
pending_vertical_align_var: None,
vertical_align_inherit_base: parent.vertical_align,
vertical_align: VerticalAlignMode::Baseline,
font_weight: parent.font_weight,
font_weight_authored: parent.font_weight_authored,
font_kerning: parent.font_kerning,
common_ligatures: parent.common_ligatures,
small_caps: parent.small_caps,
font_stretch: parent.font_stretch,
font_synthesis_weight: parent.font_synthesis_weight,
font_style: parent.font_style,
text_transform: parent.text_transform,
transform_list: Vec::new(),
translate: Vec::new(),
rotate: Vec::new(),
scale: Vec::new(),
pending_translate_var: None,
pending_rotate_var: None,
pending_scale_var: None,
pending_transform_var: None,
transform: Vec::new(),
transform_origin: CssTransformOrigin::center(),
transform_box: TransformBoxMode::BorderBox,
transform_style: TransformStyleMode::Flat,
perspective_origin: CssTransformOrigin::center(),
text_decoration: parent.text_decoration,
text_decoration_propagated: propagates_text_decoration,
text_decoration_color: if propagates_text_decoration {
parent.text_decoration_color
} else {
parent.color
},
text_decoration_color_tracks_current: if propagates_text_decoration {
parent.text_decoration_color_tracks_current
} else {
true
},
text_decoration_thickness: if propagates_text_decoration {
parent.text_decoration_thickness
} else {
TextDecorationThicknessMode::Auto
},
text_decoration_style: if propagates_text_decoration {
parent.text_decoration_style
} else {
TextDecorationStyleMode::Solid
},
text_decoration_skip_ink: parent.text_decoration_skip_ink,
text_emphasis_style: parent.text_emphasis_style,
text_emphasis_color: parent.text_emphasis_color,
text_emphasis_color_tracks_current: parent.text_emphasis_color_tracks_current,
text_emphasis_position: parent.text_emphasis_position,
initial_letter: None,
text_underline_offset: parent.text_underline_offset,
text_underline_position: parent.text_underline_position,
text_shadows: parent.text_shadows.clone(),
text_overflow: TextOverflowMode::Clip,
text_indent: parent.text_indent,
text_indent_hanging: parent.text_indent_hanging,
text_indent_each_line: parent.text_indent_each_line,
content: None,
generated_content: None,
marker_content_overridden: false,
quotes: parent.quotes.clone(),
counter_reset: Vec::new(),
counter_increment: Vec::new(),
counter_set: Vec::new(),
word_break: parent.word_break,
overflow_wrap: parent.overflow_wrap,
line_break: parent.line_break,
list_style_type: parent.list_style_type,
list_style_marker: parent.list_style_marker.clone(),
list_style_symbols: parent.list_style_symbols.clone(),
list_style_image: parent.list_style_image.clone(),
list_style_image_paint: parent.list_style_image_paint.clone(),
list_style_position: parent.list_style_position,
letter_spacing: parent.letter_spacing,
word_spacing: parent.word_spacing,
tab_size: parent.tab_size,
border_width: EdgeSizes::zero(),
border_color: None,
border_top_color: None,
border_right_color: None,
border_bottom_color: None,
border_left_color: None,
border_top_alpha: 1.0,
border_right_alpha: 1.0,
border_bottom_alpha: 1.0,
border_left_alpha: 1.0,
border_style: BorderStyleState::none(),
border_collapse: parent.border_collapse,
caption_side: parent.caption_side,
border_spacing: parent.border_spacing,
table_layout: TableLayoutMode::Auto,
empty_cells_hide: parent.empty_cells_hide,
border_radius: BorderRadiiSpec::zero(),
box_decoration_break: BoxDecorationBreak::Slice,
border_image: BorderImageSpec::default(),
pending_border_radius_var: None,
outline_width: LengthSpec::Absolute(Pt::from_f32(1.5)),
outline_offset: LengthSpec::Absolute(Pt::ZERO),
outline_style: OutlineLineStyle::Solid,
outline_color: None,
outline_visible: false,
box_shadow: None,
box_shadows: Vec::new(),
paint_filter: None,
paint_filter_stacking_context: false,
backdrop_filter: None,
will_change_backdrop_root: false,
mask_backdrop_root: false,
mix_blend_mode: MixBlendMode::Normal,
isolation: false,
opacity: 1.0,
pending_opacity_var: None,
container_names: Vec::new(),
container_type: ContainerQueryType::Normal,
clip_path: None,
clip_path_reference_box: ClipPathReferenceBox::Border,
clip_path_inset: None,
legacy_clip: None,
root_font_size: parent.root_font_size,
root_line_height: parent.root_line_height,
white_space: parent.white_space,
display: DisplayMode::Inline,
visibility: parent.visibility,
font_stack: parent.font_stack.clone(),
font_name: parent.font_name.clone(),
font_unicode_ranges: parent.font_unicode_ranges.clone(),
font_face_family: parent.font_face_family.clone(),
font_face_source_count: parent.font_face_source_count,
font_face_size_adjust: parent.font_face_size_adjust,
font_face_satisfies_weight: parent.font_face_satisfies_weight,
font_face_satisfies_style: parent.font_face_satisfies_style,
position: PositionMode::Static,
running_name: None,
string_set: Vec::new(),
float_mode: FloatMode::None,
footnote_policy: FootnotePolicy::Auto,
footnote_display: FootnoteDisplay::Block,
clear_mode: ClearMode::None,
z_index: 0,
z_index_auto: true,
perspective: None,
inset_left: LengthSpec::Auto,
inset_top: LengthSpec::Auto,
inset_right: LengthSpec::Auto,
inset_bottom: LengthSpec::Auto,
pending_outline_offset_var: None,
pending_inset_left_var: None,
pending_inset_top_var: None,
pending_inset_right_var: None,
pending_inset_bottom_var: None,
box_sizing: BoxSizingMode::ContentBox,
flex_direction: FlexDirectionMode::Row,
flex_wrap: FlexWrapMode::NoWrap,
flex_basis: LengthSpec::Auto,
pending_flex_basis_var: None,
order: 0,
justify_content: JustifyContentMode::FlexStart,
align_items: AlignItemsMode::Stretch,
align_self: AlignSelfMode::Auto,
justify_items: AlignItemsMode::Stretch,
justify_self: AlignSelfMode::Auto,
align_content: AlignContentMode::Stretch,
column_count: 1,
column_count_auto: true,
column_width: LengthSpec::Auto,
column_fill: ColumnFillMode::Balance,
column_span: ColumnSpanMode::None,
column_rule_width: LengthSpec::Absolute(Pt::from_f32(2.25)),
column_rule_style: OutlineLineStyle::Solid,
column_rule_color: None,
column_rule_visible: false,
grid_columns: None,
grid_rows: None,
grid_column_tracks: Vec::new(),
grid_row_tracks: Vec::new(),
grid_subgrid_columns: false,
grid_subgrid_rows: false,
pending_grid_column_tracks_var: None,
pending_grid_row_tracks_var: None,
grid_auto_column_tracks: Vec::new(),
grid_auto_row_tracks: Vec::new(),
grid_auto_flow: GridAutoFlowMode::Row,
grid_column_auto_repeat: None,
grid_row_auto_repeat: None,
grid_template_areas: Vec::new(),
grid_column_line_names: Vec::new(),
grid_row_line_names: Vec::new(),
grid_column_line_start: GridLineSpec::Auto,
grid_column_line_end: GridLineSpec::Auto,
grid_row_line_start: GridLineSpec::Auto,
grid_row_line_end: GridLineSpec::Auto,
grid_area_name: None,
grid_column_start: None,
grid_row_start: None,
row_gap: LengthSpec::Absolute(Pt::ZERO),
pending_row_gap_var: None,
gap: LengthSpec::Absolute(Pt::ZERO),
pending_gap_var: None,
column_gap_normal: true,
flex_grow: 0.0,
flex_shrink: 1.0,
overflow_x: OverflowMode::Visible,
overflow_y: OverflowMode::Visible,
overflow: OverflowMode::Visible,
overflow_clip_margin: LengthSpec::Absolute(Pt::ZERO),
scrollbar_gutter: ScrollbarGutterMode::Auto,
line_clamp: None,
object_fit: ObjectFitMode::Fill,
image_rendering: parent.image_rendering,
object_position: parent.object_position,
custom_lengths: parent.custom_lengths.clone(),
custom_colors: parent.custom_colors.clone(),
custom_color_alpha: parent.custom_color_alpha.clone(),
custom_color_refs: parent.custom_color_refs.clone(),
custom_raw_values: parent.custom_raw_values.clone(),
custom_font_stacks: parent.custom_font_stacks.clone(),
};
apply_custom_property_registrations(&mut computed, &self.custom_property_registrations);
let unset_base = computed.clone();
let parent_font_size = parent.font_size;
let parent_line_height = parent.line_height.clone();
let root_font_size = parent.root_font_size;
let mut matches: Vec<&RuleEntry> = self
.normal_index
.candidate_indices(element)
.into_iter()
.filter_map(|idx| self.normal_rules.get(idx))
.filter(|rule| rule.selector.pseudo_target() == Some(pseudo))
.filter(|rule| {
rule.selector
.matches_with_pseudo(element, ancestors, pseudo)
})
.filter(|rule| container_conditions_match(&rule.container_conditions, ancestors))
.filter(|rule| scope_conditions_match(&rule.scope_conditions, element, ancestors))
.collect();
if matches.is_empty() {
return None;
}
matches.sort_by(|a, b| {
a.normal_layer_rank()
.cmp(&b.normal_layer_rank())
.then_with(|| {
a.specificity
.cmp(&b.specificity)
.then_with(|| a.order.cmp(&b.order))
})
});
let mut current_normal_layer_rank = None;
let mut normal_layer_base = computed.clone();
let mut marker_content_overrides_marker = false;
for rule in matches {
let layer_rank = rule.normal_layer_rank();
if current_normal_layer_rank != Some(layer_rank) {
current_normal_layer_rank = Some(layer_rank);
normal_layer_base = computed.clone();
}
if matches!(pseudo, PseudoTarget::Marker) {
if let Some(content) = &rule.delta.content {
marker_content_overrides_marker =
!matches!(content, ContentSpec::Normal | ContentSpec::Initial);
}
}
if style_delta_authors_font_weight(&rule.delta) {
computed.font_weight_authored = rule.order >= self.author_rule_order_start;
}
self.apply_style_delta(
&mut computed,
&rule.delta,
parent,
&unset_base,
parent_font_size,
parent_line_height.clone(),
root_font_size,
Some(element),
);
apply_revert_layer_delta(&mut computed, &rule.delta.revert_layer, &normal_layer_base);
}
resolve_pending_font_size_var(&mut computed, self.viewport);
resolve_pending_line_height_var(&mut computed, self.viewport);
resolve_computed_line_height(&mut computed, self.viewport);
resolve_pending_vars(&mut computed);
self.resolve_font_face_aliases(&mut computed);
resolve_font_size_adjust_scale(&mut computed, self.font_registry.as_deref());
resolve_font_relative_lengths(&mut computed, self.font_registry.as_deref());
apply_border_style_mask(&mut computed);
resolve_named_counter_style(&mut computed, &self.counter_styles);
computed.marker_content_overridden = marker_content_overrides_marker;
let has_static_content = computed
.content
.as_deref()
.is_some_and(|text| !text.is_empty());
let has_generated_content = computed
.generated_content
.as_ref()
.is_some_and(|parts| !parts.is_empty());
if has_static_content
|| has_generated_content
|| (matches!(pseudo, PseudoTarget::Marker) && marker_content_overrides_marker)
|| (matches!(pseudo, PseudoTarget::Marker) && include_marker_presentation)
|| matches!(
pseudo,
PseudoTarget::FootnoteCall | PseudoTarget::FootnoteMarker
)
|| matches!(pseudo, PseudoTarget::FirstLetter | PseudoTarget::FirstLine)
{
Some(computed)
} else {
None
}
}
}
fn parse_selector_pattern(selector: &str) -> Option<SelectorPattern> {
let selector = selector.trim();
if selector.is_empty() {
return None;
}
let mut parts: Vec<SimpleSelector> = Vec::new();
let mut combinators: Vec<Combinator> = Vec::new();
let mut buf = String::new();
let mut pending: Option<Combinator> = None;
let mut bracket_depth = 0usize;
let mut paren_depth = 0usize;
let mut in_quote: Option<char> = None;
let flush_buf = |buf: &mut String,
parts: &mut Vec<SimpleSelector>,
combinators: &mut Vec<Combinator>,
pending: &mut Option<Combinator>|
-> Option<()> {
let trimmed = buf.trim();
if trimmed.is_empty() {
buf.clear();
return Some(());
}
let simple = parse_simple_selector(trimmed)?;
if !parts.is_empty() {
combinators.push(pending.take().unwrap_or(Combinator::Descendant));
}
parts.push(simple);
buf.clear();
Some(())
};
for ch in selector.chars() {
if let Some(quote) = in_quote {
buf.push(ch);
if ch == quote {
in_quote = None;
}
continue;
}
match ch {
'\'' | '"' if bracket_depth > 0 || paren_depth > 0 => {
in_quote = Some(ch);
buf.push(ch);
continue;
}
'[' => {
bracket_depth += 1;
buf.push(ch);
continue;
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
buf.push(ch);
continue;
}
'(' => {
paren_depth += 1;
buf.push(ch);
continue;
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
buf.push(ch);
continue;
}
_ => {}
}
let at_top_level = bracket_depth == 0 && paren_depth == 0;
if at_top_level && ch == '>' {
if flush_buf(&mut buf, &mut parts, &mut combinators, &mut pending).is_none() {
return None;
}
pending = Some(Combinator::Child);
continue;
}
if at_top_level && ch == '+' {
if flush_buf(&mut buf, &mut parts, &mut combinators, &mut pending).is_none() {
return None;
}
pending = Some(Combinator::AdjacentSibling);
continue;
}
if at_top_level && ch == '~' {
if flush_buf(&mut buf, &mut parts, &mut combinators, &mut pending).is_none() {
return None;
}
pending = Some(Combinator::GeneralSibling);
continue;
}
if at_top_level && ch.is_whitespace() {
if !buf.trim().is_empty() {
if flush_buf(&mut buf, &mut parts, &mut combinators, &mut pending).is_none() {
return None;
}
pending = Some(Combinator::Descendant);
}
continue;
}
buf.push(ch);
}
if flush_buf(&mut buf, &mut parts, &mut combinators, &mut pending).is_none() {
return None;
}
if parts.is_empty() {
return None;
}
Some(SelectorPattern { parts, combinators })
}
fn native_media_prelude_matches(
prelude: &str,
viewport: Size,
prefer_print: bool,
debug: Option<&DebugLogger>,
) -> bool {
let result = crate::css_queries::evaluate_media_list(prelude, viewport, prefer_print);
if let Some(logger) = debug {
logger.increment("css.media.matched", result.matched_queries);
logger.increment("css.media.unmatched", result.unmatched_queries);
logger.increment("css.media.unsupported", result.unsupported_queries);
}
result.matched
}
fn native_supports_prelude_matches(prelude: &str) -> bool {
crate::css_queries::evaluate_supports_condition(
prelude,
|name, value| {
let declaration = crate::css_native::Declaration {
name: name.to_string(),
value: value.to_string(),
important: false,
};
let mut delta = StyleDelta::default();
apply_raw_declaration(
&declaration,
&mut delta,
Size {
width: Pt::ZERO,
height: Pt::ZERO,
},
);
!delta.is_empty()
},
|selector| parse_selector_pattern(selector).is_some(),
)
}
fn native_container_rule_condition(prelude: &str) -> ContainerRuleCondition {
match crate::css_queries::parse_container_prelude(prelude) {
Some(parsed) => ContainerRuleCondition {
name: parsed.name,
condition: parsed.condition,
},
None => ContainerRuleCondition {
name: None,
condition: Some(ContainerConditionExpr::Unsupported),
},
}
}
fn native_scope_rule_condition(prelude: &str) -> Option<ScopeRuleCondition> {
let parsed = crate::css_queries::parse_scope_prelude(prelude)?;
let starts = parsed
.starts
.iter()
.filter_map(|selector| parse_selector_pattern(selector))
.collect::<Vec<_>>();
if starts.is_empty() {
return None;
}
let ends = parsed
.ends
.iter()
.filter_map(|selector| parse_selector_pattern(selector))
.collect::<Vec<_>>();
Some(ScopeRuleCondition { starts, ends })
}
fn native_counter_style_symbols(
prelude: &str,
declarations: &crate::css_native::DeclarationBlock,
) -> Option<(String, AnonymousListStyleSymbols)> {
let name = crate::css_native::parse_identifier(prelude.trim())?;
let mut system = AnonymousListStyleSymbolsSystem::Symbolic;
let mut fixed_start = 1;
let mut symbols: Option<Vec<String>> = None;
let mut prefix = String::new();
let mut suffix = ". ".to_string();
let mut negative_prefix = "-".to_string();
let mut negative_suffix = String::new();
let mut pad_width = 0usize;
let mut pad_symbol = "0".to_string();
for declaration in declarations.normal().chain(declarations.important()) {
match declaration.name.to_ascii_lowercase().as_str() {
"system" => {
let parts = split_top_level_whitespace(&declaration.value);
let Some(keyword) = parts
.first()
.and_then(|value| crate::css_native::parse_identifier(value))
.map(|value| value.to_ascii_lowercase())
else {
continue;
};
match keyword.as_str() {
"cyclic" => system = AnonymousListStyleSymbolsSystem::Cyclic,
"numeric" => system = AnonymousListStyleSymbolsSystem::Numeric,
"alphabetic" => system = AnonymousListStyleSymbolsSystem::Alphabetic,
"symbolic" => system = AnonymousListStyleSymbolsSystem::Symbolic,
"fixed" => {
system = AnonymousListStyleSymbolsSystem::Fixed;
fixed_start = parts
.get(1)
.and_then(|value| value.parse::<i32>().ok())
.unwrap_or(1)
.max(1);
}
"extends"
if parts
.get(1)
.and_then(|value| crate::css_native::parse_identifier(value))
.is_some_and(|value| value.eq_ignore_ascii_case("decimal")) =>
{
system = AnonymousListStyleSymbolsSystem::ExtendsDecimal;
}
_ => return None,
}
}
"symbols" => {
if let Some(parsed) =
crate::css_native::parse_string_or_ident_list(&declaration.value)
{
symbols = Some(parsed);
}
}
"suffix" => {
if let Some(parsed) =
crate::css_native::parse_string_or_ident_list(&declaration.value)
{
suffix = parsed.join("");
}
}
"prefix" => {
if let Some(parsed) =
crate::css_native::parse_string_or_ident_list(&declaration.value)
{
prefix = parsed.join("");
}
}
"negative" => {
if let Some(parsed) =
crate::css_native::parse_string_or_ident_list(&declaration.value)
{
if let Some(value) = parsed.first() {
negative_prefix = value.clone();
}
negative_suffix = parsed.get(1).cloned().unwrap_or_default();
}
}
"pad" => {
let parts = split_top_level_whitespace(&declaration.value);
if let Some(width) = parts.first().and_then(|value| value.parse::<usize>().ok()) {
if let Some(symbol) = parts.get(1).and_then(|value| {
crate::css_native::parse_string_or_ident_list(value)
.and_then(|values| values.into_iter().next())
}) {
pad_width = width;
pad_symbol = symbol;
}
}
}
_ => {}
}
}
let symbols = symbols.unwrap_or_default();
if !matches!(system, AnonymousListStyleSymbolsSystem::ExtendsDecimal)
&& (symbols.is_empty()
|| (matches!(
system,
AnonymousListStyleSymbolsSystem::Numeric
| AnonymousListStyleSymbolsSystem::Alphabetic
) && symbols.len() < 2))
{
return None;
}
Some((
name,
AnonymousListStyleSymbols {
system,
symbols,
prefix,
suffix,
negative_prefix,
negative_suffix,
pad_width,
pad_symbol,
fixed_start,
},
))
}
fn native_rule_list_has_print_media(rules: &[crate::css_native::Rule]) -> bool {
for rule in rules {
let crate::css_native::Rule::At(at_rule) = rule else {
continue;
};
if at_rule.name == "media"
&& crate::css_queries::media_list_has_print_type(&at_rule.prelude)
{
return true;
}
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) = &at_rule.block {
if native_rule_list_has_print_media(nested) {
return true;
}
}
}
false
}
fn count_native_keyframes(raw: &str) -> u64 {
let mut count = 0u64;
let mut depth = 0usize;
let mut quote = None;
let bytes = raw.as_bytes();
let mut index = 0usize;
while index < bytes.len() {
let byte = bytes[index];
if let Some(active) = quote {
if byte == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
if byte == active {
quote = None;
}
index += 1;
continue;
}
match byte {
b'\'' | b'"' => quote = Some(byte),
b'{' => {
if depth == 0 {
count = count.saturating_add(1);
}
depth = depth.saturating_add(1);
}
b'}' => depth = depth.saturating_sub(1),
_ => {}
}
index += 1;
}
count
}
fn scope_conditions_match(
conditions: &[ScopeRuleCondition],
element: &ElementInfo,
ancestors: &[ElementInfo],
) -> bool {
conditions
.iter()
.all(|condition| scope_rule_condition_matches(condition, element, ancestors))
}
fn scope_rule_condition_matches(
condition: &ScopeRuleCondition,
element: &ElementInfo,
ancestors: &[ElementInfo],
) -> bool {
let current_index = ancestors.len();
for scope_root_index in (0..=current_index).rev() {
let candidate = element_at_chain_index(scope_root_index, element, ancestors);
let candidate_ancestors = &ancestors[..scope_root_index];
if !condition
.starts
.iter()
.any(|selector| selector.matches(candidate, candidate_ancestors))
{
continue;
}
if scope_limit_blocks(condition, scope_root_index, element, ancestors) {
continue;
}
return true;
}
false
}
fn scope_limit_blocks(
condition: &ScopeRuleCondition,
scope_root_index: usize,
element: &ElementInfo,
ancestors: &[ElementInfo],
) -> bool {
if condition.ends.is_empty() {
return false;
}
for idx in (scope_root_index + 1)..=ancestors.len() {
let candidate = element_at_chain_index(idx, element, ancestors);
let candidate_ancestors = &ancestors[..idx];
if condition
.ends
.iter()
.any(|selector| selector.matches(candidate, candidate_ancestors))
{
return true;
}
}
false
}
fn element_at_chain_index<'a>(
idx: usize,
element: &'a ElementInfo,
ancestors: &'a [ElementInfo],
) -> &'a ElementInfo {
if idx == ancestors.len() {
element
} else {
&ancestors[idx]
}
}
fn container_conditions_match(
conditions: &[ContainerRuleCondition],
ancestors: &[ElementInfo],
) -> bool {
conditions
.iter()
.all(|condition| container_rule_condition_matches(condition, ancestors))
}
fn container_rule_condition_matches(
condition: &ContainerRuleCondition,
ancestors: &[ElementInfo],
) -> bool {
for ancestor in ancestors.iter().rev() {
if let Some(name) = &condition.name {
if !ancestor
.container_names
.iter()
.any(|candidate| candidate == name)
{
continue;
}
}
let Some(expr) = &condition.condition else {
return condition.name.is_some();
};
if let Some(matched) = container_condition_matches_for_ancestor(expr, ancestor) {
return matched;
}
}
false
}
fn container_condition_matches_for_ancestor(
expr: &ContainerConditionExpr,
ancestor: &ElementInfo,
) -> Option<bool> {
match expr {
ContainerConditionExpr::Feature(feature) => {
container_size_feature_matches_for_ancestor(feature, ancestor)
}
ContainerConditionExpr::Not(inner) => {
container_condition_matches_for_ancestor(inner, ancestor).map(|matched| !matched)
}
ContainerConditionExpr::And(items) => {
let mut saw_eligible = false;
for item in items {
match container_condition_matches_for_ancestor(item, ancestor) {
Some(true) => saw_eligible = true,
Some(false) => return Some(false),
None => return None,
}
}
Some(saw_eligible)
}
ContainerConditionExpr::Or(items) => {
let mut saw_eligible = false;
for item in items {
match container_condition_matches_for_ancestor(item, ancestor) {
Some(true) => return Some(true),
Some(false) => saw_eligible = true,
None => {}
}
}
if saw_eligible { Some(false) } else { None }
}
ContainerConditionExpr::Unsupported => Some(false),
}
}
fn container_size_feature_matches_for_ancestor(
feature: &ContainerSizeFeatureExpr,
ancestor: &ElementInfo,
) -> Option<bool> {
match feature {
ContainerSizeFeatureExpr::Plain { name, value } => {
let actual = container_feature_actual_value(*name, ancestor)?;
Some(container_feature_values_equal(&actual, value))
}
ContainerSizeFeatureExpr::Range {
name,
operator,
value,
} => {
let actual = container_feature_actual_value(*name, ancestor)?;
Some(compare_container_feature_values(&actual, *operator, value))
}
ContainerSizeFeatureExpr::Interval {
name,
start,
start_operator,
end,
end_operator,
} => {
let actual = container_feature_actual_value(*name, ancestor)?;
Some(
compare_container_feature_values(start, *start_operator, &actual)
&& compare_container_feature_values(&actual, *end_operator, end),
)
}
ContainerSizeFeatureExpr::Boolean => None,
}
}
fn container_feature_actual_value(
name: ContainerSizeFeatureName,
ancestor: &ElementInfo,
) -> Option<ContainerFeatureValue> {
match name {
ContainerSizeFeatureName::Width => {
if !matches!(
ancestor.container_type,
ContainerQueryType::InlineSize | ContainerQueryType::Size
) {
return None;
}
ancestor.container_width.map(ContainerFeatureValue::Length)
}
ContainerSizeFeatureName::InlineSize => {
if !matches!(
ancestor.container_type,
ContainerQueryType::InlineSize | ContainerQueryType::Size
) {
return None;
}
ancestor
.container_inline_size
.map(ContainerFeatureValue::Length)
}
ContainerSizeFeatureName::Height => {
if !matches!(ancestor.container_type, ContainerQueryType::Size) {
return None;
}
ancestor.container_height.map(ContainerFeatureValue::Length)
}
ContainerSizeFeatureName::BlockSize => {
if !matches!(ancestor.container_type, ContainerQueryType::Size) {
return None;
}
ancestor
.container_block_size
.map(ContainerFeatureValue::Length)
}
ContainerSizeFeatureName::AspectRatio => {
if !matches!(ancestor.container_type, ContainerQueryType::Size) {
return None;
}
let width = ancestor.container_width?;
let height = ancestor.container_height?;
if height <= Pt::ZERO {
None
} else {
Some(ContainerFeatureValue::Ratio(
width.to_f32() / height.to_f32(),
))
}
}
ContainerSizeFeatureName::Orientation => {
if !matches!(ancestor.container_type, ContainerQueryType::Size) {
return None;
}
let width = ancestor.container_width?;
let height = ancestor.container_height?;
let orientation = if width >= height {
"landscape"
} else {
"portrait"
};
Some(ContainerFeatureValue::Ident(orientation.to_string()))
}
}
}
fn container_feature_values_equal(
lhs: &ContainerFeatureValue,
rhs: &ContainerFeatureValue,
) -> bool {
match (lhs, rhs) {
(ContainerFeatureValue::Length(a), ContainerFeatureValue::Length(b)) => *a == *b,
(ContainerFeatureValue::Ratio(a), ContainerFeatureValue::Ratio(b)) => {
(a - b).abs() <= 0.0001
}
(ContainerFeatureValue::Ident(a), ContainerFeatureValue::Ident(b)) => {
a.eq_ignore_ascii_case(b)
}
_ => false,
}
}
fn compare_container_feature_values(
lhs: &ContainerFeatureValue,
operator: MediaFeatureComparison,
rhs: &ContainerFeatureValue,
) -> bool {
match (lhs, rhs) {
(ContainerFeatureValue::Length(a), ContainerFeatureValue::Length(b)) => {
compare_f32(a.to_f32(), operator, b.to_f32())
}
(ContainerFeatureValue::Ratio(a), ContainerFeatureValue::Ratio(b)) => {
compare_f32(*a, operator, *b)
}
(ContainerFeatureValue::Ident(a), ContainerFeatureValue::Ident(b)) => {
matches!(operator, MediaFeatureComparison::Equal) && a.eq_ignore_ascii_case(b)
}
_ => false,
}
}
fn compare_f32(lhs: f32, operator: MediaFeatureComparison, rhs: f32) -> bool {
match operator {
MediaFeatureComparison::Equal => (lhs - rhs).abs() <= 0.0001,
MediaFeatureComparison::GreaterThan => lhs > rhs,
MediaFeatureComparison::GreaterThanEqual => lhs >= rhs,
MediaFeatureComparison::LessThan => lhs < rhs,
MediaFeatureComparison::LessThanEqual => lhs <= rhs,
}
}
#[cfg(test)]
pub(crate) fn extract_css_page_setup(
css: &str,
debug: Option<&DebugLogger>,
viewport: Option<Size>,
) -> CssPageSetup {
extract_css_page_styles(css, debug, viewport).base
}
pub(crate) fn extract_css_page_styles(
css: &str,
debug: Option<&DebugLogger>,
viewport: Option<Size>,
) -> CssPageStyles {
if css.trim().is_empty() {
return CssPageStyles::default();
}
let Ok(sheet) = crate::css_native::parse_stylesheet(css) else {
return CssPageStyles::default();
};
let viewport = viewport.unwrap_or(Size {
width: Pt::ZERO,
height: Pt::ZERO,
});
let prefer_print = native_rule_list_has_print_media(&sheet.rules);
let mut styles = CssPageStyles::default();
extract_native_css_page_styles_from_rules(
&sheet.rules,
&mut styles,
viewport,
prefer_print,
debug,
);
extract_native_page_margin_box_styles(css, &mut styles);
styles
}
fn extract_native_css_page_styles_from_rules(
rules: &[crate::css_native::Rule],
styles: &mut CssPageStyles,
viewport: Size,
prefer_print: bool,
debug: Option<&DebugLogger>,
) {
for rule in rules {
let crate::css_native::Rule::At(at_rule) = rule else {
continue;
};
match at_rule.name.as_str() {
"page" => {
if let Some(crate::css_native::AtRuleBlock::Declarations(declarations)) =
&at_rule.block
{
for selector in parse_css_page_prelude(&at_rule.prelude) {
let target = page_style_for_selector_mut(styles, &selector);
apply_native_page_declarations(declarations, target);
}
}
}
"media" => {
if native_media_prelude_matches(&at_rule.prelude, viewport, prefer_print, debug) {
if let Some(crate::css_native::AtRuleBlock::Rules(nested)) = &at_rule.block {
extract_native_css_page_styles_from_rules(
nested,
styles,
viewport,
prefer_print,
debug,
);
}
}
}
_ => {}
}
}
}
#[derive(Debug, Clone)]
struct CssPageSelectorKey {
name: Option<String>,
pseudo: CssPagePseudo,
}
#[derive(Debug, Clone, Copy)]
enum CssPagePseudo {
Any,
First,
Left,
Right,
Blank,
}
fn parse_css_page_prelude(prelude: &str) -> Vec<CssPageSelectorKey> {
let mut selectors = Vec::new();
for raw in prelude.split(',') {
let raw = raw.trim();
let (name, pseudo) = if raw.is_empty() {
(None, Some(CssPagePseudo::Any))
} else if let Some(pseudo) = raw.strip_prefix(':') {
(None, pseudo_from_page_selector(pseudo))
} else if let Some((name, pseudo)) = raw.split_once(':') {
(
(!name.trim().is_empty()).then(|| name.trim().to_string()),
pseudo_from_page_selector(pseudo),
)
} else {
(Some(raw.to_string()), Some(CssPagePseudo::Any))
};
if let Some(pseudo) = pseudo {
selectors.push(CssPageSelectorKey { name, pseudo });
}
}
selectors
}
fn pseudo_from_page_selector(value: &str) -> Option<CssPagePseudo> {
match value.trim().to_ascii_lowercase().as_str() {
"first" => Some(CssPagePseudo::First),
"left" => Some(CssPagePseudo::Left),
"right" => Some(CssPagePseudo::Right),
"blank" => Some(CssPagePseudo::Blank),
_ => None,
}
}
fn css_matching_brace(raw: &str, open: usize) -> Option<usize> {
if raw.as_bytes().get(open) != Some(&b'{') {
return None;
}
let bytes = raw.as_bytes();
let mut cursor = open + 1;
let mut depth = 1usize;
let mut quote = None;
while cursor < bytes.len() {
if let Some(active_quote) = quote {
if bytes[cursor] == b'\\' {
cursor = (cursor + 2).min(bytes.len());
continue;
}
if bytes[cursor] == active_quote {
quote = None;
}
cursor += 1;
continue;
}
if bytes[cursor] == b'/' && bytes.get(cursor + 1) == Some(&b'*') {
let tail = raw.get(cursor + 2..)?;
let close = tail.find("*/")?;
cursor += close + 4;
continue;
}
match bytes[cursor] {
b'\'' | b'"' => {
quote = Some(bytes[cursor]);
cursor += 1;
}
b'{' => {
depth += 1;
cursor += 1;
}
b'}' => {
depth -= 1;
if depth == 0 {
return Some(cursor);
}
cursor += 1;
}
_ => cursor += 1,
}
}
None
}
fn page_style_for_selector_mut<'a>(
styles: &'a mut CssPageStyles,
selector: &CssPageSelectorKey,
) -> &'a mut CssPageSetup {
if let Some(name) = selector.name.as_deref() {
let page = styles.named_mut(name);
return match selector.pseudo {
CssPagePseudo::Any => &mut page.base,
CssPagePseudo::First => &mut page.first,
CssPagePseudo::Left => &mut page.left,
CssPagePseudo::Right => &mut page.right,
CssPagePseudo::Blank => &mut page.blank,
};
}
match selector.pseudo {
CssPagePseudo::Any => &mut styles.base,
CssPagePseudo::First => &mut styles.first,
CssPagePseudo::Left => &mut styles.left,
CssPagePseudo::Right => &mut styles.right,
CssPagePseudo::Blank => &mut styles.blank,
}
}
fn parse_page_margin_box_content(raw: &str) -> Option<Vec<CssPageMarginContentPart>> {
if matches!(raw.trim().to_ascii_lowercase().as_str(), "none" | "normal") {
return Some(Vec::new());
}
let tokens = crate::css_native::tokenize_component_values(raw)?;
let mut parts = Vec::new();
for token in tokens {
match token {
ComponentValue::String(value) => parts.push(CssPageMarginContentPart::Text(value)),
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("counter") =>
{
match arguments.trim().to_ascii_lowercase().as_str() {
"page" => parts.push(CssPageMarginContentPart::PageCounter),
"pages" => parts.push(CssPageMarginContentPart::PagesCounter),
_ => return None,
}
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("element") || name.eq_ignore_ascii_case("string") =>
{
let is_element = name.eq_ignore_ascii_case("element");
let mut arguments = crate::css_native::tokenize_component_values(&arguments)?
.into_iter()
.filter_map(|token| match token {
ComponentValue::Ident(value) | ComponentValue::String(value) => Some(value),
ComponentValue::Delim(',') => None,
_ => Some(String::new()),
})
.collect::<Vec<_>>();
if arguments.is_empty()
|| arguments.len() > 2
|| arguments.iter().any(|value| value.trim().is_empty())
{
return None;
}
let content_name = arguments.remove(0);
let position = match arguments
.first()
.map(|value| value.trim().to_ascii_lowercase())
.as_deref()
{
None | Some("first") => CssPageContentPosition::First,
Some("start") => CssPageContentPosition::Start,
Some("last") => CssPageContentPosition::Last,
Some("first-except") => CssPageContentPosition::FirstExcept,
_ => return None,
};
if is_element {
parts.push(CssPageMarginContentPart::RunningElement {
name: content_name,
position,
});
} else {
parts.push(CssPageMarginContentPart::NamedString {
name: content_name,
position,
});
}
}
_ => return None,
}
}
Some(parts)
}
fn parse_page_line_height(raw: &str) -> Option<CssPageLineHeight> {
let trimmed = raw.trim();
if let Ok(number) = trimmed.parse::<f32>() {
return (number.is_finite() && number >= 0.0).then_some(CssPageLineHeight::Number(number));
}
if let Some(value) = parse_absolute_pt_from_str(trimmed) {
return Some(CssPageLineHeight::Absolute(value));
}
None
}
fn parse_page_counter_value(raw: &str, counter_name: &str, default_value: i32) -> Option<i32> {
let parts: Vec<&str> = raw
.split(|ch: char| ch.is_whitespace() || ch == ',')
.filter(|part| !part.is_empty())
.collect();
for (index, part) in parts.iter().enumerate() {
if part.eq_ignore_ascii_case(counter_name) {
return parts
.get(index + 1)
.and_then(|value| value.parse::<i32>().ok())
.or(Some(default_value));
}
}
None
}
fn parse_page_font_name(raw: &str) -> Option<String> {
crate::css_native::parse_string_or_ident_list(raw)?
.into_iter()
.next()
}
fn apply_native_page_margin_box_declarations(
declarations: &crate::css_native::DeclarationBlock,
setup: &mut CssPageSetup,
kind: CssPageMarginBoxKind,
) {
let mut parsed = CssPageMarginBox {
kind: Some(kind),
..CssPageMarginBox::default()
};
let mut apply = |declaration: &crate::css_native::Declaration| match declaration
.name
.to_ascii_lowercase()
.as_str()
{
"content" => {
parsed.content = parse_page_margin_box_content(&declaration.value);
}
"background" | "background-color" => {
parsed.background = parse_color_string(&declaration.value);
}
"color" => {
parsed.color = parse_color_string(&declaration.value).map(|(color, _)| color);
}
"width" => {
parsed.width = parse_absolute_pt_from_str(&declaration.value);
}
"font-family" => {
parsed.font_name = parse_page_font_name(&declaration.value);
}
"font-size" => {
parsed.font_size = parse_absolute_pt_from_str(&declaration.value);
}
"font-weight" => {
parsed.font_weight = parse_font_weight_str(&declaration.value);
}
"line-height" => {
parsed.line_height = parse_page_line_height(&declaration.value);
}
_ => {}
};
for declaration in declarations.normal() {
apply(declaration);
}
for declaration in declarations.important() {
apply(declaration);
}
if let Some(existing) = setup
.margin_boxes
.iter_mut()
.find(|candidate| candidate.kind == Some(kind))
{
*existing = existing.cascaded_with(&parsed);
} else {
setup.margin_boxes.push(parsed);
}
}
fn apply_page_footnote_border_shorthand(raw: &str, area: &mut CssPageFootnoteArea) {
let mut visible = None;
for part in raw.split_whitespace() {
let lowered = part.to_ascii_lowercase();
match lowered.as_str() {
"none" | "hidden" => visible = Some(false),
"solid" | "dotted" | "dashed" | "double" | "groove" | "ridge" | "inset" | "outset" => {
visible = Some(true)
}
_ => {
if let Some(width) = parse_absolute_pt_from_str(part) {
area.border_top_width = Some(width.max(Pt::ZERO));
} else if let Some((color, _)) = parse_color_string(part) {
area.border_top_color = Some(color);
}
}
}
}
if visible.is_some() {
area.border_top_visible = visible;
}
}
fn apply_native_page_footnote_declarations(
declarations: &crate::css_native::DeclarationBlock,
setup: &mut CssPageSetup,
) {
let mut parsed = CssPageFootnoteArea::default();
let mut apply = |declaration: &crate::css_native::Declaration| match declaration
.name
.to_ascii_lowercase()
.as_str()
{
"border-top" => apply_page_footnote_border_shorthand(&declaration.value, &mut parsed),
"border-top-width" => {
parsed.border_top_width =
parse_absolute_pt_from_str(&declaration.value).map(|value| value.max(Pt::ZERO));
}
"border-top-color" => {
parsed.border_top_color =
parse_color_string(&declaration.value).map(|(color, _)| color);
}
"border-top-style" => {
parsed.border_top_visible = match declaration.value.trim().to_ascii_lowercase().as_str()
{
"none" | "hidden" => Some(false),
"solid" | "dotted" | "dashed" | "double" | "groove" | "ridge" | "inset"
| "outset" => Some(true),
_ => None,
};
}
"padding-top" => {
parsed.padding_top =
parse_absolute_pt_from_str(&declaration.value).map(|value| value.max(Pt::ZERO));
}
"max-height" => {
parsed.max_height =
parse_absolute_pt_from_str(&declaration.value).map(|value| value.max(Pt::ZERO));
}
_ => {}
};
for declaration in declarations.normal() {
apply(declaration);
}
for declaration in declarations.important() {
apply(declaration);
}
setup.footnote_area = setup.footnote_area.cascaded_with(&parsed);
}
fn page_margin_box_kind(name: &str) -> Option<CssPageMarginBoxKind> {
match name {
"top-left-corner" => Some(CssPageMarginBoxKind::TopLeftCorner),
"top-left" => Some(CssPageMarginBoxKind::TopLeft),
"top-center" => Some(CssPageMarginBoxKind::TopCenter),
"top-right" => Some(CssPageMarginBoxKind::TopRight),
"top-right-corner" => Some(CssPageMarginBoxKind::TopRightCorner),
"bottom-left-corner" => Some(CssPageMarginBoxKind::BottomLeftCorner),
"bottom-left" => Some(CssPageMarginBoxKind::BottomLeft),
"bottom-center" => Some(CssPageMarginBoxKind::BottomCenter),
"bottom-right" => Some(CssPageMarginBoxKind::BottomRight),
"bottom-right-corner" => Some(CssPageMarginBoxKind::BottomRightCorner),
"left-top" => Some(CssPageMarginBoxKind::LeftTop),
"left-middle" => Some(CssPageMarginBoxKind::LeftMiddle),
"left-bottom" => Some(CssPageMarginBoxKind::LeftBottom),
"right-top" => Some(CssPageMarginBoxKind::RightTop),
"right-middle" => Some(CssPageMarginBoxKind::RightMiddle),
"right-bottom" => Some(CssPageMarginBoxKind::RightBottom),
_ => None,
}
}
fn extract_native_page_margin_box_styles(css: &str, styles: &mut CssPageStyles) {
let lowered = css.to_ascii_lowercase();
let mut page_cursor = 0usize;
while let Some(relative_page) = lowered[page_cursor..].find("@page") {
let page_start = page_cursor + relative_page;
let after_name = page_start + "@page".len();
if lowered
.as_bytes()
.get(after_name)
.is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
page_cursor = after_name;
continue;
}
let Some(relative_open) = lowered[after_name..].find('{') else {
break;
};
let open = after_name + relative_open;
if lowered[after_name..open].contains(';') {
page_cursor = open + 1;
continue;
}
let Some(close) = css_matching_brace(css, open) else {
break;
};
let prelude = &css[after_name..open];
let body_start = open + 1;
let mut box_cursor = body_start;
while box_cursor < close {
let Some(relative_at) = lowered[box_cursor..close].find('@') else {
break;
};
let box_start = box_cursor + relative_at;
let mut name_end = box_start + 1;
while lowered
.as_bytes()
.get(name_end)
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
{
name_end += 1;
}
let nested_name = &lowered[box_start + 1..name_end];
let kind = page_margin_box_kind(nested_name);
let is_footnote = nested_name == "footnote";
if kind.is_none() && !is_footnote {
box_cursor = name_end.max(box_start + 1);
continue;
}
let mut box_open = name_end;
while lowered
.as_bytes()
.get(box_open)
.is_some_and(u8::is_ascii_whitespace)
{
box_open += 1;
}
if lowered.as_bytes().get(box_open) != Some(&b'{') {
box_cursor = name_end;
continue;
}
let Some(box_close) = css_matching_brace(css, box_open) else {
break;
};
if box_close > close {
break;
}
if let Ok(declarations) =
crate::css_native::parse_declaration_block(&css[box_open + 1..box_close])
{
for selector in parse_css_page_prelude(prelude) {
let target = page_style_for_selector_mut(styles, &selector);
if is_footnote {
apply_native_page_footnote_declarations(&declarations, target);
} else if let Some(kind) = kind {
apply_native_page_margin_box_declarations(&declarations, target, kind);
}
}
}
box_cursor = box_close + 1;
}
page_cursor = close + 1;
}
}
fn apply_native_page_declarations(
declarations: &crate::css_native::DeclarationBlock,
setup: &mut CssPageSetup,
) {
for declaration in declarations.normal() {
apply_native_page_property(declaration, setup);
}
for declaration in declarations.important() {
apply_native_page_property(declaration, setup);
}
}
fn apply_native_page_property(
declaration: &crate::css_native::Declaration,
setup: &mut CssPageSetup,
) {
match declaration.name.to_ascii_lowercase().as_str() {
"size" => {
if let Some(size) = parse_page_size_from_str(&declaration.value) {
setup.size = Some(size);
}
}
"bleed" => {
let value = declaration.value.trim();
if value.eq_ignore_ascii_case("auto") {
setup.bleed = Some(Pt::ZERO);
} else if let Some(value) = parse_absolute_pt_from_str(value) {
setup.bleed = Some(value.max(Pt::ZERO));
}
}
"marks" => {
let normalized = declaration.value.to_ascii_lowercase();
let tokens = normalized.split_whitespace().collect::<Vec<_>>();
if tokens.iter().any(|token| *token == "none") {
setup.marks = Some(PageMarks::default());
} else {
let marks = PageMarks {
crop: tokens.iter().any(|token| *token == "crop"),
cross: tokens.iter().any(|token| *token == "cross"),
};
if marks.crop || marks.cross {
setup.marks = Some(marks);
}
}
}
"page-orientation" => {
setup.page_orientation = match declaration.value.trim().to_ascii_lowercase().as_str() {
"upright" => Some(PageOrientation::Upright),
"rotate-left" => Some(PageOrientation::RotateLeft),
"rotate-right" => Some(PageOrientation::RotateRight),
_ => None,
};
}
"margin" => apply_margin_shorthand_str(setup, &declaration.value),
"margin-top" => {
if let Some((absolute, percent)) = parse_page_margin_value(&declaration.value) {
setup.margin_top = absolute;
setup.margin_top_percent = percent;
}
}
"margin-right" => {
if let Some((absolute, percent)) = parse_page_margin_value(&declaration.value) {
setup.margin_right = absolute;
setup.margin_right_percent = percent;
}
}
"margin-bottom" => {
if let Some((absolute, percent)) = parse_page_margin_value(&declaration.value) {
setup.margin_bottom = absolute;
setup.margin_bottom_percent = percent;
}
}
"margin-left" => {
if let Some((absolute, percent)) = parse_page_margin_value(&declaration.value) {
setup.margin_left = absolute;
setup.margin_left_percent = percent;
}
}
"background" | "background-color" => {
setup.background = parse_color_string(&declaration.value)
}
"color" => {
setup.color = parse_color_string(&declaration.value).map(|(color, _)| color);
}
"font-family" => setup.font_name = parse_page_font_name(&declaration.value),
"font-size" => setup.font_size = parse_absolute_pt_from_str(&declaration.value),
"font-weight" => setup.font_weight = parse_font_weight_str(&declaration.value),
"line-height" => setup.line_height = parse_page_line_height(&declaration.value),
"counter-reset" => {
setup.page_counter_reset = parse_page_counter_value(&declaration.value, "page", 0)
}
"counter-increment" => {
setup.page_counter_increment = parse_page_counter_value(&declaration.value, "page", 1)
}
_ => {}
}
}
fn parse_absolute_pt_from_str(raw: &str) -> Option<Pt> {
match length_spec_from_string(raw) {
Some(LengthSpec::Absolute(value)) => Some(value),
_ => None,
}
}
fn parse_page_margin_value(raw: &str) -> Option<(Option<Pt>, Option<f32>)> {
match length_spec_from_string(raw)? {
LengthSpec::Absolute(value) => Some((Some(value), None)),
LengthSpec::Percent(value) if value.is_finite() => Some((None, Some(value))),
_ => None,
}
}
fn apply_margin_shorthand_str(setup: &mut CssPageSetup, raw: &str) {
let values: Vec<(Option<Pt>, Option<f32>)> = parse_length_list(raw)
.into_iter()
.filter_map(|spec| match spec {
LengthSpec::Absolute(value) => Some((Some(value), None)),
LengthSpec::Percent(value) if value.is_finite() => Some((None, Some(value))),
_ => None,
})
.collect();
if values.is_empty() {
return;
}
let (top, right, bottom, left) = match values.len() {
1 => (values[0], values[0], values[0], values[0]),
2 => (values[0], values[1], values[0], values[1]),
3 => (values[0], values[1], values[2], values[1]),
_ => (values[0], values[1], values[2], values[3]),
};
setup.margin_top = top.0;
setup.margin_top_percent = top.1;
setup.margin_right = right.0;
setup.margin_right_percent = right.1;
setup.margin_bottom = bottom.0;
setup.margin_bottom_percent = bottom.1;
setup.margin_left = left.0;
setup.margin_left_percent = left.1;
}
fn parse_page_size_from_str(raw: &str) -> Option<Size> {
let normalized = raw.replace(',', " ").replace('\n', " ").replace('\r', " ");
let mut orientation: Option<&str> = None;
let mut parts: Vec<&str> = Vec::new();
for token in normalized.split_whitespace() {
let lower = token.to_ascii_lowercase();
if lower == "landscape" || lower == "portrait" {
orientation = Some(if lower == "landscape" {
"landscape"
} else {
"portrait"
});
continue;
}
parts.push(token);
}
if parts.is_empty() {
return None;
}
if parts.len() == 1 {
let ident = parts[0].to_ascii_lowercase();
if ident == "auto" {
return None;
}
let mut size = page_named_size(&ident)?;
if let Some(value) = orientation {
size = orient_page_size(size, value);
}
return Some(size);
}
if parts.len() == 2 {
let mut size = Size {
width: parse_absolute_pt_from_str(parts[0])?,
height: parse_absolute_pt_from_str(parts[1])?,
};
if let Some(value) = orientation {
size = orient_page_size(size, value);
}
return Some(size);
}
None
}
fn page_named_size(name: &str) -> Option<Size> {
match name {
"a5" => Some(Size::from_mm(148.0, 210.0)),
"a4" => Some(Size::from_mm(210.0, 297.0)),
"a3" => Some(Size::from_mm(297.0, 420.0)),
// Chromium's print preset is quantized to whole PDF points for this
// named sheet (nominally 257 mm by 364 mm).
"jis-b4" => Some(Size {
width: Pt::from_f32(729.0),
height: Pt::from_f32(1032.0),
}),
"letter" => Some(Size::from_inches(8.5, 11.0)),
"legal" => Some(Size::from_inches(8.5, 14.0)),
"ledger" | "tabloid" => Some(Size::from_inches(11.0, 17.0)),
_ => None,
}
}
fn orient_page_size(size: Size, orientation: &str) -> Size {
match orientation {
"landscape" => {
if size.width < size.height {
Size {
width: size.height,
height: size.width,
}
} else {
size
}
}
"portrait" => {
if size.width > size.height {
Size {
width: size.height,
height: size.width,
}
} else {
size
}
}
_ => size,
}
}
fn collect_native_custom_properties(
declarations: &crate::css_native::DeclarationBlock,
) -> Vec<(String, String)> {
declarations
.declarations
.iter()
.filter(|declaration| declaration.name.starts_with("--"))
.map(|declaration| (declaration.name.clone(), declaration.value.clone()))
.collect()
}
fn log_native_declaration_no_effects(
declarations: &crate::css_native::DeclarationBlock,
selectors: &str,
logger: &DebugLogger,
) {
let selector = selectors.trim();
for declaration in &declarations.declarations {
let name = declaration.name.to_ascii_lowercase();
let raw = declaration.value.trim();
if let Some((requested, applied, reason)) = native_layout_mode_normalization(&name, raw) {
let json = format!(
"{{\"type\":\"jit.known_loss\",\"code\":\"LAYOUT_MODE_NORMALIZED\",\"property\":\"display\",\"requested\":{},\"applied\":{},\"reason\":{},\"selector\":{}}}",
json_string(&requested),
json_string(applied),
json_string(reason),
json_string(selector)
);
logger.log_json(&json);
logger.increment("jit.known_loss.layout_mode_normalized", 1);
}
if native_filters_effects_fallback(&name, raw) {
let json = format!(
"{{\"type\":\"jit.known_loss\",\"code\":\"FILTERS_EFFECTS_FALLBACK\",\"property\":{},\"fallback\":\"effect-ignored\",\"selector\":{}}}",
json_string(&name),
json_string(selector)
);
logger.log_json(&json);
logger.increment("jit.known_loss.filters_effects_fallback", 1);
}
if name == "background"
&& has_conic_gradient_function(raw)
&& parse_conic_gradient_str(raw).is_none()
&& !is_dynamic_conic_gradient_expression(raw)
{
let json = format!(
"{{\"type\":\"jit.known_loss\",\"code\":\"CONIC_GRADIENT_FALLBACK\",\"property\":\"background\",\"fallback\":\"gradient-ignored\",\"selector\":{}}}",
json_string(selector)
);
logger.log_json(&json);
logger.increment("jit.known_loss.conic_gradient_fallback", 1);
}
let parsed_no_effect = is_native_parsed_no_effect_property(&name);
let mut probe = StyleDelta::default();
let supported =
apply_native_raw_property(&declaration.name, &declaration.value, &mut probe);
if supported
|| name.starts_with("--")
|| !parsed_no_effect && is_filters_effects_fallback_property_name(&name)
{
continue;
}
let json = format!(
"{{\"type\":\"jit.known_loss\",\"code\":\"DECLARATION_PARSED_NO_EFFECT\",\"property\":{},\"selector\":{}}}",
json_string(&name),
json_string(selector)
);
logger.log_json(&json);
logger.increment("jit.known_loss.declaration_parsed_no_effect", 1);
}
}
fn native_layout_mode_normalization(
property_name: &str,
raw: &str,
) -> Option<(String, &'static str, &'static str)> {
if property_name != "display" {
return None;
}
let lowered = raw.trim().to_ascii_lowercase();
let reason = if lowered.contains("ruby") {
if matches!(
lowered.as_str(),
"ruby-base" | "ruby-text" | "ruby-base-container" | "ruby-text-container"
) {
"display_ruby_internal_not_supported"
} else {
"display_ruby_not_supported"
}
} else if lowered.contains("box") && lowered != "contents" {
"display_legacy_box_not_supported"
} else {
return None;
};
let mode = parse_display_mode_str(&lowered)?;
Some((lowered, display_mode_name(mode), reason))
}
fn native_filters_effects_fallback(property_name: &str, raw: &str) -> bool {
match property_name {
"opacity" => parse_opacity_spec_str(raw).is_none(),
"filter" => !filter_spec_or_revert_layer_is_native_supported(raw),
"backdrop-filter" => !backdrop_filter_spec_or_revert_layer_is_native_supported(raw),
"clip-path" => !clip_path_spec_or_revert_layer_is_supported(raw),
"mix-blend-mode" => parse_mix_blend_mode_str(raw).is_none(),
"isolation" => parse_isolation_spec_str(raw).is_none(),
name if is_filters_effects_fallback_property_name(name) => true,
_ => false,
}
}
fn filter_spec_or_revert_layer_is_native_supported(raw: &str) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return true;
}
parse_filter_spec_str(raw).is_some_and(|filter| {
filter.operations.iter().all(|operation| {
!matches!(operation, PaintFilterOperation::Url(source) if !source.trim().starts_with('#'))
})
})
}
fn backdrop_filter_spec_or_revert_layer_is_native_supported(raw: &str) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return true;
}
parse_backdrop_filter_spec_str(raw).is_some_and(|filter| {
filter.operations.iter().all(|operation| {
!matches!(operation, PaintFilterOperation::Url(source) if !source.trim().starts_with('#'))
})
})
}
fn is_native_parsed_no_effect_property(name: &str) -> bool {
let _ = name;
false
}
fn is_filters_effects_fallback_property_name(name: &str) -> bool {
matches!(
name,
"opacity"
| "filter"
| "backdrop-filter"
| "clip-path"
| "mix-blend-mode"
| "isolation"
| "mask"
| "mask-image"
| "mask-border"
| "mask-border-source"
| "mask-composite"
)
}
fn has_conic_gradient_function(raw: &str) -> bool {
let lower = raw.to_ascii_lowercase();
lower.contains("conic-gradient(") || lower.contains("repeating-conic-gradient(")
}
fn is_dynamic_conic_gradient_expression(raw: &str) -> bool {
let lower = raw.to_ascii_lowercase();
has_conic_gradient_function(&lower)
&& (lower.contains("var(")
|| lower.contains("currentcolor")
|| lower.contains("color-mix("))
}
fn parse_simple_selector(selector: &str) -> Option<SimpleSelector> {
let selector = selector.trim();
if selector.is_empty() {
return None;
}
let mut base = String::new();
let mut pseudos_raw: Vec<String> = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
let mut in_pseudo = false;
for ch in selector.chars() {
if ch == ':' && depth == 0 {
if !in_pseudo {
base = current.clone();
current.clear();
in_pseudo = true;
} else if current.trim().is_empty() {
// Handle pseudo-element double-colon spellings.
continue;
} else {
pseudos_raw.push(current.clone());
current.clear();
}
continue;
}
if ch == '(' {
depth += 1;
} else if ch == ')' {
depth = depth.saturating_sub(1);
}
current.push(ch);
}
if in_pseudo {
if !current.trim().is_empty() {
pseudos_raw.push(current);
}
} else {
base = current;
}
let (base, attrs_raw) = extract_attr_selectors(&base);
let mut tag = None;
let mut id = None;
let mut classes = Vec::new();
let mut buffer = String::new();
let mut mode = SelectorPart::Tag;
for ch in base.chars() {
match ch {
'#' => {
flush_selector_part(&mut mode, &mut buffer, &mut tag, &mut id, &mut classes);
mode = SelectorPart::Id;
}
'.' => {
flush_selector_part(&mut mode, &mut buffer, &mut tag, &mut id, &mut classes);
mode = SelectorPart::Class;
}
_ => buffer.push(ch),
}
}
flush_selector_part(&mut mode, &mut buffer, &mut tag, &mut id, &mut classes);
let mut pseudos = Vec::new();
for pseudo_raw in pseudos_raw {
if let Some(pseudo) = parse_pseudo_class(&pseudo_raw) {
pseudos.push(pseudo);
}
}
let mut attrs = Vec::new();
for raw in attrs_raw {
if let Some(attr) = parse_attr_selector(&raw) {
attrs.push(attr);
}
}
if tag.is_none() && id.is_none() && classes.is_empty() && attrs.is_empty() && pseudos.is_empty()
{
return None;
}
Some(SimpleSelector {
tag: tag.map(|t| t.to_ascii_lowercase()),
id: id.map(|i| i.to_ascii_lowercase()),
classes: classes
.into_iter()
.map(|c| c.to_ascii_lowercase())
.collect(),
attrs,
pseudos,
})
}
fn extract_attr_selectors(input: &str) -> (String, Vec<String>) {
let mut base = String::new();
let mut attrs = Vec::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '[' {
base.push(ch);
continue;
}
let mut buf = String::new();
let mut in_quote: Option<char> = None;
while let Some(c) = chars.next() {
if let Some(q) = in_quote {
if c == q {
in_quote = None;
}
buf.push(c);
continue;
}
if c == '"' || c == '\'' {
in_quote = Some(c);
buf.push(c);
continue;
}
if c == ']' {
break;
}
buf.push(c);
}
if !buf.trim().is_empty() {
attrs.push(buf.trim().to_string());
}
}
(base, attrs)
}
fn parse_attr_selector(raw: &str) -> Option<AttrSelector> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let mut op: Option<AttrOp> = None;
let mut split_at: Option<usize> = None;
let ops = ["~=", "|=", "^=", "$=", "*=", "="];
let mut in_quote: Option<char> = None;
let chars: Vec<char> = raw.chars().collect();
let mut i = 0usize;
while i < chars.len() {
let ch = chars[i];
if let Some(q) = in_quote {
if ch == q {
in_quote = None;
}
i += 1;
continue;
}
if ch == '"' || ch == '\'' {
in_quote = Some(ch);
i += 1;
continue;
}
for token in ops {
if i + token.len() <= chars.len() {
let slice: String = chars[i..i + token.len()].iter().collect();
if slice == token {
split_at = Some(i);
op = Some(match token {
"~=" => AttrOp::Includes,
"|=" => AttrOp::DashMatch,
"^=" => AttrOp::Prefix,
"$=" => AttrOp::Suffix,
"*=" => AttrOp::Substring,
"=" => AttrOp::Equals,
_ => AttrOp::Exists,
});
i = chars.len();
break;
}
}
}
i += 1;
}
let (name, value) = if let (Some(pos), Some(op_kind)) = (split_at, op) {
let name = raw[..pos].trim();
let value_raw = raw[(pos + if op_kind == AttrOp::Equals { 1 } else { 2 })..].trim();
(name, Some(value_raw))
} else {
(raw, None)
};
let name = name.trim().to_ascii_lowercase();
if name.is_empty() {
return None;
}
let mut case_insensitive = false;
let value = value.and_then(|v| {
let v = v.trim();
if v.is_empty() {
return None;
}
let (value, modifier) =
if let Some(quote) = v.chars().next().filter(|ch| *ch == '"' || *ch == '\'') {
let tail = v.get(quote.len_utf8()..)?;
let closing = tail.find(quote)?;
let value = tail.get(..closing)?;
let modifier = tail.get(closing + quote.len_utf8()..)?.trim();
(value, modifier)
} else {
let mut parts = v.split_whitespace();
(parts.next()?, parts.next().unwrap_or(""))
};
case_insensitive = modifier.eq_ignore_ascii_case("i");
Some(value.to_string())
});
let op = op.unwrap_or(AttrOp::Exists);
Some(AttrSelector {
name,
op,
value,
case_insensitive,
})
}
fn parse_pseudo_class(raw: &str) -> Option<PseudoClass> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if let Some(selectors) = parse_functional_selector_list(raw, "not") {
return Some(PseudoClass::Not(selectors));
}
if let Some(selectors) = parse_functional_selector_list(raw, "is") {
return Some(PseudoClass::Is(selectors));
}
if let Some(selectors) = parse_functional_selector_list(raw, "where") {
return Some(PseudoClass::Where(selectors));
}
if let Some(selectors) = parse_relative_selector_list(raw, "has") {
return Some(PseudoClass::Has(selectors));
}
if raw.eq_ignore_ascii_case("root") {
return Some(PseudoClass::Root);
}
if raw.eq_ignore_ascii_case("scope") {
return Some(PseudoClass::Scope);
}
if raw.eq_ignore_ascii_case("empty") {
return Some(PseudoClass::Empty);
}
if raw.eq_ignore_ascii_case("defined") {
return Some(PseudoClass::Defined);
}
if let Some(value) = parse_single_pseudo_argument(raw, "dir") {
let direction = value.to_ascii_lowercase();
if matches!(direction.as_str(), "ltr" | "rtl") {
return Some(PseudoClass::Direction(direction));
}
return Some(PseudoClass::Unsupported);
}
if let Some(value) = parse_single_pseudo_argument(raw, "lang") {
let language = value
.trim_matches('"')
.trim_matches('\'')
.to_ascii_lowercase();
if !language.is_empty() {
return Some(PseudoClass::Language(language));
}
return Some(PseudoClass::Unsupported);
}
if raw.eq_ignore_ascii_case("first-child") {
return Some(PseudoClass::FirstChild);
}
if raw.eq_ignore_ascii_case("last-child") {
return Some(PseudoClass::LastChild);
}
if raw.eq_ignore_ascii_case("only-child") {
return Some(PseudoClass::OnlyChild);
}
if raw.eq_ignore_ascii_case("first-of-type") {
return Some(PseudoClass::FirstOfType);
}
if raw.eq_ignore_ascii_case("last-of-type") {
return Some(PseudoClass::LastOfType);
}
if raw.eq_ignore_ascii_case("only-of-type") {
return Some(PseudoClass::OnlyOfType);
}
if raw.eq_ignore_ascii_case("hover") {
return Some(PseudoClass::Hover);
}
if raw.eq_ignore_ascii_case("before") {
return Some(PseudoClass::Before);
}
if raw.eq_ignore_ascii_case("after") {
return Some(PseudoClass::After);
}
if raw.eq_ignore_ascii_case("marker") {
return Some(PseudoClass::Marker);
}
if raw.eq_ignore_ascii_case("footnote-call") {
return Some(PseudoClass::FootnoteCall);
}
if raw.eq_ignore_ascii_case("footnote-marker") {
return Some(PseudoClass::FootnoteMarker);
}
if raw.eq_ignore_ascii_case("first-letter") {
return Some(PseudoClass::FirstLetter);
}
if raw.eq_ignore_ascii_case("first-line") {
return Some(PseudoClass::FirstLine);
}
if let Some(args) = raw.strip_prefix("nth-child(") {
let args = args.trim_end_matches(')').trim();
if let Some((formula, selector_list)) = split_nth_of_selector(args) {
let (a, b) = parse_nth_expression(formula)?;
let selectors = crate::css_native::split_top_level(selector_list, ',')
.ok()?
.into_iter()
.filter_map(|selector| parse_simple_selector(selector.trim()))
.collect::<Vec<_>>();
if selectors.is_empty() {
return None;
}
return Some(PseudoClass::NthChildOf { a, b, selectors });
}
if args.eq_ignore_ascii_case("even") {
return Some(PseudoClass::NthChildEven);
}
if args.eq_ignore_ascii_case("odd") {
return Some(PseudoClass::NthChildOdd);
}
if let Ok(n) = args.parse::<usize>() {
if n >= 1 {
return Some(PseudoClass::NthChild(n));
}
}
if let Some((a, b)) = parse_nth_formula(args) {
return Some(PseudoClass::NthChildFormula { a, b });
}
return None;
}
if let Some(args) = raw.strip_prefix("nth-last-child(") {
let args = args.trim_end_matches(')').trim();
let (a, b) = parse_nth_expression(args)?;
return Some(PseudoClass::NthLastChildFormula { a, b });
}
if let Some(args) = raw.strip_prefix("nth-of-type(") {
let args = args.trim_end_matches(')').trim();
if args.eq_ignore_ascii_case("even") {
return Some(PseudoClass::NthOfTypeEven);
}
if args.eq_ignore_ascii_case("odd") {
return Some(PseudoClass::NthOfTypeOdd);
}
if let Ok(n) = args.parse::<usize>() {
if n >= 1 {
return Some(PseudoClass::NthOfType(n));
}
}
if let Some((a, b)) = parse_nth_formula(args) {
return Some(PseudoClass::NthOfTypeFormula { a, b });
}
return None;
}
if let Some(args) = raw.strip_prefix("nth-last-of-type(") {
let args = args.trim_end_matches(')').trim();
let (a, b) = parse_nth_expression(args)?;
return Some(PseudoClass::NthLastOfTypeFormula { a, b });
}
Some(PseudoClass::Unsupported)
}
fn parse_single_pseudo_argument<'a>(raw: &'a str, name: &str) -> Option<&'a str> {
let open = raw.find('(')?;
if !raw.get(..open)?.eq_ignore_ascii_case(name) || !raw.ends_with(')') {
return None;
}
Some(raw.get(open + 1..raw.len() - 1)?.trim())
}
fn split_nth_of_selector(args: &str) -> Option<(&str, &str)> {
let lower = args.to_ascii_lowercase();
let index = lower.find(" of ")?;
let formula = args.get(..index)?.trim();
let selectors = args.get(index + 4..)?.trim();
(!formula.is_empty() && !selectors.is_empty()).then_some((formula, selectors))
}
fn parse_nth_expression(raw: &str) -> Option<(i32, i32)> {
if raw.eq_ignore_ascii_case("even") {
return Some((2, 0));
}
if raw.eq_ignore_ascii_case("odd") {
return Some((2, 1));
}
if let Ok(index) = raw.parse::<i32>() {
return Some((0, index));
}
parse_nth_formula(raw)
}
fn parse_functional_selector_list(raw: &str, name: &str) -> Option<Vec<SimpleSelector>> {
let prefix_len = name.len() + 1;
if raw.len() <= prefix_len || !raw.ends_with(')') {
return None;
}
let prefix = raw.get(..prefix_len)?;
if !prefix[..name.len()].eq_ignore_ascii_case(name) || !prefix.ends_with('(') {
return None;
}
let args = raw.get(prefix_len..raw.len() - 1)?.trim();
let selectors = crate::css_native::split_top_level(args, ',')
.ok()?
.into_iter()
.filter_map(|selector| parse_simple_selector(selector.trim()))
.collect::<Vec<_>>();
(!selectors.is_empty()).then_some(selectors)
}
fn parse_relative_selector_list(raw: &str, name: &str) -> Option<Vec<RelativeSelector>> {
let args = parse_single_pseudo_argument(raw, name)?;
let selectors = crate::css_native::split_top_level(args, ',')
.ok()?
.into_iter()
.filter_map(|raw_selector| {
let raw_selector = raw_selector.trim();
let (combinator, selector) = match raw_selector.chars().next()? {
'>' => (Combinator::Child, raw_selector.get(1..)?.trim()),
'+' => (Combinator::AdjacentSibling, raw_selector.get(1..)?.trim()),
'~' => (Combinator::GeneralSibling, raw_selector.get(1..)?.trim()),
_ => (Combinator::Descendant, raw_selector),
};
Some(RelativeSelector {
combinator,
selector: parse_simple_selector(selector)?,
})
})
.collect::<Vec<_>>();
(!selectors.is_empty()).then_some(selectors)
}
fn parse_nth_formula(raw: &str) -> Option<(i32, i32)> {
let mut s = raw.replace(' ', "");
if s.is_empty() {
return None;
}
let lower = s.to_ascii_lowercase();
s = lower;
if !s.contains('n') {
let b = s.parse::<i32>().ok()?;
return Some((0, b));
}
let n_pos = s.find('n')?;
let (a_str, b_str) = s.split_at(n_pos);
let b_str = b_str.trim_start_matches('n');
let a = if a_str.is_empty() {
1
} else if a_str == "+" {
1
} else if a_str == "-" {
-1
} else {
a_str.parse::<i32>().ok()?
};
let b = if b_str.is_empty() {
0
} else {
b_str.parse::<i32>().ok()?
};
Some((a, b))
}
enum SelectorPart {
Tag,
Id,
Class,
}
fn flush_selector_part(
mode: &mut SelectorPart,
buffer: &mut String,
tag: &mut Option<String>,
id: &mut Option<String>,
classes: &mut Vec<String>,
) {
if buffer.is_empty() {
return;
}
let value = std::mem::take(buffer);
match mode {
SelectorPart::Tag => {
*tag = Some(value);
}
SelectorPart::Id => {
*id = Some(value);
}
SelectorPart::Class => {
classes.push(value);
}
}
}
fn style_from_native_declarations(
declarations: &crate::css_native::DeclarationBlock,
viewport: Size,
) -> (StyleDelta, StyleDelta) {
let mut normal = StyleDelta::default();
let mut important = StyleDelta::default();
for declaration in declarations.normal() {
apply_raw_declaration(declaration, &mut normal, viewport);
}
for declaration in declarations.important() {
apply_raw_declaration(declaration, &mut important, viewport);
}
(normal, important)
}
fn apply_raw_declaration(
declaration: &crate::css_native::Declaration,
delta: &mut StyleDelta,
viewport: Size,
) {
let resolved = resolve_viewport_units_in_raw(&declaration.value, viewport);
let _ = apply_native_raw_property(&declaration.name, &resolved, delta);
}
fn resolve_viewport_units_in_raw(raw: &str, viewport: Size) -> String {
let lower = raw.to_ascii_lowercase();
if ![
"vw", "vh", "vmin", "vmax", "svw", "svh", "svmin", "svmax", "lvw", "lvh", "lvmin", "lvmax",
"dvw", "dvh", "dvmin", "dvmax",
]
.iter()
.any(|unit| lower.contains(unit))
{
return raw.to_string();
}
let bytes = raw.as_bytes();
let mut output = String::with_capacity(raw.len());
let mut cursor = 0usize;
let mut quote = None;
while cursor < bytes.len() {
let byte = bytes[cursor];
if let Some(active_quote) = quote {
let character = raw[cursor..].chars().next().expect("valid UTF-8 cursor");
output.push(character);
cursor += character.len_utf8();
if byte == b'\\' && cursor < bytes.len() {
let escaped = raw[cursor..]
.chars()
.next()
.expect("valid escaped character");
output.push(escaped);
cursor += escaped.len_utf8();
} else if byte == active_quote {
quote = None;
}
continue;
}
if matches!(byte, b'\'' | b'"') {
quote = Some(byte);
output.push(byte as char);
cursor += 1;
continue;
}
let begins_number = byte.is_ascii_digit()
|| (byte == b'.'
&& bytes
.get(cursor + 1)
.is_some_and(|next| next.is_ascii_digit()));
let has_identifier_prefix = cursor > 0 && bytes[cursor - 1].is_ascii_alphanumeric()
|| cursor > 0 && matches!(bytes[cursor - 1], b'_' | b'-');
if begins_number && !has_identifier_prefix {
let number_start = cursor;
let mut number_end = cursor;
while bytes
.get(number_end)
.is_some_and(|value| value.is_ascii_digit())
{
number_end += 1;
}
if bytes.get(number_end) == Some(&b'.') {
number_end += 1;
while bytes
.get(number_end)
.is_some_and(|value| value.is_ascii_digit())
{
number_end += 1;
}
}
if matches!(bytes.get(number_end), Some(b'e' | b'E')) {
let exponent = number_end;
number_end += 1;
if matches!(bytes.get(number_end), Some(b'+' | b'-')) {
number_end += 1;
}
let exponent_digits = number_end;
while bytes
.get(number_end)
.is_some_and(|value| value.is_ascii_digit())
{
number_end += 1;
}
if number_end == exponent_digits {
number_end = exponent;
}
}
let mut unit_end = number_end;
while bytes
.get(unit_end)
.is_some_and(|value| value.is_ascii_alphabetic())
{
unit_end += 1;
}
let unit = raw[number_end..unit_end].to_ascii_lowercase();
let axis = match unit.as_str() {
"vw" | "svw" | "lvw" | "dvw" => Some(viewport.width),
"vh" | "svh" | "lvh" | "dvh" => Some(viewport.height),
"vmin" | "svmin" | "lvmin" | "dvmin" => Some(viewport.width.min(viewport.height)),
"vmax" | "svmax" | "lvmax" | "dvmax" => Some(viewport.width.max(viewport.height)),
_ => None,
};
if let Some(axis) = axis {
if let Ok(value) = raw[number_start..number_end].parse::<f32>() {
let points = axis.to_f32() * value / 100.0;
output.push_str(&format!("{points:.6}pt"));
cursor = unit_end;
continue;
}
}
}
let character = raw[cursor..].chars().next().expect("valid UTF-8 cursor");
output.push(character);
cursor += character.len_utf8();
}
output
}
fn parse_cascade_wide_keyword(raw: &str) -> Option<CascadeWideKeyword> {
match raw.trim().to_ascii_lowercase().as_str() {
"inherit" => Some(CascadeWideKeyword::Inherit),
"initial" => Some(CascadeWideKeyword::Initial),
"unset" => Some(CascadeWideKeyword::Unset),
_ => None,
}
}
fn apply_native_all_value(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(keyword) = parse_cascade_wide_keyword(raw) else {
return false;
};
// `all` resets every supported property at this declaration position, but
// custom properties and `direction` are explicitly outside its scope. By
// rebuilding the delta here, declarations before `all` are discarded while
// declarations after it can still override the reset normally.
let direction = delta.direction;
let revert_layer_direction = delta.revert_layer.direction;
let custom_lengths = std::mem::take(&mut delta.custom_lengths);
let custom_colors = std::mem::take(&mut delta.custom_colors);
let custom_color_alpha = std::mem::take(&mut delta.custom_color_alpha);
let custom_color_refs = std::mem::take(&mut delta.custom_color_refs);
let custom_raw_values = std::mem::take(&mut delta.custom_raw_values);
let custom_font_stacks = std::mem::take(&mut delta.custom_font_stacks);
*delta = StyleDelta::default();
delta.all = Some(keyword);
delta.direction = direction;
delta.revert_layer.direction = revert_layer_direction;
delta.custom_lengths = custom_lengths;
delta.custom_colors = custom_colors;
delta.custom_color_alpha = custom_color_alpha;
delta.custom_color_refs = custom_color_refs;
delta.custom_raw_values = custom_raw_values;
delta.custom_font_stacks = custom_font_stacks;
true
}
fn apply_native_background_shorthand_keyword(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(keyword) = parse_cascade_wide_keyword(raw) else {
return false;
};
reset_native_background_shorthand(delta, keyword);
true
}
fn reset_native_background_shorthand(delta: &mut StyleDelta, keyword: CascadeWideKeyword) {
delta.background_reset = Some(keyword);
delta.background_color = None;
delta.background_color_var = None;
delta.background_paint = None;
delta.background_paints = None;
delta.background_sizes = None;
delta.background_positions = None;
delta.background_position_x = None;
delta.background_position_y = None;
delta.background_repeats = None;
delta.background_attachments = None;
delta.background_blend_modes = None;
delta.background_origins = None;
delta.background_clips = None;
delta.revert_layer.background_color = false;
}
fn apply_native_raw_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
if property_name.starts_with("--") {
return apply_native_custom_property(property_name, raw, delta);
}
let name = property_name.to_ascii_lowercase();
match name.as_str() {
"all" => apply_native_all_value(delta, raw),
"color" => apply_native_color_value(delta, raw),
"background-color" => apply_native_background_color_value(delta, raw),
"background" => {
if apply_native_background_shorthand_keyword(delta, raw) {
return true;
}
reset_native_background_shorthand(delta, CascadeWideKeyword::Initial);
apply_background_from_string(raw, delta);
true
}
"background-image" => {
apply_background_image_from_string(raw, delta);
true
}
"background-size" => {
apply_background_size_from_string(raw, delta);
true
}
"background-position" => {
apply_background_position_from_string(raw, delta);
true
}
"background-position-x" => {
apply_background_position_axis_from_string(raw, BackgroundPositionAxis::X, delta);
true
}
"background-position-y" => {
apply_background_position_axis_from_string(raw, BackgroundPositionAxis::Y, delta);
true
}
"background-repeat" => {
apply_background_repeat_from_string(raw, delta);
true
}
"background-attachment" => {
apply_background_attachment_from_string(raw, delta);
true
}
"background-blend-mode" => {
apply_background_blend_mode_from_string(raw, delta);
true
}
"background-origin" => {
apply_background_origin_from_string(raw, delta);
true
}
"background-clip" => {
apply_background_clip_from_string(raw, delta);
true
}
"font" => apply_font_shorthand_from_raw(delta, raw),
"font-family" => apply_font_family_from_raw(delta, raw),
"font-size" => apply_font_size_from_raw(delta, raw),
"font-size-adjust" => apply_font_size_adjust_from_raw(delta, raw),
"font-kerning" => apply_font_kerning_from_raw(delta, raw),
"font-variant" => apply_font_variant_from_raw(delta, raw),
"font-variant-caps" => apply_font_variant_caps_from_raw(delta, raw),
"font-variant-ligatures" => apply_font_variant_ligatures_from_raw(delta, raw),
"font-feature-settings" => apply_font_feature_settings_from_raw(delta, raw),
"font-stretch" => apply_font_stretch_from_raw(delta, raw),
"line-height" => apply_line_height_from_raw(delta, raw),
"font-weight" => apply_font_weight_from_raw(delta, raw),
"font-synthesis" => apply_font_synthesis_from_raw(delta, raw),
"font-style" => apply_font_style_from_raw(delta, raw),
"vertical-align" => apply_vertical_align_from_raw(delta, raw),
"opacity" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.opacity = None;
delta.opacity_var = None;
delta.revert_layer.opacity = true;
true
} else {
apply_opacity_from_raw(delta, raw)
}
}
"display" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.display = None;
delta.revert_layer.display = true;
true
} else {
apply_display_from_raw(delta, raw)
}
}
"visibility" => apply_visibility_from_raw(delta, raw),
"position" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.position = None;
delta.running_name = None;
delta.revert_layer.position = true;
true
} else {
apply_position_from_raw(delta, raw)
}
}
"string-set" => apply_named_string_set_from_raw(delta, raw),
"float" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.float_mode = None;
delta.revert_layer.float_mode = true;
true
} else {
apply_float_from_raw(delta, raw)
}
}
"footnote-policy" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.footnote_policy = None;
delta.revert_layer.footnote_policy = true;
true
} else {
apply_footnote_policy_from_raw(delta, raw)
}
}
"footnote-display" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.footnote_display = None;
delta.revert_layer.footnote_display = true;
true
} else {
apply_footnote_display_from_raw(delta, raw)
}
}
"clear" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.clear_mode = None;
delta.revert_layer.clear_mode = true;
true
} else {
apply_clear_from_raw(delta, raw)
}
}
"width" | "height" | "min-width" | "max-width" | "min-height" | "max-height"
| "inline-size" | "block-size" | "min-inline-size" | "max-inline-size"
| "min-block-size" | "max-block-size" => apply_native_size_property(&name, raw, delta),
"top" | "right" | "bottom" | "left" | "inset" | "inset-inline" | "inset-inline-start"
| "inset-inline-end" | "inset-block" | "inset-block-start" | "inset-block-end" => {
apply_native_inset_property(&name, raw, delta)
}
"margin"
| "margin-top"
| "margin-right"
| "margin-bottom"
| "margin-left"
| "margin-inline"
| "margin-inline-start"
| "margin-inline-end"
| "margin-block"
| "margin-block-start"
| "margin-block-end"
| "padding"
| "padding-top"
| "padding-right"
| "padding-bottom"
| "padding-left"
| "padding-inline"
| "padding-inline-start"
| "padding-inline-end"
| "padding-block"
| "padding-block-start"
| "padding-block-end" => apply_native_box_edge_property(&name, raw, delta),
"z-index" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.z_index = None;
delta.revert_layer.z_index = true;
true
} else {
apply_z_index_from_raw(delta, raw)
}
}
"perspective" => apply_perspective_from_raw(delta, raw),
"aspect-ratio" => {
if let Some(value) = parse_aspect_ratio_spec(raw) {
delta.aspect_ratio = Some(value);
true
} else {
false
}
}
"container" => {
apply_container_shorthand_from_raw(raw, delta);
true
}
"container-name" => {
if let Some(value) = parse_container_names_spec_str(raw) {
delta.container_names = Some(value);
true
} else {
false
}
}
"container-type" => {
if let Some(value) = parse_container_query_type_spec_str(raw) {
delta.container_type = Some(value);
true
} else {
false
}
}
"flex-direction" => {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"row" => Some(FlexDirectionMode::Row),
"row-reverse" => Some(FlexDirectionMode::RowReverse),
"column" => Some(FlexDirectionMode::Column),
"column-reverse" => Some(FlexDirectionMode::ColumnReverse),
_ => None,
};
if let Some(value) = value {
delta.flex_direction = Some(value);
true
} else {
false
}
}
"flex-wrap" => {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"nowrap" => Some(FlexWrapMode::NoWrap),
"wrap" => Some(FlexWrapMode::Wrap),
"wrap-reverse" => Some(FlexWrapMode::WrapReverse),
_ => None,
};
if let Some(value) = value {
delta.flex_wrap = Some(value);
true
} else {
false
}
}
"flex-flow" => {
if let Some((direction, wrap)) = parse_flex_flow_str(raw) {
delta.flex_direction = Some(direction);
delta.flex_wrap = Some(wrap);
true
} else {
false
}
}
"order" | "-webkit-box-ordinal-group" => {
if let Ok(value) = raw.trim().parse::<i32>() {
delta.order = Some(value);
true
} else {
false
}
}
"justify-content" => {
if let Some(value) = justify_content_mode_from_value(raw) {
delta.justify_content = Some(value);
true
} else {
false
}
}
"align-items" => {
if let Some(value) = align_items_mode_from_value(raw) {
delta.align_items = Some(value);
true
} else {
false
}
}
"align-self" => {
if let Some(value) = align_self_mode_from_value(raw) {
delta.align_self = Some(value);
true
} else {
false
}
}
"justify-items" => {
if let Some(value) = align_items_mode_from_value(raw) {
delta.justify_items = Some(value);
true
} else {
false
}
}
"justify-self" => {
if let Some(value) = align_self_mode_from_value(raw) {
delta.justify_self = Some(value);
true
} else {
false
}
}
"align-content" => {
if let Some(value) = last_css_keyword(raw).and_then(align_content_mode_from_keyword) {
delta.align_content = Some(value);
true
} else {
false
}
}
"place-content" => {
if let Some((align, justify)) = parse_place_content_str(raw) {
delta.align_content = Some(align);
delta.justify_content = Some(justify);
true
} else {
false
}
}
"place-items" => {
if let Some((align, justify)) = parse_place_items_str(raw) {
delta.align_items = Some(align);
delta.justify_items = Some(justify);
true
} else {
false
}
}
"place-self" => {
if let Some((align, justify)) = parse_place_self_str(raw) {
delta.align_self = Some(align);
delta.justify_self = Some(justify);
true
} else {
false
}
}
"column-count" => {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"auto" | "initial" | "unset" | "revert" => Some((1, true)),
_ => lowered
.parse::<usize>()
.ok()
.filter(|count| (1..=256).contains(count))
.map(|count| (count, false)),
};
if let Some((value, auto)) = value {
delta.column_count = Some(value);
delta.column_count_auto = Some(auto);
true
} else {
false
}
}
"column-width" => apply_native_column_width_property(raw, delta),
"columns" => apply_native_columns_shorthand(raw, delta),
"column-fill" => {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Some(ColumnFillMode::Auto),
"balance-all" => Some(ColumnFillMode::BalanceAll),
"balance" | "initial" | "unset" | "revert" => Some(ColumnFillMode::Balance),
_ => None,
};
if let Some(value) = value {
delta.column_fill = Some(value);
true
} else {
false
}
}
"column-span" => {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"all" => Some(ColumnSpanMode::All),
"inherit" => Some(ColumnSpanMode::Inherit),
"none" | "initial" | "unset" | "revert" => Some(ColumnSpanMode::None),
_ => None,
};
if let Some(value) = value {
delta.column_span = Some(value);
true
} else {
false
}
}
"column-rule" | "column-rule-width" | "column-rule-style" | "column-rule-color" => {
apply_native_column_rule_property(&name, raw, delta)
}
"grid-template-columns" => {
if let Some(line_names) = parse_subgrid_line_names(raw) {
delta.grid_columns = Some(0);
delta.grid_column_tracks = Some(Vec::new());
delta.grid_subgrid_columns = Some(true);
delta.grid_column_line_names = Some(line_names);
delta.grid_column_auto_repeat = Some(None);
delta.grid_column_tracks_var = Some(None);
true
} else if let Some(tracks) = parse_grid_track_list(raw) {
delta.grid_columns = Some(tracks.len());
delta.grid_column_tracks = Some(tracks);
delta.grid_subgrid_columns = Some(false);
delta.grid_column_line_names = Some(parse_grid_track_line_names(raw));
delta.grid_column_auto_repeat = Some(parse_grid_auto_repeat(raw));
delta.grid_column_tracks_var = Some(None);
true
} else if raw.to_ascii_lowercase().contains("var(") {
delta.grid_columns = Some(0);
delta.grid_column_tracks = Some(Vec::new());
delta.grid_subgrid_columns = Some(false);
delta.grid_column_line_names = Some(Vec::new());
delta.grid_column_auto_repeat = Some(None);
delta.grid_column_tracks_var = Some(Some(raw.to_string()));
true
} else {
false
}
}
"grid-template-rows" => {
if let Some(line_names) = parse_subgrid_line_names(raw) {
delta.grid_rows = Some(0);
delta.grid_row_tracks = Some(Vec::new());
delta.grid_subgrid_rows = Some(true);
delta.grid_row_line_names = Some(line_names);
delta.grid_row_auto_repeat = Some(None);
delta.grid_row_tracks_var = Some(None);
true
} else if let Some(tracks) = parse_grid_track_list(raw) {
delta.grid_rows = Some(tracks.len());
delta.grid_row_tracks = Some(tracks);
delta.grid_subgrid_rows = Some(false);
delta.grid_row_line_names = Some(parse_grid_track_line_names(raw));
delta.grid_row_auto_repeat = Some(parse_grid_auto_repeat(raw));
delta.grid_row_tracks_var = Some(None);
true
} else if raw.to_ascii_lowercase().contains("var(") {
delta.grid_rows = Some(0);
delta.grid_row_tracks = Some(Vec::new());
delta.grid_subgrid_rows = Some(false);
delta.grid_row_line_names = Some(Vec::new());
delta.grid_row_auto_repeat = Some(None);
delta.grid_row_tracks_var = Some(Some(raw.to_string()));
true
} else {
false
}
}
"grid-auto-columns" => {
if let Some(tracks) = parse_grid_track_list(raw) {
delta.grid_auto_column_tracks = Some(tracks);
true
} else {
false
}
}
"grid-auto-rows" => {
if let Some(tracks) = parse_grid_track_list(raw) {
delta.grid_auto_row_tracks = Some(tracks);
true
} else {
false
}
}
"grid-auto-flow" => {
if let Some(flow) = parse_grid_auto_flow(raw) {
delta.grid_auto_flow = Some(flow);
true
} else {
false
}
}
"grid-template-areas" => {
if let Some(areas) = parse_grid_template_areas(raw) {
delta.grid_template_areas = Some(areas);
true
} else {
false
}
}
"grid-template" => apply_native_grid_template_shorthand(raw, delta),
"grid" => apply_native_grid_shorthand(raw, delta),
"grid-column-start" => {
let line = parse_grid_line_spec(raw);
delta.grid_column_start = Some(legacy_positive_grid_start(&line));
delta.grid_column_line_start = Some(line);
true
}
"grid-column-end" => {
delta.grid_column_line_end = Some(parse_grid_line_spec(raw));
true
}
"grid-column" => {
let (start, end) = parse_grid_placement_shorthand(raw);
delta.grid_column_start = Some(legacy_positive_grid_start(&start));
delta.grid_column_line_start = Some(start);
delta.grid_column_line_end = Some(end);
true
}
"grid-row-start" => {
let line = parse_grid_line_spec(raw);
delta.grid_row_start = Some(legacy_positive_grid_start(&line));
delta.grid_row_line_start = Some(line);
true
}
"grid-row-end" => {
delta.grid_row_line_end = Some(parse_grid_line_spec(raw));
true
}
"grid-row" => {
let (start, end) = parse_grid_placement_shorthand(raw);
delta.grid_row_start = Some(legacy_positive_grid_start(&start));
delta.grid_row_line_start = Some(start);
delta.grid_row_line_end = Some(end);
true
}
"grid-area" => {
if raw.contains('/') {
let (row_start, column_start, row_end, column_end) = parse_grid_area_lines(raw);
delta.grid_row_start = Some(legacy_positive_grid_start(&row_start));
delta.grid_column_start = Some(legacy_positive_grid_start(&column_start));
delta.grid_row_line_start = Some(row_start);
delta.grid_column_line_start = Some(column_start);
delta.grid_row_line_end = Some(row_end);
delta.grid_column_line_end = Some(column_end);
delta.grid_area_name = Some(None);
} else {
let name = raw.trim();
delta.grid_area_name = Some(
(!name.is_empty() && !name.eq_ignore_ascii_case("auto"))
.then(|| name.to_string()),
);
}
true
}
"gap" | "grid-gap" => apply_native_gap_value(raw, delta, GapProperty::Both),
"row-gap" | "grid-row-gap" => apply_native_gap_value(raw, delta, GapProperty::Row),
"column-gap" | "grid-column-gap" => apply_native_gap_value(raw, delta, GapProperty::Column),
"flex-grow" => apply_native_nonnegative_number(raw, &mut delta.flex_grow),
"flex-shrink" => apply_native_nonnegative_number(raw, &mut delta.flex_shrink),
"flex-basis" => apply_native_flex_basis(raw, delta),
"flex" => apply_native_flex_shorthand(raw, delta),
"overflow" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_overflow_axes(delta)
} else if let Some((x, y)) = parse_overflow_axis_modes_str(raw) {
set_delta_overflow_axes(delta, x, y);
true
} else {
false
}
}
"overflow-x" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_overflow_x(delta)
} else if let Some(value) = parse_overflow_mode_str(raw) {
set_delta_overflow_x(delta, value);
true
} else {
false
}
}
"overflow-y" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_overflow_y(delta)
} else if let Some(value) = parse_overflow_mode_str(raw) {
set_delta_overflow_y(delta, value);
true
} else {
false
}
}
"overflow-inline" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_overflow_inline(delta)
} else if let Some(value) = parse_overflow_mode_str(raw) {
set_delta_overflow_inline(delta, value);
true
} else {
false
}
}
"overflow-block" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_overflow_block(delta)
} else if let Some(value) = parse_overflow_mode_str(raw) {
set_delta_overflow_block(delta, value);
true
} else {
false
}
}
"overflow-clip-margin" => {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"initial" | "unset" | "revert" | "revert-layer" => {
Some(LengthSpec::Absolute(Pt::ZERO))
}
"inherit" => Some(LengthSpec::Inherit),
_ => parse_single_length_spec(&lowered)
.filter(|value| native_length_is_nonnegative(*value)),
};
if let Some(value) = value {
delta.overflow_clip_margin = Some(value);
true
} else {
false
}
}
"scrollbar-gutter" => {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"stable" => Some(ScrollbarGutterMode::Stable),
"stable both-edges" | "both-edges stable" => {
Some(ScrollbarGutterMode::StableBothEdges)
}
"auto" | "initial" | "unset" | "revert" | "revert-layer" => {
Some(ScrollbarGutterMode::Auto)
}
_ => None,
};
if let Some(value) = value {
delta.scrollbar_gutter = Some(value);
true
} else {
false
}
}
"line-clamp" | "-webkit-line-clamp" => {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"none" | "initial" | "unset" | "revert" | "revert-layer" => Some(None),
_ => lowered
.parse::<usize>()
.ok()
.filter(|value| *value > 0)
.map(Some),
};
if let Some(value) = value {
delta.line_clamp = Some(value);
true
} else {
false
}
}
"box-sizing" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
delta.box_sizing = None;
delta.revert_layer.box_sizing = true;
true
} else {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"border-box" => Some(BoxSizingMode::BorderBox),
"content-box" | "initial" | "unset" | "revert" => {
Some(BoxSizingMode::ContentBox)
}
_ => None,
};
if let Some(value) = value {
set_delta_box_sizing(delta, value);
true
} else {
false
}
}
}
"border"
| "border-top"
| "border-right"
| "border-bottom"
| "border-left"
| "border-inline-start"
| "border-inline-end"
| "border-block-start"
| "border-block-end"
| "border-inline"
| "border-block"
| "border-width"
| "border-top-width"
| "border-right-width"
| "border-bottom-width"
| "border-left-width"
| "border-inline-start-width"
| "border-inline-end-width"
| "border-block-start-width"
| "border-block-end-width"
| "border-inline-width"
| "border-block-width"
| "border-color"
| "border-top-color"
| "border-right-color"
| "border-bottom-color"
| "border-left-color"
| "border-inline-start-color"
| "border-inline-end-color"
| "border-block-start-color"
| "border-block-end-color"
| "border-inline-color"
| "border-block-color"
| "border-style"
| "border-top-style"
| "border-right-style"
| "border-bottom-style"
| "border-left-style"
| "border-inline-start-style"
| "border-inline-end-style"
| "border-block-start-style"
| "border-block-end-style"
| "border-inline-style"
| "border-block-style" => {
let applied = apply_native_border_property(&name, raw, delta);
if applied && name == "border" {
delta.border_image = BorderImageDelta {
reset: true,
..BorderImageDelta::default()
};
}
applied
}
"border-image" => apply_border_image_shorthand(raw, delta),
"border-image-source" => apply_border_image_source(raw, delta),
"border-image-slice" => apply_border_image_slice(raw, delta),
"border-image-width" => apply_border_image_width(raw, delta),
"border-image-outset" => apply_border_image_outset(raw, delta),
"border-image-repeat" => apply_border_image_repeat(raw, delta),
"outline" | "outline-width" | "outline-style" | "outline-color" | "outline-offset" => {
apply_native_outline_property(&name, raw, delta)
}
"border-collapse" => {
delta.border_collapse = match raw.trim().to_ascii_lowercase().as_str() {
"collapse" => Some(BorderCollapseMode::Collapse),
"separate" | "initial" | "unset" | "revert" => Some(BorderCollapseMode::Separate),
_ => None,
};
delta.border_collapse.is_some()
}
"caption-side" => {
if let Some(value) = parse_caption_side_ident(&raw.trim().to_ascii_lowercase()) {
delta.caption_side = Some(value);
true
} else {
false
}
}
"table-layout" => {
delta.table_layout = match raw.trim().to_ascii_lowercase().as_str() {
"fixed" => Some(TableLayoutMode::Fixed),
"auto" | "initial" | "unset" | "revert" => Some(TableLayoutMode::Auto),
_ => None,
};
delta.table_layout.is_some()
}
"empty-cells" => match raw.trim().to_ascii_lowercase().as_str() {
"hide" => {
delta.empty_cells_hide = Some(true);
true
}
"show" | "initial" | "unset" | "revert" => {
delta.empty_cells_hide = Some(false);
true
}
_ => false,
},
"page" => {
let value = raw.trim();
let keyword = value.to_ascii_lowercase();
if matches!(
keyword.as_str(),
"auto" | "initial" | "unset" | "revert" | "revert-layer"
) {
delta.page_name = Some(None);
true
} else if !value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
delta.page_name = Some(Some(css_page_name_id(&value)));
true
} else {
false
}
}
"break-before" | "page-break-before" => {
if let Some(value) = parse_break_before_value(&name, &raw.trim().to_ascii_lowercase()) {
delta.pagination.break_before = Some(value);
true
} else {
false
}
}
"break-after" | "page-break-after" => {
if let Some(value) = parse_break_after_value(&name, &raw.trim().to_ascii_lowercase()) {
delta.pagination.break_after = Some(value);
true
} else {
false
}
}
"break-inside" | "page-break-inside" => {
if let Some(value) = parse_break_inside_value(&name, &raw.trim().to_ascii_lowercase()) {
delta.pagination.break_inside = Some(value);
true
} else {
false
}
}
"orphans" | "widows" => apply_native_pagination_count(&name, raw, delta),
"border-radius" => {
if let Some(value) = parse_border_radius_str(raw) {
delta.border_radius = Some(value);
delta.border_radius_var = None;
delta
.border_radius_ops
.push(BorderRadiusOp::Shorthand(value));
true
} else if raw.to_ascii_lowercase().contains("var(") {
delta.border_radius = None;
delta.border_radius_var = Some(raw.trim().to_ascii_lowercase());
true
} else {
false
}
}
"border-top-left-radius"
| "border-top-right-radius"
| "border-bottom-right-radius"
| "border-bottom-left-radius"
| "border-start-start-radius"
| "border-start-end-radius"
| "border-end-start-radius"
| "border-end-end-radius" => apply_border_radius_corner_longhand(&name, raw, delta),
"box-decoration-break" | "-webkit-box-decoration-break" => {
delta.box_decoration_break = match raw.trim().to_ascii_lowercase().as_str() {
"clone" => Some(BoxDecorationBreak::Clone),
"slice" | "initial" | "unset" | "revert" => Some(BoxDecorationBreak::Slice),
_ => None,
};
delta.box_decoration_break.is_some()
}
"border-spacing" => {
if let Some(value) = parse_border_spacing_str(raw) {
delta.border_spacing = Some(value);
true
} else {
false
}
}
"box-shadow" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_box_shadow(delta)
} else if let Some(value) = parse_box_shadow_list_str(raw) {
set_delta_box_shadows(delta, value);
true
} else {
false
}
}
"text-shadow" => apply_text_shadow_from_raw(delta, raw),
"clip" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_legacy_clip(delta)
} else if let Some(value) = parse_legacy_clip_spec_str(raw) {
set_delta_legacy_clip(delta, value);
true
} else {
false
}
}
"clip-path" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_clip_path(delta)
} else if let Some(value) = parse_clip_path_spec_str(raw) {
set_delta_clip_path(delta, value);
true
} else {
false
}
}
"filter" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_paint_filter(delta)
} else {
set_delta_paint_filter_from_raw(delta, raw);
true
}
}
"backdrop-filter" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_backdrop_filter(delta)
} else {
set_delta_backdrop_filter_from_raw(delta, raw);
true
}
}
"will-change" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_will_change_backdrop_root(delta)
} else {
set_delta_will_change_backdrop_root_from_raw(delta, raw);
true
}
}
"mask" | "-webkit-mask" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_mask_backdrop_root(delta)
} else {
apply_mask_shorthand_from_string(raw, delta)
}
}
"mask-image" | "-webkit-mask-image" => apply_mask_image_from_string(raw, delta),
"mask-size" | "-webkit-mask-size" => apply_mask_size_from_string(raw, delta),
"mask-position" | "-webkit-mask-position" => apply_mask_position_from_string(raw, delta),
"mask-repeat" | "-webkit-mask-repeat" => apply_mask_repeat_from_string(raw, delta),
"mask-origin" | "-webkit-mask-origin" => apply_mask_origin_from_string(raw, delta),
"mask-clip" | "-webkit-mask-clip" => apply_mask_clip_from_string(raw, delta),
"mask-mode" => apply_mask_mode_from_string(raw, delta),
"mask-composite" => apply_mask_composite_from_string(raw, false, delta),
"-webkit-mask-composite" => apply_mask_composite_from_string(raw, true, delta),
"mask-border" => apply_mask_border_shorthand(raw, delta),
"mask-border-source" => apply_mask_border_source(raw, delta),
"mask-border-slice" => apply_mask_border_slice(raw, delta),
"mask-border-width" => apply_mask_border_width(raw, delta),
"mask-border-outset" => apply_mask_border_outset(raw, delta),
"mask-border-repeat" => apply_mask_border_repeat(raw, delta),
"mix-blend-mode" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_mix_blend_mode(delta)
} else if let Some(value) = parse_mix_blend_mode_str(raw) {
set_delta_mix_blend_mode(delta, value);
true
} else {
false
}
}
"isolation" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_isolation(delta)
} else if let Some(value) = parse_isolation_spec_str(raw) {
set_delta_isolation(delta, value);
true
} else {
false
}
}
"object-fit" => {
if let Some(value) = parse_object_fit_spec_str(raw) {
delta.object_fit = Some(value);
true
} else {
false
}
}
"image-rendering" => {
if let Some(value) = parse_image_rendering_spec_str(raw) {
delta.image_rendering = Some(value);
true
} else {
false
}
}
"object-position" => {
if let Some(value) = parse_object_position_spec_str(raw) {
delta.object_position = Some(value);
true
} else {
false
}
}
"transform" | "translate" | "rotate" | "scale" => {
apply_native_transform_value(&name, raw, delta)
}
"transform-origin" => apply_native_transform_origin(raw, delta),
"transform-box" => apply_native_transform_box(raw, delta),
"transform-style" => apply_native_transform_style(raw, delta),
"perspective-origin" => apply_native_perspective_origin(raw, delta),
"text-transform" => {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" => {
set_delta_text_transform(delta, TextTransformSpec::Inherit);
true
}
"initial" | "revert" => {
set_delta_text_transform(delta, TextTransformSpec::Initial);
true
}
"revert-layer" => set_revert_layer_text_transform(delta),
_ => {
if let Some(value) = parse_text_transform_str(&lowered) {
set_delta_text_transform(delta, TextTransformSpec::Value(value));
true
} else {
false
}
}
}
}
"text-decoration-line" => apply_native_text_decoration_line(raw, delta),
"text-decoration" => apply_native_text_decoration_shorthand(raw, delta),
"text-decoration-color" => apply_text_decoration_color_from_raw(delta, raw),
"text-decoration-thickness" => apply_text_decoration_thickness_from_raw(delta, raw),
"text-decoration-style" => apply_text_decoration_style_from_raw(delta, raw),
"text-decoration-skip-ink" => apply_text_decoration_skip_ink_from_raw(delta, raw),
"text-emphasis" => apply_native_text_emphasis_shorthand(raw, delta),
"text-emphasis-style" => apply_native_text_emphasis_style(raw, delta),
"text-emphasis-color" => apply_native_text_emphasis_color(raw, delta),
"text-emphasis-position" => apply_native_text_emphasis_position(raw, delta),
"initial-letter" => apply_native_initial_letter(raw, delta),
"text-underline-offset" => apply_text_underline_offset_from_raw(delta, raw),
"text-underline-position" => apply_text_underline_position_from_raw(delta, raw),
"text-overflow" => {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"clip" => {
set_delta_text_overflow_spec(
delta,
TextOverflowSpec::Value(TextOverflowMode::Clip),
);
true
}
"ellipsis" => {
set_delta_text_overflow_spec(
delta,
TextOverflowSpec::Value(TextOverflowMode::Ellipsis),
);
true
}
"inherit" | "unset" => {
set_delta_text_overflow_spec(delta, TextOverflowSpec::Inherit);
true
}
"initial" | "revert" => {
set_delta_text_overflow_spec(delta, TextOverflowSpec::Initial);
true
}
"revert-layer" => set_revert_layer_text_overflow(delta),
_ => false,
}
}
"text-indent" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_text_indent(delta)
} else if let Some(value) = parse_text_indent_str(raw) {
set_delta_text_indent_value(delta, value);
true
} else {
false
}
}
"letter-spacing" | "word-spacing" => apply_native_text_spacing(&name, raw, delta),
"tab-size" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_tab_size(delta)
} else if let Some(value) = parse_tab_size_str(raw) {
set_delta_tab_size(delta, value);
true
} else {
false
}
}
"line-break" => apply_line_break_from_raw(delta, raw),
"word-break" => apply_word_break_from_raw(delta, raw),
"overflow-wrap" | "word-wrap" => apply_overflow_wrap_from_raw(delta, raw),
"text-justify" => apply_text_justify_from_raw(delta, raw),
"text-wrap" => apply_text_wrap_from_raw(delta, raw),
"text-wrap-mode" => apply_text_wrap_mode_from_raw(delta, raw),
"text-wrap-style" => apply_text_wrap_style_from_raw(delta, raw),
"white-space" => apply_white_space_from_raw(delta, raw),
"white-space-collapse" => apply_white_space_collapse_from_raw(delta, raw),
"hyphens" | "-webkit-hyphens" => apply_hyphens_from_raw(delta, raw),
"hyphenate-character" | "-webkit-hyphenate-character" => {
apply_hyphenate_character_from_raw(delta, raw)
}
"text-align" => apply_native_text_align(raw, delta),
"text-align-last" => {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" => {
set_delta_text_align_last(delta, TextAlignLastSpec::Inherit);
true
}
"initial" | "revert" => {
set_delta_text_align_last(delta, TextAlignLastSpec::Initial);
true
}
"revert-layer" => set_revert_layer_text_align_last(delta),
_ => {
if let Some(value) = parse_text_align_last_str(&lowered) {
set_delta_text_align_last(delta, TextAlignLastSpec::Value(value));
true
} else {
false
}
}
}
}
"direction" => apply_native_direction(raw, delta),
"unicode-bidi" => apply_native_unicode_bidi(raw, delta),
"writing-mode" => {
apply_writing_mode_from_str(raw, delta);
true
}
"text-orientation" => apply_text_orientation_from_str(raw, delta),
"content" => {
if let Some(value) = content_from_raw(raw) {
delta.content = Some(value);
true
} else {
false
}
}
"counter-reset" => apply_native_counter_property("counter-reset", raw, delta),
"counter-increment" => apply_native_counter_property("counter-increment", raw, delta),
"counter-set" => apply_native_counter_property("counter-set", raw, delta),
"quotes" => {
if let Some(value) = quotes_from_raw(raw) {
delta.quotes = Some(value);
true
} else {
false
}
}
"list-style-type" => apply_native_list_style_type(raw, delta),
"list-style-image" => apply_native_list_style_image(raw, delta),
"list-style-position" => apply_native_list_style_position(raw, delta),
"list-style" => apply_native_list_style_shorthand(raw, delta),
_ => false,
}
}
#[derive(Debug, Clone)]
enum NativeLengthComponent {
Spec(LengthSpec),
Var(String),
}
fn is_css_wide_keyword(raw: &str) -> bool {
matches!(
raw,
"inherit" | "initial" | "unset" | "revert" | "revert-layer"
)
}
fn css_wide_length_spec(raw: &str) -> Option<LengthSpec> {
match raw {
"inherit" => Some(LengthSpec::Inherit),
"initial" | "unset" | "revert" => Some(LengthSpec::Initial),
_ => None,
}
}
fn native_length_is_nonnegative(value: LengthSpec) -> bool {
match value {
LengthSpec::Absolute(value) => value >= Pt::ZERO,
LengthSpec::Percent(value) | LengthSpec::Em(value) | LengthSpec::Rem(value) => value >= 0.0,
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Calc(_)
| LengthSpec::Clamped(_)
| LengthSpec::FontRelative(_)
| LengthSpec::Inherit
| LengthSpec::Initial => true,
}
}
fn parse_native_length_component(
raw: &str,
allow_auto: bool,
allow_negative: bool,
) -> Option<NativeLengthComponent> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() || lowered == "revert-layer" {
return None;
}
if let Some(value) = css_wide_length_spec(&lowered) {
return Some(NativeLengthComponent::Spec(value));
}
if allow_auto && lowered == "auto" {
return Some(NativeLengthComponent::Spec(LengthSpec::Auto));
}
if lowered.contains("var(") {
return Some(NativeLengthComponent::Var(lowered));
}
let value = length_spec_from_string(&lowered)?;
// CSS math functions are range-checked at used-value time. A negative
// result for a non-negative property (for example `width: calc(50vw -
// 280px)`) is valid syntax and clamps to zero instead of invalidating the
// declaration and falling back to `auto`.
if !allow_negative && !native_length_is_nonnegative(value) && !lowered.contains('(') {
return None;
}
Some(NativeLengthComponent::Spec(value))
}
fn parse_native_length_components(
raw: &str,
max_parts: usize,
allow_auto: bool,
allow_negative: bool,
) -> Option<Vec<NativeLengthComponent>> {
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > max_parts {
return None;
}
if parts.len() > 1
&& parts
.iter()
.any(|part| is_css_wide_keyword(&part.to_ascii_lowercase()))
{
return None;
}
parts
.iter()
.map(|part| parse_native_length_component(part, allow_auto, allow_negative))
.collect()
}
fn expand_native_four(values: &[NativeLengthComponent]) -> Option<[NativeLengthComponent; 4]> {
match values {
[all] => Some([all.clone(), all.clone(), all.clone(), all.clone()]),
[vertical, horizontal] => Some([
vertical.clone(),
horizontal.clone(),
vertical.clone(),
horizontal.clone(),
]),
[top, horizontal, bottom] => Some([
top.clone(),
horizontal.clone(),
bottom.clone(),
horizontal.clone(),
]),
[top, right, bottom, left] => {
Some([top.clone(), right.clone(), bottom.clone(), left.clone()])
}
_ => None,
}
}
fn expand_native_pair(values: &[NativeLengthComponent]) -> Option<[NativeLengthComponent; 2]> {
match values {
[both] => Some([both.clone(), both.clone()]),
[start, end] => Some([start.clone(), end.clone()]),
_ => None,
}
}
fn apply_native_size_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "revert-layer" {
match property_name {
"width" => {
delta.width = None;
delta.width_var = None;
delta.revert_layer.width = true;
}
"height" => {
delta.height = None;
delta.height_var = None;
delta.revert_layer.height = true;
}
_ => {
return apply_native_size_component(
property_name,
NativeLengthComponent::Spec(LengthSpec::Initial),
delta,
);
}
}
return true;
}
let is_max = matches!(
property_name,
"max-width" | "max-height" | "max-inline-size" | "max-block-size"
);
let intrinsic = match lowered.as_str() {
"min-content" => Some(LengthSpec::MinContent),
"max-content" => Some(LengthSpec::MaxContent),
"fit-content" => Some(LengthSpec::FitContent),
_ => None,
};
let component = if let Some(value) = intrinsic {
NativeLengthComponent::Spec(value)
} else if is_max && lowered == "none" {
NativeLengthComponent::Spec(LengthSpec::Auto)
} else {
let Some(value) = parse_native_length_component(&lowered, !is_max, false) else {
return false;
};
value
};
apply_native_size_component(property_name, component, delta)
}
fn apply_native_size_component(
property_name: &str,
component: NativeLengthComponent,
delta: &mut StyleDelta,
) -> bool {
match (property_name, component) {
("width", NativeLengthComponent::Spec(value)) => set_delta_width(delta, value),
("width", NativeLengthComponent::Var(value)) => set_delta_width_var(delta, value),
("height", NativeLengthComponent::Spec(value)) => set_delta_height(delta, value),
("height", NativeLengthComponent::Var(value)) => set_delta_height_var(delta, value),
("min-width", NativeLengthComponent::Spec(value)) => {
delta.min_width = Some(value);
delta.min_width_var = None;
}
("min-width", NativeLengthComponent::Var(value)) => {
delta.min_width = None;
delta.min_width_var = Some(value);
}
("max-width", NativeLengthComponent::Spec(value)) => {
delta.max_width = Some(value);
delta.max_width_var = None;
}
("max-width", NativeLengthComponent::Var(value)) => {
delta.max_width = None;
delta.max_width_var = Some(value);
}
("min-height", NativeLengthComponent::Spec(value)) => {
delta.min_height = Some(value);
delta.min_height_var = None;
}
("min-height", NativeLengthComponent::Var(value)) => {
delta.min_height = None;
delta.min_height_var = Some(value);
}
("max-height", NativeLengthComponent::Spec(value)) => {
delta.max_height = Some(value);
delta.max_height_var = None;
}
("max-height", NativeLengthComponent::Var(value)) => {
delta.max_height = None;
delta.max_height_var = Some(value);
}
("inline-size", NativeLengthComponent::Spec(value)) => {
delta.inline_size = Some(value);
delta.inline_size_var = None;
}
("inline-size", NativeLengthComponent::Var(value)) => {
delta.inline_size = None;
delta.inline_size_var = Some(value);
}
("block-size", NativeLengthComponent::Spec(value)) => {
delta.block_size = Some(value);
delta.block_size_var = None;
}
("block-size", NativeLengthComponent::Var(value)) => {
delta.block_size = None;
delta.block_size_var = Some(value);
}
("min-inline-size", NativeLengthComponent::Spec(value)) => {
delta.min_inline_size = Some(value);
delta.min_inline_size_var = None;
}
("min-inline-size", NativeLengthComponent::Var(value)) => {
delta.min_inline_size = None;
delta.min_inline_size_var = Some(value);
}
("max-inline-size", NativeLengthComponent::Spec(value)) => {
delta.max_inline_size = Some(value);
delta.max_inline_size_var = None;
}
("max-inline-size", NativeLengthComponent::Var(value)) => {
delta.max_inline_size = None;
delta.max_inline_size_var = Some(value);
}
("min-block-size", NativeLengthComponent::Spec(value)) => {
delta.min_block_size = Some(value);
delta.min_block_size_var = None;
}
("min-block-size", NativeLengthComponent::Var(value)) => {
delta.min_block_size = None;
delta.min_block_size_var = Some(value);
}
("max-block-size", NativeLengthComponent::Spec(value)) => {
delta.max_block_size = Some(value);
delta.max_block_size_var = None;
}
("max-block-size", NativeLengthComponent::Var(value)) => {
delta.max_block_size = None;
delta.max_block_size_var = Some(value);
}
_ => return false,
}
true
}
fn apply_native_inset_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let physical_target = match property_name {
"inset" => Some(BoxEdgeTarget::All),
"top" => Some(BoxEdgeTarget::Top),
"right" => Some(BoxEdgeTarget::Right),
"bottom" => Some(BoxEdgeTarget::Bottom),
"left" => Some(BoxEdgeTarget::Left),
_ => None,
};
if let Some(target) = physical_target {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
clear_delta_inset_edge(delta, target);
delta.revert_layer.inset.set(target);
return true;
}
if target == BoxEdgeTarget::All {
let Some(values) = parse_native_length_components(raw, 4, true, true) else {
return false;
};
let Some([top, right, bottom, left]) = expand_native_four(&values) else {
return false;
};
set_delta_inset_component(delta, BoxEdgeTarget::Top, top);
set_delta_inset_component(delta, BoxEdgeTarget::Right, right);
set_delta_inset_component(delta, BoxEdgeTarget::Bottom, bottom);
set_delta_inset_component(delta, BoxEdgeTarget::Left, left);
} else {
let Some(value) = parse_native_length_component(raw, true, true) else {
return false;
};
set_delta_inset_component(delta, target, value);
}
return true;
}
let Some(logical_target) = (match property_name {
"inset-inline" => Some(LogicalBoxEdgeTarget::Inline),
"inset-inline-start" => Some(LogicalBoxEdgeTarget::InlineStart),
"inset-inline-end" => Some(LogicalBoxEdgeTarget::InlineEnd),
"inset-block" => Some(LogicalBoxEdgeTarget::Block),
"inset-block-start" => Some(LogicalBoxEdgeTarget::BlockStart),
"inset-block-end" => Some(LogicalBoxEdgeTarget::BlockEnd),
_ => None,
}) else {
return false;
};
if raw.trim().eq_ignore_ascii_case("revert-layer") {
clear_delta_logical_inset_edge(delta, logical_target);
delta.revert_layer.inset_logical.set(logical_target);
return true;
}
match logical_target {
LogicalBoxEdgeTarget::Inline | LogicalBoxEdgeTarget::Block => {
let Some(values) = parse_native_length_components(raw, 2, true, true) else {
return false;
};
let Some([start, end]) = expand_native_pair(&values) else {
return false;
};
let (start_target, end_target) = if logical_target == LogicalBoxEdgeTarget::Inline {
(
LogicalBoxEdgeTarget::InlineStart,
LogicalBoxEdgeTarget::InlineEnd,
)
} else {
(
LogicalBoxEdgeTarget::BlockStart,
LogicalBoxEdgeTarget::BlockEnd,
)
};
set_delta_logical_inset_component(delta, start_target, start);
set_delta_logical_inset_component(delta, end_target, end);
}
_ => {
let Some(value) = parse_native_length_component(raw, true, true) else {
return false;
};
set_delta_logical_inset_component(delta, logical_target, value);
}
}
true
}
fn set_delta_inset_component(
delta: &mut StyleDelta,
target: BoxEdgeTarget,
component: NativeLengthComponent,
) {
match component {
NativeLengthComponent::Spec(value) => {
set_delta_inset_edge_spec(delta, target, Some(value));
}
NativeLengthComponent::Var(value) => set_delta_inset_edge_var(delta, target, value),
}
}
fn set_delta_logical_inset_component(
delta: &mut StyleDelta,
target: LogicalBoxEdgeTarget,
component: NativeLengthComponent,
) {
delta.revert_layer.inset_logical.clear(target);
match (target, component) {
(LogicalBoxEdgeTarget::InlineStart, NativeLengthComponent::Spec(value)) => {
delta.inset_inline_start = Some(value);
delta.inset_inline_start_var = None;
}
(LogicalBoxEdgeTarget::InlineStart, NativeLengthComponent::Var(value)) => {
delta.inset_inline_start = None;
delta.inset_inline_start_var = Some(value);
}
(LogicalBoxEdgeTarget::InlineEnd, NativeLengthComponent::Spec(value)) => {
delta.inset_inline_end = Some(value);
delta.inset_inline_end_var = None;
}
(LogicalBoxEdgeTarget::InlineEnd, NativeLengthComponent::Var(value)) => {
delta.inset_inline_end = None;
delta.inset_inline_end_var = Some(value);
}
(LogicalBoxEdgeTarget::BlockStart, NativeLengthComponent::Spec(value)) => {
delta.inset_block_start = Some(value);
delta.inset_block_start_var = None;
}
(LogicalBoxEdgeTarget::BlockStart, NativeLengthComponent::Var(value)) => {
delta.inset_block_start = None;
delta.inset_block_start_var = Some(value);
}
(LogicalBoxEdgeTarget::BlockEnd, NativeLengthComponent::Spec(value)) => {
delta.inset_block_end = Some(value);
delta.inset_block_end_var = None;
}
(LogicalBoxEdgeTarget::BlockEnd, NativeLengthComponent::Var(value)) => {
delta.inset_block_end = None;
delta.inset_block_end_var = Some(value);
}
(LogicalBoxEdgeTarget::Inline, _) | (LogicalBoxEdgeTarget::Block, _) => {}
}
}
fn apply_native_box_edge_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let is_margin = property_name.starts_with("margin");
let physical_target = match property_name {
"margin" | "padding" => Some(BoxEdgeTarget::All),
"margin-top" | "padding-top" => Some(BoxEdgeTarget::Top),
"margin-right" | "padding-right" => Some(BoxEdgeTarget::Right),
"margin-bottom" | "padding-bottom" => Some(BoxEdgeTarget::Bottom),
"margin-left" | "padding-left" => Some(BoxEdgeTarget::Left),
_ => None,
};
if let Some(target) = physical_target {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_native_box_edge(delta, is_margin, target);
return true;
}
if target == BoxEdgeTarget::All {
let Some(values) = parse_native_length_components(raw, 4, is_margin, is_margin) else {
return false;
};
let Some([top, right, bottom, left]) = expand_native_four(&values) else {
return false;
};
set_delta_native_box_edge_component(delta, is_margin, BoxEdgeTarget::Top, top);
set_delta_native_box_edge_component(delta, is_margin, BoxEdgeTarget::Right, right);
set_delta_native_box_edge_component(delta, is_margin, BoxEdgeTarget::Bottom, bottom);
set_delta_native_box_edge_component(delta, is_margin, BoxEdgeTarget::Left, left);
} else {
let Some(value) = parse_native_length_component(raw, is_margin, is_margin) else {
return false;
};
set_delta_native_box_edge_component(delta, is_margin, target, value);
}
return true;
}
let Some(logical_target) = (match property_name {
"margin-inline" | "padding-inline" => Some(LogicalBoxEdgeTarget::Inline),
"margin-inline-start" | "padding-inline-start" => Some(LogicalBoxEdgeTarget::InlineStart),
"margin-inline-end" | "padding-inline-end" => Some(LogicalBoxEdgeTarget::InlineEnd),
"margin-block" | "padding-block" => Some(LogicalBoxEdgeTarget::Block),
"margin-block-start" | "padding-block-start" => Some(LogicalBoxEdgeTarget::BlockStart),
"margin-block-end" | "padding-block-end" => Some(LogicalBoxEdgeTarget::BlockEnd),
_ => None,
}) else {
return false;
};
if raw.trim().eq_ignore_ascii_case("revert-layer") {
clear_delta_logical_box_edge(delta, is_margin, logical_target);
if is_margin {
delta.revert_layer.margin_logical.set(logical_target);
} else {
delta.revert_layer.padding_logical.set(logical_target);
}
return true;
}
match logical_target {
LogicalBoxEdgeTarget::Inline | LogicalBoxEdgeTarget::Block => {
let Some(values) = parse_native_length_components(raw, 2, is_margin, is_margin) else {
return false;
};
let Some([start, end]) = expand_native_pair(&values) else {
return false;
};
let (start_target, end_target) = if logical_target == LogicalBoxEdgeTarget::Inline {
(
LogicalBoxEdgeTarget::InlineStart,
LogicalBoxEdgeTarget::InlineEnd,
)
} else {
(
LogicalBoxEdgeTarget::BlockStart,
LogicalBoxEdgeTarget::BlockEnd,
)
};
set_delta_native_logical_box_edge_component(delta, is_margin, start_target, start);
set_delta_native_logical_box_edge_component(delta, is_margin, end_target, end);
}
_ => {
let Some(value) = parse_native_length_component(raw, is_margin, is_margin) else {
return false;
};
set_delta_native_logical_box_edge_component(delta, is_margin, logical_target, value);
}
}
true
}
fn set_revert_layer_native_box_edge(
delta: &mut StyleDelta,
is_margin: bool,
target: BoxEdgeTarget,
) {
if is_margin {
delta.margin.clear(target);
delta.revert_layer.margin.set(target);
} else {
delta.padding.clear(target);
delta.revert_layer.padding.set(target);
}
}
fn set_delta_native_box_edge_component(
delta: &mut StyleDelta,
is_margin: bool,
target: BoxEdgeTarget,
component: NativeLengthComponent,
) {
if is_margin {
delta.revert_layer.margin.clear(target);
} else {
delta.revert_layer.padding.clear(target);
}
let edge = if is_margin {
&mut delta.margin
} else {
&mut delta.padding
};
edge.clear(target);
let (spec, var) = match component {
NativeLengthComponent::Spec(value) => (Some(value), None),
NativeLengthComponent::Var(value) => (
None,
Some(LengthVarExpr {
name: value,
scale: 1.0,
}),
),
};
match target {
BoxEdgeTarget::All => {
edge.top = spec;
edge.right = spec;
edge.bottom = spec;
edge.left = spec;
edge.top_var = var.clone();
edge.right_var = var.clone();
edge.bottom_var = var.clone();
edge.left_var = var;
}
BoxEdgeTarget::Top => {
edge.top = spec;
edge.top_var = var;
}
BoxEdgeTarget::Right => {
edge.right = spec;
edge.right_var = var;
}
BoxEdgeTarget::Bottom => {
edge.bottom = spec;
edge.bottom_var = var;
}
BoxEdgeTarget::Left => {
edge.left = spec;
edge.left_var = var;
}
}
}
fn set_delta_native_logical_box_edge_component(
delta: &mut StyleDelta,
is_margin: bool,
target: LogicalBoxEdgeTarget,
component: NativeLengthComponent,
) {
match component {
NativeLengthComponent::Spec(value) => match target {
LogicalBoxEdgeTarget::InlineStart => {
set_delta_logical_inline_edge_spec(delta, is_margin, Some(value), None)
}
LogicalBoxEdgeTarget::InlineEnd => {
set_delta_logical_inline_edge_spec(delta, is_margin, None, Some(value))
}
LogicalBoxEdgeTarget::BlockStart => {
set_delta_logical_block_edge_spec(delta, is_margin, Some(value), None)
}
LogicalBoxEdgeTarget::BlockEnd => {
set_delta_logical_block_edge_spec(delta, is_margin, None, Some(value))
}
LogicalBoxEdgeTarget::Inline | LogicalBoxEdgeTarget::Block => {}
},
NativeLengthComponent::Var(value) => {
let expr = LengthVarExpr {
name: value,
scale: 1.0,
};
match target {
LogicalBoxEdgeTarget::InlineStart => {
set_delta_logical_inline_edge_var(delta, is_margin, Some(expr), None)
}
LogicalBoxEdgeTarget::InlineEnd => {
set_delta_logical_inline_edge_var(delta, is_margin, None, Some(expr))
}
LogicalBoxEdgeTarget::BlockStart => {
set_delta_logical_block_edge_var(delta, is_margin, Some(expr), None)
}
LogicalBoxEdgeTarget::BlockEnd => {
set_delta_logical_block_edge_var(delta, is_margin, None, Some(expr))
}
LogicalBoxEdgeTarget::Inline | LogicalBoxEdgeTarget::Block => {}
}
}
}
}
#[derive(Debug, Clone)]
enum NativeBorderColorComponent {
Spec(ColorSpec),
Var(String),
}
fn native_medium_border_width() -> LengthSpec {
LengthSpec::Absolute(Pt::from_f32(2.25))
}
fn parse_native_border_width_component(raw: &str) -> Option<NativeLengthComponent> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"thin" => {
return Some(NativeLengthComponent::Spec(LengthSpec::Absolute(
Pt::from_f32(0.75),
)));
}
"medium" | "initial" | "unset" | "revert" => {
return Some(NativeLengthComponent::Spec(native_medium_border_width()));
}
"thick" => {
return Some(NativeLengthComponent::Spec(LengthSpec::Absolute(
Pt::from_f32(3.75),
)));
}
"inherit" => return Some(NativeLengthComponent::Spec(LengthSpec::Inherit)),
"revert-layer" | "" => return None,
_ => {}
}
if lowered.contains("var(") {
return Some(NativeLengthComponent::Var(lowered));
}
let value = length_spec_from_string(&lowered)?;
native_length_is_nonnegative(value).then_some(NativeLengthComponent::Spec(value))
}
fn parse_native_border_color_component(raw: &str) -> Option<NativeBorderColorComponent> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => return Some(NativeBorderColorComponent::Spec(ColorSpec::Inherit)),
"initial" | "unset" | "revert" => {
return Some(NativeBorderColorComponent::Spec(ColorSpec::Initial));
}
"currentcolor" => {
return Some(NativeBorderColorComponent::Spec(ColorSpec::CurrentColor));
}
"revert-layer" | "" => return None,
_ => {}
}
if let Some((color, alpha)) = parse_color_string(&lowered) {
let alpha = alpha.clamp(0.0, 1.0);
return Some(NativeBorderColorComponent::Spec(if alpha >= 1.0 {
ColorSpec::Value(color)
} else {
ColorSpec::AlphaValue(color, alpha)
}));
}
if should_defer_color_expr(&lowered) || lowered.contains("var(") {
return Some(NativeBorderColorComponent::Var(lowered));
}
None
}
fn apply_native_border_width_component(
delta: &mut StyleDelta,
target: BorderColorTarget,
value: NativeLengthComponent,
) {
match value {
NativeLengthComponent::Spec(value) => set_delta_border_width_spec(delta, target, value),
NativeLengthComponent::Var(value) => set_delta_border_width_var(
delta,
target,
LengthVarExpr {
name: value,
scale: 1.0,
},
),
}
}
fn apply_native_border_color_component(
delta: &mut StyleDelta,
target: BorderColorTarget,
value: NativeBorderColorComponent,
) {
match value {
NativeBorderColorComponent::Spec(value) => {
set_delta_border_color_spec(delta, target, value)
}
NativeBorderColorComponent::Var(value) => set_delta_border_color_var(delta, target, value),
}
}
fn native_border_targets(property_name: &str, family: &str) -> Option<Vec<BorderColorTarget>> {
let all_physical = || {
vec![
BorderColorTarget::Top,
BorderColorTarget::Right,
BorderColorTarget::Bottom,
BorderColorTarget::Left,
]
};
let inline = || vec![BorderColorTarget::InlineStart, BorderColorTarget::InlineEnd];
let block = || vec![BorderColorTarget::BlockStart, BorderColorTarget::BlockEnd];
let expected = format!("border-{family}");
if property_name == expected {
return Some(all_physical());
}
if property_name == format!("border-inline-{family}") {
return Some(inline());
}
if property_name == format!("border-block-{family}") {
return Some(block());
}
let suffix = format!("-{family}");
property_name
.strip_suffix(&suffix)
.filter(|base| {
matches!(
*base,
"border-top"
| "border-right"
| "border-bottom"
| "border-left"
| "border-inline-start"
| "border-inline-end"
| "border-block-start"
| "border-block-end"
)
})
.map(|_| vec![border_color_target_from_property_name(property_name)])
}
fn expand_native_components_for_targets<T: Clone>(
values: &[T],
target_count: usize,
) -> Option<Vec<T>> {
match target_count {
1 if values.len() == 1 => Some(vec![values[0].clone()]),
2 => match values {
[both] => Some(vec![both.clone(), both.clone()]),
[start, end] => Some(vec![start.clone(), end.clone()]),
_ => None,
},
4 => match values {
[all] => Some(vec![all.clone(), all.clone(), all.clone(), all.clone()]),
[vertical, horizontal] => Some(vec![
vertical.clone(),
horizontal.clone(),
vertical.clone(),
horizontal.clone(),
]),
[top, horizontal, bottom] => Some(vec![
top.clone(),
horizontal.clone(),
bottom.clone(),
horizontal.clone(),
]),
[top, right, bottom, left] => Some(vec![
top.clone(),
right.clone(),
bottom.clone(),
left.clone(),
]),
_ => None,
},
_ => None,
}
}
fn apply_native_border_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
if let Some(targets) = native_border_targets(property_name, "width") {
return apply_native_border_width_property(&targets, raw, delta);
}
if let Some(targets) = native_border_targets(property_name, "color") {
return apply_native_border_color_property(&targets, raw, delta);
}
if let Some(targets) = native_border_targets(property_name, "style") {
return apply_native_border_style_property(&targets, raw, delta);
}
let targets = match property_name {
"border" => vec![BorderColorTarget::All],
"border-inline" => vec![BorderColorTarget::InlineStart, BorderColorTarget::InlineEnd],
"border-block" => vec![BorderColorTarget::BlockStart, BorderColorTarget::BlockEnd],
"border-top"
| "border-right"
| "border-bottom"
| "border-left"
| "border-inline-start"
| "border-inline-end"
| "border-block-start"
| "border-block-end" => vec![border_color_target_from_property_name(property_name)],
_ => return false,
};
if raw.trim().eq_ignore_ascii_case("revert-layer") {
for target in targets {
set_revert_layer_border_width(delta, target);
set_revert_layer_border_style(delta, target);
set_revert_layer_border_color(delta, target);
}
return true;
}
let Some((width, style, color)) = parse_native_border_side_shorthand(raw) else {
return false;
};
for target in targets {
apply_native_border_width_component(delta, target, width.clone());
set_delta_border_style(delta, target, style);
apply_native_border_color_component(delta, target, color.clone());
}
true
}
fn expand_border_image_quad<T: Copy>(values: &[T]) -> Option<[T; 4]> {
match values {
[all] => Some([*all; 4]),
[vertical, horizontal] => Some([*vertical, *horizontal, *vertical, *horizontal]),
[top, horizontal, bottom] => Some([*top, *horizontal, *bottom, *horizontal]),
[top, right, bottom, left] => Some([*top, *right, *bottom, *left]),
_ => None,
}
}
fn parse_border_image_source_value(raw: &str) -> Option<Option<BackgroundPaint>> {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("none") {
return Some(None);
}
parse_background_layer_paint_str(trimmed).map(Some)
}
fn parse_border_image_slice_value(raw: &str) -> Option<BorderImageSliceValue> {
let trimmed = raw.trim();
if let Some(percent) = trimmed.strip_suffix('%') {
let value = percent.trim().parse::<f32>().ok()?;
return (value.is_finite() && value >= 0.0)
.then_some(BorderImageSliceValue::Percent(value / 100.0));
}
let value = trimmed.parse::<f32>().ok()?;
(value.is_finite() && value >= 0.0).then_some(BorderImageSliceValue::Number(value))
}
fn parse_border_image_width_value(raw: &str) -> Option<BorderImageWidthValue> {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("auto") {
return Some(BorderImageWidthValue::Auto);
}
if let Ok(value) = trimmed.parse::<f32>() {
return (value.is_finite() && value >= 0.0).then_some(BorderImageWidthValue::Number(value));
}
let value = length_spec_from_string(&trimmed.to_ascii_lowercase())?;
native_length_is_nonnegative(value).then_some(BorderImageWidthValue::Length(value))
}
fn parse_border_image_outset_value(raw: &str) -> Option<BorderImageOutsetValue> {
let trimmed = raw.trim();
if let Ok(value) = trimmed.parse::<f32>() {
return (value.is_finite() && value >= 0.0)
.then_some(BorderImageOutsetValue::Number(value));
}
let value = length_spec_from_string(&trimmed.to_ascii_lowercase())?;
// Percentages and `auto` are not valid for border-image-outset.
if matches!(value, LengthSpec::Percent(_) | LengthSpec::Auto) {
return None;
}
native_length_is_nonnegative(value).then_some(BorderImageOutsetValue::Length(value))
}
fn parse_border_image_repeat_mode(raw: &str) -> Option<BorderImageRepeatMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"stretch" => Some(BorderImageRepeatMode::Stretch),
"repeat" => Some(BorderImageRepeatMode::Repeat),
"round" => Some(BorderImageRepeatMode::Round),
"space" => Some(BorderImageRepeatMode::Space),
_ => None,
}
}
fn parse_border_image_slice(raw: &str) -> Option<([BorderImageSliceValue; 4], bool)> {
let mut fill = false;
let mut values = Vec::new();
for token in split_top_level_whitespace(raw) {
if token.eq_ignore_ascii_case("fill") {
if fill {
return None;
}
fill = true;
} else {
values.push(parse_border_image_slice_value(&token)?);
}
}
Some((expand_border_image_quad(&values)?, fill))
}
fn parse_border_image_width(raw: &str) -> Option<[BorderImageWidthValue; 4]> {
let values = split_top_level_whitespace(raw)
.iter()
.map(|token| parse_border_image_width_value(token))
.collect::<Option<Vec<_>>>()?;
expand_border_image_quad(&values)
}
fn parse_border_image_outset(raw: &str) -> Option<[BorderImageOutsetValue; 4]> {
let values = split_top_level_whitespace(raw)
.iter()
.map(|token| parse_border_image_outset_value(token))
.collect::<Option<Vec<_>>>()?;
expand_border_image_quad(&values)
}
fn parse_border_image_repeat(raw: &str) -> Option<(BorderImageRepeatMode, BorderImageRepeatMode)> {
let values = split_top_level_whitespace(raw)
.iter()
.map(|token| parse_border_image_repeat_mode(token))
.collect::<Option<Vec<_>>>()?;
match values.as_slice() {
[both] => Some((*both, *both)),
[x, y] => Some((*x, *y)),
_ => None,
}
}
fn apply_border_image_source(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(source) = parse_border_image_source_value(raw) else {
return false;
};
delta.border_image.source = Some(source);
true
}
fn apply_border_image_slice(raw: &str, delta: &mut StyleDelta) -> bool {
let Some((slice, fill)) = parse_border_image_slice(raw) else {
return false;
};
delta.border_image.slice = Some(slice);
delta.border_image.fill = Some(fill);
true
}
fn apply_border_image_width(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(width) = parse_border_image_width(raw) else {
return false;
};
delta.border_image.width = Some(width);
true
}
fn apply_border_image_outset(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(outset) = parse_border_image_outset(raw) else {
return false;
};
delta.border_image.outset = Some(outset);
true
}
fn apply_border_image_repeat(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(repeat) = parse_border_image_repeat(raw) else {
return false;
};
delta.border_image.repeat = Some(repeat);
true
}
fn apply_border_image_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let mut parsed = BorderImageDelta {
reset: true,
..BorderImageDelta::default()
};
let mut section = 0usize;
let mut slice_tokens = Vec::new();
let mut width_tokens = Vec::new();
let mut outset_tokens = Vec::new();
let mut repeat_tokens = Vec::new();
let mut source = None;
for token in split_top_level_whitespace(raw) {
if token == "/" {
section += 1;
if section > 2 {
return false;
}
continue;
}
if source.is_none() {
if let Some(value) = parse_border_image_source_value(&token) {
source = Some(value);
continue;
}
}
if parse_border_image_repeat_mode(&token).is_some() {
repeat_tokens.push(token);
continue;
}
match section {
0 => slice_tokens.push(token),
1 => width_tokens.push(token),
2 => outset_tokens.push(token),
_ => unreachable!(),
}
}
if let Some(source) = source {
parsed.source = Some(source);
}
if !slice_tokens.is_empty() {
let Some((slice, fill)) = parse_border_image_slice(&slice_tokens.join(" ")) else {
return false;
};
parsed.slice = Some(slice);
parsed.fill = Some(fill);
}
if !width_tokens.is_empty() {
let Some(width) = parse_border_image_width(&width_tokens.join(" ")) else {
return false;
};
parsed.width = Some(width);
}
if !outset_tokens.is_empty() {
let Some(outset) = parse_border_image_outset(&outset_tokens.join(" ")) else {
return false;
};
parsed.outset = Some(outset);
}
if !repeat_tokens.is_empty() {
let Some(repeat) = parse_border_image_repeat(&repeat_tokens.join(" ")) else {
return false;
};
parsed.repeat = Some(repeat);
}
delta.border_image = parsed;
true
}
fn apply_native_border_width_property(
targets: &[BorderColorTarget],
raw: &str,
delta: &mut StyleDelta,
) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
for target in targets {
set_revert_layer_border_width(delta, *target);
}
return true;
}
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > targets.len() {
return false;
}
if parts.len() > 1
&& parts
.iter()
.any(|part| is_css_wide_keyword(&part.to_ascii_lowercase()))
{
return false;
}
let Some(values) = parts
.iter()
.map(|part| parse_native_border_width_component(part))
.collect::<Option<Vec<_>>>()
else {
return false;
};
let Some(values) = expand_native_components_for_targets(&values, targets.len()) else {
return false;
};
for (target, value) in targets.iter().copied().zip(values) {
apply_native_border_width_component(delta, target, value);
}
true
}
fn apply_native_border_color_property(
targets: &[BorderColorTarget],
raw: &str,
delta: &mut StyleDelta,
) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
for target in targets {
set_revert_layer_border_color(delta, *target);
}
return true;
}
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > targets.len() {
return false;
}
if parts.len() > 1
&& parts
.iter()
.any(|part| is_css_wide_keyword(&part.to_ascii_lowercase()))
{
return false;
}
let Some(values) = parts
.iter()
.map(|part| parse_native_border_color_component(part))
.collect::<Option<Vec<_>>>()
else {
return false;
};
if targets.len() == 4 && values.len() == 1 {
apply_native_border_color_component(delta, BorderColorTarget::All, values[0].clone());
return true;
}
let Some(values) = expand_native_components_for_targets(&values, targets.len()) else {
return false;
};
for (target, value) in targets.iter().copied().zip(values) {
apply_native_border_color_component(delta, target, value);
}
true
}
fn parse_native_border_style_component(raw: &str) -> Option<BorderLineStyle> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"initial" | "unset" | "revert" => Some(BorderLineStyle::None),
"inherit" | "revert-layer" | "" => None,
_ => border_line_style_from_ident(&lowered),
}
}
fn apply_native_border_style_property(
targets: &[BorderColorTarget],
raw: &str,
delta: &mut StyleDelta,
) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
for target in targets {
set_revert_layer_border_style(delta, *target);
}
return true;
}
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > targets.len() {
return false;
}
if parts.len() > 1
&& parts
.iter()
.any(|part| is_css_wide_keyword(&part.to_ascii_lowercase()))
{
return false;
}
let Some(values) = parts
.iter()
.map(|part| parse_native_border_style_component(part))
.collect::<Option<Vec<_>>>()
else {
return false;
};
let Some(values) = expand_native_components_for_targets(&values, targets.len()) else {
return false;
};
for (target, value) in targets.iter().copied().zip(values) {
set_delta_border_style(delta, target, value);
}
true
}
fn parse_native_border_side_shorthand(
raw: &str,
) -> Option<(
NativeLengthComponent,
BorderLineStyle,
NativeBorderColorComponent,
)> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() {
return None;
}
match lowered.as_str() {
"inherit" => {
return Some((
NativeLengthComponent::Spec(LengthSpec::Inherit),
BorderLineStyle::None,
NativeBorderColorComponent::Spec(ColorSpec::Inherit),
));
}
"initial" | "unset" | "revert" => {
return Some((
NativeLengthComponent::Spec(native_medium_border_width()),
BorderLineStyle::None,
NativeBorderColorComponent::Spec(ColorSpec::Initial),
));
}
_ => {}
}
let parts = split_top_level_whitespace(&lowered);
let mut width: Option<NativeLengthComponent> = None;
let mut style: Option<BorderLineStyle> = None;
let mut color: Option<NativeBorderColorComponent> = None;
for part in parts {
if style.is_none() {
if let Some(value) = border_line_style_from_ident(&part) {
style = Some(value);
continue;
}
}
if width.is_none() && !part.contains("var(") {
if let Some(value) = parse_native_border_width_component(&part) {
width = Some(value);
continue;
}
}
if !part.contains("var(") {
if color.is_none() {
if let Some(value) = parse_native_border_color_component(&part) {
color = Some(value);
continue;
}
}
return None;
}
// A bare custom-property reference is ambiguous until substitution. Preserve both
// viable channels; a later color reference supersedes the color channel, matching
// common `var(--width) solid var(--color)` framework output.
if width.is_none() {
width = Some(NativeLengthComponent::Var(part.clone()));
}
color = Some(NativeBorderColorComponent::Var(part));
}
Some((
width.unwrap_or_else(|| NativeLengthComponent::Spec(native_medium_border_width())),
style.unwrap_or(BorderLineStyle::None),
color.unwrap_or(NativeBorderColorComponent::Spec(ColorSpec::CurrentColor)),
))
}
fn apply_native_outline_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
match property_name {
"outline-width" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_outline(delta, true, false, false);
return true;
}
let Some(value) = parse_native_border_width_component(raw) else {
return false;
};
match value {
NativeLengthComponent::Spec(value) => set_delta_outline_width(delta, value),
NativeLengthComponent::Var(_) => return false,
}
true
}
"outline-style" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_outline(delta, false, true, false);
return true;
}
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"initial" | "unset" | "revert" => Some(None),
"inherit" => None,
_ => outline_line_style_from_ident(&lowered),
};
let Some(value) = value else {
return false;
};
set_delta_outline_style(delta, value);
true
}
"outline-color" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_outline(delta, false, false, true);
return true;
}
let Some(value) = parse_native_border_color_component(raw) else {
return false;
};
match value {
NativeBorderColorComponent::Spec(value) => set_delta_outline_color(delta, value),
NativeBorderColorComponent::Var(_) => return false,
}
true
}
"outline-offset" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_outline_offset(delta);
return true;
}
let Some(value) = parse_native_length_component(raw, false, true) else {
return false;
};
match value {
NativeLengthComponent::Spec(value) => set_delta_outline_offset(delta, value),
NativeLengthComponent::Var(value) => set_delta_outline_offset_var(delta, value),
}
true
}
"outline" => {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
set_revert_layer_outline(delta, true, true, true);
return true;
}
let Some((width, style, color)) = parse_native_outline_shorthand(raw) else {
return false;
};
match width {
NativeLengthComponent::Spec(value) => set_delta_outline_width(delta, value),
NativeLengthComponent::Var(_) => return false,
}
set_delta_outline_style(delta, style);
match color {
NativeBorderColorComponent::Spec(value) => set_delta_outline_color(delta, value),
NativeBorderColorComponent::Var(_) => return false,
}
true
}
_ => false,
}
}
fn apply_native_column_rule_property(
property_name: &str,
raw: &str,
delta: &mut StyleDelta,
) -> bool {
match property_name {
"column-rule-width" => {
let Some(NativeLengthComponent::Spec(value)) = parse_native_border_width_component(raw)
else {
return false;
};
delta.column_rule_width = Some(value);
true
}
"column-rule-style" => {
let Some(value) = parse_native_border_style_component(raw) else {
return false;
};
delta.column_rule_style = Some(value);
true
}
"column-rule-color" => {
let Some(NativeBorderColorComponent::Spec(value)) =
parse_native_border_color_component(raw)
else {
return false;
};
delta.column_rule_color = Some(value);
true
}
"column-rule" => {
let Some((width, style, color)) = parse_native_border_side_shorthand(raw) else {
return false;
};
let NativeLengthComponent::Spec(width) = width else {
return false;
};
let NativeBorderColorComponent::Spec(color) = color else {
return false;
};
delta.column_rule_width = Some(width);
delta.column_rule_style = Some(style);
delta.column_rule_color = Some(color);
true
}
_ => false,
}
}
fn parse_native_column_width(raw: &str) -> Option<LengthSpec> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"auto" | "initial" | "unset" | "revert" => return Some(LengthSpec::Auto),
"inherit" => return Some(LengthSpec::Inherit),
_ => {}
}
let NativeLengthComponent::Spec(value) = parse_native_length_component(&lowered, false, false)?
else {
return None;
};
match value {
LengthSpec::Absolute(value) if value > Pt::ZERO => Some(LengthSpec::Absolute(value)),
LengthSpec::Em(value) if value > 0.0 => Some(LengthSpec::Em(value)),
LengthSpec::Rem(value) if value > 0.0 => Some(LengthSpec::Rem(value)),
LengthSpec::Calc(_) | LengthSpec::Clamped(_) | LengthSpec::FontRelative(_) => Some(value),
_ => None,
}
}
fn apply_native_column_width_property(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(width) = parse_native_column_width(raw) else {
return false;
};
delta.column_width = Some(width);
true
}
fn apply_native_columns_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > 2 {
return false;
}
if parts.len() == 1
&& matches!(
parts[0].trim().to_ascii_lowercase().as_str(),
"auto" | "initial" | "unset" | "revert"
)
{
delta.column_count = Some(1);
delta.column_count_auto = Some(true);
delta.column_width = Some(LengthSpec::Auto);
return true;
}
let mut count = None;
let mut width = None;
let mut auto_tokens = 0usize;
for part in parts {
let lowered = part.trim().to_ascii_lowercase();
if lowered == "auto" {
auto_tokens += 1;
continue;
}
if let Ok(value) = lowered.parse::<usize>() {
if count.is_some() || !(1..=256).contains(&value) {
return false;
}
count = Some(value);
continue;
}
let Some(value) = parse_native_column_width(&lowered) else {
return false;
};
if width.is_some() || matches!(value, LengthSpec::Auto | LengthSpec::Inherit) {
return false;
}
width = Some(value);
}
if auto_tokens > 2 || count.is_none() && width.is_none() {
return false;
}
delta.column_count = Some(count.unwrap_or(1));
delta.column_count_auto = Some(count.is_none());
delta.column_width = Some(width.unwrap_or(LengthSpec::Auto));
true
}
fn parse_native_outline_shorthand(
raw: &str,
) -> Option<(
NativeLengthComponent,
Option<OutlineLineStyle>,
NativeBorderColorComponent,
)> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() {
return None;
}
match lowered.as_str() {
"initial" | "unset" | "revert" => {
return Some((
NativeLengthComponent::Spec(native_medium_border_width()),
None,
NativeBorderColorComponent::Spec(ColorSpec::Initial),
));
}
_ => {}
}
let parts = split_top_level_whitespace(&lowered);
let mut width = None;
let mut style = None;
let mut saw_style = false;
let mut color = None;
for part in parts {
if !saw_style {
if let Some(value) = outline_line_style_from_ident(&part) {
style = value;
saw_style = true;
continue;
}
}
if width.is_none() && !part.contains("var(") {
if let Some(value) = parse_native_border_width_component(&part) {
width = Some(value);
continue;
}
}
if color.is_none() {
if let Some(value) = parse_native_border_color_component(&part) {
color = Some(value);
continue;
}
}
return None;
}
Some((
width.unwrap_or_else(|| NativeLengthComponent::Spec(native_medium_border_width())),
style,
color.unwrap_or(NativeBorderColorComponent::Spec(ColorSpec::CurrentColor)),
))
}
fn apply_native_custom_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let name = property_name.to_ascii_lowercase();
clear_custom_property_delta_value(delta, &name);
let preserved = raw.trim().to_string();
let lowered = preserved.to_ascii_lowercase();
delta
.custom_raw_values
.insert(name.clone(), preserved.clone());
if let Some((color, alpha)) = parse_color_string(&preserved) {
let blended = if alpha < 1.0 {
Color::rgb(
color.r * alpha + (1.0 - alpha),
color.g * alpha + (1.0 - alpha),
color.b * alpha + (1.0 - alpha),
)
} else {
color
};
delta.custom_colors.insert(name.clone(), blended);
// Direct custom-property colors are stored as their flattened PDF color, matching the
// former typed-token path. Alpha remains one so consumers do not blend the already
// flattened value a second time; expressions containing var() retain deferred alpha.
delta.custom_color_alpha.insert(name.clone(), 1.0);
} else if let Some(color) = parse_rgb_triplet_string(&preserved) {
delta.custom_colors.insert(name.clone(), color);
delta.custom_color_alpha.insert(name.clone(), 1.0);
} else if lowered.contains("var(") {
delta.custom_color_refs.insert(name.clone(), lowered);
} else if let Some(spec) = length_spec_from_string(&lowered) {
delta.custom_lengths.insert(name.clone(), spec);
} else if let Some(stack) = font_stack_from_string(&preserved) {
delta.custom_font_stacks.insert(name, stack);
} else {
// Keep an opaque expression in the same resolver channel used by var() chains. This also
// guarantees that a later declaration clears stale typed caches from an earlier value.
delta.custom_color_refs.insert(name, lowered);
}
true
}
fn content_from_raw(raw: &str) -> Option<ContentSpec> {
let tokens = crate::css_native::tokenize_component_values(raw)?;
if tokens.len() == 1 {
if let ComponentValue::Ident(value) = &tokens[0] {
return match value.to_ascii_lowercase().as_str() {
"none" => Some(ContentSpec::None),
"normal" => Some(ContentSpec::Normal),
"inherit" => Some(ContentSpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" => Some(ContentSpec::Initial),
_ => content_quote_keyword(value).map(|part| ContentSpec::Parts(vec![part])),
};
}
}
let mut parts = Vec::new();
for token in tokens {
match token {
ComponentValue::String(value) => parts.push(ContentPart::Text(value)),
ComponentValue::Ident(value) => parts.push(content_quote_keyword(&value)?),
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("attr") => {
parts.push(ContentPart::Attr(content_attr_name_from_raw(&arguments)?));
}
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("url") => {
parts.push(ContentPart::Image(parse_native_url_argument(&arguments)?));
}
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("leader") => {
parts.push(ContentPart::Leader(content_leader_pattern_from_raw(
&arguments,
)?));
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("counter") =>
{
parts.push(ContentPart::Counter(content_counter_from_raw(&arguments)?));
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("counters") =>
{
parts.push(ContentPart::Counters(content_counters_from_raw(
&arguments,
)?));
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("target-text") =>
{
parts.push(ContentPart::TargetTextAttr(content_target_attr_from_raw(
&arguments,
)?));
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("target-counter") =>
{
let (attribute, name, style) = content_target_counter_from_raw(&arguments)?;
parts.push(ContentPart::TargetCounterAttr {
attribute,
name,
style,
});
}
// Preserve the engine's prior treatment of the optional `/ <alt-text>` separator: it
// did not expose accessibility text separately and concatenated subsequent strings.
ComponentValue::Delim('/') => {}
_ => return None,
}
}
if parts.is_empty() {
return None;
}
if parts
.iter()
.all(|part| matches!(part, ContentPart::Text(_)))
{
let mut text = String::new();
for part in parts {
if let ContentPart::Text(value) = part {
text.push_str(&value);
}
}
Some(ContentSpec::Text(text))
} else {
Some(ContentSpec::Parts(parts))
}
}
fn content_quote_keyword(raw: &str) -> Option<ContentPart> {
match raw.to_ascii_lowercase().as_str() {
"open-quote" => Some(ContentPart::OpenQuote),
"close-quote" => Some(ContentPart::CloseQuote),
"no-open-quote" => Some(ContentPart::NoOpenQuote),
"no-close-quote" => Some(ContentPart::NoCloseQuote),
_ => None,
}
}
fn content_attr_name_from_raw(raw: &str) -> Option<String> {
crate::css_native::tokenize_component_values(raw)?
.into_iter()
.find_map(|token| match token {
ComponentValue::Ident(value) => Some(value.to_ascii_lowercase()),
_ => None,
})
}
fn content_target_attr_from_raw(raw: &str) -> Option<String> {
let args = split_args(raw);
let first = crate::css_native::tokenize_component_values(args.first()?.trim())?;
let [ComponentValue::Function { name, arguments }] = first.as_slice() else {
return None;
};
if !name.eq_ignore_ascii_case("attr") {
return None;
}
if let Some(selector) = args.get(1) {
let selector = selector.trim().to_ascii_lowercase();
if !matches!(
selector.as_str(),
"content" | "before" | "after" | "first-letter"
) {
return None;
}
}
content_attr_name_from_raw(arguments)
}
fn content_leader_pattern_from_raw(raw: &str) -> Option<String> {
match crate::css_native::tokenize_component_values(raw.trim())?.as_slice() {
[ComponentValue::String(pattern)] if !pattern.is_empty() => Some(pattern.clone()),
[ComponentValue::Ident(keyword)] if keyword.eq_ignore_ascii_case("dotted") => {
Some(".".to_string())
}
[ComponentValue::Ident(keyword)] if keyword.eq_ignore_ascii_case("solid") => {
Some("_".to_string())
}
[ComponentValue::Ident(keyword)] if keyword.eq_ignore_ascii_case("space") => {
Some(" ".to_string())
}
_ => None,
}
}
fn content_target_counter_from_raw(raw: &str) -> Option<(String, String, GeneratedCounterStyle)> {
let args = split_args(raw);
let target = crate::css_native::tokenize_component_values(args.first()?.trim())?;
let [ComponentValue::Function { name, arguments }] = target.as_slice() else {
return None;
};
if !name.eq_ignore_ascii_case("attr") {
return None;
}
let attribute = content_attr_name_from_raw(arguments)?;
let name = crate::css_native::parse_identifier(args.get(1)?.trim())?;
if !is_valid_counter_name(&name) {
return None;
}
let style = args
.get(2)
.and_then(|value| generated_counter_style_from_raw(value))
.unwrap_or_else(decimal_generated_counter_style);
Some((attribute, name, style))
}
fn content_counter_from_raw(raw: &str) -> Option<GeneratedCounterContent> {
let args = split_args(raw);
let name = crate::css_native::parse_identifier(args.first()?.trim())?;
if !is_valid_counter_name(&name) {
return None;
}
let style = args
.get(1)
.and_then(|value| generated_counter_style_from_raw(value))
.unwrap_or_else(decimal_generated_counter_style);
Some(GeneratedCounterContent { name, style })
}
fn content_counters_from_raw(raw: &str) -> Option<GeneratedCountersContent> {
let args = split_args(raw);
let name = crate::css_native::parse_identifier(args.first()?.trim())?;
if !is_valid_counter_name(&name) {
return None;
}
let separator = args.get(1).and_then(|value| {
match crate::css_native::tokenize_component_values(value)?.as_slice() {
[ComponentValue::String(value)] => Some(value.clone()),
_ => None,
}
})?;
let style = args
.get(2)
.and_then(|value| generated_counter_style_from_raw(value))
.unwrap_or_else(decimal_generated_counter_style);
Some(GeneratedCountersContent {
name,
separator,
style,
})
}
fn quotes_from_raw(raw: &str) -> Option<QuotesSpec> {
let tokens = crate::css_native::tokenize_component_values(raw)?;
if tokens.len() == 1 {
if let ComponentValue::Ident(value) = &tokens[0] {
return match value.to_ascii_lowercase().as_str() {
"none" => Some(QuotesSpec::Value(Vec::new())),
"auto" | "initial" | "revert" | "revert-layer" => Some(QuotesSpec::Initial),
"inherit" | "unset" | "match-parent" => Some(QuotesSpec::Inherit),
_ => None,
};
}
}
let strings = tokens
.into_iter()
.map(|token| match token {
ComponentValue::String(value) => Some(value),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
if strings.len() < 2 || strings.len() % 2 != 0 {
return None;
}
Some(QuotesSpec::Value(
strings
.chunks_exact(2)
.map(|pair| (pair[0].clone(), pair[1].clone()))
.collect(),
))
}
fn apply_native_counter_property(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let default_value = if property_name == "counter-increment" {
1
} else {
0
};
let Some(mut values) = counter_mutations_from_raw(raw, default_value) else {
return false;
};
if property_name == "counter-reset" {
values = last_counter_reset_mutations(values);
delta.counter_reset = Some(values);
} else if property_name == "counter-increment" {
delta.counter_increment = Some(values);
} else {
delta.counter_set = Some(values);
}
true
}
fn counter_mutations_from_raw(raw: &str, default_value: i32) -> Option<Vec<CounterMutation>> {
let lowered = raw.trim().to_ascii_lowercase();
if matches!(
lowered.as_str(),
"none" | "initial" | "unset" | "revert" | "revert-layer"
) {
return Some(Vec::new());
}
let tokens = crate::css_native::tokenize_component_values(raw)?;
let mut out = Vec::new();
let mut cursor = 0usize;
while cursor < tokens.len() {
if matches!(tokens[cursor], ComponentValue::Delim(',')) {
cursor += 1;
continue;
}
let (name, reversed) = match &tokens[cursor] {
ComponentValue::Ident(name) => (name.clone(), false),
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("reversed") =>
{
(crate::css_native::parse_identifier(arguments.trim())?, true)
}
_ => return None,
};
if !is_valid_counter_name(&name) {
return None;
}
cursor += 1;
let explicit = tokens.get(cursor).and_then(|token| match token {
ComponentValue::Number(value) => parse_css_integer(value),
_ => None,
});
if explicit.is_some() {
cursor += 1;
}
out.push(CounterMutation {
name,
value: explicit.unwrap_or(default_value),
reversed,
auto_reversed_initial: reversed && explicit.is_none(),
});
}
Some(out)
}
fn parse_css_integer(raw: &str) -> Option<i32> {
let value = raw.parse::<f64>().ok()?;
if !value.is_finite()
|| value.fract() != 0.0
|| value < i32::MIN as f64
|| value > i32::MAX as f64
{
return None;
}
Some(value as i32)
}
fn apply_native_list_style_type(raw: &str, delta: &mut StyleDelta) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return set_revert_layer_list_style_type(delta);
}
let lowered = raw.trim().to_ascii_lowercase();
if matches!(lowered.as_str(), "inherit" | "unset") {
// The computed style starts from its parent for inherited list properties.
delta.list_style_type = None;
delta.revert_layer.list_style_type = false;
return true;
}
let value = if matches!(lowered.as_str(), "initial" | "revert") {
(ListStyleTypeMode::Disc, None, None)
} else {
let Some(value) = parse_native_list_style_type(raw) else {
return false;
};
value
};
set_delta_list_style_type(delta, value);
true
}
fn parse_native_list_style_type(
raw: &str,
) -> Option<(
ListStyleTypeMode,
Option<String>,
Option<AnonymousListStyleSymbols>,
)> {
let tokens = crate::css_native::tokenize_component_values(raw)?;
match tokens.as_slice() {
[ComponentValue::String(value)] => {
Some((ListStyleTypeMode::CustomString, Some(value.clone()), None))
}
[ComponentValue::Function { name, arguments }] if name.eq_ignore_ascii_case("symbols") => {
let symbols = parse_native_anonymous_symbols(arguments)?;
Some((ListStyleTypeMode::AnonymousSymbols, None, Some(symbols)))
}
[ComponentValue::Ident(value)] => {
let lowered = value.to_ascii_lowercase();
if lowered == "none" {
return Some((ListStyleTypeMode::None, None, None));
}
if let Some(mode) = predefined_list_style_type(&lowered) {
return Some((mode, None, None));
}
is_valid_counter_name(value).then(|| {
(
ListStyleTypeMode::CustomCounterStyleName,
Some(value.clone()),
None,
)
})
}
_ => None,
}
}
fn predefined_list_style_type(raw: &str) -> Option<ListStyleTypeMode> {
Some(match raw {
"disc" => ListStyleTypeMode::Disc,
"circle" => ListStyleTypeMode::Circle,
"square" => ListStyleTypeMode::Square,
"decimal" => ListStyleTypeMode::Decimal,
"decimal-leading-zero" => ListStyleTypeMode::DecimalLeadingZero,
"arabic-indic" => ListStyleTypeMode::ArabicIndic,
"armenian" | "upper-armenian" => ListStyleTypeMode::Armenian,
"bengali" => ListStyleTypeMode::Bengali,
"cambodian" | "khmer" => ListStyleTypeMode::Cambodian,
"cjk-decimal" => ListStyleTypeMode::CjkDecimal,
"cjk-earthly-branch" => ListStyleTypeMode::CjkEarthlyBranch,
"cjk-heavenly-stem" => ListStyleTypeMode::CjkHeavenlyStem,
"devanagari" => ListStyleTypeMode::Devanagari,
"ethiopic-numeric" => ListStyleTypeMode::EthiopicNumeric,
"georgian" => ListStyleTypeMode::Georgian,
"gujarati" => ListStyleTypeMode::Gujarati,
"gurmukhi" => ListStyleTypeMode::Gurmukhi,
"hebrew" => ListStyleTypeMode::Hebrew,
"japanese-informal" => ListStyleTypeMode::JapaneseInformal,
"japanese-formal" => ListStyleTypeMode::JapaneseFormal,
"kannada" => ListStyleTypeMode::Kannada,
"korean-hangul-formal" => ListStyleTypeMode::KoreanHangulFormal,
"korean-hanja-informal" => ListStyleTypeMode::KoreanHanjaInformal,
"korean-hanja-formal" => ListStyleTypeMode::KoreanHanjaFormal,
"lao" => ListStyleTypeMode::Lao,
"lower-armenian" => ListStyleTypeMode::LowerArmenian,
"malayalam" => ListStyleTypeMode::Malayalam,
"mongolian" => ListStyleTypeMode::Mongolian,
"myanmar" => ListStyleTypeMode::Myanmar,
"oriya" => ListStyleTypeMode::Oriya,
"persian" => ListStyleTypeMode::Persian,
"simp-chinese-informal" => ListStyleTypeMode::SimpChineseInformal,
"simp-chinese-formal" => ListStyleTypeMode::SimpChineseFormal,
"tamil" => ListStyleTypeMode::Tamil,
"telugu" => ListStyleTypeMode::Telugu,
"thai" => ListStyleTypeMode::Thai,
"tibetan" => ListStyleTypeMode::Tibetan,
"trad-chinese-informal" | "cjk-ideographic" => ListStyleTypeMode::TradChineseInformal,
"trad-chinese-formal" => ListStyleTypeMode::TradChineseFormal,
"lower-roman" => ListStyleTypeMode::LowerRoman,
"upper-roman" => ListStyleTypeMode::UpperRoman,
"lower-alpha" | "lower-latin" => ListStyleTypeMode::LowerAlpha,
"upper-alpha" | "upper-latin" => ListStyleTypeMode::UpperAlpha,
"lower-greek" => ListStyleTypeMode::LowerGreek,
"hiragana" => ListStyleTypeMode::Hiragana,
"hiragana-iroha" => ListStyleTypeMode::HiraganaIroha,
"katakana" => ListStyleTypeMode::Katakana,
"katakana-iroha" => ListStyleTypeMode::KatakanaIroha,
"disclosure-open" => ListStyleTypeMode::DisclosureOpen,
"disclosure-closed" => ListStyleTypeMode::DisclosureClosed,
_ => return None,
})
}
fn parse_native_anonymous_symbols(raw: &str) -> Option<AnonymousListStyleSymbols> {
let mut tokens = crate::css_native::tokenize_component_values(raw)?;
let mut system = AnonymousListStyleSymbolsSystem::Symbolic;
if let Some(ComponentValue::Ident(value)) = tokens.first() {
let parsed = match value.to_ascii_lowercase().as_str() {
"cyclic" => Some(AnonymousListStyleSymbolsSystem::Cyclic),
"numeric" => Some(AnonymousListStyleSymbolsSystem::Numeric),
"alphabetic" => Some(AnonymousListStyleSymbolsSystem::Alphabetic),
"symbolic" => Some(AnonymousListStyleSymbolsSystem::Symbolic),
"fixed" => Some(AnonymousListStyleSymbolsSystem::Fixed),
_ => None,
};
if let Some(parsed) = parsed {
system = parsed;
tokens.remove(0);
}
}
let symbols = tokens
.into_iter()
.map(|token| match token {
ComponentValue::String(value) => Some(value),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
if symbols.is_empty()
|| matches!(
system,
AnonymousListStyleSymbolsSystem::Numeric | AnonymousListStyleSymbolsSystem::Alphabetic
) && symbols.len() < 2
{
return None;
}
Some(AnonymousListStyleSymbols {
system,
symbols,
prefix: String::new(),
suffix: " ".to_string(),
negative_prefix: "-".to_string(),
negative_suffix: String::new(),
pad_width: 0,
pad_symbol: "0".to_string(),
fixed_start: 1,
})
}
fn apply_native_list_style_image(raw: &str, delta: &mut StyleDelta) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return set_revert_layer_list_style_image(delta);
}
let Some(value) = parse_native_list_style_image(raw) else {
return false;
};
set_delta_list_style_image(delta, value);
true
}
fn parse_native_list_style_image(raw: &str) -> Option<ListStyleImageSpec> {
let trimmed = raw.trim();
match trimmed.to_ascii_lowercase().as_str() {
"none" | "initial" | "revert" => return Some(ListStyleImageSpec::Initial),
"inherit" | "unset" => return Some(ListStyleImageSpec::Inherit),
_ => {}
}
let tokens = crate::css_native::tokenize_component_values(trimmed)?;
match tokens.as_slice() {
[ComponentValue::Function { name, arguments }] if name.eq_ignore_ascii_case("url") => Some(
ListStyleImageSpec::Url(parse_native_url_argument(arguments)?),
),
[ComponentValue::Function { name, .. }]
if name.to_ascii_lowercase().ends_with("gradient") =>
{
let paint = parse_background_paint_str(trimmed);
if paint.is_some() || is_supported_gradient_image_expression(trimmed) {
Some(ListStyleImageSpec::Image {
css: trimmed.to_string(),
paint,
})
} else {
Some(ListStyleImageSpec::None)
}
}
_ => None,
}
}
fn parse_native_url_argument(raw: &str) -> Option<String> {
let trimmed = raw.trim();
let tokens = crate::css_native::tokenize_component_values(trimmed)?;
match tokens.as_slice() {
[ComponentValue::String(value)] => Some(value.clone()),
_ if !trimmed.is_empty() && !trimmed.chars().any(char::is_whitespace) => {
Some(trimmed.to_string())
}
_ => None,
}
}
fn apply_native_list_style_position(raw: &str, delta: &mut StyleDelta) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return set_revert_layer_list_style_position(delta);
}
let value = match raw.trim().to_ascii_lowercase().as_str() {
"inside" => ListStylePositionSpec::Value(ListStylePositionMode::Inside),
"outside" => ListStylePositionSpec::Value(ListStylePositionMode::Outside),
"inherit" | "unset" => ListStylePositionSpec::Inherit,
"initial" | "revert" => ListStylePositionSpec::Initial,
_ => return false,
};
set_delta_list_style_position(delta, value);
true
}
fn apply_native_list_style_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "revert-layer" {
set_revert_layer_list_style_type(delta);
set_revert_layer_list_style_image(delta);
set_revert_layer_list_style_position(delta);
return true;
}
if matches!(lowered.as_str(), "inherit" | "unset") {
delta.list_style_type = None;
set_delta_list_style_image(delta, ListStyleImageSpec::Inherit);
set_delta_list_style_position(delta, ListStylePositionSpec::Inherit);
return true;
}
if matches!(lowered.as_str(), "initial" | "revert") {
set_delta_list_style_type(delta, (ListStyleTypeMode::Disc, None, None));
set_delta_list_style_image(delta, ListStyleImageSpec::Initial);
set_delta_list_style_position(delta, ListStylePositionSpec::Initial);
return true;
}
let tokens = crate::css_native::tokenize_component_values(raw);
let Some(tokens) = tokens else {
return false;
};
let mut list_type = None;
let mut image = None;
let mut position = None;
for token in tokens {
match token {
ComponentValue::Ident(value) if value.eq_ignore_ascii_case("inside") => {
if position.replace(ListStylePositionMode::Inside).is_some() {
return false;
}
}
ComponentValue::Ident(value) if value.eq_ignore_ascii_case("outside") => {
if position.replace(ListStylePositionMode::Outside).is_some() {
return false;
}
}
ComponentValue::Ident(value) if value.eq_ignore_ascii_case("none") => {
if list_type.is_none() {
list_type = Some((ListStyleTypeMode::None, None, None));
}
if image.is_none() {
image = Some(ListStyleImageSpec::None);
}
}
ComponentValue::Ident(value) => {
if list_type.is_some() {
return false;
}
let Some(parsed) = parse_native_list_style_type(&value) else {
return false;
};
list_type = Some(parsed);
}
ComponentValue::String(value) => {
if list_type.is_some() {
return false;
}
list_type = Some((ListStyleTypeMode::CustomString, Some(value), None));
}
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("url") => {
if image.is_some() {
return false;
}
let Some(url) = parse_native_url_argument(&arguments) else {
return false;
};
image = Some(ListStyleImageSpec::Url(url));
}
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("symbols") =>
{
if list_type.is_some() {
return false;
}
let Some(symbols) = parse_native_anonymous_symbols(&arguments) else {
return false;
};
list_type = Some((ListStyleTypeMode::AnonymousSymbols, None, Some(symbols)));
}
ComponentValue::Function { name, arguments }
if name.to_ascii_lowercase().ends_with("gradient") =>
{
if image.is_some() {
return false;
}
let css = format!("{name}({arguments})");
image = Some(ListStyleImageSpec::Image {
paint: parse_background_paint_str(&css),
css,
});
}
_ => return false,
}
}
set_delta_list_style_type(
delta,
list_type.unwrap_or((ListStyleTypeMode::Disc, None, None)),
);
set_delta_list_style_image(delta, image.unwrap_or(ListStyleImageSpec::None));
set_delta_list_style_position(
delta,
ListStylePositionSpec::Value(position.unwrap_or(ListStylePositionMode::Outside)),
);
true
}
fn apply_native_transform_value(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let (slot, pending) = match property_name {
"transform" => (&mut delta.transform, &mut delta.transform_var),
"translate" => (&mut delta.translate, &mut delta.translate_var),
"rotate" => (&mut delta.rotate, &mut delta.rotate_var),
"scale" => (&mut delta.scale, &mut delta.scale_var),
_ => return false,
};
match lowered.as_str() {
"inherit" => {
*slot = Some(TransformSpec::Inherit);
*pending = None;
return true;
}
"initial" | "unset" | "revert" | "revert-layer" => {
*slot = Some(TransformSpec::Initial);
*pending = None;
return true;
}
"none" => {
*slot = Some(TransformSpec::Value(Vec::new()));
*pending = None;
return true;
}
_ => {}
}
let parsed = match property_name {
"transform" => transform_ops_from_string(raw),
"translate" => transform_ops_from_translate_string(raw),
"rotate" => transform_ops_from_rotate_string(raw),
"scale" => transform_ops_from_scale_string(raw),
_ => None,
};
if let Some(value) = parsed {
*slot = Some(TransformSpec::Value(value));
*pending = None;
return true;
}
if lowered.contains("var(") {
*slot = None;
*pending = Some(lowered);
return true;
}
false
}
fn apply_native_transform_origin(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => {
delta.transform_origin = Some(TransformOriginSpec::Inherit);
return true;
}
"initial" | "unset" | "revert" | "revert-layer" => {
delta.transform_origin = Some(TransformOriginSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = transform_origin_from_string(raw) {
delta.transform_origin = Some(TransformOriginSpec::Value(value));
true
} else {
false
}
}
fn apply_native_transform_box(raw: &str, delta: &mut StyleDelta) -> bool {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"content-box" => TransformBoxSpec::Value(TransformBoxMode::ContentBox),
// For HTML boxes, the SVG reference-box keywords compute to the CSS
// border box. Retain the compact two-mode representation downstream.
"border-box" | "fill-box" | "stroke-box" | "view-box" => {
TransformBoxSpec::Value(TransformBoxMode::BorderBox)
}
"inherit" => TransformBoxSpec::Inherit,
"initial" | "unset" | "revert" | "revert-layer" => TransformBoxSpec::Initial,
_ => return false,
};
delta.transform_box = Some(value);
true
}
fn apply_native_transform_style(raw: &str, delta: &mut StyleDelta) -> bool {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"flat" => TransformStyleSpec::Value(TransformStyleMode::Flat),
"preserve-3d" => TransformStyleSpec::Value(TransformStyleMode::Preserve3d),
"inherit" => TransformStyleSpec::Inherit,
"initial" | "unset" | "revert" | "revert-layer" => TransformStyleSpec::Initial,
_ => return false,
};
delta.transform_style = Some(value);
true
}
fn apply_native_perspective_origin(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"inherit" => TransformOriginSpec::Inherit,
"initial" | "unset" | "revert" | "revert-layer" => TransformOriginSpec::Initial,
_ => {
let origin = match transform_origin_from_string(raw) {
Some(origin) if origin.z == LengthSpec::Absolute(Pt::ZERO) => origin,
_ => return false,
};
TransformOriginSpec::Value(origin)
}
};
delta.perspective_origin = Some(value);
true
}
fn apply_native_text_decoration_line(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => {
set_delta_text_decoration_spec(delta, TextDecorationSpec::Inherit);
true
}
"initial" | "unset" | "revert" => {
set_delta_text_decoration_spec(delta, TextDecorationSpec::Initial);
true
}
"revert-layer" => set_revert_layer_text_decoration_line(delta),
_ => {
if let Some(value) = parse_text_decoration_str(&lowered) {
set_delta_text_decoration_spec(delta, TextDecorationSpec::Value(value));
true
} else {
false
}
}
}
}
fn apply_native_text_decoration_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "revert-layer" {
return set_revert_layer_text_decoration_shorthand(delta);
}
if matches!(lowered.as_str(), "inherit" | "initial" | "unset" | "revert") {
let line = if lowered == "inherit" {
TextDecorationSpec::Inherit
} else {
TextDecorationSpec::Initial
};
set_delta_text_decoration_spec(delta, line);
set_delta_text_decoration_color_spec(delta, ColorSpec::CurrentColor);
set_delta_text_decoration_thickness_spec(delta, TextDecorationThicknessSpec::Initial);
set_delta_text_decoration_style_spec(delta, TextDecorationStyleSpec::Initial);
return true;
}
let Some(line) = parse_text_decoration_str(&lowered) else {
return false;
};
set_delta_text_decoration_spec(delta, TextDecorationSpec::Value(line));
if !apply_text_decoration_color_from_shorthand(delta, &lowered) {
set_delta_text_decoration_color_spec(delta, ColorSpec::CurrentColor);
}
if !apply_text_decoration_thickness_from_shorthand(delta, &lowered) {
set_delta_text_decoration_thickness_spec(delta, TextDecorationThicknessSpec::Initial);
}
if !apply_text_decoration_style_from_shorthand(delta, &lowered) {
set_delta_text_decoration_style_spec(delta, TextDecorationStyleSpec::Initial);
}
true
}
fn parse_text_emphasis_style_value(raw: &str) -> Option<TextEmphasisStyleMode> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "none" {
return Some(TextEmphasisStyleMode::None);
}
let tokens = split_top_level_whitespace(&lowered);
let has_dot = tokens.iter().any(|token| token == "dot");
let supported = tokens
.iter()
.all(|token| matches!(token.as_str(), "filled" | "dot"));
(has_dot && supported).then_some(TextEmphasisStyleMode::FilledDot)
}
fn apply_native_text_emphasis_style(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" => {
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Inherit);
true
}
"initial" | "revert" => {
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Initial);
true
}
"revert-layer" => set_revert_layer_text_emphasis_style(delta),
_ => {
if let Some(value) = parse_text_emphasis_style_value(&lowered) {
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Value(value));
true
} else {
false
}
}
}
}
fn parse_text_emphasis_color_value(raw: &str) -> Option<ColorSpec> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "currentcolor" {
return Some(ColorSpec::CurrentColor);
}
parse_color_string(&lowered)
.map(|(color, alpha)| ColorSpec::Value(blend_over_white(color, alpha)))
}
fn apply_native_text_emphasis_color(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"inherit" | "unset" => Some(ColorSpec::Inherit),
"initial" | "revert" => Some(ColorSpec::Initial),
"revert-layer" => return set_revert_layer_text_emphasis_color(delta),
_ => parse_text_emphasis_color_value(&lowered),
};
let Some(spec) = spec else {
return false;
};
set_delta_text_emphasis_color(delta, spec);
true
}
fn apply_native_text_emphasis_position(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"inherit" | "unset" => TextEmphasisPositionSpec::Inherit,
"initial" | "revert" => TextEmphasisPositionSpec::Initial,
"revert-layer" => return set_revert_layer_text_emphasis_position(delta),
_ => {
let tokens = split_top_level_whitespace(&lowered);
if tokens.iter().any(|token| token == "under") {
TextEmphasisPositionSpec::Value(TextEmphasisPositionMode::Under)
} else if tokens.iter().any(|token| token == "over") {
TextEmphasisPositionSpec::Value(TextEmphasisPositionMode::Over)
} else {
return false;
}
}
};
set_delta_text_emphasis_position(delta, spec);
true
}
fn parse_initial_letter_value(raw: &str) -> Option<InitialLetterValue> {
let mut size = None;
let mut sink = None;
let mut drop = false;
for token in raw.split_whitespace() {
match token.to_ascii_lowercase().as_str() {
"drop" => drop = true,
"raise" => sink = Some(1),
_ => {
let value = token.parse::<f32>().ok()?;
if !value.is_finite() || value < 1.0 {
return None;
}
if size.is_none() {
size = Some(value);
} else if sink.is_none() && value.fract().abs() <= f32::EPSILON {
sink = Some(value as usize);
} else {
return None;
}
}
}
}
let size = size?;
let sink = sink.unwrap_or_else(|| {
if drop {
size.floor().max(1.0) as usize
} else {
size.floor().max(1.0) as usize
}
});
Some(InitialLetterValue {
size,
sink: sink.max(1),
})
}
fn apply_native_initial_letter(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
delta.initial_letter = match lowered.as_str() {
"normal" | "initial" | "unset" | "revert" | "revert-layer" => {
Some(InitialLetterSpec::Initial)
}
"inherit" => Some(InitialLetterSpec::Inherit),
_ => parse_initial_letter_value(&lowered).map(InitialLetterSpec::Value),
};
delta.initial_letter.is_some()
}
fn apply_native_text_emphasis_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" => {
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Inherit);
set_delta_text_emphasis_color(delta, ColorSpec::Inherit);
return true;
}
"initial" | "revert" => {
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Initial);
set_delta_text_emphasis_color(delta, ColorSpec::Initial);
return true;
}
"revert-layer" => return set_revert_layer_text_emphasis_shorthand(delta),
_ => {}
}
let tokens = split_top_level_whitespace(&lowered);
let color = tokens
.iter()
.find_map(|token| parse_text_emphasis_color_value(token))
.unwrap_or(ColorSpec::CurrentColor);
let style_value = tokens
.iter()
.filter(|token| parse_text_emphasis_color_value(token).is_none())
.map(String::as_str)
.collect::<Vec<_>>()
.join(" ");
let Some(style) = parse_text_emphasis_style_value(&style_value) else {
return false;
};
set_delta_text_emphasis_style(delta, TextEmphasisStyleSpec::Value(style));
set_delta_text_emphasis_color(delta, color);
true
}
fn apply_native_text_spacing(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return if property_name == "letter-spacing" {
set_revert_layer_letter_spacing(delta)
} else {
set_revert_layer_word_spacing(delta)
};
}
let spec = match raw.trim().to_ascii_lowercase().as_str() {
"inherit" | "unset" => Some(LengthSpec::Inherit),
"initial" | "revert" => Some(LengthSpec::Initial),
_ if property_name == "letter-spacing" => parse_letter_spacing_str(raw),
_ => parse_word_spacing_str(raw),
};
let Some(spec) = spec else {
return false;
};
if property_name == "letter-spacing" {
set_delta_letter_spacing(delta, spec);
} else {
set_delta_word_spacing(delta, spec);
}
true
}
fn apply_native_text_align(raw: &str, delta: &mut StyleDelta) -> bool {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"start" => TextAlignMode::Start,
"match-parent" | "inherit" | "unset" => TextAlignMode::MatchParent,
"end" => TextAlignMode::End,
"left" => TextAlignMode::Left,
"right" => TextAlignMode::Right,
"center" => TextAlignMode::Center,
"justify" => TextAlignMode::Justify,
"justify-all" => TextAlignMode::JustifyAll,
"initial" | "revert" => TextAlignMode::Start,
"revert-layer" => return set_revert_layer_text_align(delta),
_ => return false,
};
set_delta_text_align(delta, value);
true
}
fn apply_native_direction(raw: &str, delta: &mut StyleDelta) -> bool {
let spec = match raw.trim().to_ascii_lowercase().as_str() {
"ltr" => DirectionSpec::Value(DirectionMode::Ltr),
"rtl" => DirectionSpec::Value(DirectionMode::Rtl),
"inherit" | "unset" => DirectionSpec::Inherit,
"initial" | "revert" => DirectionSpec::Initial,
"revert-layer" => return set_revert_layer_direction(delta),
_ => return false,
};
set_delta_direction(delta, spec);
true
}
fn apply_native_unicode_bidi(raw: &str, delta: &mut StyleDelta) -> bool {
let spec = match raw.trim().to_ascii_lowercase().as_str() {
"normal" => UnicodeBidiSpec::Value(UnicodeBidiMode::Normal),
"embed" => UnicodeBidiSpec::Value(UnicodeBidiMode::Embed),
"isolate" => UnicodeBidiSpec::Value(UnicodeBidiMode::Isolate),
"bidi-override" => UnicodeBidiSpec::Value(UnicodeBidiMode::BidiOverride),
"isolate-override" => UnicodeBidiSpec::Value(UnicodeBidiMode::IsolateOverride),
"plaintext" => UnicodeBidiSpec::Value(UnicodeBidiMode::Plaintext),
"inherit" => UnicodeBidiSpec::Inherit,
"unset" | "initial" | "revert" => UnicodeBidiSpec::Initial,
"revert-layer" => return set_revert_layer_unicode_bidi(delta),
_ => return false,
};
set_delta_unicode_bidi(delta, spec);
true
}
fn last_css_keyword(raw: &str) -> Option<&str> {
raw.split_ascii_whitespace()
.filter(|value| !matches!(value.to_ascii_lowercase().as_str(), "safe" | "unsafe"))
.next_back()
}
#[derive(Clone, Copy)]
enum GapProperty {
Both,
Row,
Column,
}
enum NativeGapComponent {
Spec(LengthSpec),
Var(String),
}
fn parse_native_gap_component(raw: &str) -> Option<NativeGapComponent> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"normal" | "initial" | "unset" | "revert" => {
return Some(NativeGapComponent::Spec(LengthSpec::Absolute(Pt::ZERO)));
}
"inherit" => return Some(NativeGapComponent::Spec(LengthSpec::Inherit)),
_ => {}
}
if lowered.contains("var(") {
return Some(NativeGapComponent::Var(lowered));
}
length_spec_from_string(&lowered).map(NativeGapComponent::Spec)
}
fn set_row_gap_component(delta: &mut StyleDelta, component: NativeGapComponent) {
match component {
NativeGapComponent::Spec(value) => {
delta.row_gap = Some(value);
delta.row_gap_var = None;
}
NativeGapComponent::Var(value) => {
delta.row_gap = None;
delta.row_gap_var = Some(value);
}
}
}
fn set_column_gap_component(delta: &mut StyleDelta, component: NativeGapComponent) {
match component {
NativeGapComponent::Spec(value) => {
delta.gap = Some(value);
delta.gap_var = None;
}
NativeGapComponent::Var(value) => {
delta.gap = None;
delta.gap_var = Some(value);
}
}
}
fn gap_component_is_normal(raw: &str) -> bool {
matches!(
raw.trim().to_ascii_lowercase().as_str(),
"normal" | "initial" | "unset" | "revert"
)
}
fn apply_native_gap_value(raw: &str, delta: &mut StyleDelta, property: GapProperty) -> bool {
let parts = split_top_level_whitespace(raw);
if parts.is_empty() {
return false;
}
match property {
GapProperty::Row => {
if parts.len() != 1 {
return false;
}
let Some(component) = parse_native_gap_component(&parts[0]) else {
return false;
};
set_row_gap_component(delta, component);
}
GapProperty::Column => {
if parts.len() != 1 {
return false;
}
let Some(component) = parse_native_gap_component(&parts[0]) else {
return false;
};
set_column_gap_component(delta, component);
delta.column_gap_normal = Some(gap_component_is_normal(&parts[0]));
}
GapProperty::Both => {
if parts.len() > 2 {
return false;
}
let Some(row) = parse_native_gap_component(&parts[0]) else {
return false;
};
let column_raw = parts.get(1).unwrap_or(&parts[0]);
let Some(column) = parse_native_gap_component(column_raw) else {
return false;
};
set_row_gap_component(delta, row);
set_column_gap_component(delta, column);
delta.column_gap_normal = Some(gap_component_is_normal(column_raw));
}
}
true
}
fn apply_native_nonnegative_number(raw: &str, slot: &mut Option<f32>) -> bool {
let Ok(value) = raw.trim().parse::<f32>() else {
return false;
};
if !value.is_finite() || value < 0.0 {
return false;
}
*slot = Some(value);
true
}
fn apply_native_flex_basis(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
delta.flex_basis_var = None;
let value = match lowered.as_str() {
"auto" => Some(LengthSpec::Auto),
"content" => Some(LengthSpec::Content),
"min-content" => Some(LengthSpec::MinContent),
"max-content" => Some(LengthSpec::MaxContent),
"fit-content" => Some(LengthSpec::FitContent),
"inherit" => Some(LengthSpec::Inherit),
"initial" | "unset" | "revert" => Some(LengthSpec::Initial),
_ => length_spec_from_string(&lowered),
};
if let Some(value) = value {
delta.flex_basis = Some(value);
return true;
}
if lowered.contains("var(") {
delta.flex_basis = None;
delta.flex_basis_var = Some(lowered);
return true;
}
false
}
fn parse_native_flex_number(raw: &str) -> Option<f32> {
let value = raw.trim().parse::<f32>().ok()?;
(value.is_finite() && value >= 0.0).then_some(value)
}
fn parse_native_flex_basis_component(raw: &str) -> Option<NativeLengthComponent> {
let lowered = raw.trim().to_ascii_lowercase();
let intrinsic = match lowered.as_str() {
"auto" => Some(LengthSpec::Auto),
"content" => Some(LengthSpec::Content),
"min-content" => Some(LengthSpec::MinContent),
"max-content" => Some(LengthSpec::MaxContent),
"fit-content" => Some(LengthSpec::FitContent),
_ => None,
};
if let Some(value) = intrinsic {
return Some(NativeLengthComponent::Spec(value));
}
if lowered.contains("var(") {
return Some(NativeLengthComponent::Var(lowered));
}
// A unitless zero is valid as a <flex-basis>, but any other bare number is not.
if let Ok(number) = lowered.parse::<f32>() {
if !number.is_finite() || number != 0.0 {
return None;
}
}
let value = length_spec_from_string(&lowered)?;
native_length_is_nonnegative(value).then_some(NativeLengthComponent::Spec(value))
}
fn set_native_flex_components(
delta: &mut StyleDelta,
grow: f32,
shrink: f32,
basis: NativeLengthComponent,
) {
delta.flex_grow = Some(grow);
delta.flex_shrink = Some(shrink);
match basis {
NativeLengthComponent::Spec(value) => {
delta.flex_basis = Some(value);
delta.flex_basis_var = None;
}
NativeLengthComponent::Var(value) => {
delta.flex_basis = None;
delta.flex_basis_var = Some(value);
}
}
}
fn apply_native_flex_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"none" => {
delta.flex_grow = Some(0.0);
delta.flex_shrink = Some(0.0);
delta.flex_basis = Some(LengthSpec::Auto);
delta.flex_basis_var = None;
return true;
}
"auto" => {
delta.flex_grow = Some(1.0);
delta.flex_shrink = Some(1.0);
delta.flex_basis = Some(LengthSpec::Auto);
delta.flex_basis_var = None;
return true;
}
"initial" | "unset" | "revert" => {
delta.flex_grow = Some(0.0);
delta.flex_shrink = Some(1.0);
delta.flex_basis = Some(LengthSpec::Auto);
delta.flex_basis_var = None;
return true;
}
_ => {}
}
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > 3 {
return false;
}
if parts.len() == 1 {
if let Some(grow) = parse_native_flex_number(&parts[0]) {
set_native_flex_components(
delta,
grow,
1.0,
NativeLengthComponent::Spec(LengthSpec::Percent(0.0)),
);
return true;
}
if let Some(basis) = parse_native_flex_basis_component(&parts[0]) {
set_native_flex_components(delta, 1.0, 1.0, basis);
return true;
}
return false;
}
if parts.len() == 2 {
let first_number = parse_native_flex_number(&parts[0]);
let second_number = parse_native_flex_number(&parts[1]);
if let (Some(grow), Some(shrink)) = (first_number, second_number) {
set_native_flex_components(
delta,
grow,
shrink,
NativeLengthComponent::Spec(LengthSpec::Percent(0.0)),
);
return true;
}
if let Some(grow) = first_number {
if let Some(basis) = parse_native_flex_basis_component(&parts[1]) {
set_native_flex_components(delta, grow, 1.0, basis);
return true;
}
}
if let Some(grow) = second_number {
if let Some(basis) = parse_native_flex_basis_component(&parts[0]) {
set_native_flex_components(delta, grow, 1.0, basis);
return true;
}
}
return false;
}
// The grammar permits the basis before, between, or after the two numeric factors. Try the
// conventional trailing-basis form first so `flex: 1 1 0` treats the final zero as a basis.
for basis_index in [2usize, 0, 1] {
let Some(basis) = parse_native_flex_basis_component(&parts[basis_index]) else {
continue;
};
let numbers: Vec<f32> = parts
.iter()
.enumerate()
.filter(|(index, _)| *index != basis_index)
.filter_map(|(_, part)| parse_native_flex_number(part))
.collect();
if numbers.len() == 2 {
set_native_flex_components(delta, numbers[0], numbers[1], basis);
return true;
}
}
false
}
fn apply_native_pagination_count(property_name: &str, raw: &str, delta: &mut StyleDelta) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => Some(PaginationLineCountSpec::Inherit),
"initial" => Some(PaginationLineCountSpec::Initial),
_ => lowered
.parse::<usize>()
.ok()
.filter(|value| *value >= 1)
.map(PaginationLineCountSpec::Value),
};
let Some(value) = value else {
return false;
};
if property_name == "orphans" {
delta.pagination.orphans = Some(value);
} else {
delta.pagination.widows = Some(value);
}
true
}
fn apply_native_color_value(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" | "currentcolor" => {
set_delta_color(delta, ColorSpec::Inherit);
return true;
}
"initial" => {
set_delta_color(delta, ColorSpec::Initial);
return true;
}
"revert-layer" => {
delta.color = None;
delta.color_var = None;
delta.revert_layer.color = true;
return true;
}
_ => {}
}
if should_defer_color_expr(&lowered) {
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_color(delta, ColorSpec::Value(blend_over_white(color, alpha)));
} else {
set_delta_color_var(delta, lowered);
}
return true;
}
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_color(delta, ColorSpec::Value(blend_over_white(color, alpha)));
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_color_var(delta, var);
return true;
}
false
}
fn apply_native_background_color_value(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => {
set_delta_background_color(delta, BackgroundSpec::Inherit);
return true;
}
"initial" | "unset" | "revert" => {
set_delta_background_color(delta, BackgroundSpec::Initial);
return true;
}
"currentcolor" => {
set_delta_background_color(delta, BackgroundSpec::CurrentColor);
return true;
}
"revert-layer" => {
delta.background_color = None;
delta.background_color_var = None;
delta.revert_layer.background_color = true;
return true;
}
_ => {}
}
if should_defer_color_expr(&lowered) {
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_background_color(delta, background_spec_from_color_alpha(color, alpha));
} else {
set_delta_background_color_var(delta, lowered);
}
return true;
}
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_background_color(delta, background_spec_from_color_alpha(color, alpha));
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_background_color_var(delta, var);
return true;
}
false
}
fn set_revert_layer_outline(delta: &mut StyleDelta, width: bool, style: bool, color: bool) {
if width {
delta.outline_width = None;
delta.revert_layer.outline_width = true;
}
if style {
delta.outline_style = None;
delta.outline_visible = None;
delta.revert_layer.outline_style = true;
}
if color {
delta.outline_color = None;
delta.revert_layer.outline_color = true;
}
}
fn set_revert_layer_outline_offset(delta: &mut StyleDelta) {
delta.outline_offset = None;
delta.outline_offset_var = None;
delta.revert_layer.outline_offset = true;
}
fn set_revert_layer_overflow_axes(delta: &mut StyleDelta) -> bool {
delta.overflow_x = None;
delta.overflow_y = None;
delta.revert_layer.overflow_x = true;
delta.revert_layer.overflow_y = true;
true
}
fn set_revert_layer_overflow_x(delta: &mut StyleDelta) -> bool {
delta.overflow_x = None;
delta.revert_layer.overflow_x = true;
true
}
fn set_revert_layer_overflow_y(delta: &mut StyleDelta) -> bool {
delta.overflow_y = None;
delta.revert_layer.overflow_y = true;
true
}
fn set_revert_layer_overflow_inline(delta: &mut StyleDelta) -> bool {
delta.overflow_inline = None;
delta.revert_layer.overflow_inline = true;
true
}
fn set_revert_layer_overflow_block(delta: &mut StyleDelta) -> bool {
delta.overflow_block = None;
delta.revert_layer.overflow_block = true;
true
}
fn set_revert_layer_paint_filter(delta: &mut StyleDelta) -> bool {
delta.paint_filter = None;
delta.paint_filter_raw = None;
delta.revert_layer.paint_filter = true;
true
}
fn set_revert_layer_backdrop_filter(delta: &mut StyleDelta) -> bool {
delta.backdrop_filter = None;
delta.backdrop_filter_raw = None;
delta.revert_layer.backdrop_filter = true;
true
}
fn set_revert_layer_will_change_backdrop_root(delta: &mut StyleDelta) -> bool {
delta.will_change_backdrop_root = None;
delta.revert_layer.will_change_backdrop_root = true;
true
}
fn set_revert_layer_mask_backdrop_root(delta: &mut StyleDelta) -> bool {
delta.mask_backdrop_root = None;
delta.revert_layer.mask_backdrop_root = true;
true
}
fn set_revert_layer_clip_path(delta: &mut StyleDelta) -> bool {
delta.clip_path = None;
delta.revert_layer.clip_path = true;
true
}
fn set_revert_layer_legacy_clip(delta: &mut StyleDelta) -> bool {
delta.legacy_clip = None;
delta.revert_layer.legacy_clip = true;
true
}
fn set_revert_layer_box_shadow(delta: &mut StyleDelta) -> bool {
delta.box_shadow = None;
delta.box_shadows = None;
delta.revert_layer.box_shadow = true;
true
}
fn set_revert_layer_text_shadow(delta: &mut StyleDelta) -> bool {
delta.text_shadow = None;
delta.text_shadow_var = None;
delta.revert_layer.text_shadow = true;
true
}
fn set_revert_layer_text_decoration_line(delta: &mut StyleDelta) -> bool {
delta.text_decoration = None;
delta.revert_layer.text_decoration_line = true;
true
}
fn set_revert_layer_text_decoration_color(delta: &mut StyleDelta) -> bool {
delta.text_decoration_color = None;
delta.text_decoration_color_var = None;
delta.revert_layer.text_decoration_color = true;
true
}
fn set_revert_layer_text_decoration_thickness(delta: &mut StyleDelta) -> bool {
delta.text_decoration_thickness = None;
delta.text_decoration_thickness_var = None;
delta.revert_layer.text_decoration_thickness = true;
true
}
fn set_revert_layer_text_decoration_style(delta: &mut StyleDelta) -> bool {
delta.text_decoration_style = None;
delta.revert_layer.text_decoration_style = true;
true
}
fn set_revert_layer_text_decoration_skip_ink(delta: &mut StyleDelta) -> bool {
delta.text_decoration_skip_ink = None;
delta.revert_layer.text_decoration_skip_ink = true;
true
}
fn set_revert_layer_text_emphasis_style(delta: &mut StyleDelta) -> bool {
delta.text_emphasis_style = None;
delta.revert_layer.text_emphasis_style = true;
true
}
fn set_revert_layer_text_emphasis_color(delta: &mut StyleDelta) -> bool {
delta.text_emphasis_color = None;
delta.revert_layer.text_emphasis_color = true;
true
}
fn set_revert_layer_text_emphasis_position(delta: &mut StyleDelta) -> bool {
delta.text_emphasis_position = None;
delta.revert_layer.text_emphasis_position = true;
true
}
fn set_revert_layer_text_emphasis_shorthand(delta: &mut StyleDelta) -> bool {
set_revert_layer_text_emphasis_style(delta);
set_revert_layer_text_emphasis_color(delta);
true
}
fn set_revert_layer_text_underline_offset(delta: &mut StyleDelta) -> bool {
delta.text_underline_offset = None;
delta.text_underline_offset_var = None;
delta.revert_layer.text_underline_offset = true;
true
}
fn set_revert_layer_text_underline_position(delta: &mut StyleDelta) -> bool {
delta.text_underline_position = None;
delta.text_underline_position_var = None;
delta.revert_layer.text_underline_position = true;
true
}
fn set_revert_layer_text_decoration_shorthand(delta: &mut StyleDelta) -> bool {
set_revert_layer_text_decoration_line(delta);
set_revert_layer_text_decoration_color(delta);
set_revert_layer_text_decoration_thickness(delta);
set_revert_layer_text_decoration_style(delta);
true
}
fn set_revert_layer_text_overflow(delta: &mut StyleDelta) -> bool {
delta.text_overflow = None;
delta.revert_layer.text_overflow = true;
true
}
fn set_revert_layer_text_indent(delta: &mut StyleDelta) -> bool {
delta.text_indent = None;
delta.revert_layer.text_indent = true;
true
}
fn set_revert_layer_letter_spacing(delta: &mut StyleDelta) -> bool {
delta.letter_spacing = None;
delta.revert_layer.letter_spacing = true;
true
}
fn set_revert_layer_word_spacing(delta: &mut StyleDelta) -> bool {
delta.word_spacing = None;
delta.revert_layer.word_spacing = true;
true
}
fn set_revert_layer_tab_size(delta: &mut StyleDelta) -> bool {
delta.tab_size = None;
delta.revert_layer.tab_size = true;
true
}
fn set_revert_layer_text_align(delta: &mut StyleDelta) -> bool {
delta.text_align = None;
delta.revert_layer.text_align = true;
true
}
fn set_revert_layer_text_align_last(delta: &mut StyleDelta) -> bool {
delta.text_align_last = None;
delta.revert_layer.text_align_last = true;
true
}
fn set_revert_layer_text_justify(delta: &mut StyleDelta) -> bool {
delta.text_justify = None;
delta.text_justify_var = None;
delta.revert_layer.text_justify = true;
true
}
fn set_revert_layer_text_transform(delta: &mut StyleDelta) -> bool {
delta.text_transform = None;
delta.revert_layer.text_transform = true;
true
}
fn set_revert_layer_direction(delta: &mut StyleDelta) -> bool {
delta.direction = None;
delta.revert_layer.direction = true;
true
}
fn set_revert_layer_unicode_bidi(delta: &mut StyleDelta) -> bool {
delta.unicode_bidi = None;
delta.revert_layer.unicode_bidi = true;
true
}
fn set_revert_layer_writing_mode(delta: &mut StyleDelta) -> bool {
delta.writing_mode = None;
delta.revert_layer.writing_mode = true;
true
}
fn set_revert_layer_text_orientation(delta: &mut StyleDelta) -> bool {
delta.text_orientation = None;
delta.revert_layer.text_orientation = true;
true
}
fn set_revert_layer_white_space(delta: &mut StyleDelta) -> bool {
delta.white_space = None;
delta.white_space_var = None;
delta.revert_layer.white_space = true;
true
}
fn set_revert_layer_text_wrap_mode(delta: &mut StyleDelta) -> bool {
delta.text_wrap_mode = None;
delta.text_wrap_mode_var = None;
delta.revert_layer.text_wrap_mode = true;
true
}
fn set_revert_layer_text_wrap_style(delta: &mut StyleDelta) -> bool {
delta.text_wrap_style = None;
delta.text_wrap_style_var = None;
delta.revert_layer.text_wrap_style = true;
true
}
fn set_revert_layer_white_space_shorthand(delta: &mut StyleDelta) -> bool {
set_revert_layer_white_space(delta);
set_revert_layer_text_wrap_mode(delta);
true
}
fn set_revert_layer_text_wrap_shorthand(delta: &mut StyleDelta) -> bool {
set_revert_layer_text_wrap_mode(delta);
set_revert_layer_text_wrap_style(delta);
true
}
fn set_revert_layer_vertical_align(delta: &mut StyleDelta) -> bool {
delta.vertical_align = None;
delta.vertical_align_var = None;
delta.revert_layer.vertical_align = true;
true
}
fn set_revert_layer_list_style_type(delta: &mut StyleDelta) -> bool {
delta.list_style_type = None;
delta.revert_layer.list_style_type = true;
true
}
fn set_revert_layer_list_style_image(delta: &mut StyleDelta) -> bool {
delta.list_style_image = None;
delta.revert_layer.list_style_image = true;
true
}
fn set_revert_layer_list_style_position(delta: &mut StyleDelta) -> bool {
delta.list_style_position = None;
delta.revert_layer.list_style_position = true;
true
}
fn set_revert_layer_word_break(delta: &mut StyleDelta) -> bool {
delta.word_break = None;
delta.revert_layer.word_break = true;
true
}
fn set_revert_layer_overflow_wrap(delta: &mut StyleDelta) -> bool {
delta.overflow_wrap = None;
delta.revert_layer.overflow_wrap = true;
true
}
fn set_revert_layer_line_break(delta: &mut StyleDelta) -> bool {
delta.line_break = None;
delta.line_break_var = None;
delta.revert_layer.line_break = true;
true
}
fn set_revert_layer_hyphens(delta: &mut StyleDelta) -> bool {
delta.hyphens = None;
delta.hyphens_var = None;
delta.revert_layer.hyphens = true;
true
}
fn set_revert_layer_hyphenate_character(delta: &mut StyleDelta) -> bool {
delta.hyphenate_character = None;
delta.hyphenate_character_var = None;
delta.revert_layer.hyphenate_character = true;
true
}
fn set_revert_layer_mix_blend_mode(delta: &mut StyleDelta) -> bool {
delta.mix_blend_mode = None;
delta.revert_layer.mix_blend_mode = true;
true
}
fn set_revert_layer_isolation(delta: &mut StyleDelta) -> bool {
delta.isolation = None;
delta.revert_layer.isolation = true;
true
}
fn clear_delta_logical_box_edge(
delta: &mut StyleDelta,
is_margin: bool,
target: LogicalBoxEdgeTarget,
) {
match (is_margin, target) {
(true, LogicalBoxEdgeTarget::Inline) => {
delta.margin_inline_start = None;
delta.margin_inline_end = None;
delta.margin_inline_start_var = None;
delta.margin_inline_end_var = None;
}
(true, LogicalBoxEdgeTarget::InlineStart) => {
delta.margin_inline_start = None;
delta.margin_inline_start_var = None;
}
(true, LogicalBoxEdgeTarget::InlineEnd) => {
delta.margin_inline_end = None;
delta.margin_inline_end_var = None;
}
(true, LogicalBoxEdgeTarget::Block) => {
delta.margin_block_start = None;
delta.margin_block_end = None;
delta.margin_block_start_var = None;
delta.margin_block_end_var = None;
}
(true, LogicalBoxEdgeTarget::BlockStart) => {
delta.margin_block_start = None;
delta.margin_block_start_var = None;
}
(true, LogicalBoxEdgeTarget::BlockEnd) => {
delta.margin_block_end = None;
delta.margin_block_end_var = None;
}
(false, LogicalBoxEdgeTarget::Inline) => {
delta.padding_inline_start = None;
delta.padding_inline_end = None;
delta.padding_inline_start_var = None;
delta.padding_inline_end_var = None;
}
(false, LogicalBoxEdgeTarget::InlineStart) => {
delta.padding_inline_start = None;
delta.padding_inline_start_var = None;
}
(false, LogicalBoxEdgeTarget::InlineEnd) => {
delta.padding_inline_end = None;
delta.padding_inline_end_var = None;
}
(false, LogicalBoxEdgeTarget::Block) => {
delta.padding_block_start = None;
delta.padding_block_end = None;
delta.padding_block_start_var = None;
delta.padding_block_end_var = None;
}
(false, LogicalBoxEdgeTarget::BlockStart) => {
delta.padding_block_start = None;
delta.padding_block_start_var = None;
}
(false, LogicalBoxEdgeTarget::BlockEnd) => {
delta.padding_block_end = None;
delta.padding_block_end_var = None;
}
}
}
fn clear_delta_logical_inset_edge(delta: &mut StyleDelta, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
delta.inset_inline_start = None;
delta.inset_inline_end = None;
delta.inset_inline_start_var = None;
delta.inset_inline_end_var = None;
delta.inset_left = None;
delta.inset_right = None;
delta.inset_left_var = None;
delta.inset_right_var = None;
}
LogicalBoxEdgeTarget::InlineStart => {
delta.inset_inline_start = None;
delta.inset_inline_start_var = None;
delta.inset_left = None;
delta.inset_left_var = None;
}
LogicalBoxEdgeTarget::InlineEnd => {
delta.inset_inline_end = None;
delta.inset_inline_end_var = None;
delta.inset_right = None;
delta.inset_right_var = None;
}
LogicalBoxEdgeTarget::Block => {
delta.inset_block_start = None;
delta.inset_block_end = None;
delta.inset_block_start_var = None;
delta.inset_block_end_var = None;
delta.inset_top = None;
delta.inset_bottom = None;
delta.inset_top_var = None;
delta.inset_bottom_var = None;
}
LogicalBoxEdgeTarget::BlockStart => {
delta.inset_block_start = None;
delta.inset_block_start_var = None;
delta.inset_top = None;
delta.inset_top_var = None;
}
LogicalBoxEdgeTarget::BlockEnd => {
delta.inset_block_end = None;
delta.inset_block_end_var = None;
delta.inset_bottom = None;
delta.inset_bottom_var = None;
}
}
}
fn physical_box_edge_target_from_border_target(target: BorderColorTarget) -> Option<BoxEdgeTarget> {
match target {
BorderColorTarget::All => Some(BoxEdgeTarget::All),
BorderColorTarget::Top => Some(BoxEdgeTarget::Top),
BorderColorTarget::Right => Some(BoxEdgeTarget::Right),
BorderColorTarget::Bottom => Some(BoxEdgeTarget::Bottom),
BorderColorTarget::Left => Some(BoxEdgeTarget::Left),
BorderColorTarget::InlineStart
| BorderColorTarget::InlineEnd
| BorderColorTarget::BlockStart
| BorderColorTarget::BlockEnd => None,
}
}
fn logical_box_edge_target_from_border_target(
target: BorderColorTarget,
) -> Option<LogicalBoxEdgeTarget> {
match target {
BorderColorTarget::InlineStart => Some(LogicalBoxEdgeTarget::InlineStart),
BorderColorTarget::InlineEnd => Some(LogicalBoxEdgeTarget::InlineEnd),
BorderColorTarget::BlockStart => Some(LogicalBoxEdgeTarget::BlockStart),
BorderColorTarget::BlockEnd => Some(LogicalBoxEdgeTarget::BlockEnd),
BorderColorTarget::All
| BorderColorTarget::Top
| BorderColorTarget::Right
| BorderColorTarget::Bottom
| BorderColorTarget::Left => None,
}
}
fn set_revert_layer_border_color(delta: &mut StyleDelta, target: BorderColorTarget) -> bool {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
clear_delta_border_color(delta, target);
delta.revert_layer.border_color.set(edge);
return true;
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_color(delta, edge);
delta.revert_layer.border_color_logical.set(edge);
return true;
}
false
}
fn clear_revert_layer_border_color(delta: &mut StyleDelta, target: BorderColorTarget) {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_color.clear(edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_color_logical.clear(edge);
}
}
fn clear_delta_border_color(delta: &mut StyleDelta, target: BorderColorTarget) {
split_uniform_border_color_delta(delta);
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
clear_delta_physical_border_color(delta, edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_color(delta, edge);
}
}
fn split_uniform_border_color_delta(delta: &mut StyleDelta) {
if let Some(spec) = delta.border_color.take() {
if delta.border_top_color.is_none() {
delta.border_top_color = Some(spec.clone());
}
if delta.border_right_color.is_none() {
delta.border_right_color = Some(spec.clone());
}
if delta.border_bottom_color.is_none() {
delta.border_bottom_color = Some(spec.clone());
}
if delta.border_left_color.is_none() {
delta.border_left_color = Some(spec);
}
}
if let Some(var) = delta.border_color_var.take() {
if delta.border_top_color_var.is_none() {
delta.border_top_color_var = Some(var.clone());
}
if delta.border_right_color_var.is_none() {
delta.border_right_color_var = Some(var.clone());
}
if delta.border_bottom_color_var.is_none() {
delta.border_bottom_color_var = Some(var.clone());
}
if delta.border_left_color_var.is_none() {
delta.border_left_color_var = Some(var);
}
}
}
fn clear_delta_physical_border_color(delta: &mut StyleDelta, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
delta.border_top_color = None;
delta.border_right_color = None;
delta.border_bottom_color = None;
delta.border_left_color = None;
delta.border_top_color_var = None;
delta.border_right_color_var = None;
delta.border_bottom_color_var = None;
delta.border_left_color_var = None;
}
BoxEdgeTarget::Top => {
delta.border_top_color = None;
delta.border_top_color_var = None;
}
BoxEdgeTarget::Right => {
delta.border_right_color = None;
delta.border_right_color_var = None;
}
BoxEdgeTarget::Bottom => {
delta.border_bottom_color = None;
delta.border_bottom_color_var = None;
}
BoxEdgeTarget::Left => {
delta.border_left_color = None;
delta.border_left_color_var = None;
}
}
}
fn clear_delta_logical_border_color(delta: &mut StyleDelta, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
delta.border_inline_start_color = None;
delta.border_inline_end_color = None;
delta.border_inline_start_color_var = None;
delta.border_inline_end_color_var = None;
}
LogicalBoxEdgeTarget::InlineStart => {
delta.border_inline_start_color = None;
delta.border_inline_start_color_var = None;
}
LogicalBoxEdgeTarget::InlineEnd => {
delta.border_inline_end_color = None;
delta.border_inline_end_color_var = None;
}
LogicalBoxEdgeTarget::Block => {
delta.border_block_start_color = None;
delta.border_block_end_color = None;
delta.border_block_start_color_var = None;
delta.border_block_end_color_var = None;
}
LogicalBoxEdgeTarget::BlockStart => {
delta.border_block_start_color = None;
delta.border_block_start_color_var = None;
}
LogicalBoxEdgeTarget::BlockEnd => {
delta.border_block_end_color = None;
delta.border_block_end_color_var = None;
}
}
}
fn set_revert_layer_border_width(delta: &mut StyleDelta, target: BorderColorTarget) -> bool {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.border_width.clear(edge);
delta.revert_layer.border_width.set(edge);
return true;
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_width(delta, edge);
delta.revert_layer.border_width_logical.set(edge);
return true;
}
false
}
fn clear_revert_layer_border_width(delta: &mut StyleDelta, target: BorderColorTarget) {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_width.clear(edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_width_logical.clear(edge);
}
}
fn clear_delta_border_width(delta: &mut StyleDelta, target: BorderColorTarget) {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.border_width.clear(edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_width(delta, edge);
}
}
fn clear_delta_logical_border_width(delta: &mut StyleDelta, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
delta.border_inline_start_width = None;
delta.border_inline_end_width = None;
delta.border_inline_start_width_var = None;
delta.border_inline_end_width_var = None;
}
LogicalBoxEdgeTarget::InlineStart => {
delta.border_inline_start_width = None;
delta.border_inline_start_width_var = None;
}
LogicalBoxEdgeTarget::InlineEnd => {
delta.border_inline_end_width = None;
delta.border_inline_end_width_var = None;
}
LogicalBoxEdgeTarget::Block => {
delta.border_block_start_width = None;
delta.border_block_end_width = None;
delta.border_block_start_width_var = None;
delta.border_block_end_width_var = None;
}
LogicalBoxEdgeTarget::BlockStart => {
delta.border_block_start_width = None;
delta.border_block_start_width_var = None;
}
LogicalBoxEdgeTarget::BlockEnd => {
delta.border_block_end_width = None;
delta.border_block_end_width_var = None;
}
}
}
fn set_revert_layer_border_style(delta: &mut StyleDelta, target: BorderColorTarget) -> bool {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.border_style.clear(edge);
delta.revert_layer.border_style.set(edge);
return true;
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_style(delta, edge);
delta.revert_layer.border_style_logical.set(edge);
return true;
}
false
}
fn clear_revert_layer_border_style(delta: &mut StyleDelta, target: BorderColorTarget) {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_style.clear(edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
delta.revert_layer.border_style_logical.clear(edge);
}
}
fn clear_delta_border_style(delta: &mut StyleDelta, target: BorderColorTarget) {
if let Some(edge) = physical_box_edge_target_from_border_target(target) {
delta.border_style.clear(edge);
}
if let Some(edge) = logical_box_edge_target_from_border_target(target) {
clear_delta_logical_border_style(delta, edge);
}
}
fn clear_delta_logical_border_style(delta: &mut StyleDelta, target: LogicalBoxEdgeTarget) {
match target {
LogicalBoxEdgeTarget::Inline => {
delta.border_inline_start_style = None;
delta.border_inline_end_style = None;
}
LogicalBoxEdgeTarget::InlineStart => delta.border_inline_start_style = None,
LogicalBoxEdgeTarget::InlineEnd => delta.border_inline_end_style = None,
LogicalBoxEdgeTarget::Block => {
delta.border_block_start_style = None;
delta.border_block_end_style = None;
}
LogicalBoxEdgeTarget::BlockStart => delta.border_block_start_style = None,
LogicalBoxEdgeTarget::BlockEnd => delta.border_block_end_style = None,
}
}
fn set_delta_color(delta: &mut StyleDelta, value: ColorSpec) {
delta.color = Some(value);
delta.color_var = None;
delta.revert_layer.color = false;
}
fn set_delta_color_var(delta: &mut StyleDelta, value: String) {
delta.color = None;
delta.color_var = Some(value);
delta.revert_layer.color = false;
}
fn set_delta_background_color(delta: &mut StyleDelta, value: BackgroundSpec) {
delta.background_color = Some(value);
delta.background_color_var = None;
delta.revert_layer.background_color = false;
}
fn set_delta_background_color_var(delta: &mut StyleDelta, value: String) {
delta.background_color = None;
delta.background_color_var = Some(value);
delta.revert_layer.background_color = false;
}
fn set_delta_width(delta: &mut StyleDelta, value: LengthSpec) {
delta.width = Some(value);
delta.width_var = None;
delta.revert_layer.width = false;
}
fn set_delta_width_var(delta: &mut StyleDelta, value: String) {
delta.width = None;
delta.width_var = Some(value);
delta.revert_layer.width = false;
}
fn set_delta_height(delta: &mut StyleDelta, value: LengthSpec) {
delta.height = Some(value);
delta.height_var = None;
delta.revert_layer.height = false;
}
fn set_delta_height_var(delta: &mut StyleDelta, value: String) {
delta.height = None;
delta.height_var = Some(value);
delta.revert_layer.height = false;
}
fn set_delta_box_sizing(delta: &mut StyleDelta, value: BoxSizingMode) {
delta.box_sizing = Some(value);
delta.revert_layer.box_sizing = false;
}
fn set_delta_display(delta: &mut StyleDelta, value: DisplaySpec) {
delta.display = Some(value);
delta.revert_layer.display = false;
}
fn set_delta_visibility(delta: &mut StyleDelta, value: VisibilitySpec) {
delta.visibility = Some(value);
delta.revert_layer.visibility = false;
}
fn set_revert_layer_visibility(delta: &mut StyleDelta) -> bool {
delta.visibility = None;
delta.revert_layer.visibility = true;
true
}
fn set_delta_position(delta: &mut StyleDelta, value: PositionMode) {
delta.position = Some(value);
delta.running_name = Some(None);
delta.revert_layer.position = false;
}
fn set_delta_running_position(delta: &mut StyleDelta, name: Arc<str>) {
delta.position = Some(PositionMode::Static);
delta.running_name = Some(Some(name));
delta.revert_layer.position = false;
}
fn set_delta_float_mode(delta: &mut StyleDelta, value: FloatMode) {
delta.float_mode = Some(value);
delta.revert_layer.float_mode = false;
}
fn set_delta_footnote_policy(delta: &mut StyleDelta, value: FootnotePolicySpec) {
delta.footnote_policy = Some(value);
delta.revert_layer.footnote_policy = false;
}
fn set_delta_footnote_display(delta: &mut StyleDelta, value: FootnoteDisplaySpec) {
delta.footnote_display = Some(value);
delta.revert_layer.footnote_display = false;
}
fn set_delta_clear_mode(delta: &mut StyleDelta, value: ClearMode) {
delta.clear_mode = Some(value);
delta.revert_layer.clear_mode = false;
}
fn set_delta_z_index(delta: &mut StyleDelta, value: ZIndexSpec) {
delta.z_index = Some(value);
delta.revert_layer.z_index = false;
}
fn set_delta_perspective(delta: &mut StyleDelta, value: PerspectiveSpec) {
delta.perspective = Some(value);
delta.revert_layer.perspective = false;
}
fn set_delta_opacity(delta: &mut StyleDelta, value: OpacitySpec) {
delta.opacity = Some(value);
delta.opacity_var = None;
delta.revert_layer.opacity = false;
}
fn set_delta_mix_blend_mode(delta: &mut StyleDelta, value: MixBlendMode) {
delta.mix_blend_mode = Some(value);
delta.revert_layer.mix_blend_mode = false;
}
fn set_delta_isolation(delta: &mut StyleDelta, value: IsolationSpec) {
delta.isolation = Some(value);
delta.revert_layer.isolation = false;
}
fn set_delta_clip_path(delta: &mut StyleDelta, value: ClipPathSpec) {
delta.clip_path = Some(value);
delta.revert_layer.clip_path = false;
}
fn set_delta_legacy_clip(delta: &mut StyleDelta, value: LegacyClipSpec) {
delta.legacy_clip = Some(value);
delta.revert_layer.legacy_clip = false;
}
fn set_delta_box_shadows(delta: &mut StyleDelta, value: Vec<BoxShadowSpec>) {
delta.box_shadow = value.first().cloned();
delta.box_shadows = Some(value);
delta.revert_layer.box_shadow = false;
}
fn set_delta_overflow_axes(delta: &mut StyleDelta, x: OverflowMode, y: OverflowMode) {
delta.overflow_x = Some(x);
delta.overflow_y = Some(y);
delta.revert_layer.overflow_x = false;
delta.revert_layer.overflow_y = false;
}
fn set_delta_overflow_x(delta: &mut StyleDelta, value: OverflowMode) {
delta.overflow_x = Some(value);
delta.revert_layer.overflow_x = false;
}
fn set_delta_overflow_y(delta: &mut StyleDelta, value: OverflowMode) {
delta.overflow_y = Some(value);
delta.revert_layer.overflow_y = false;
}
fn set_delta_overflow_inline(delta: &mut StyleDelta, value: OverflowMode) {
delta.overflow_inline = Some(value);
delta.revert_layer.overflow_inline = false;
}
fn set_delta_overflow_block(delta: &mut StyleDelta, value: OverflowMode) {
delta.overflow_block = Some(value);
delta.revert_layer.overflow_block = false;
}
fn clear_delta_inset_edge(delta: &mut StyleDelta, target: BoxEdgeTarget) {
match target {
BoxEdgeTarget::All => {
delta.inset_top = None;
delta.inset_right = None;
delta.inset_bottom = None;
delta.inset_left = None;
delta.inset_top_var = None;
delta.inset_right_var = None;
delta.inset_bottom_var = None;
delta.inset_left_var = None;
}
BoxEdgeTarget::Top => {
delta.inset_top = None;
delta.inset_top_var = None;
}
BoxEdgeTarget::Right => {
delta.inset_right = None;
delta.inset_right_var = None;
}
BoxEdgeTarget::Bottom => {
delta.inset_bottom = None;
delta.inset_bottom_var = None;
}
BoxEdgeTarget::Left => {
delta.inset_left = None;
delta.inset_left_var = None;
}
}
}
fn set_delta_inset_edge_spec(
delta: &mut StyleDelta,
target: BoxEdgeTarget,
value: Option<LengthSpec>,
) {
let Some(value) = value else {
return;
};
delta.revert_layer.inset.clear(target);
match target {
BoxEdgeTarget::All => {
delta.inset_top = Some(value);
delta.inset_right = Some(value);
delta.inset_bottom = Some(value);
delta.inset_left = Some(value);
delta.inset_top_var = None;
delta.inset_right_var = None;
delta.inset_bottom_var = None;
delta.inset_left_var = None;
}
BoxEdgeTarget::Top => {
delta.inset_top = Some(value);
delta.inset_top_var = None;
}
BoxEdgeTarget::Right => {
delta.inset_right = Some(value);
delta.inset_right_var = None;
}
BoxEdgeTarget::Bottom => {
delta.inset_bottom = Some(value);
delta.inset_bottom_var = None;
}
BoxEdgeTarget::Left => {
delta.inset_left = Some(value);
delta.inset_left_var = None;
}
}
}
fn set_delta_inset_edge_var(delta: &mut StyleDelta, target: BoxEdgeTarget, value: String) {
delta.revert_layer.inset.clear(target);
match target {
BoxEdgeTarget::All => {
delta.inset_top = None;
delta.inset_right = None;
delta.inset_bottom = None;
delta.inset_left = None;
delta.inset_top_var = Some(value.clone());
delta.inset_right_var = Some(value.clone());
delta.inset_bottom_var = Some(value.clone());
delta.inset_left_var = Some(value);
}
BoxEdgeTarget::Top => {
delta.inset_top = None;
delta.inset_top_var = Some(value);
}
BoxEdgeTarget::Right => {
delta.inset_right = None;
delta.inset_right_var = Some(value);
}
BoxEdgeTarget::Bottom => {
delta.inset_bottom = None;
delta.inset_bottom_var = Some(value);
}
BoxEdgeTarget::Left => {
delta.inset_left = None;
delta.inset_left_var = Some(value);
}
}
}
fn parse_caption_side_ident(value: &str) -> Option<CaptionSideMode> {
match value {
"top" | "initial" => Some(CaptionSideMode::Top),
"bottom" => Some(CaptionSideMode::Bottom),
_ => None,
}
}
fn parse_object_fit_spec_str(raw: &str) -> Option<ObjectFitSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"fill" => Some(ObjectFitSpec::Value(ObjectFitMode::Fill)),
"contain" => Some(ObjectFitSpec::Value(ObjectFitMode::Contain)),
"cover" => Some(ObjectFitSpec::Value(ObjectFitMode::Cover)),
"none" => Some(ObjectFitSpec::Value(ObjectFitMode::None)),
"scale-down" => Some(ObjectFitSpec::Value(ObjectFitMode::ScaleDown)),
"inherit" => Some(ObjectFitSpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" => Some(ObjectFitSpec::Initial),
_ => None,
}
}
fn parse_image_rendering_spec_str(raw: &str) -> Option<ImageRenderingSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" | "smooth" | "high-quality" => {
Some(ImageRenderingSpec::Value(ImageRenderingMode::Auto))
}
"pixelated" | "crisp-edges" => {
Some(ImageRenderingSpec::Value(ImageRenderingMode::Pixelated))
}
"inherit" | "unset" => Some(ImageRenderingSpec::Inherit),
"initial" | "revert" | "revert-layer" => Some(ImageRenderingSpec::Initial),
_ => None,
}
}
fn parse_object_position_spec_str(raw: &str) -> Option<ObjectPositionSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"inherit" | "unset" => Some(ObjectPositionSpec::Inherit),
"initial" | "revert" | "revert-layer" => Some(ObjectPositionSpec::Initial),
_ => parse_background_position_layer(raw).map(ObjectPositionSpec::Value),
}
}
fn clear_custom_property_delta_value(delta: &mut StyleDelta, name: &str) {
delta.custom_lengths.remove(name);
delta.custom_colors.remove(name);
delta.custom_color_alpha.remove(name);
delta.custom_color_refs.remove(name);
delta.custom_raw_values.remove(name);
delta.custom_font_stacks.remove(name);
}
fn combine_overflow_modes(x: OverflowMode, y: OverflowMode) -> OverflowMode {
// Static PDF output cannot expose scrollbars, so any clipped or scrollable axis clips paint.
if matches!(x, OverflowMode::Hidden) || matches!(y, OverflowMode::Hidden) {
OverflowMode::Hidden
} else if matches!(x, OverflowMode::Clip) || matches!(y, OverflowMode::Clip) {
OverflowMode::Clip
} else {
OverflowMode::Visible
}
}
fn parse_overflow_mode_str(raw: &str) -> Option<OverflowMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"visible" => Some(OverflowMode::Visible),
"clip" => Some(OverflowMode::Clip),
"hidden" | "scroll" | "auto" => Some(OverflowMode::Hidden),
_ => None,
}
}
fn parse_overflow_axis_modes_str(raw: &str) -> Option<(OverflowMode, OverflowMode)> {
let lower = raw.trim().to_ascii_lowercase();
let parts: Vec<&str> = lower.split_whitespace().collect();
match parts.as_slice() {
[single] => {
let mode = parse_overflow_mode_str(single)?;
Some((mode, mode))
}
[x, y] => Some((parse_overflow_mode_str(x)?, parse_overflow_mode_str(y)?)),
_ => None,
}
}
fn parse_break_inside_value(property_name: &str, value: &str) -> Option<BreakInside> {
match property_name {
"page-break-inside" => match value {
"auto" | "initial" | "unset" => Some(BreakInside::Auto),
"avoid" => Some(BreakInside::Avoid),
_ => None,
},
_ => match value {
"auto" | "initial" | "unset" => Some(BreakInside::Auto),
"avoid" => Some(BreakInside::Avoid),
"avoid-page" => Some(BreakInside::AvoidPage),
"avoid-column" => Some(BreakInside::AvoidColumn),
"avoid-region" => Some(BreakInside::AvoidRegion),
_ => None,
},
}
}
fn parse_break_before_value(property_name: &str, value: &str) -> Option<BreakBefore> {
match property_name {
"page-break-before" => match value {
"auto" | "initial" | "unset" => Some(BreakBefore::Auto),
"avoid" => Some(BreakBefore::Avoid),
"always" => Some(BreakBefore::Page),
"left" => Some(BreakBefore::Left),
"right" => Some(BreakBefore::Right),
_ => None,
},
_ => match value {
"auto" | "initial" | "unset" => Some(BreakBefore::Auto),
"avoid" => Some(BreakBefore::Avoid),
"always" => Some(BreakBefore::Always),
"all" => Some(BreakBefore::All),
"avoid-page" => Some(BreakBefore::AvoidPage),
"page" => Some(BreakBefore::Page),
"left" => Some(BreakBefore::Left),
"right" => Some(BreakBefore::Right),
"recto" => Some(BreakBefore::Recto),
"verso" => Some(BreakBefore::Verso),
"avoid-column" => Some(BreakBefore::AvoidColumn),
"column" => Some(BreakBefore::Column),
"avoid-region" => Some(BreakBefore::AvoidRegion),
"region" => Some(BreakBefore::Region),
_ => None,
},
}
}
fn parse_break_after_value(property_name: &str, value: &str) -> Option<BreakAfter> {
match property_name {
"page-break-after" => match value {
"auto" | "initial" | "unset" => Some(BreakAfter::Auto),
"avoid" => Some(BreakAfter::Avoid),
"always" => Some(BreakAfter::Page),
"left" => Some(BreakAfter::Left),
"right" => Some(BreakAfter::Right),
_ => None,
},
_ => match value {
"auto" | "initial" | "unset" => Some(BreakAfter::Auto),
"avoid" => Some(BreakAfter::Avoid),
"always" => Some(BreakAfter::Always),
"all" => Some(BreakAfter::All),
"avoid-page" => Some(BreakAfter::AvoidPage),
"page" => Some(BreakAfter::Page),
"left" => Some(BreakAfter::Left),
"right" => Some(BreakAfter::Right),
"recto" => Some(BreakAfter::Recto),
"verso" => Some(BreakAfter::Verso),
"avoid-column" => Some(BreakAfter::AvoidColumn),
"column" => Some(BreakAfter::Column),
"avoid-region" => Some(BreakAfter::AvoidRegion),
"region" => Some(BreakAfter::Region),
_ => None,
},
}
}
fn last_counter_reset_mutations(mutations: Vec<CounterMutation>) -> Vec<CounterMutation> {
let mut out = Vec::new();
for (index, mutation) in mutations.iter().enumerate() {
if mutations[index + 1..]
.iter()
.any(|later| later.name == mutation.name)
{
continue;
}
out.push(mutation.clone());
}
out
}
fn default_quotes() -> Vec<(String, String)> {
vec![("\"".to_string(), "\"".to_string())]
}
fn decimal_generated_counter_style() -> GeneratedCounterStyle {
GeneratedCounterStyle {
list_style_type: ListStyleTypeMode::Decimal,
marker: None,
symbols: None,
}
}
fn generated_counter_style_from_raw(raw: &str) -> Option<GeneratedCounterStyle> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let (list_style_type, marker, symbols) = parse_native_list_style_type(trimmed)?;
let list_style_type = match list_style_type {
ListStyleTypeMode::Auto => ListStyleTypeMode::Decimal,
value => value,
};
Some(GeneratedCounterStyle {
list_style_type,
marker,
symbols,
})
}
fn is_valid_counter_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
!name.is_empty()
&& !matches!(
lower.as_str(),
"none" | "inherit" | "initial" | "unset" | "revert" | "revert-layer"
)
}
#[derive(Debug, Clone)]
struct ResolvedContent {
text: String,
parts: Vec<GeneratedContentPart>,
}
fn resolve_content_spec(
content: &ContentSpec,
parent: &ComputedStyle,
element: Option<&ElementInfo>,
quotes: &[(String, String)],
) -> Option<ResolvedContent> {
match content {
ContentSpec::Normal => None,
ContentSpec::None => None,
ContentSpec::Text(value) => Some(ResolvedContent {
text: value.clone(),
parts: vec![GeneratedContentPart::Text(value.clone())],
}),
ContentSpec::Parts(parts) => {
let mut out = String::new();
let mut resolved_parts = Vec::new();
let push_text =
|text: &str, out: &mut String, parts: &mut Vec<GeneratedContentPart>| {
if text.is_empty() {
return;
}
out.push_str(text);
match parts.last_mut() {
Some(GeneratedContentPart::Text(existing)) => existing.push_str(text),
_ => parts.push(GeneratedContentPart::Text(text.to_string())),
}
};
for part in parts {
match part {
ContentPart::Text(value) => {
push_text(value, &mut out, &mut resolved_parts);
}
ContentPart::Attr(name) => {
if let Some(value) = element.and_then(|info| info.attrs.get(name)) {
push_text(value, &mut out, &mut resolved_parts);
}
}
ContentPart::Image(source) => {
resolved_parts.push(GeneratedContentPart::Image(source.clone()));
}
ContentPart::Leader(pattern) => {
resolved_parts.push(GeneratedContentPart::Leader(pattern.clone()));
}
ContentPart::Counter(counter) => {
resolved_parts.push(GeneratedContentPart::Counter(counter.clone()));
}
ContentPart::Counters(counters) => {
resolved_parts.push(GeneratedContentPart::Counters(counters.clone()));
}
ContentPart::TargetTextAttr(name) => {
if let Some(target) = element.and_then(|info| info.attrs.get(name)) {
resolved_parts
.push(GeneratedContentPart::TargetText(target.to_string()));
}
}
ContentPart::TargetCounterAttr {
attribute,
name,
style,
} => {
if let Some(target) = element.and_then(|info| info.attrs.get(attribute)) {
resolved_parts.push(GeneratedContentPart::TargetCounter(
GeneratedTargetCounterContent {
target: target.to_string(),
name: name.clone(),
style: style.clone(),
},
));
}
}
ContentPart::OpenQuote => {
if let Some((open, _)) = quotes.first() {
out.push_str(open);
}
resolved_parts.push(GeneratedContentPart::OpenQuote);
}
ContentPart::CloseQuote => {
if let Some((_, close)) = quotes.first() {
out.push_str(close);
}
resolved_parts.push(GeneratedContentPart::CloseQuote);
}
ContentPart::NoOpenQuote => {
resolved_parts.push(GeneratedContentPart::NoOpenQuote);
}
ContentPart::NoCloseQuote => {
resolved_parts.push(GeneratedContentPart::NoCloseQuote);
}
}
}
Some(ResolvedContent {
text: out,
parts: resolved_parts,
})
}
ContentSpec::Inherit => parent.content.clone().map(|text| ResolvedContent {
parts: parent
.generated_content
.clone()
.unwrap_or_else(|| vec![GeneratedContentPart::Text(text.clone())]),
text,
}),
ContentSpec::Initial => None,
}
}
fn scale_length_spec(spec: LengthSpec, scale: f32) -> LengthSpec {
match spec {
LengthSpec::Absolute(value) => LengthSpec::Absolute(value * scale),
LengthSpec::Percent(pct) => LengthSpec::Percent(pct * scale),
LengthSpec::Em(value) => LengthSpec::Em(value * scale),
LengthSpec::Rem(value) => LengthSpec::Rem(value * scale),
LengthSpec::Calc(calc) => LengthSpec::Calc(CalcLength {
abs: calc.abs * scale,
percent: calc.percent * scale,
em: calc.em * scale,
rem: calc.rem * scale,
}),
_ => spec,
}
}
fn parse_rgb_triplet_string(raw: &str) -> Option<Color> {
let parts: Vec<&str> = raw
.split(',')
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect();
if parts.len() != 3 {
return None;
}
let r = parts[0].parse::<f32>().ok()?;
let g = parts[1].parse::<f32>().ok()?;
let b = parts[2].parse::<f32>().ok()?;
Some(Color::rgb(r / 255.0, g / 255.0, b / 255.0))
}
fn is_quoted_css_string(raw: &str) -> bool {
let trimmed = raw.trim();
if trimmed.len() < 2 {
return false;
}
let mut chars = trimmed.chars();
let Some(first) = chars.next() else {
return false;
};
let Some(last) = trimmed.chars().last() else {
return false;
};
(first == '"' || first == '\'') && first == last
}
fn map_font_family_name(raw: &str, quoted: bool) -> Option<Arc<str>> {
let cleaned = raw.trim().trim_matches('"').trim_matches('\'');
if cleaned.is_empty() {
return None;
}
if quoted {
return Some(Arc::<str>::from(cleaned.to_string()));
}
Some(Arc::<str>::from(cleaned.to_string()))
}
fn generic_font_family_stack(raw: &str) -> Option<&'static [&'static str]> {
const SANS: &[&str] = &["Liberation Sans", "Arial", "DejaVu Sans", "Helvetica"];
const SERIF: &[&str] = &[
"Liberation Serif",
"Times New Roman",
"DejaVu Serif",
"Times-Roman",
];
const MONO: &[&str] = &[
"DejaVu Sans Mono",
"Liberation Mono",
"Courier New",
"Courier",
];
let cleaned = raw.trim().trim_matches('"').trim_matches('\'');
match cleaned.to_ascii_lowercase().as_str() {
"sans-serif" | "ui-sans-serif" | "system-ui" => Some(SANS),
"serif" | "ui-serif" => Some(SERIF),
"monospace" | "ui-monospace" => Some(MONO),
_ => None,
}
}
fn finalize_font_stack(mut stack: Vec<Arc<str>>) -> Option<Vec<Arc<str>>> {
if stack.is_empty() {
return None;
}
let has_base14 = stack.iter().any(|name| {
let lower = name.to_ascii_lowercase();
lower == "helvetica" || lower == "times-roman" || lower == "courier"
});
if !has_base14 {
stack.push(Arc::<str>::from("Helvetica"));
}
Some(stack)
}
fn font_stack_from_string(raw: &str) -> Option<Vec<Arc<str>>> {
let mut stack = Vec::new();
for part in split_args(raw) {
let quoted = is_quoted_css_string(&part);
if !quoted {
if let Some(generic) = generic_font_family_stack(&part) {
stack.extend(generic.iter().map(|name| Arc::<str>::from(*name)));
continue;
}
}
if let Some(mapped) = map_font_family_name(&part, quoted) {
stack.push(mapped);
}
}
finalize_font_stack(stack)
}
fn font_spec_from_raw_string(raw: &str) -> Option<FontSpec> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if !is_quoted_css_string(trimmed) {
match trimmed.to_ascii_lowercase().as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => return Some(FontSpec::Inherit),
"initial" => return Some(FontSpec::Initial),
_ => {}
}
}
font_stack_from_string(trimmed).map(FontSpec::Value)
}
fn json_string(value: &str) -> String {
format!("\"{}\"", json_escape(value))
}
fn json_array(values: &[String]) -> String {
let mut out = String::from("[");
for (idx, value) in values.iter().enumerate() {
if idx > 0 {
out.push(',');
}
out.push_str(&json_string(value));
}
out.push(']');
out
}
fn json_opt_string(value: Option<String>) -> String {
match value {
Some(v) => json_string(&v),
None => "null".to_string(),
}
}
fn color_to_hex(color: Color) -> String {
let r = (color.r.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (color.g.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (color.b.clamp(0.0, 1.0) * 255.0).round() as u8;
format!("#{:02x}{:02x}{:02x}", r, g, b)
}
fn blend_over_white(color: Color, alpha: f32) -> Color {
blend_over_color(color, alpha, Color::rgb(1.0, 1.0, 1.0))
}
fn background_spec_from_color_alpha(color: Color, alpha: f32) -> BackgroundSpec {
if alpha <= 0.0 {
BackgroundSpec::Transparent
} else {
BackgroundSpec::Value(color, alpha.clamp(0.0, 1.0))
}
}
fn blend_over_color(color: Color, alpha: f32, backdrop: Color) -> Color {
let alpha = alpha.clamp(0.0, 1.0);
if alpha >= 1.0 {
return color;
}
Color::rgb(
color.r * alpha + backdrop.r * (1.0 - alpha),
color.g * alpha + backdrop.g * (1.0 - alpha),
color.b * alpha + backdrop.b * (1.0 - alpha),
)
}
fn should_defer_color_expr(raw: &str) -> bool {
let lower = raw.to_ascii_lowercase();
lower.contains("var(") || lower.contains("color-mix(")
}
fn length_spec_debug(value: LengthSpec) -> String {
match value {
LengthSpec::Auto => "auto".to_string(),
LengthSpec::Content => "content".to_string(),
LengthSpec::MinContent => "min-content".to_string(),
LengthSpec::MaxContent => "max-content".to_string(),
LengthSpec::FitContent => "fit-content".to_string(),
LengthSpec::Absolute(pt) => format!("{:.3}pt", pt.to_f32()),
LengthSpec::Percent(v) => format!("{:.3}%", v * 100.0),
LengthSpec::Em(v) => format!("{:.3}em", v),
LengthSpec::Rem(v) => format!("{:.3}rem", v),
LengthSpec::Calc(calc) => format!(
"calc({:.3}pt + {:.3}% + {:.3}em + {:.3}rem)",
calc.abs.to_f32(),
calc.percent * 100.0,
calc.em,
calc.rem
),
LengthSpec::Clamped(calc) => format!(
"clamp({:?}, calc({:.3}pt + {:.3}% + {:.3}em + {:.3}rem), {:?})",
calc.min.map(Pt::to_f32),
calc.value.abs.to_f32(),
calc.value.percent * 100.0,
calc.value.em,
calc.value.rem,
calc.max.map(Pt::to_f32),
),
LengthSpec::FontRelative(calc) => format!(
"calc({:.3}pt + {:.3}% + {:.3}em + {:.3}rem + {:.3}ex + {:.3}ch + {:.3}cap + {:.3}lh + {:.3}rlh)",
calc.base.abs.to_f32(),
calc.base.percent * 100.0,
calc.base.em,
calc.base.rem,
calc.ex,
calc.ch,
calc.cap,
calc.lh,
calc.rlh,
),
LengthSpec::Inherit => "inherit".to_string(),
LengthSpec::Initial => "initial".to_string(),
}
}
fn tab_size_debug(value: TabSizeSpec) -> String {
match value {
TabSizeSpec::Spaces(spaces) => format!("{:.3}", spaces),
TabSizeSpec::Length(length) => length_spec_debug(length),
TabSizeSpec::Inherit => "inherit".to_string(),
TabSizeSpec::Initial => "initial".to_string(),
}
}
fn edges_debug(edges: EdgeSizes) -> String {
format!(
"{} {} {} {}",
length_spec_debug(edges.top),
length_spec_debug(edges.right),
length_spec_debug(edges.bottom),
length_spec_debug(edges.left)
)
}
fn position_component_debug(
component: BackgroundPositionComponent,
start_label: &str,
end_label: &str,
) -> String {
match component {
BackgroundPositionComponent::Start(offset) => match offset {
LengthSpec::Absolute(value) if value == Pt::ZERO => start_label.to_string(),
_ => length_spec_debug(offset),
},
BackgroundPositionComponent::Center => "center".to_string(),
BackgroundPositionComponent::End(offset) => match offset {
LengthSpec::Absolute(value) if value == Pt::ZERO => end_label.to_string(),
_ => format!("{} {}", end_label, length_spec_debug(offset)),
},
}
}
fn position_debug(position: BackgroundPositionSpec) -> String {
format!(
"{} {}",
position_component_debug(position.x, "left", "right"),
position_component_debug(position.y, "top", "bottom")
)
}
fn border_line_style_debug(style: BorderLineStyle) -> &'static str {
match style {
BorderLineStyle::None => "None",
BorderLineStyle::Hidden => "Hidden",
BorderLineStyle::Style(OutlineLineStyle::Solid) => "Solid",
BorderLineStyle::Style(OutlineLineStyle::Dotted) => "Dotted",
BorderLineStyle::Style(OutlineLineStyle::Dashed) => "Dashed",
BorderLineStyle::Style(OutlineLineStyle::Double) => "Double",
BorderLineStyle::Style(OutlineLineStyle::Groove) => "Groove",
BorderLineStyle::Style(OutlineLineStyle::Ridge) => "Ridge",
BorderLineStyle::Style(OutlineLineStyle::Inset) => "Inset",
BorderLineStyle::Style(OutlineLineStyle::Outset) => "Outset",
}
}
fn border_style_debug(style: BorderStyleState) -> String {
format!(
"{} {} {} {}",
border_line_style_debug(style.top),
border_line_style_debug(style.right),
border_line_style_debug(style.bottom),
border_line_style_debug(style.left)
)
}
fn border_spacing_debug(spacing: BorderSpacingSpec) -> String {
format!(
"{} {}",
length_spec_debug(spacing.horizontal),
length_spec_debug(spacing.vertical)
)
}
fn border_radius_axis_debug(radius: BorderRadiusSpec) -> String {
format!(
"{} {} {} {}",
length_spec_debug(radius.top_left),
length_spec_debug(radius.top_right),
length_spec_debug(radius.bottom_right),
length_spec_debug(radius.bottom_left)
)
}
fn border_radius_debug(radius: BorderRadiiSpec) -> String {
let horizontal = border_radius_axis_debug(radius.horizontal);
let vertical = border_radius_axis_debug(radius.vertical);
if radius.horizontal == radius.vertical {
horizontal
} else {
format!("{} / {}", horizontal, vertical)
}
}
fn clip_path_radius_axis_debug(radius: BorderRadiusSpec) -> String {
if radius.top_left == radius.top_right
&& radius.top_left == radius.bottom_right
&& radius.top_left == radius.bottom_left
{
return length_spec_debug(radius.top_left);
}
if radius.top_left == radius.bottom_right && radius.top_right == radius.bottom_left {
return format!(
"{} {}",
length_spec_debug(radius.top_left),
length_spec_debug(radius.top_right)
);
}
if radius.top_right == radius.bottom_left {
return format!(
"{} {} {}",
length_spec_debug(radius.top_left),
length_spec_debug(radius.top_right),
length_spec_debug(radius.bottom_right)
);
}
border_radius_axis_debug(radius)
}
fn clip_path_radius_debug(radius: ClipPathRadiusSpec) -> String {
let horizontal = clip_path_radius_axis_debug(radius.horizontal);
let vertical = clip_path_radius_axis_debug(radius.vertical);
if radius.horizontal == radius.vertical {
horizontal
} else {
format!("{} / {}", horizontal, vertical)
}
}
fn text_decoration_debug(decoration: TextDecorationMode) -> String {
let mut parts = Vec::new();
if decoration.underline {
parts.push("underline");
}
if decoration.overline {
parts.push("overline");
}
if decoration.line_through {
parts.push("line-through");
}
if parts.is_empty() {
"none".to_string()
} else {
parts.join(" ")
}
}
fn text_decoration_thickness_debug(thickness: TextDecorationThicknessMode) -> String {
match thickness {
TextDecorationThicknessMode::Auto => "auto".to_string(),
TextDecorationThicknessMode::FromFont => "from-font".to_string(),
TextDecorationThicknessMode::Length(value) => length_spec_debug(value),
}
}
fn text_decoration_style_debug(style: TextDecorationStyleMode) -> &'static str {
match style {
TextDecorationStyleMode::Solid => "solid",
TextDecorationStyleMode::Double => "double",
TextDecorationStyleMode::Dotted => "dotted",
TextDecorationStyleMode::Dashed => "dashed",
TextDecorationStyleMode::Wavy => "wavy",
}
}
fn text_underline_offset_debug(offset: TextUnderlineOffsetMode) -> String {
match offset {
TextUnderlineOffsetMode::Auto => "auto".to_string(),
TextUnderlineOffsetMode::Length(value) => length_spec_debug(value),
}
}
fn text_underline_position_debug(position: TextUnderlinePositionMode) -> String {
let mut parts = Vec::new();
match position.line {
TextUnderlinePositionLine::Auto => {}
TextUnderlinePositionLine::FromFont => parts.push("from-font"),
TextUnderlinePositionLine::Under => parts.push("under"),
}
match position.side {
Some(TextUnderlinePositionSide::Left) => parts.push("left"),
Some(TextUnderlinePositionSide::Right) => parts.push("right"),
None => {}
}
if parts.is_empty() {
"auto".to_string()
} else {
parts.join(" ")
}
}
fn text_shadow_debug(shadows: &[BoxShadowSpec]) -> String {
if shadows.is_empty() {
return "none".to_string();
}
shadows
.iter()
.map(|shadow| {
let color = if shadow.opacity < 0.999 {
format!(
"rgba({:.0}, {:.0}, {:.0}, {:.3})",
shadow.color.r.clamp(0.0, 1.0) * 255.0,
shadow.color.g.clamp(0.0, 1.0) * 255.0,
shadow.color.b.clamp(0.0, 1.0) * 255.0,
shadow.opacity.clamp(0.0, 1.0)
)
} else {
color_to_hex(shadow.color)
};
format!(
"{} {} {} {}",
length_spec_debug(shadow.offset_x),
length_spec_debug(shadow.offset_y),
length_spec_debug(shadow.blur),
color
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn transform_op_debug(op: CssTransformOp) -> String {
match op {
CssTransformOp::Translate { x, y } => format!(
"translate({}, {})",
length_spec_debug(x),
length_spec_debug(y)
),
CssTransformOp::Translate3d { x, y, z } => format!(
"translate3d({}, {}, {})",
length_spec_debug(x),
length_spec_debug(y),
length_spec_debug(z)
),
CssTransformOp::Scale { x, y } => format!("scale({:.6}, {:.6})", x, y),
CssTransformOp::Scale3d { x, y, z } => {
format!("scale3d({:.6}, {:.6}, {:.6})", x, y, z)
}
CssTransformOp::Rotate { radians } => format!("rotate({:.6}rad)", radians),
CssTransformOp::Rotate3d { x, y, z, radians } => {
format!("rotate3d({:.6}, {:.6}, {:.6}, {:.6}rad)", x, y, z, radians)
}
CssTransformOp::Perspective { distance } => {
format!("perspective({})", length_spec_debug(distance))
}
CssTransformOp::Matrix3d { values } => format!(
"matrix3d({})",
values
.iter()
.map(|value| format!("{value:.6}"))
.collect::<Vec<_>>()
.join(", ")
),
CssTransformOp::Skew {
x_radians,
y_radians,
} => format!("skew({:.6}rad, {:.6}rad)", x_radians, y_radians),
CssTransformOp::Matrix { a, b, c, d, e, f } => format!(
"matrix({:.6}, {:.6}, {:.6}, {:.6}, {}, {})",
a,
b,
c,
d,
length_spec_debug(LengthSpec::Absolute(e)),
length_spec_debug(LengthSpec::Absolute(f))
),
}
}
fn transform_debug(ops: &[CssTransformOp]) -> String {
if ops.is_empty() {
"none".to_string()
} else {
ops.iter()
.map(|op| transform_op_debug(*op))
.collect::<Vec<_>>()
.join(" ")
}
}
fn transform_origin_debug(origin: CssTransformOrigin) -> String {
format!(
"{} {}",
length_spec_debug(origin.x),
length_spec_debug(origin.y)
)
}
fn clip_path_inset_debug(spec: ClipPathInsetSpec) -> String {
let mut out = format!(
"{} {} {} {}",
length_spec_debug(spec.top),
length_spec_debug(spec.right),
length_spec_debug(spec.bottom),
length_spec_debug(spec.left)
);
if let Some(radius) = spec.radius {
out.push_str(&format!(" round {}", clip_path_radius_debug(radius)));
}
out
}
fn clip_path_shape_debug(spec: &ClipPathShapeSpec) -> String {
match spec {
ClipPathShapeSpec::Inset(inset) => format!("inset({})", clip_path_inset_debug(*inset)),
ClipPathShapeSpec::Circle(circle) => format!(
"circle({} at {} {})",
clip_path_shape_radius_debug(circle.radius),
length_spec_debug(circle.center_x),
length_spec_debug(circle.center_y)
),
ClipPathShapeSpec::Ellipse(ellipse) => format!(
"ellipse({} {} at {} {})",
clip_path_shape_radius_debug(ellipse.radius_x),
clip_path_shape_radius_debug(ellipse.radius_y),
length_spec_debug(ellipse.center_x),
length_spec_debug(ellipse.center_y)
),
ClipPathShapeSpec::Xywh(xywh) => {
let mut out = format!(
"xywh({} {} {} {}",
length_spec_debug(xywh.x),
length_spec_debug(xywh.y),
length_spec_debug(xywh.width),
length_spec_debug(xywh.height)
);
if let Some(radius) = xywh.radius {
out.push_str(&format!(" round {}", clip_path_radius_debug(radius)));
}
out.push(')');
out
}
ClipPathShapeSpec::Rect(rect) => {
let mut out = format!(
"rect({} {} {} {}",
length_spec_debug(rect.top),
length_spec_debug(rect.right),
length_spec_debug(rect.bottom),
length_spec_debug(rect.left)
);
if let Some(radius) = rect.radius {
out.push_str(&format!(" round {}", clip_path_radius_debug(radius)));
}
out.push(')');
out
}
ClipPathShapeSpec::Polygon(polygon) => {
let mut points = polygon
.points
.iter()
.map(|(x, y)| format!("{} {}", length_spec_debug(*x), length_spec_debug(*y)))
.collect::<Vec<_>>();
if let Some(rule) =
clip_path_fill_rule_debug(polygon.evenodd, polygon.fill_rule_explicit)
{
points.insert(0, rule.to_string());
}
format!("polygon({})", points.join(", "))
}
ClipPathShapeSpec::Path(path) => {
let data = css_string_escape(&path.data);
if let Some(rule) = clip_path_fill_rule_debug(path.evenodd, path.fill_rule_explicit) {
format!("path({rule}, \"{data}\")")
} else {
format!("path(\"{data}\")")
}
}
ClipPathShapeSpec::ShapeFunction(shape) => clip_path_shape_function_debug(shape),
ClipPathShapeSpec::Url(source) => format!("url(\"{}\")", css_string_escape(source)),
ClipPathShapeSpec::ReferenceBox => String::new(),
}
}
fn clip_path_shape_radius_debug(radius: ClipPathShapeRadius) -> String {
match radius {
ClipPathShapeRadius::Length(length) => length_spec_debug(length),
ClipPathShapeRadius::ClosestSide => "closest-side".to_string(),
ClipPathShapeRadius::FarthestSide => "farthest-side".to_string(),
ClipPathShapeRadius::ClosestCorner => "closest-corner".to_string(),
ClipPathShapeRadius::FarthestCorner => "farthest-corner".to_string(),
}
}
fn clip_path_shape_function_debug(spec: &ClipPathShapeFunctionSpec) -> String {
let mut parts = Vec::with_capacity(spec.commands.len() + 1);
let prefix = clip_path_fill_rule_debug(spec.evenodd, spec.fill_rule_explicit)
.map(|rule| format!("{rule} "))
.unwrap_or_default();
parts.push(format!(
"{prefix}from {} {}",
length_spec_debug(spec.start_x),
length_spec_debug(spec.start_y)
));
for command in &spec.commands {
let part = match *command {
ClipPathShapeFunctionCommand::MoveTo { x, y, relative } => format!(
"move {} {} {}",
shape_command_mode_debug(relative),
length_spec_debug(x),
length_spec_debug(y)
),
ClipPathShapeFunctionCommand::LineTo { x, y, relative } => format!(
"line {} {} {}",
shape_command_mode_debug(relative),
length_spec_debug(x),
length_spec_debug(y)
),
ClipPathShapeFunctionCommand::HLine { x, relative } => format!(
"hline {} {}",
shape_command_mode_debug(relative),
length_spec_debug(x)
),
ClipPathShapeFunctionCommand::VLine { y, relative } => format!(
"vline {} {}",
shape_command_mode_debug(relative),
length_spec_debug(y)
),
ClipPathShapeFunctionCommand::CurveTo {
x,
y,
relative,
control1_x,
control1_y,
control1_anchor,
control2_x,
control2_y,
control2_anchor,
} => {
let mut out = format!(
"curve {} {} {} with {}",
shape_command_mode_debug(relative),
length_spec_debug(x),
length_spec_debug(y),
shape_control_point_debug(control1_x, control1_y, control1_anchor)
);
if let (Some(control2_x), Some(control2_y), Some(control2_anchor)) =
(control2_x, control2_y, control2_anchor)
{
out.push_str(&format!(
" / {}",
shape_control_point_debug(control2_x, control2_y, control2_anchor)
));
}
out
}
ClipPathShapeFunctionCommand::SmoothTo {
x,
y,
relative,
control_x,
control_y,
control_anchor,
} => {
let mut out = format!(
"smooth {} {} {}",
shape_command_mode_debug(relative),
length_spec_debug(x),
length_spec_debug(y)
);
if let (Some(control_x), Some(control_y), Some(control_anchor)) =
(control_x, control_y, control_anchor)
{
out.push_str(&format!(
" with {}",
shape_control_point_debug(control_x, control_y, control_anchor)
));
}
out
}
ClipPathShapeFunctionCommand::ArcTo {
x,
y,
relative,
radius_x,
radius_y,
large_arc,
sweep,
rotation_deg,
} => {
let mut out = format!(
"arc {} {} {} of {}",
shape_command_mode_debug(relative),
length_spec_debug(x),
length_spec_debug(y),
length_spec_debug(radius_x)
);
if radius_y != radius_x {
out.push_str(&format!(" {}", length_spec_debug(radius_y)));
}
if large_arc {
out.push_str(" large");
}
if sweep {
out.push_str(" cw");
}
if rotation_deg.abs() > 0.0005 {
out.push_str(&format!(" rotate {:.3}deg", rotation_deg));
}
out
}
ClipPathShapeFunctionCommand::Close => "close".to_string(),
};
parts.push(part);
}
format!("shape({})", parts.join(", "))
}
fn clip_path_fill_rule_debug(evenodd: bool, explicit: bool) -> Option<&'static str> {
if evenodd {
Some("evenodd")
} else if explicit {
Some("nonzero")
} else {
None
}
}
fn shape_command_mode_debug(relative: bool) -> &'static str {
if relative { "by" } else { "to" }
}
fn shape_control_point_debug(
x: LengthSpec,
y: LengthSpec,
anchor: ClipPathShapeControlAnchor,
) -> String {
let mut out = format!("{} {}", length_spec_debug(x), length_spec_debug(y));
if anchor != ClipPathShapeControlAnchor::Start {
out.push_str(&format!(" from {}", shape_control_anchor_debug(anchor)));
}
out
}
fn shape_control_anchor_debug(anchor: ClipPathShapeControlAnchor) -> &'static str {
match anchor {
ClipPathShapeControlAnchor::Start => "start",
ClipPathShapeControlAnchor::End => "end",
ClipPathShapeControlAnchor::Origin => "origin",
}
}
fn css_string_escape(raw: &str) -> String {
raw.chars()
.flat_map(|ch| match ch {
'\\' => "\\\\".chars().collect::<Vec<_>>(),
'"' => "\\\"".chars().collect::<Vec<_>>(),
'\n' => "\\a ".chars().collect::<Vec<_>>(),
'\r' => "\\d ".chars().collect::<Vec<_>>(),
'\t' => "\\9 ".chars().collect::<Vec<_>>(),
_ => vec![ch],
})
.collect()
}
fn clip_path_reference_box_debug(reference_box: ClipPathReferenceBox) -> &'static str {
match reference_box {
ClipPathReferenceBox::Margin => "margin-box",
ClipPathReferenceBox::Border => "border-box",
ClipPathReferenceBox::HalfBorder => "half-border-box",
ClipPathReferenceBox::Padding => "padding-box",
ClipPathReferenceBox::Content => "content-box",
}
}
fn clip_path_debug(style: &ComputedStyle) -> Option<String> {
let shape = style.clip_path.as_ref()?;
let reference_box = clip_path_reference_box_debug(style.clip_path_reference_box);
if matches!(shape, ClipPathShapeSpec::ReferenceBox) {
return Some(reference_box.to_string());
}
let shape = clip_path_shape_debug(shape);
if matches!(style.clip_path_reference_box, ClipPathReferenceBox::Border) {
Some(shape)
} else {
Some(format!("{} {}", reference_box, shape))
}
}
fn filter_spec_debug(spec: &PaintFilterSpec) -> String {
let mut parts = Vec::new();
if spec.blur_radius > Pt::ZERO {
parts.push(format!("blur({:.3}pt)", spec.blur_radius.to_f32()));
}
for shadow in &spec.drop_shadows {
parts.push(format!(
"drop-shadow({:.3}pt {:.3}pt {:.3}pt rgba({:.0},{:.0},{:.0},{:.3}))",
shadow.offset_x.to_f32(),
shadow.offset_y.to_f32(),
shadow.blur_radius.to_f32(),
shadow.color.r.clamp(0.0, 1.0) * 255.0,
shadow.color.g.clamp(0.0, 1.0) * 255.0,
shadow.color.b.clamp(0.0, 1.0) * 255.0,
shadow.opacity.clamp(0.0, 1.0)
));
}
if (spec.brightness - 1.0).abs() > 1.0e-6 {
parts.push(format!("brightness({:.3})", spec.brightness));
}
if (spec.contrast - 1.0).abs() > 1.0e-6 {
parts.push(format!("contrast({:.3})", spec.contrast));
}
if spec.invert.abs() > 1.0e-6 {
parts.push(format!("invert({:.3})", spec.invert));
}
if spec.sepia.abs() > 1.0e-6 {
parts.push(format!("sepia({:.3})", spec.sepia));
}
if spec.hue_rotate.abs() > 1.0e-6 {
parts.push(format!(
"hue-rotate({:.3}deg)",
spec.hue_rotate.to_degrees()
));
}
if (spec.opacity - 1.0).abs() > 1.0e-6 {
parts.push(format!("opacity({:.3})", spec.opacity));
}
if (spec.saturate - 1.0).abs() > 1.0e-6 {
parts.push(format!("saturate({:.3})", spec.saturate));
}
if parts.is_empty() {
"none".to_string()
} else {
parts.join(" ")
}
}
fn debug_style_json(style: &ComputedStyle) -> String {
let mut fields = Vec::new();
fields.push(format!(
"\"display\":{}",
json_string(&format!("{:?}", style.display))
));
fields.push(format!(
"\"visibility\":{}",
json_string(&format!("{:?}", style.visibility))
));
fields.push(format!(
"\"position\":{}",
json_string(&format!("{:?}", style.position))
));
fields.push(format!(
"\"font_name\":{}",
json_string(style.font_name.as_ref())
));
fields.push(format!("\"font_size\":{:.3}", style.font_size.to_f32()));
fields.push(format!(
"\"line_height\":{:.3}",
style.line_height.to_line_height(style.font_size).to_f32()
));
fields.push(format!("\"font_weight\":{}", style.font_weight));
fields.push(format!(
"\"font_style\":{}",
json_string(&font_style_debug_string(style.font_style))
));
fields.push(format!(
"\"text_transform\":{}",
json_string(&format!("{:?}", style.text_transform))
));
fields.push(format!(
"\"letter_spacing\":{:.3}",
style.letter_spacing.to_f32()
));
fields.push(format!(
"\"word_spacing\":{:.3}",
style.word_spacing.to_f32()
));
fields.push(format!(
"\"tab_size\":{}",
json_string(&tab_size_debug(style.tab_size))
));
fields.push(format!(
"\"text_indent\":{}",
json_string(&length_spec_debug(style.text_indent))
));
fields.push(format!(
"\"text_indent_hanging\":{}",
style.text_indent_hanging
));
fields.push(format!(
"\"text_indent_each_line\":{}",
style.text_indent_each_line
));
fields.push(format!(
"\"transform\":{}",
json_string(&transform_debug(&style.transform))
));
fields.push(format!(
"\"transform_origin\":{}",
json_string(&transform_origin_debug(style.transform_origin))
));
fields.push(format!(
"\"text_decoration\":{}",
json_string(&text_decoration_debug(style.text_decoration))
));
fields.push(format!(
"\"text_decoration_color\":{}",
json_string(&color_to_hex(style.text_decoration_color))
));
fields.push(format!(
"\"text_decoration_thickness\":{}",
json_string(&text_decoration_thickness_debug(
style.text_decoration_thickness
))
));
fields.push(format!(
"\"text_decoration_style\":{}",
json_string(text_decoration_style_debug(style.text_decoration_style))
));
fields.push(format!(
"\"text_underline_offset\":{}",
json_string(&text_underline_offset_debug(style.text_underline_offset))
));
fields.push(format!(
"\"text_underline_position\":{}",
json_string(&text_underline_position_debug(
style.text_underline_position
))
));
fields.push(format!(
"\"text_shadow\":{}",
json_string(&text_shadow_debug(&style.text_shadows))
));
fields.push(format!(
"\"text_overflow\":{}",
json_string(&format!("{:?}", style.text_overflow))
));
fields.push(format!(
"\"word_break\":{}",
json_string(&format!("{:?}", style.word_break))
));
fields.push(format!(
"\"overflow_wrap\":{}",
json_string(&format!("{:?}", style.overflow_wrap))
));
fields.push(format!(
"\"line_break\":{}",
json_string(&format!("{:?}", style.line_break))
));
fields.push(format!(
"\"object_fit\":{}",
json_string(&format!("{:?}", style.object_fit))
));
fields.push(format!(
"\"object_position\":{}",
json_string(&position_debug(style.object_position))
));
fields.push(format!(
"\"text_align\":{}",
json_string(&format!("{:?}", style.text_align))
));
fields.push(format!(
"\"text_align_last\":{}",
json_string(&format!("{:?}", style.text_align_last))
));
fields.push(format!(
"\"text_justify\":{}",
json_string(&format!("{:?}", style.text_justify))
));
fields.push(format!(
"\"hyphens\":{}",
json_string(&format!("{:?}", style.hyphens))
));
fields.push(format!(
"\"hyphenate_character\":{}",
json_string(style.hyphenate_character.as_deref().unwrap_or("auto"))
));
fields.push(format!(
"\"direction\":{}",
json_string(&format!("{:?}", style.direction))
));
fields.push(format!(
"\"writing_mode\":{}",
json_string(&format!("{:?}", style.writing_mode))
));
fields.push(format!(
"\"text_orientation\":{}",
json_string(&format!("{:?}", style.text_orientation))
));
fields.push(format!(
"\"vertical_align\":{}",
json_string(&format!("{:?}", style.vertical_align))
));
fields.push(format!(
"\"white_space\":{}",
json_string(&format!("{:?}", style.white_space))
));
fields.push(format!(
"\"text_wrap_mode\":{}",
json_string(&format!("{:?}", style.text_wrap_mode))
));
fields.push(format!(
"\"text_wrap_style\":{}",
json_string(&format!("{:?}", style.text_wrap_style))
));
fields.push(format!(
"\"list_style_type\":{}",
json_string(&format!("{:?}", style.list_style_type))
));
fields.push(format!(
"\"list_style_marker\":{}",
json_opt_string(style.list_style_marker.clone())
));
fields.push(format!(
"\"list_style_symbols\":{}",
json_opt_string(
style
.list_style_symbols
.as_ref()
.map(|symbols| format!("{:?}", symbols))
)
));
fields.push(format!(
"\"list_style_image\":{}",
json_opt_string(style.list_style_image.clone())
));
fields.push(format!(
"\"list_style_position\":{}",
json_string(&format!("{:?}", style.list_style_position))
));
fields.push(format!(
"\"color\":{}",
json_string(&color_to_hex(style.color))
));
fields.push(format!(
"\"background\":{}",
json_opt_string(style.background_color.map(color_to_hex))
));
if let Some(paint) = &style.background_paint {
fields.push(format!(
"\"background_paint\":{}",
json_string(&format!("{:?}", paint))
));
}
fields.push(format!(
"\"background_paint_count\":{}",
style.background_paints.len()
));
fields.push(format!(
"\"background_size_count\":{}",
style.background_sizes.len()
));
fields.push(format!(
"\"background_position_count\":{}",
style.background_positions.len()
));
fields.push(format!(
"\"background_repeat_count\":{}",
style.background_repeats.len()
));
fields.push(format!(
"\"background_attachment_count\":{}",
style.background_attachments.len()
));
fields.push(format!(
"\"background_blend_mode_count\":{}",
style.background_blend_modes.len()
));
fields.push(format!(
"\"background_origin_count\":{}",
style.background_origins.len()
));
fields.push(format!(
"\"background_clip_count\":{}",
style.background_clips.len()
));
fields.push(format!(
"\"clip_path_inset\":{}",
json_opt_string(style.clip_path_inset.map(clip_path_inset_debug))
));
fields.push(format!(
"\"clip_path\":{}",
json_opt_string(clip_path_debug(style))
));
fields.push(format!(
"\"paint_filter\":{}",
json_opt_string(style.paint_filter.as_ref().map(filter_spec_debug))
));
fields.push(format!(
"\"backdrop_filter\":{}",
json_opt_string(style.backdrop_filter.as_ref().map(filter_spec_debug))
));
fields.push(format!(
"\"will_change_backdrop_root\":{}",
style.will_change_backdrop_root
));
fields.push(format!(
"\"mask_backdrop_root\":{}",
style.mask_backdrop_root
));
fields.push(format!(
"\"mix_blend_mode\":{}",
json_string(&format!("{:?}", style.mix_blend_mode))
));
fields.push(format!("\"isolation\":{}", style.isolation));
fields.push(format!("\"opacity\":{:.3}", style.opacity));
fields.push(format!(
"\"border_width\":{}",
json_string(&edges_debug(style.border_width))
));
fields.push(format!(
"\"border_style\":{}",
json_string(&border_style_debug(style.border_style))
));
fields.push(format!(
"\"border_color\":{}",
json_opt_string(style.border_color.map(color_to_hex))
));
fields.push(format!(
"\"border_top_color\":{}",
json_opt_string(style.border_top_color.map(color_to_hex))
));
fields.push(format!(
"\"border_right_color\":{}",
json_opt_string(style.border_right_color.map(color_to_hex))
));
fields.push(format!(
"\"border_bottom_color\":{}",
json_opt_string(style.border_bottom_color.map(color_to_hex))
));
fields.push(format!(
"\"border_left_color\":{}",
json_opt_string(style.border_left_color.map(color_to_hex))
));
fields.push(format!(
"\"border_collapse\":{}",
json_string(&format!("{:?}", style.border_collapse))
));
fields.push(format!(
"\"caption_side\":{}",
json_string(&format!("{:?}", style.caption_side))
));
fields.push(format!(
"\"border_spacing\":{}",
json_string(&border_spacing_debug(style.border_spacing))
));
fields.push(format!(
"\"table_layout\":{}",
json_string(&format!("{:?}", style.table_layout))
));
fields.push(format!(
"\"empty_cells\":{}",
json_string(if style.empty_cells_hide {
"Hide"
} else {
"Show"
})
));
fields.push(format!(
"\"border_radius\":{}",
json_string(&border_radius_debug(style.border_radius))
));
fields.push(format!(
"\"outline_width\":{}",
json_string(&length_spec_debug(style.outline_width))
));
fields.push(format!(
"\"outline_offset\":{}",
json_string(&length_spec_debug(style.outline_offset))
));
fields.push(format!(
"\"outline_style\":{}",
json_string(&format!("{:?}", style.outline_style))
));
fields.push(format!(
"\"outline_color\":{}",
json_string(&color_to_hex(style.resolved_outline_color()))
));
fields.push(format!("\"outline_visible\":{}", style.outline_visible));
fields.push(format!(
"\"margin\":{}",
json_string(&edges_debug(style.margin))
));
fields.push(format!(
"\"padding\":{}",
json_string(&edges_debug(style.padding))
));
fields.push(format!(
"\"width\":{}",
json_string(&length_spec_debug(style.width))
));
fields.push(format!(
"\"max_width\":{}",
json_string(&length_spec_debug(style.max_width))
));
fields.push(format!(
"\"min_width\":{}",
json_string(&length_spec_debug(style.min_width))
));
fields.push(format!(
"\"height\":{}",
json_string(&length_spec_debug(style.height))
));
fields.push(format!(
"\"min_height\":{}",
json_string(&length_spec_debug(style.min_height))
));
fields.push(format!(
"\"max_height\":{}",
json_string(&length_spec_debug(style.max_height))
));
fields.push(format!(
"\"box_sizing\":{}",
json_string(&format!("{:?}", style.box_sizing))
));
fields.push(format!(
"\"inset_left\":{}",
json_string(&length_spec_debug(style.inset_left))
));
fields.push(format!(
"\"inset_top\":{}",
json_string(&length_spec_debug(style.inset_top))
));
fields.push(format!(
"\"inset_right\":{}",
json_string(&length_spec_debug(style.inset_right))
));
fields.push(format!(
"\"inset_bottom\":{}",
json_string(&length_spec_debug(style.inset_bottom))
));
fields.push(format!(
"\"row_gap\":{}",
json_string(&length_spec_debug(style.row_gap))
));
fields.push(format!(
"\"gap\":{}",
json_string(&length_spec_debug(style.gap))
));
fields.push(format!(
"\"grid_columns\":{}",
style
.grid_columns
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string())
));
fields.push(format!(
"\"grid_rows\":{}",
style
.grid_rows
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string())
));
fields.push(format!(
"\"grid_column_start\":{}",
style
.grid_column_start
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string())
));
fields.push(format!(
"\"grid_row_start\":{}",
style
.grid_row_start
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string())
));
fields.push(format!(
"\"flex_direction\":{}",
json_string(&format!("{:?}", style.flex_direction))
));
fields.push(format!(
"\"flex_wrap\":{}",
json_string(&format!("{:?}", style.flex_wrap))
));
fields.push(format!(
"\"justify_content\":{}",
json_string(&format!("{:?}", style.justify_content))
));
fields.push(format!(
"\"align_items\":{}",
json_string(&format!("{:?}", style.align_items))
));
fields.push(format!(
"\"align_self\":{}",
json_string(&format!("{:?}", style.align_self))
));
fields.push(format!(
"\"align_content\":{}",
json_string(&format!("{:?}", style.align_content))
));
fields.push(format!(
"\"flex_basis\":{}",
json_string(&length_spec_debug(style.flex_basis))
));
fields.push(format!("\"order\":{}", style.order));
fields.push(format!("\"flex_grow\":{:.3}", style.flex_grow));
fields.push(format!("\"flex_shrink\":{:.3}", style.flex_shrink));
fields.push(format!(
"\"overflow\":{}",
json_string(&format!("{:?}", style.overflow))
));
fields.push(format!(
"\"break_before\":{}",
json_string(&format!("{:?}", style.pagination.break_before))
));
fields.push(format!(
"\"break_after\":{}",
json_string(&format!("{:?}", style.pagination.break_after))
));
fields.push(format!(
"\"break_inside\":{}",
json_string(&format!("{:?}", style.pagination.break_inside))
));
fields.push(format!("\"orphans\":{}", style.pagination.orphans));
fields.push(format!("\"widows\":{}", style.pagination.widows));
format!("{{{}}}", fields.join(","))
}
fn format_element_path(element: &ElementInfo, ancestors: &[ElementInfo]) -> String {
let mut parts = Vec::new();
for ancestor in ancestors {
parts.push(format_element_segment(ancestor));
}
parts.push(format_element_segment(element));
parts.join(" > ")
}
fn format_element_segment(info: &ElementInfo) -> String {
let mut out = info.tag.clone();
if let Some(id) = &info.id {
out.push('#');
out.push_str(id);
}
for class in &info.classes {
out.push('.');
out.push_str(class);
}
if info.child_count > 0 {
out.push_str(&format!(":nth-child({})", info.child_index));
}
out
}
fn recompose_transform_ops(style: &mut ComputedStyle) {
style.transform.clear();
style.transform.extend_from_slice(&style.translate);
style.transform.extend_from_slice(&style.rotate);
style.transform.extend_from_slice(&style.scale);
style.transform.extend_from_slice(&style.transform_list);
}
fn sync_text_decoration_current_color(style: &mut ComputedStyle) {
if style.text_decoration_color_tracks_current {
style.text_decoration_color = style.color;
}
}
fn sync_text_emphasis_current_color(style: &mut ComputedStyle) {
if style.text_emphasis_color_tracks_current {
style.text_emphasis_color = style.color;
}
}
fn apply_text_decoration_color_spec_to_computed(
computed: &mut ComputedStyle,
parent: &ComputedStyle,
spec: &ColorSpec,
) {
computed.pending_text_decoration_color_var = None;
match spec {
ColorSpec::Value(value) => {
computed.text_decoration_color = *value;
computed.text_decoration_color_tracks_current = false;
}
ColorSpec::AlphaValue(value, alpha) => {
computed.text_decoration_color = blend_over_white(*value, *alpha);
computed.text_decoration_color_tracks_current = false;
}
ColorSpec::Inherit => {
computed.text_decoration_color = parent.text_decoration_color;
computed.text_decoration_color_tracks_current = false;
}
ColorSpec::Initial | ColorSpec::CurrentColor => {
computed.text_decoration_color_tracks_current = true;
computed.text_decoration_color = computed.color;
}
}
}
fn apply_text_emphasis_color_spec_to_computed(
computed: &mut ComputedStyle,
parent: &ComputedStyle,
spec: &ColorSpec,
) {
match spec {
ColorSpec::Value(value) => {
computed.text_emphasis_color = *value;
computed.text_emphasis_color_tracks_current = false;
}
ColorSpec::AlphaValue(value, alpha) => {
computed.text_emphasis_color = blend_over_white(*value, *alpha);
computed.text_emphasis_color_tracks_current = false;
}
ColorSpec::Inherit => {
computed.text_emphasis_color = parent.text_emphasis_color;
computed.text_emphasis_color_tracks_current = parent.text_emphasis_color_tracks_current;
}
ColorSpec::Initial | ColorSpec::CurrentColor => {
computed.text_emphasis_color_tracks_current = true;
computed.text_emphasis_color = computed.color;
}
}
}
fn normalize_text_decoration_thickness(
value: TextDecorationThicknessMode,
) -> TextDecorationThicknessMode {
match value {
TextDecorationThicknessMode::Length(spec) => match spec {
LengthSpec::Absolute(value) if value >= Pt::ZERO => {
TextDecorationThicknessMode::Length(LengthSpec::Absolute(value))
}
LengthSpec::Percent(value) if value >= 0.0 => {
TextDecorationThicknessMode::Length(LengthSpec::Percent(value))
}
LengthSpec::Em(value) if value >= 0.0 => {
TextDecorationThicknessMode::Length(LengthSpec::Em(value))
}
LengthSpec::Rem(value) if value >= 0.0 => {
TextDecorationThicknessMode::Length(LengthSpec::Rem(value))
}
LengthSpec::Calc(calc) => TextDecorationThicknessMode::Length(LengthSpec::Calc(calc)),
_ => TextDecorationThicknessMode::Auto,
},
TextDecorationThicknessMode::Auto | TextDecorationThicknessMode::FromFont => value,
}
}
#[derive(Debug, Clone, Copy)]
struct FontSizeRelativeMetrics {
ex: Pt,
ch: Pt,
cap: Pt,
lh: Pt,
rlh: Pt,
}
fn font_size_relative_metrics(
style: &ComputedStyle,
registry: Option<&FontRegistry>,
) -> FontSizeRelativeMetrics {
let fallback_ex = style.font_size.mul_ratio(1, 2);
let fallback_ch = style.font_size.mul_ratio(3, 5);
let fallback_cap = style.font_size.mul_ratio(7, 10);
let (ex, ch, cap) = registry
.and_then(|registry| {
registry.css_font_relative_metrics(style.font_name.as_ref(), style.font_size)
})
.unwrap_or((fallback_ex, fallback_ch, fallback_cap));
FontSizeRelativeMetrics {
ex,
ch,
cap,
lh: style.line_height.to_line_height(style.font_size),
rlh: style.root_line_height,
}
}
fn resolve_font_size_spec(
font_size: &FontSizeSpec,
parent_font_size: Pt,
root_font_size: Pt,
viewport: Size,
metrics: FontSizeRelativeMetrics,
) -> Option<Pt> {
let value = match font_size {
FontSizeSpec::AbsolutePt(value) => *value,
FontSizeSpec::RelativeScale(scale) => parent_font_size.mul_fixed(*scale),
FontSizeSpec::Calc(calc) => calc.resolve(parent_font_size, root_font_size, viewport),
FontSizeSpec::FontRelative(value) => {
value
.base
.resolve(parent_font_size, parent_font_size, root_font_size)
+ metrics.ex * value.ex
+ metrics.ch * value.ch
+ metrics.cap * value.cap
+ metrics.lh * value.lh
+ metrics.rlh * value.rlh
}
FontSizeSpec::Inherit => parent_font_size,
FontSizeSpec::Initial => root_font_size,
};
(value >= Pt::ZERO).then_some(value)
}
fn resolve_computed_line_height(style: &mut ComputedStyle, viewport: Size) {
if let Some(value) = resolve_line_height_spec_to_computed(
&style.line_height,
style.font_size,
style.root_font_size,
viewport,
) {
style.line_height = value;
}
}
fn resolve_line_height_spec_to_computed(
line_height: &LineHeightSpec,
font_size: Pt,
root_font_size: Pt,
viewport: Size,
) -> Option<LineHeightSpec> {
match line_height {
LineHeightSpec::Normal => Some(LineHeightSpec::Normal),
LineHeightSpec::Number(value) if *value >= 0.0 => Some(LineHeightSpec::Number(*value)),
LineHeightSpec::Number(_) => None,
LineHeightSpec::AbsolutePt(value) if *value >= Pt::ZERO => {
Some(LineHeightSpec::AbsolutePt(*value))
}
LineHeightSpec::AbsolutePt(_) => None,
LineHeightSpec::RelativeScale(scale) if *scale >= I32F32::from_num(0.0) => {
Some(LineHeightSpec::AbsolutePt(font_size.mul_fixed(*scale)))
}
LineHeightSpec::RelativeScale(_) => None,
LineHeightSpec::Calc(calc) => {
let value = calc.resolve(font_size, root_font_size, viewport);
(value >= Pt::ZERO).then_some(LineHeightSpec::AbsolutePt(value))
}
LineHeightSpec::Inherit | LineHeightSpec::Initial => None,
}
}
fn resolve_font_relative_lengths(style: &mut ComputedStyle, registry: Option<&FontRegistry>) {
let fallback_ex = style.font_size.mul_ratio(1, 2);
let fallback_ch = style.font_size.mul_ratio(3, 5);
let fallback_cap = style.font_size.mul_ratio(7, 10);
let (ex, ch, cap) = registry
.and_then(|registry| {
registry.css_font_relative_metrics(style.font_name.as_ref(), style.font_size)
})
.unwrap_or((fallback_ex, fallback_ch, fallback_cap));
let line_height = style.line_height.to_line_height(style.font_size);
let root_line_height = style.root_line_height;
let resolve = |spec: LengthSpec| match spec {
LengthSpec::FontRelative(value) => {
let mut calc = value.base;
calc.abs = calc.abs
+ ex * value.ex
+ ch * value.ch
+ cap * value.cap
+ line_height * value.lh
+ root_line_height * value.rlh;
length_spec_from_calc_length(calc)
}
value => value,
};
style.width = resolve(style.width);
style.height = resolve(style.height);
style.min_width = resolve(style.min_width);
style.max_width = resolve(style.max_width);
style.min_height = resolve(style.min_height);
style.max_height = resolve(style.max_height);
style.margin.top = resolve(style.margin.top);
style.margin.right = resolve(style.margin.right);
style.margin.bottom = resolve(style.margin.bottom);
style.margin.left = resolve(style.margin.left);
style.padding.top = resolve(style.padding.top);
style.padding.right = resolve(style.padding.right);
style.padding.bottom = resolve(style.padding.bottom);
style.padding.left = resolve(style.padding.left);
style.border_width.top = resolve(style.border_width.top);
style.border_width.right = resolve(style.border_width.right);
style.border_width.bottom = resolve(style.border_width.bottom);
style.border_width.left = resolve(style.border_width.left);
style.inset_top = resolve(style.inset_top);
style.inset_right = resolve(style.inset_right);
style.inset_bottom = resolve(style.inset_bottom);
style.inset_left = resolve(style.inset_left);
style.text_indent = resolve(style.text_indent);
style.outline_width = resolve(style.outline_width);
style.outline_offset = resolve(style.outline_offset);
style.flex_basis = resolve(style.flex_basis);
style.row_gap = resolve(style.row_gap);
style.gap = resolve(style.gap);
style.column_rule_width = resolve(style.column_rule_width);
if let TabSizeSpec::Length(value) = style.tab_size {
style.tab_size = TabSizeSpec::Length(resolve(value));
}
for value in style.custom_lengths.values_mut() {
*value = resolve(*value);
}
}
fn resolve_font_size_adjust_scale(style: &mut ComputedStyle, registry: Option<&FontRegistry>) {
style.font_size_adjust_scale = I32F32::from_num(1.0);
let Some(desired_aspect) = style.font_size_adjust else {
return;
};
let base_font_size = style.font_size.mul_fixed(style.font_face_size_adjust);
if base_font_size <= Pt::ZERO {
return;
}
let actual_ex = registry
.and_then(|registry| {
registry
.css_font_relative_metrics(style.font_name.as_ref(), base_font_size)
.map(|(ex, _, _)| ex)
})
.unwrap_or_else(|| base_font_size.mul_ratio(1, 2));
if actual_ex <= Pt::ZERO {
return;
}
let desired_ex = style.font_size.mul_fixed(desired_aspect);
let scale = desired_ex.to_f32() / actual_ex.to_f32();
if scale.is_finite() && scale >= 0.0 {
style.font_size_adjust_scale = I32F32::from_num(scale.clamp(0.0, 1024.0));
}
}
fn replace_computed_style_for_all(computed: &mut ComputedStyle, source: &ComputedStyle) {
// CSS-wide `all` leaves custom properties and `direction` untouched. Keep
// the element's custom-property environment while replacing every other
// supported computed property from the requested cascade-wide baseline.
let direction = computed.direction;
let root_font_size = computed.root_font_size;
let root_line_height = computed.root_line_height;
let custom_lengths = std::mem::take(&mut computed.custom_lengths);
let custom_colors = std::mem::take(&mut computed.custom_colors);
let custom_color_alpha = std::mem::take(&mut computed.custom_color_alpha);
let custom_color_refs = std::mem::take(&mut computed.custom_color_refs);
let custom_raw_values = std::mem::take(&mut computed.custom_raw_values);
let custom_font_stacks = std::mem::take(&mut computed.custom_font_stacks);
*computed = source.clone();
computed.direction = direction;
computed.root_font_size = root_font_size;
computed.root_line_height = root_line_height;
computed.custom_lengths = custom_lengths;
computed.custom_colors = custom_colors;
computed.custom_color_alpha = custom_color_alpha;
computed.custom_color_refs = custom_color_refs;
computed.custom_raw_values = custom_raw_values;
computed.custom_font_stacks = custom_font_stacks;
}
fn apply_delta(
computed: &mut ComputedStyle,
delta: &StyleDelta,
custom_property_registrations: &HashMap<String, RegisteredCustomProperty>,
parent: &ComputedStyle,
font_registry: Option<&FontRegistry>,
parent_font_size: Pt,
parent_line_height: LineHeightSpec,
root_font_size: Pt,
element: Option<&ElementInfo>,
viewport: Size,
) {
if let Some(font_size) = &delta.font_size {
computed.pending_font_size_var = None;
computed.font_size = resolve_font_size_spec(
font_size,
parent_font_size,
root_font_size,
viewport,
font_size_relative_metrics(parent, font_registry),
)
.unwrap_or(parent_font_size);
} else if let Some(var) = &delta.font_size_var {
computed.font_size = parent_font_size;
computed.pending_font_size_var = Some(var.clone());
}
if let Some(adjust) = delta.font_size_adjust {
computed.font_size_adjust = match adjust {
FontSizeAdjustSpec::None | FontSizeAdjustSpec::Initial => None,
FontSizeAdjustSpec::Number(value) => Some(value),
FontSizeAdjustSpec::Inherit => parent.font_size_adjust,
};
}
if let Some(line_height) = &delta.line_height {
computed.pending_line_height_var = None;
computed.line_height = match line_height {
LineHeightSpec::Inherit => parent_line_height,
LineHeightSpec::Initial => LineHeightSpec::Normal,
_ => line_height.clone(),
};
} else if let Some(var) = &delta.line_height_var {
computed.line_height = parent_line_height;
computed.pending_line_height_var = Some(var.clone());
}
if let Some(color) = &delta.color {
computed.color = match color {
ColorSpec::Value(value) => *value,
ColorSpec::AlphaValue(value, alpha) => blend_over_white(*value, *alpha),
ColorSpec::Inherit => parent.color,
ColorSpec::Initial => Color::BLACK,
ColorSpec::CurrentColor => computed.color,
};
}
if let Some(var) = &delta.color_var {
computed.pending_color_var = Some(var.clone());
}
sync_text_decoration_current_color(computed);
sync_text_emphasis_current_color(computed);
if let Some(keyword) = delta.background_reset {
match keyword {
CascadeWideKeyword::Inherit => {
computed.background_color = parent.background_color;
computed.background_source_color = parent.background_source_color;
computed.background_alpha = parent.background_alpha;
computed.background_paint = parent.background_paint.clone();
computed.background_paints = parent.background_paints.clone();
computed.background_sizes = parent.background_sizes.clone();
computed.background_positions = parent.background_positions.clone();
computed.background_repeats = parent.background_repeats.clone();
computed.background_attachments = parent.background_attachments.clone();
computed.background_blend_modes = parent.background_blend_modes.clone();
computed.background_origins = parent.background_origins.clone();
computed.background_clips = parent.background_clips.clone();
computed.pending_background_color_var = parent.pending_background_color_var.clone();
}
CascadeWideKeyword::Initial | CascadeWideKeyword::Unset => {
computed.background_color = None;
computed.background_source_color = None;
computed.background_alpha = 1.0;
computed.background_paint = None;
computed.background_paints.clear();
computed.background_sizes.clear();
computed.background_positions.clear();
computed.background_repeats.clear();
computed.background_attachments.clear();
computed.background_blend_modes.clear();
computed.background_origins.clear();
computed.background_clips.clear();
computed.pending_background_color_var = None;
}
}
}
if let Some(color) = &delta.background_color {
let (flattened, source, alpha) = match color {
BackgroundSpec::Value(value, alpha) => (
Some(blend_over_white(*value, *alpha)),
Some(*value),
alpha.clamp(0.0, 1.0),
),
BackgroundSpec::Transparent => (None, None, 0.0),
BackgroundSpec::Inherit => (
parent.background_color,
parent.background_source_color,
parent.background_alpha,
),
BackgroundSpec::CurrentColor => (Some(computed.color), Some(computed.color), 1.0),
BackgroundSpec::Initial => (None, None, 1.0),
};
computed.background_color = flattened;
computed.background_source_color = source;
computed.background_alpha = alpha;
}
if let Some(paint) = &delta.background_paint {
computed.background_paint = Some(paint.clone());
computed.background_paints = vec![paint.clone()];
}
if let Some(paints) = &delta.background_paints {
computed.background_paint = paints.first().cloned();
computed.background_paints = paints.clone();
}
if let Some(sizes) = &delta.background_sizes {
computed.background_sizes = sizes.clone();
}
if let Some(positions) = &delta.background_positions {
computed.background_positions = positions.clone();
}
if let Some(components) = &delta.background_position_x {
merge_background_position_axis(
&mut computed.background_positions,
components,
BackgroundPositionAxis::X,
);
}
if let Some(components) = &delta.background_position_y {
merge_background_position_axis(
&mut computed.background_positions,
components,
BackgroundPositionAxis::Y,
);
}
if let Some(repeats) = &delta.background_repeats {
computed.background_repeats = repeats.clone();
}
if let Some(attachments) = &delta.background_attachments {
computed.background_attachments = attachments.clone();
}
if let Some(modes) = &delta.background_blend_modes {
computed.background_blend_modes = modes.clone();
}
if let Some(origins) = &delta.background_origins {
computed.background_origins = origins.clone();
}
if let Some(clips) = &delta.background_clips {
computed.background_clips = clips.clone();
}
if let Some(paints) = &delta.mask_paints {
computed.mask.paints = paints.clone();
computed.mask_backdrop_root = computed.mask.has_effective_source();
}
if let Some(sizes) = &delta.mask_sizes {
computed.mask.sizes = sizes.clone();
}
if let Some(positions) = &delta.mask_positions {
computed.mask.positions = positions.clone();
}
if let Some(repeats) = &delta.mask_repeats {
computed.mask.repeats = repeats.clone();
}
if let Some(origins) = &delta.mask_origins {
computed.mask.origins = origins.clone();
}
if let Some(clips) = &delta.mask_clips {
computed.mask.clips = clips.clone();
}
if let Some(modes) = &delta.mask_modes {
computed.mask.modes = modes.clone();
}
if let Some(composites) = &delta.mask_composites {
computed.mask.composites = composites.clone();
}
if delta.mask_border.reset {
computed.mask.border = BorderImageSpec::default();
}
if let Some(source) = &delta.mask_border.source {
computed.mask.border.source = source.clone();
computed.mask_backdrop_root = computed.mask.has_effective_source();
}
if let Some(slice) = delta.mask_border.slice {
computed.mask.border.slice = slice;
}
if let Some(fill) = delta.mask_border.fill {
computed.mask.border.fill = fill;
}
if let Some(width) = delta.mask_border.width {
computed.mask.border.width = width;
}
if let Some(outset) = delta.mask_border.outset {
computed.mask.border.outset = outset;
}
if let Some((repeat_x, repeat_y)) = delta.mask_border.repeat {
computed.mask.border.repeat_x = repeat_x;
computed.mask.border.repeat_y = repeat_y;
}
if let Some(var) = &delta.background_color_var {
computed.pending_background_color_var = Some(var.clone());
}
if let Some(font_name) = &delta.font_name {
match font_name {
FontSpec::Value(stack) => {
if !stack.is_empty() {
computed.font_stack = stack.clone();
computed.font_name = stack[0].clone();
computed.font_unicode_ranges = vec![None; stack.len()];
computed.font_face_family = None;
computed.font_face_source_count = 0;
computed.font_face_size_adjust = I32F32::from_num(1.0);
computed.font_face_satisfies_weight = false;
computed.font_face_satisfies_style = false;
}
}
FontSpec::Inherit => {
computed.font_stack = parent.font_stack.clone();
computed.font_name = parent.font_name.clone();
computed.font_unicode_ranges = parent.font_unicode_ranges.clone();
computed.font_face_family = parent.font_face_family.clone();
computed.font_face_source_count = parent.font_face_source_count;
computed.font_face_size_adjust = parent.font_face_size_adjust;
computed.font_face_satisfies_weight = parent.font_face_satisfies_weight;
computed.font_face_satisfies_style = parent.font_face_satisfies_style;
}
FontSpec::Initial => {
computed.font_stack = vec![Arc::<str>::from("Helvetica")];
computed.font_name = Arc::<str>::from("Helvetica");
computed.font_unicode_ranges = vec![None];
computed.font_face_family = None;
computed.font_face_source_count = 0;
computed.font_face_size_adjust = I32F32::from_num(1.0);
computed.font_face_satisfies_weight = false;
computed.font_face_satisfies_style = false;
}
}
}
if let Some(var) = &delta.font_name_var {
computed.pending_font_name_var = Some(var.clone());
computed.font_unicode_ranges = vec![None; computed.font_stack.len()];
computed.font_face_family = None;
computed.font_face_source_count = 0;
computed.font_face_size_adjust = I32F32::from_num(1.0);
computed.font_face_satisfies_weight = false;
computed.font_face_satisfies_style = false;
}
if let Some(weight) = delta.font_weight {
computed.pending_font_weight_var = None;
computed.font_weight = match weight {
FontWeightSpec::Value(value) => value,
FontWeightSpec::Inherit => parent.font_weight,
FontWeightSpec::Initial => 400,
FontWeightSpec::Bolder => relative_bolder_font_weight(parent.font_weight),
FontWeightSpec::Lighter => relative_lighter_font_weight(parent.font_weight),
};
} else if let Some(var) = &delta.font_weight_var {
computed.pending_font_weight_var = Some(var.clone());
}
if let Some(spec) = delta.font_kerning {
computed.font_kerning = match spec {
FontFeatureToggleSpec::Value(value) => value,
FontFeatureToggleSpec::Inherit => parent.font_kerning,
FontFeatureToggleSpec::Initial => true,
};
}
if let Some(spec) = delta.common_ligatures {
computed.common_ligatures = match spec {
FontFeatureToggleSpec::Value(value) => value,
FontFeatureToggleSpec::Inherit => parent.common_ligatures,
FontFeatureToggleSpec::Initial => true,
};
}
if let Some(spec) = delta.small_caps {
computed.small_caps = match spec {
FontFeatureToggleSpec::Value(value) => value,
FontFeatureToggleSpec::Inherit => parent.small_caps,
FontFeatureToggleSpec::Initial => false,
};
}
if let Some(spec) = delta.font_stretch {
computed.font_stretch = match spec {
FontStretchSpec::Value(value) => value,
FontStretchSpec::Inherit => parent.font_stretch,
FontStretchSpec::Initial => 1000,
};
}
if let Some(enabled) = delta.font_synthesis_weight {
computed.font_synthesis_weight = enabled;
}
if let Some(style) = delta.font_style {
computed.pending_font_style_var = None;
computed.font_style = style;
} else if let Some(var) = &delta.font_style_var {
computed.pending_font_style_var = Some(var.clone());
}
if let Some(transform) = delta.text_transform {
computed.text_transform = match transform {
TextTransformSpec::Value(value) => value,
TextTransformSpec::Inherit => parent.text_transform,
TextTransformSpec::Initial => TextTransformMode::None,
};
}
if let Some(transform) = &delta.translate {
computed.pending_translate_var = None;
computed.translate = match transform {
TransformSpec::Value(value) => value.clone(),
TransformSpec::Inherit => parent.translate.clone(),
TransformSpec::Initial => Vec::new(),
};
}
if delta.translate.is_none() {
if let Some(var) = &delta.translate_var {
computed.pending_translate_var = Some(var.clone());
}
}
if let Some(transform) = &delta.rotate {
computed.pending_rotate_var = None;
computed.rotate = match transform {
TransformSpec::Value(value) => value.clone(),
TransformSpec::Inherit => parent.rotate.clone(),
TransformSpec::Initial => Vec::new(),
};
}
if delta.rotate.is_none() {
if let Some(var) = &delta.rotate_var {
computed.pending_rotate_var = Some(var.clone());
}
}
if let Some(transform) = &delta.scale {
computed.pending_scale_var = None;
computed.scale = match transform {
TransformSpec::Value(value) => value.clone(),
TransformSpec::Inherit => parent.scale.clone(),
TransformSpec::Initial => Vec::new(),
};
}
if delta.scale.is_none() {
if let Some(var) = &delta.scale_var {
computed.pending_scale_var = Some(var.clone());
}
}
if let Some(transform) = &delta.transform {
computed.pending_transform_var = None;
computed.transform_list = match transform {
TransformSpec::Value(value) => value.clone(),
TransformSpec::Inherit => parent.transform_list.clone(),
TransformSpec::Initial => Vec::new(),
};
}
if delta.transform.is_none() {
if let Some(var) = &delta.transform_var {
computed.pending_transform_var = Some(var.clone());
}
}
recompose_transform_ops(computed);
if let Some(transform_origin) = &delta.transform_origin {
computed.transform_origin = match transform_origin {
TransformOriginSpec::Value(value) => *value,
TransformOriginSpec::Inherit => parent.transform_origin,
TransformOriginSpec::Initial => CssTransformOrigin::center(),
};
}
if let Some(transform_box) = delta.transform_box {
computed.transform_box = match transform_box {
TransformBoxSpec::Value(value) => value,
TransformBoxSpec::Inherit => parent.transform_box,
TransformBoxSpec::Initial => TransformBoxMode::BorderBox,
};
}
if let Some(transform_style) = delta.transform_style {
computed.transform_style = match transform_style {
TransformStyleSpec::Value(value) => value,
TransformStyleSpec::Inherit => parent.transform_style,
TransformStyleSpec::Initial => TransformStyleMode::Flat,
};
}
if let Some(perspective_origin) = delta.perspective_origin {
computed.perspective_origin = match perspective_origin {
TransformOriginSpec::Value(value) => value,
TransformOriginSpec::Inherit => parent.perspective_origin,
TransformOriginSpec::Initial => CssTransformOrigin::center(),
};
}
if let Some(decoration) = &delta.text_decoration {
computed.text_decoration = match decoration {
TextDecorationSpec::Value(value) => *value,
TextDecorationSpec::Inherit => parent.text_decoration,
TextDecorationSpec::Initial => TextDecorationMode::default(),
};
// `text-decoration-line` itself is not inherited. A declaration on
// this box establishes a new decoration root; only the implicit copy
// performed while descending an inline tree is propagated paint.
computed.text_decoration_propagated = false;
}
if let Some(color) = &delta.text_decoration_color {
apply_text_decoration_color_spec_to_computed(computed, parent, color);
} else if let Some(var) = &delta.text_decoration_color_var {
computed.pending_text_decoration_color_var = Some(var.clone());
}
if let Some(thickness) = &delta.text_decoration_thickness {
computed.pending_text_decoration_thickness_var = None;
computed.text_decoration_thickness = match thickness {
TextDecorationThicknessSpec::Value(value) => {
normalize_text_decoration_thickness(*value)
}
TextDecorationThicknessSpec::Inherit => parent.text_decoration_thickness,
TextDecorationThicknessSpec::Initial => TextDecorationThicknessMode::Auto,
};
} else if let Some(var) = &delta.text_decoration_thickness_var {
computed.pending_text_decoration_thickness_var = Some(var.clone());
}
if let Some(decoration_style) = &delta.text_decoration_style {
computed.text_decoration_style = match decoration_style {
TextDecorationStyleSpec::Value(value) => *value,
TextDecorationStyleSpec::Inherit => parent.text_decoration_style,
TextDecorationStyleSpec::Initial => TextDecorationStyleMode::Solid,
};
}
if let Some(skip_ink) = &delta.text_decoration_skip_ink {
computed.text_decoration_skip_ink = match skip_ink {
TextDecorationSkipInkSpec::Value(value) => *value,
TextDecorationSkipInkSpec::Inherit => parent.text_decoration_skip_ink,
TextDecorationSkipInkSpec::Initial => TextDecorationSkipInkMode::Auto,
};
}
if let Some(emphasis_style) = &delta.text_emphasis_style {
computed.text_emphasis_style = match emphasis_style {
TextEmphasisStyleSpec::Value(value) => *value,
TextEmphasisStyleSpec::Inherit => parent.text_emphasis_style,
TextEmphasisStyleSpec::Initial => TextEmphasisStyleMode::None,
};
}
if let Some(color) = &delta.text_emphasis_color {
apply_text_emphasis_color_spec_to_computed(computed, parent, color);
}
if let Some(position) = &delta.text_emphasis_position {
computed.text_emphasis_position = match position {
TextEmphasisPositionSpec::Value(value) => *value,
TextEmphasisPositionSpec::Inherit => parent.text_emphasis_position,
TextEmphasisPositionSpec::Initial => TextEmphasisPositionMode::Over,
};
}
if let Some(initial_letter) = delta.initial_letter {
computed.initial_letter = match initial_letter {
InitialLetterSpec::Value(value) => Some(value),
InitialLetterSpec::Inherit => parent.initial_letter,
InitialLetterSpec::Initial => None,
};
}
if let Some(offset) = &delta.text_underline_offset {
computed.pending_text_underline_offset_var = None;
computed.text_underline_offset = match offset {
TextUnderlineOffsetSpec::Value(value) => *value,
TextUnderlineOffsetSpec::Inherit => parent.text_underline_offset,
TextUnderlineOffsetSpec::Initial => TextUnderlineOffsetMode::Auto,
};
} else if let Some(var) = &delta.text_underline_offset_var {
computed.pending_text_underline_offset_var = Some(var.clone());
}
if let Some(position) = &delta.text_underline_position {
computed.pending_text_underline_position_var = None;
computed.text_underline_position = match position {
TextUnderlinePositionSpec::Value(value) => *value,
TextUnderlinePositionSpec::Inherit => parent.text_underline_position,
TextUnderlinePositionSpec::Initial => TextUnderlinePositionMode::auto(),
};
} else if let Some(var) = &delta.text_underline_position_var {
computed.pending_text_underline_position_var = Some(var.clone());
}
if let Some(shadows) = &delta.text_shadow {
computed.pending_text_shadow_var = None;
computed.text_shadows = match shadows {
TextShadowSpec::Value(value) => value.clone(),
TextShadowSpec::Inherit => parent.text_shadows.clone(),
TextShadowSpec::Initial => Vec::new(),
};
} else if let Some(var) = &delta.text_shadow_var {
computed.pending_text_shadow_var = Some(var.clone());
}
if let Some(overflow) = delta.text_overflow {
computed.text_overflow = match overflow {
TextOverflowSpec::Value(value) => value,
TextOverflowSpec::Inherit => parent.text_overflow,
TextOverflowSpec::Initial => TextOverflowMode::Clip,
};
}
if let Some(indent) = delta.text_indent {
match indent.value {
LengthSpec::Inherit => {
computed.text_indent = parent.text_indent;
computed.text_indent_hanging = parent.text_indent_hanging;
computed.text_indent_each_line = parent.text_indent_each_line;
}
LengthSpec::Initial => {
computed.text_indent = LengthSpec::Absolute(Pt::ZERO);
computed.text_indent_hanging = false;
computed.text_indent_each_line = false;
}
value => {
computed.text_indent = normalize_length_spec(value, parent.text_indent);
computed.text_indent_hanging = indent.hanging;
computed.text_indent_each_line = indent.each_line;
}
}
}
if let Some(quotes) = &delta.quotes {
computed.quotes = match quotes {
QuotesSpec::Value(pairs) => pairs.clone(),
QuotesSpec::Inherit => parent.quotes.clone(),
QuotesSpec::Initial => default_quotes(),
};
}
if let Some(content) = &delta.content {
if let Some(resolved) = resolve_content_spec(content, parent, element, &computed.quotes) {
computed.content = Some(resolved.text);
computed.generated_content = Some(resolved.parts);
} else {
computed.content = None;
computed.generated_content = None;
}
}
if let Some(counter_reset) = &delta.counter_reset {
computed.counter_reset = counter_reset.clone();
}
if let Some(counter_increment) = &delta.counter_increment {
computed.counter_increment = counter_increment.clone();
}
if let Some(counter_set) = &delta.counter_set {
computed.counter_set = counter_set.clone();
}
if let Some(word_break) = delta.word_break {
computed.word_break = match word_break {
WordBreakSpec::Value(value) => value,
WordBreakSpec::Inherit => parent.word_break,
WordBreakSpec::Initial => WordBreakMode::Normal,
};
}
if let Some(overflow_wrap) = delta.overflow_wrap {
computed.overflow_wrap = match overflow_wrap {
OverflowWrapSpec::Value(value) => value,
OverflowWrapSpec::Inherit => parent.overflow_wrap,
OverflowWrapSpec::Initial => OverflowWrapMode::Normal,
};
}
if let Some(line_break) = delta.line_break {
computed.pending_line_break_var = None;
computed.line_break = match line_break {
LineBreakSpec::Value(value) => value,
LineBreakSpec::Inherit => parent.line_break,
LineBreakSpec::Initial => LineBreakMode::Auto,
};
} else if let Some(var) = &delta.line_break_var {
computed.pending_line_break_var = Some(var.clone());
}
if let Some((list_style_type, marker, symbols)) = &delta.list_style_type {
computed.list_style_type = *list_style_type;
computed.list_style_marker = marker.clone();
computed.list_style_symbols = symbols.clone();
}
if let Some(list_style_image) = &delta.list_style_image {
match list_style_image {
ListStyleImageSpec::Url(source) => {
computed.list_style_image = Some(source.clone());
computed.list_style_image_paint = None;
}
ListStyleImageSpec::Image { css, paint } => {
let resolved_paint = paint
.clone()
.or_else(|| parse_background_paint_with_style(computed, css));
computed.list_style_image = Some(css.clone());
computed.list_style_image_paint = resolved_paint;
}
ListStyleImageSpec::None | ListStyleImageSpec::Initial => {
computed.list_style_image = None;
computed.list_style_image_paint = None;
}
ListStyleImageSpec::Inherit => {
computed.list_style_image = parent.list_style_image.clone();
computed.list_style_image_paint = parent.list_style_image_paint.clone();
}
};
}
if let Some(list_style_position) = delta.list_style_position {
computed.list_style_position = match list_style_position {
ListStylePositionSpec::Value(value) => value,
ListStylePositionSpec::Inherit => parent.list_style_position,
ListStylePositionSpec::Initial => ListStylePositionMode::Outside,
};
}
if let Some(spacing) = &delta.letter_spacing {
let resolved = normalize_length_spec(*spacing, LengthSpec::Absolute(parent.letter_spacing));
computed.letter_spacing = match resolved {
LengthSpec::Absolute(value) => value,
LengthSpec::Percent(pct) => computed.font_size * pct,
LengthSpec::Em(scale) => computed.font_size * scale,
LengthSpec::Rem(scale) => root_font_size * scale,
LengthSpec::Calc(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::Clamped(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::FontRelative(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => Pt::ZERO,
};
}
if let Some(spacing) = &delta.word_spacing {
let resolved = normalize_length_spec(*spacing, LengthSpec::Absolute(parent.word_spacing));
computed.word_spacing = match resolved {
LengthSpec::Absolute(value) => value,
LengthSpec::Percent(pct) => computed.font_size * pct,
LengthSpec::Em(scale) => computed.font_size * scale,
LengthSpec::Rem(scale) => root_font_size * scale,
LengthSpec::Calc(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::Clamped(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::FontRelative(calc) => {
calc.resolve(computed.font_size, computed.font_size, root_font_size)
}
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => Pt::ZERO,
};
}
if let Some(tab_size) = delta.tab_size {
computed.tab_size = normalize_tab_size_spec(tab_size, parent.tab_size);
}
if let Some(value) = delta.pagination.break_before {
computed.pagination.break_before = value;
}
if let Some(value) = delta.pagination.break_after {
computed.pagination.break_after = value;
}
if let Some(value) = delta.pagination.break_inside {
computed.pagination.break_inside = value;
}
if let Some(value) = delta.page_name {
computed.pagination.page_name = value;
}
if let Some(value) = delta.pagination.orphans {
computed.pagination.orphans = resolve_pagination_line_count(
value,
parent.pagination.orphans,
Pagination::default().orphans,
);
}
if let Some(value) = delta.pagination.widows {
computed.pagination.widows = resolve_pagination_line_count(
value,
parent.pagination.widows,
Pagination::default().widows,
);
}
if let Some(direction) = delta.direction {
computed.direction = match direction {
DirectionSpec::Value(value) => value,
DirectionSpec::Inherit => parent.direction,
DirectionSpec::Initial => DirectionMode::Ltr,
};
}
if let Some(unicode_bidi) = delta.unicode_bidi {
computed.unicode_bidi = match unicode_bidi {
UnicodeBidiSpec::Value(value) => value,
UnicodeBidiSpec::Inherit => parent.unicode_bidi,
UnicodeBidiSpec::Initial => UnicodeBidiMode::Normal,
};
}
if let Some(writing_mode) = delta.writing_mode {
computed.writing_mode = match writing_mode {
WritingModeSpec::Value(value) => value,
WritingModeSpec::Inherit => parent.writing_mode,
WritingModeSpec::Initial => WritingModeMode::HorizontalTb,
};
}
if let Some(text_orientation) = delta.text_orientation {
computed.text_orientation = match text_orientation {
TextOrientationSpec::Value(value) => value,
TextOrientationSpec::Inherit => parent.text_orientation,
TextOrientationSpec::Initial => TextOrientationMode::Mixed,
};
}
apply_edge_delta(&mut computed.margin, &delta.margin, &parent.margin);
apply_logical_box_edge_delta(
&mut computed.margin,
&parent.margin,
delta.margin_inline_start,
delta.margin_inline_end,
delta.margin_block_start,
delta.margin_block_end,
computed.writing_mode,
computed.direction,
);
apply_edge_delta(&mut computed.padding, &delta.padding, &parent.padding);
apply_logical_box_edge_delta(
&mut computed.padding,
&parent.padding,
delta.padding_inline_start,
delta.padding_inline_end,
delta.padding_block_start,
delta.padding_block_end,
computed.writing_mode,
computed.direction,
);
if let Some(width) = delta.width {
computed.width = normalize_size_spec(width, parent.width);
computed.pending_width_var = None;
}
if let Some(min_width) = delta.min_width {
computed.min_width = normalize_size_spec(min_width, parent.min_width);
computed.pending_min_width_var = None;
}
if let Some(max_width) = delta.max_width {
computed.max_width = normalize_size_spec(max_width, parent.max_width);
computed.pending_max_width_var = None;
}
if let Some(height) = delta.height {
computed.height = normalize_size_spec(height, parent.height);
computed.pending_height_var = None;
}
if let Some(min_height) = delta.min_height {
computed.min_height = normalize_size_spec(min_height, parent.min_height);
computed.pending_min_height_var = None;
}
if let Some(max_height) = delta.max_height {
computed.max_height = normalize_size_spec(max_height, parent.max_height);
computed.pending_max_height_var = None;
}
if let Some(aspect_ratio) = delta.aspect_ratio {
computed.aspect_ratio = match aspect_ratio {
AspectRatioSpec::Ratio(value) => Some(value),
AspectRatioSpec::Inherit => parent.aspect_ratio,
AspectRatioSpec::Auto | AspectRatioSpec::Initial => None,
};
}
apply_logical_size_delta(computed, delta, parent);
if let Some(var) = &delta.width_var {
// A winning declaration that becomes invalid after var() substitution
// resolves to the property's unset value; it must not expose a lower
// declaration from the cascade.
computed.width = LengthSpec::Auto;
computed.pending_width_var = Some(var.clone());
}
if let Some(var) = &delta.max_width_var {
computed.max_width = LengthSpec::Auto;
computed.pending_max_width_var = Some(var.clone());
}
if let Some(var) = &delta.height_var {
computed.height = LengthSpec::Auto;
computed.pending_height_var = Some(var.clone());
}
if let Some(var) = &delta.min_width_var {
computed.min_width = LengthSpec::Auto;
computed.pending_min_width_var = Some(var.clone());
}
if let Some(var) = &delta.min_height_var {
computed.min_height = LengthSpec::Auto;
computed.pending_min_height_var = Some(var.clone());
}
if let Some(var) = &delta.max_height_var {
computed.max_height = LengthSpec::Auto;
computed.pending_max_height_var = Some(var.clone());
}
apply_logical_size_var_delta(computed, delta);
if let Some(align) = delta.text_align {
computed.text_align = if matches!(align, TextAlignMode::MatchParent) {
parent.text_align
} else {
align
};
}
if let Some(align) = delta.text_align_last {
computed.text_align_last = match align {
TextAlignLastSpec::Value(value) => value,
TextAlignLastSpec::Inherit => parent.text_align_last,
TextAlignLastSpec::Initial => TextAlignLastMode::Auto,
};
}
if let Some(justify) = delta.text_justify {
computed.pending_text_justify_var = None;
computed.text_justify = match justify {
TextJustifySpec::Value(value) => value,
TextJustifySpec::Inherit => parent.text_justify,
TextJustifySpec::Initial => TextJustifyMode::Auto,
};
} else if let Some(var) = &delta.text_justify_var {
computed.pending_text_justify_var = Some(var.clone());
}
if let Some(hyphens) = delta.hyphens {
computed.pending_hyphens_var = None;
computed.hyphens = match hyphens {
HyphensSpec::Value(value) => value,
HyphensSpec::Inherit => parent.hyphens,
HyphensSpec::Initial => HyphensMode::Manual,
};
} else if let Some(var) = &delta.hyphens_var {
computed.pending_hyphens_var = Some(var.clone());
}
if let Some(hyphenate_character) = &delta.hyphenate_character {
computed.pending_hyphenate_character_var = None;
computed.hyphenate_character = match hyphenate_character {
HyphenateCharacterSpec::Auto => None,
HyphenateCharacterSpec::Value(value) => Some(value.clone()),
HyphenateCharacterSpec::Inherit => parent.hyphenate_character.clone(),
HyphenateCharacterSpec::Initial => None,
};
} else if let Some(var) = &delta.hyphenate_character_var {
computed.pending_hyphenate_character_var = Some(var.clone());
}
if let Some(mode) = delta.text_wrap_mode {
computed.pending_text_wrap_mode_var = None;
computed.text_wrap_mode = match mode {
TextWrapModeSpec::Value(value) => value,
TextWrapModeSpec::Inherit => parent.text_wrap_mode,
TextWrapModeSpec::Initial => TextWrapMode::Wrap,
};
} else if let Some(var) = &delta.text_wrap_mode_var {
computed.pending_text_wrap_mode_var = Some(var.clone());
}
if let Some(style) = delta.text_wrap_style {
computed.pending_text_wrap_style_var = None;
computed.text_wrap_style = match style {
TextWrapStyleSpec::Value(value) => value,
TextWrapStyleSpec::Inherit => parent.text_wrap_style,
TextWrapStyleSpec::Initial => TextWrapStyleMode::Auto,
};
} else if let Some(var) = &delta.text_wrap_style_var {
computed.pending_text_wrap_style_var = Some(var.clone());
}
if let Some(align) = delta.vertical_align {
computed.pending_vertical_align_var = None;
computed.vertical_align = align;
} else if let Some(var) = &delta.vertical_align_var {
computed.pending_vertical_align_var = Some(var.clone());
}
sync_text_decoration_current_color(computed);
sync_text_emphasis_current_color(computed);
if let Some(spec) = delta.border_width.top {
computed.border_width.top = normalize_length_spec(spec, parent.border_width.top);
}
if let Some(spec) = delta.border_width.right {
computed.border_width.right = normalize_length_spec(spec, parent.border_width.right);
}
if let Some(spec) = delta.border_width.bottom {
computed.border_width.bottom = normalize_length_spec(spec, parent.border_width.bottom);
}
if let Some(spec) = delta.border_width.left {
computed.border_width.left = normalize_length_spec(spec, parent.border_width.left);
}
apply_logical_box_edge_delta(
&mut computed.border_width,
&parent.border_width,
delta.border_inline_start_width,
delta.border_inline_end_width,
delta.border_block_start_width,
delta.border_block_end_width,
computed.writing_mode,
computed.direction,
);
if let Some(color) = &delta.border_color {
let (resolved, alpha) = match color {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => (
parent.border_color.unwrap_or(parent.color),
parent.border_top_alpha,
),
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed.color, 1.0),
};
computed.border_color = Some(resolved);
computed.border_top_color = Some(resolved);
computed.border_right_color = Some(resolved);
computed.border_bottom_color = Some(resolved);
computed.border_left_color = Some(resolved);
computed.border_top_alpha = alpha;
computed.border_right_alpha = alpha;
computed.border_bottom_alpha = alpha;
computed.border_left_alpha = alpha;
computed.pending_border_top_color_var = None;
computed.pending_border_right_color_var = None;
computed.pending_border_bottom_color_var = None;
computed.pending_border_left_color_var = None;
}
if let Some(var) = &delta.border_color_var {
computed.pending_border_color_var = Some(var.clone());
computed.pending_border_top_color_var = Some(var.clone());
computed.pending_border_right_color_var = Some(var.clone());
computed.pending_border_bottom_color_var = Some(var.clone());
computed.pending_border_left_color_var = Some(var.clone());
}
if let Some(color) = &delta.border_top_color {
let (resolved, alpha) = match color {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => (
parent
.border_top_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_top_alpha,
),
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed.color, 1.0),
};
computed.border_top_color = Some(resolved);
computed.border_top_alpha = alpha;
}
if let Some(color) = &delta.border_right_color {
let (resolved, alpha) = match color {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => (
parent
.border_right_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_right_alpha,
),
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed.color, 1.0),
};
computed.border_right_color = Some(resolved);
computed.border_right_alpha = alpha;
}
if let Some(color) = &delta.border_bottom_color {
let (resolved, alpha) = match color {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => (
parent
.border_bottom_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_bottom_alpha,
),
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed.color, 1.0),
};
computed.border_bottom_color = Some(resolved);
computed.border_bottom_alpha = alpha;
}
if let Some(color) = &delta.border_left_color {
let (resolved, alpha) = match color {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => (
parent
.border_left_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_left_alpha,
),
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed.color, 1.0),
};
computed.border_left_color = Some(resolved);
computed.border_left_alpha = alpha;
}
if let Some(var) = &delta.border_top_color_var {
computed.pending_border_top_color_var = Some(var.clone());
}
if let Some(var) = &delta.border_right_color_var {
computed.pending_border_right_color_var = Some(var.clone());
}
if let Some(var) = &delta.border_bottom_color_var {
computed.pending_border_bottom_color_var = Some(var.clone());
}
if let Some(var) = &delta.border_left_color_var {
computed.pending_border_left_color_var = Some(var.clone());
}
apply_logical_border_color_delta(
computed,
delta.border_inline_start_color.as_ref(),
delta.border_inline_end_color.as_ref(),
delta.border_block_start_color.as_ref(),
delta.border_block_end_color.as_ref(),
parent,
);
apply_logical_border_color_var_delta(
computed,
delta.border_inline_start_color_var.as_ref(),
delta.border_inline_end_color_var.as_ref(),
delta.border_block_start_color_var.as_ref(),
delta.border_block_end_color_var.as_ref(),
);
if let Some(style) = delta.border_style.top {
computed.border_style.top = style;
}
if let Some(style) = delta.border_style.right {
computed.border_style.right = style;
}
if let Some(style) = delta.border_style.bottom {
computed.border_style.bottom = style;
}
if let Some(style) = delta.border_style.left {
computed.border_style.left = style;
}
apply_logical_border_style_delta(
&mut computed.border_style,
delta.border_inline_start_style,
delta.border_inline_end_style,
delta.border_block_start_style,
delta.border_block_end_style,
computed.writing_mode,
computed.direction,
);
apply_border_cascade_ops(computed, &delta.border_cascade_ops, parent);
if let Some(spec) = delta.outline_width {
computed.outline_width = normalize_length_spec(spec, parent.outline_width);
}
if let Some(spec) = delta.outline_offset {
computed.outline_offset = normalize_length_spec(spec, parent.outline_offset);
computed.pending_outline_offset_var = None;
}
if delta.outline_offset.is_none() {
if let Some(var) = &delta.outline_offset_var {
computed.pending_outline_offset_var = Some(var.clone());
}
}
if let Some(style) = delta.outline_style {
computed.outline_style = style;
}
if let Some(color) = &delta.outline_color {
computed.outline_color = match color {
ColorSpec::Value(value) => Some(*value),
ColorSpec::AlphaValue(value, alpha) => Some(blend_over_white(*value, *alpha)),
ColorSpec::Inherit => Some(parent.resolved_outline_color()),
ColorSpec::Initial | ColorSpec::CurrentColor => None,
};
}
if let Some(visible) = delta.outline_visible {
computed.outline_visible = visible;
}
if let Some(mode) = delta.border_collapse {
computed.border_collapse = mode;
}
if let Some(side) = delta.caption_side {
computed.caption_side = side;
}
if let Some(spacing) = &delta.border_spacing {
computed.border_spacing = *spacing;
}
if let Some(mode) = delta.table_layout {
computed.table_layout = mode;
}
if let Some(hide) = delta.empty_cells_hide {
computed.empty_cells_hide = hide;
}
if delta.border_radius_ops.is_empty() {
if let Some(radius) = &delta.border_radius {
computed.border_radius = *radius;
computed.pending_border_radius_var = None;
}
} else {
apply_border_radius_ops(computed, &delta.border_radius_ops);
computed.pending_border_radius_var = None;
}
if let Some(mode) = delta.box_decoration_break {
computed.box_decoration_break = mode;
}
if delta.border_image.reset {
computed.border_image = BorderImageSpec::default();
}
if let Some(source) = &delta.border_image.source {
computed.border_image.source = source.clone();
}
if let Some(slice) = delta.border_image.slice {
computed.border_image.slice = slice;
}
if let Some(fill) = delta.border_image.fill {
computed.border_image.fill = fill;
}
if let Some(width) = delta.border_image.width {
computed.border_image.width = width;
}
if let Some(outset) = delta.border_image.outset {
computed.border_image.outset = outset;
}
if let Some((repeat_x, repeat_y)) = delta.border_image.repeat {
computed.border_image.repeat_x = repeat_x;
computed.border_image.repeat_y = repeat_y;
}
if delta.border_radius.is_none() && delta.border_radius_ops.is_empty() {
if let Some(var) = &delta.border_radius_var {
computed.pending_border_radius_var = Some(var.clone());
}
} else if let Some(var) = &delta.border_radius_var {
computed.pending_border_radius_var = Some(var.clone());
}
if let Some(shadow) = &delta.box_shadow {
computed.box_shadow = Some(shadow.clone());
}
if let Some(shadows) = &delta.box_shadows {
computed.box_shadows = shadows.clone();
computed.box_shadow = computed.box_shadows.first().cloned();
}
if let Some(raw) = &delta.paint_filter_raw {
if let Some(mut filter) =
parse_filter_spec_str_for_computed(raw, computed.font_size, computed.root_font_size)
{
resolve_filter_current_color(&mut filter, computed.color);
computed.paint_filter_stacking_context = !raw.trim().eq_ignore_ascii_case("none");
computed.paint_filter = if filter.is_identity() {
None
} else {
Some(filter)
};
}
} else if let Some(filter) = &delta.paint_filter {
let mut filter = filter.clone();
resolve_filter_current_color(&mut filter, computed.color);
computed.paint_filter_stacking_context = !filter.is_identity();
computed.paint_filter = if filter.is_identity() {
None
} else {
Some(filter)
};
}
if let Some(raw) = &delta.backdrop_filter_raw {
if let Some(mut filter) = parse_backdrop_filter_spec_str_for_computed(
raw,
computed.font_size,
computed.root_font_size,
) {
resolve_filter_current_color(&mut filter, computed.color);
computed.backdrop_filter = if filter.is_identity() {
None
} else {
Some(filter)
};
}
} else if let Some(filter) = &delta.backdrop_filter {
let mut filter = filter.clone();
resolve_filter_current_color(&mut filter, computed.color);
computed.backdrop_filter = if filter.is_identity() {
None
} else {
Some(filter)
};
}
if let Some(root) = delta.will_change_backdrop_root {
computed.will_change_backdrop_root = root;
}
if let Some(root) = delta.mask_backdrop_root {
computed.mask_backdrop_root = root;
}
if delta.mask_paints.is_some() || !delta.mask_border.is_empty() {
computed.mask_backdrop_root = computed.mask.has_effective_source();
}
if let Some(mode) = delta.mix_blend_mode {
computed.mix_blend_mode = mode;
}
if let Some(spec) = delta.isolation {
computed.isolation = match spec {
IsolationSpec::Value(value) => value,
IsolationSpec::Inherit => parent.isolation,
IsolationSpec::Initial => false,
};
}
if let Some(spec) = delta.opacity {
computed.pending_opacity_var = None;
computed.opacity = match spec {
OpacitySpec::Value(value) => value.clamp(0.0, 1.0),
OpacitySpec::Inherit => parent.opacity,
OpacitySpec::Initial => 1.0,
};
} else if let Some(expr) = &delta.opacity_var {
computed.pending_opacity_var = Some(expr.clone());
}
if let Some(spec) = &delta.container_names {
computed.container_names = match spec {
ContainerNamesSpec::Value(value) => value.clone(),
ContainerNamesSpec::Inherit => parent.container_names.clone(),
ContainerNamesSpec::Initial => Vec::new(),
};
}
if let Some(spec) = delta.container_type {
computed.container_type = match spec {
ContainerQueryTypeSpec::Value(value) => value,
ContainerQueryTypeSpec::Inherit => parent.container_type,
ContainerQueryTypeSpec::Initial => ContainerQueryType::Normal,
};
}
if let Some(clip_path) = &delta.clip_path {
computed.clip_path = match clip_path {
ClipPathSpec::Shape { shape, .. } => Some(shape.clone()),
ClipPathSpec::None => None,
ClipPathSpec::Inherit => parent.clip_path.clone(),
ClipPathSpec::Initial => None,
};
computed.clip_path_reference_box = match clip_path {
ClipPathSpec::Shape { reference_box, .. } => *reference_box,
ClipPathSpec::Inherit => parent.clip_path_reference_box,
ClipPathSpec::None | ClipPathSpec::Initial => ClipPathReferenceBox::Border,
};
computed.clip_path_inset = match computed.clip_path.as_ref() {
Some(ClipPathShapeSpec::Inset(spec)) => Some(*spec),
_ => None,
};
}
if let Some(clip) = delta.legacy_clip {
computed.legacy_clip = match clip {
LegacyClipSpec::Rect(rect) => Some(rect),
LegacyClipSpec::None | LegacyClipSpec::Initial => None,
LegacyClipSpec::Inherit => parent.legacy_clip,
};
}
if let Some(mode) = delta.white_space {
computed.pending_white_space_var = None;
computed.white_space = match mode {
WhiteSpaceSpec::Value(value) => value,
WhiteSpaceSpec::Inherit => parent.white_space,
WhiteSpaceSpec::Initial => WhiteSpaceMode::Normal,
};
} else if let Some(var) = &delta.white_space_var {
computed.pending_white_space_var = Some(var.clone());
}
if let Some(display) = delta.display {
computed.display = match display {
DisplaySpec::Value(value) => value,
DisplaySpec::Inherit => parent.display,
DisplaySpec::Initial => DisplayMode::Inline,
};
}
if let Some(visibility) = delta.visibility {
computed.visibility = match visibility {
VisibilitySpec::Value(value) => value,
VisibilitySpec::Inherit => parent.visibility,
VisibilitySpec::Initial => VisibilityMode::Visible,
};
}
if let Some(position) = delta.position {
computed.position = position;
}
if let Some(running_name) = &delta.running_name {
computed.running_name = running_name.clone();
}
if let Some(string_set) = &delta.string_set {
computed.string_set = match string_set {
NamedStringSetSpec::Value(assignments) => assignments.clone(),
NamedStringSetSpec::Inherit => parent.string_set.clone(),
NamedStringSetSpec::Initial => Vec::new(),
};
}
if let Some(float_mode) = delta.float_mode {
computed.float_mode = float_mode;
}
if let Some(policy) = delta.footnote_policy {
computed.footnote_policy = match policy {
FootnotePolicySpec::Value(value) => value,
FootnotePolicySpec::Inherit => parent.footnote_policy,
FootnotePolicySpec::Initial => FootnotePolicy::Auto,
};
}
if let Some(display) = delta.footnote_display {
computed.footnote_display = match display {
FootnoteDisplaySpec::Value(value) => value,
FootnoteDisplaySpec::Inherit => parent.footnote_display,
FootnoteDisplaySpec::Initial => FootnoteDisplay::Block,
};
}
if let Some(clear_mode) = delta.clear_mode {
computed.clear_mode = clear_mode;
}
if let Some(z_index) = delta.z_index {
match z_index {
ZIndexSpec::Auto => {
computed.z_index = 0;
computed.z_index_auto = true;
}
ZIndexSpec::Integer(value) => {
computed.z_index = value;
computed.z_index_auto = false;
}
ZIndexSpec::Inherit => {
computed.z_index = parent.z_index;
computed.z_index_auto = parent.z_index_auto;
}
}
}
if let Some(perspective) = delta.perspective {
computed.perspective = match perspective {
PerspectiveSpec::None => None,
PerspectiveSpec::Length(value) => Some(value),
PerspectiveSpec::Inherit => parent.perspective,
};
}
if let Some(box_sizing) = delta.box_sizing {
computed.box_sizing = box_sizing;
}
if let Some(inset_left) = delta.inset_left {
computed.inset_left = normalize_size_spec(inset_left, parent.inset_left);
computed.pending_inset_left_var = None;
}
if let Some(inset_top) = delta.inset_top {
computed.inset_top = normalize_size_spec(inset_top, parent.inset_top);
computed.pending_inset_top_var = None;
}
if let Some(inset_right) = delta.inset_right {
computed.inset_right = normalize_size_spec(inset_right, parent.inset_right);
computed.pending_inset_right_var = None;
}
if let Some(inset_bottom) = delta.inset_bottom {
computed.inset_bottom = normalize_size_spec(inset_bottom, parent.inset_bottom);
computed.pending_inset_bottom_var = None;
}
apply_logical_inset_delta(computed, delta, parent);
if let Some(var) = &delta.inset_left_var {
computed.pending_inset_left_var = Some(var.clone());
}
if let Some(var) = &delta.inset_top_var {
computed.pending_inset_top_var = Some(var.clone());
}
if let Some(var) = &delta.inset_right_var {
computed.pending_inset_right_var = Some(var.clone());
}
if let Some(var) = &delta.inset_bottom_var {
computed.pending_inset_bottom_var = Some(var.clone());
}
if let Some(dir) = delta.flex_direction {
computed.flex_direction = dir;
}
if let Some(wrap) = delta.flex_wrap {
computed.flex_wrap = wrap;
}
if let Some(basis) = delta.flex_basis {
computed.flex_basis = normalize_size_spec(basis, parent.flex_basis);
computed.pending_flex_basis_var = None;
}
if let Some(var) = &delta.flex_basis_var {
computed.pending_flex_basis_var = Some(var.clone());
}
if let Some(order) = delta.order {
computed.order = order;
}
if let Some(justify) = delta.justify_content {
computed.justify_content = justify;
}
if let Some(align) = delta.align_items {
computed.align_items = align;
}
if let Some(align_self) = delta.align_self {
computed.align_self = align_self;
}
if let Some(justify_items) = delta.justify_items {
computed.justify_items = justify_items;
}
if let Some(justify_self) = delta.justify_self {
computed.justify_self = justify_self;
}
if let Some(align_content) = delta.align_content {
computed.align_content = align_content;
}
if let Some(column_count) = delta.column_count {
computed.column_count = column_count.clamp(1, 256);
}
if let Some(column_count_auto) = delta.column_count_auto {
computed.column_count_auto = column_count_auto;
}
if let Some(column_width) = delta.column_width {
computed.column_width = normalize_length_spec(column_width, parent.column_width);
}
if let Some(column_fill) = delta.column_fill {
computed.column_fill = column_fill;
}
if let Some(column_span) = delta.column_span {
computed.column_span = match column_span {
ColumnSpanMode::Inherit => parent.column_span,
value => value,
};
}
if let Some(column_rule_width) = delta.column_rule_width {
computed.column_rule_width =
normalize_length_spec(column_rule_width, parent.column_rule_width);
}
if let Some(column_rule_style) = delta.column_rule_style {
match column_rule_style {
BorderLineStyle::None | BorderLineStyle::Hidden => {
computed.column_rule_visible = false;
computed.column_rule_style = OutlineLineStyle::Solid;
}
BorderLineStyle::Style(style) => {
computed.column_rule_visible = true;
computed.column_rule_style = style;
}
}
}
if let Some(column_rule_color) = &delta.column_rule_color {
computed.column_rule_color = match column_rule_color {
ColorSpec::Value(value) => Some(*value),
ColorSpec::AlphaValue(value, alpha) => Some(blend_over_white(*value, *alpha)),
ColorSpec::Inherit => Some(parent.resolved_column_rule_color()),
ColorSpec::Initial | ColorSpec::CurrentColor => None,
};
}
if let Some(columns) = delta.grid_columns {
computed.grid_columns = if columns == 0 { None } else { Some(columns) };
}
if let Some(rows) = delta.grid_rows {
computed.grid_rows = if rows == 0 { None } else { Some(rows) };
}
if let Some(tracks) = &delta.grid_column_tracks {
computed.grid_column_tracks = tracks.clone();
computed.grid_columns = (!tracks.is_empty()).then_some(tracks.len());
}
if let Some(tracks) = &delta.grid_row_tracks {
computed.grid_row_tracks = tracks.clone();
computed.grid_rows = (!tracks.is_empty()).then_some(tracks.len());
}
if let Some(subgrid) = delta.grid_subgrid_columns {
computed.grid_subgrid_columns = subgrid;
}
if let Some(subgrid) = delta.grid_subgrid_rows {
computed.grid_subgrid_rows = subgrid;
}
if let Some(raw) = &delta.grid_column_tracks_var {
computed.pending_grid_column_tracks_var = raw.clone();
}
if let Some(raw) = &delta.grid_row_tracks_var {
computed.pending_grid_row_tracks_var = raw.clone();
}
if let Some(line_names) = &delta.grid_column_line_names {
computed.grid_column_line_names = line_names.clone();
}
if let Some(line_names) = &delta.grid_row_line_names {
computed.grid_row_line_names = line_names.clone();
}
if let Some(tracks) = &delta.grid_auto_column_tracks {
computed.grid_auto_column_tracks = tracks.clone();
}
if let Some(tracks) = &delta.grid_auto_row_tracks {
computed.grid_auto_row_tracks = tracks.clone();
}
if let Some(flow) = delta.grid_auto_flow {
computed.grid_auto_flow = flow;
}
if let Some(repeat) = &delta.grid_column_auto_repeat {
computed.grid_column_auto_repeat = repeat.clone();
}
if let Some(repeat) = &delta.grid_row_auto_repeat {
computed.grid_row_auto_repeat = repeat.clone();
}
if let Some(areas) = &delta.grid_template_areas {
computed.grid_template_areas = areas.clone();
}
if let Some(line) = &delta.grid_column_line_start {
computed.grid_column_line_start = line.clone();
}
if let Some(line) = &delta.grid_column_line_end {
computed.grid_column_line_end = line.clone();
}
if let Some(line) = &delta.grid_row_line_start {
computed.grid_row_line_start = line.clone();
}
if let Some(line) = &delta.grid_row_line_end {
computed.grid_row_line_end = line.clone();
}
if let Some(name) = &delta.grid_area_name {
computed.grid_area_name = name.clone();
}
if let Some(column_start) = delta.grid_column_start {
computed.grid_column_start = if column_start == 0 {
None
} else {
Some(column_start)
};
}
if let Some(row_start) = delta.grid_row_start {
computed.grid_row_start = if row_start == 0 {
None
} else {
Some(row_start)
};
}
if let Some(row_gap) = delta.row_gap {
computed.row_gap = normalize_length_spec(row_gap, parent.row_gap);
computed.pending_row_gap_var = None;
}
if let Some(var) = &delta.row_gap_var {
computed.pending_row_gap_var = Some(var.clone());
}
if let Some(gap) = delta.gap {
computed.gap = normalize_length_spec(gap, parent.gap);
computed.pending_gap_var = None;
}
if let Some(var) = &delta.gap_var {
computed.pending_gap_var = Some(var.clone());
}
if let Some(column_gap_normal) = delta.column_gap_normal {
computed.column_gap_normal = column_gap_normal;
}
if let Some(grow) = delta.flex_grow {
computed.flex_grow = grow.max(0.0);
}
if let Some(shrink) = delta.flex_shrink {
computed.flex_shrink = shrink.max(0.0);
}
let mut overflow_changed = false;
if let Some(overflow_x) = delta.overflow_x {
computed.overflow_x = overflow_x;
overflow_changed = true;
}
if let Some(overflow_y) = delta.overflow_y {
computed.overflow_y = overflow_y;
overflow_changed = true;
}
let vertical_writing_mode = is_vertical_writing_mode(computed.writing_mode);
if let Some(overflow_inline) = delta.overflow_inline {
if vertical_writing_mode {
computed.overflow_y = overflow_inline;
} else {
computed.overflow_x = overflow_inline;
}
overflow_changed = true;
}
if let Some(overflow_block) = delta.overflow_block {
if vertical_writing_mode {
computed.overflow_x = overflow_block;
} else {
computed.overflow_y = overflow_block;
}
overflow_changed = true;
}
if overflow_changed {
computed.overflow = combine_overflow_modes(computed.overflow_x, computed.overflow_y);
}
if let Some(value) = delta.overflow_clip_margin {
computed.overflow_clip_margin = match value {
LengthSpec::Inherit => parent.overflow_clip_margin,
LengthSpec::Initial => LengthSpec::Absolute(Pt::ZERO),
_ => normalize_length_spec(value, parent.overflow_clip_margin),
};
}
if let Some(value) = delta.scrollbar_gutter {
computed.scrollbar_gutter = value;
}
if let Some(value) = delta.line_clamp {
computed.line_clamp = value;
}
if let Some(object_fit) = delta.object_fit {
computed.object_fit = match object_fit {
ObjectFitSpec::Value(value) => value,
ObjectFitSpec::Inherit => parent.object_fit,
ObjectFitSpec::Initial => ObjectFitMode::Fill,
};
}
if let Some(image_rendering) = delta.image_rendering {
computed.image_rendering = match image_rendering {
ImageRenderingSpec::Value(value) => value,
ImageRenderingSpec::Inherit => parent.image_rendering,
ImageRenderingSpec::Initial => ImageRenderingMode::Auto,
};
}
if let Some(object_position) = delta.object_position {
computed.object_position = match object_position {
ObjectPositionSpec::Value(value) => value,
ObjectPositionSpec::Inherit => parent.object_position,
ObjectPositionSpec::Initial => BackgroundPositionSpec::center(),
};
}
let invalid_registered_custom_properties: HashSet<String> = delta
.custom_raw_values
.iter()
.filter_map(|(name, raw)| {
custom_property_registrations
.get(name)
.filter(|registration| {
!registered_custom_property_value_is_valid(registration, raw)
})
.map(|_| name.clone())
})
.collect();
if !delta.custom_lengths.is_empty() {
for (k, v) in &delta.custom_lengths {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_lengths.insert(k.clone(), *v);
computed.custom_colors.remove(k);
computed.custom_color_alpha.remove(k);
computed.custom_color_refs.remove(k);
computed.custom_font_stacks.remove(k);
}
}
if !delta.custom_colors.is_empty() {
for (k, v) in &delta.custom_colors {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_colors.insert(k.clone(), *v);
computed.custom_lengths.remove(k);
computed.custom_color_refs.remove(k);
computed.custom_font_stacks.remove(k);
}
}
if !delta.custom_color_alpha.is_empty() {
for (k, v) in &delta.custom_color_alpha {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_color_alpha.insert(k.clone(), *v);
}
}
if !delta.custom_color_refs.is_empty() {
for (k, v) in &delta.custom_color_refs {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_color_refs.insert(k.clone(), v.clone());
computed.custom_lengths.remove(k);
computed.custom_colors.remove(k);
computed.custom_color_alpha.remove(k);
computed.custom_font_stacks.remove(k);
}
}
if !delta.custom_raw_values.is_empty() {
for (k, v) in &delta.custom_raw_values {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_raw_values.insert(k.clone(), v.clone());
}
}
if !delta.custom_font_stacks.is_empty() {
for (k, v) in &delta.custom_font_stacks {
if invalid_registered_custom_properties.contains(k) {
continue;
}
computed.custom_font_stacks.insert(k.clone(), v.clone());
computed.custom_lengths.remove(k);
computed.custom_colors.remove(k);
computed.custom_color_alpha.remove(k);
computed.custom_color_refs.remove(k);
}
}
for name in invalid_registered_custom_properties {
if let Some(registration) = custom_property_registrations.get(&name) {
apply_registered_custom_property_initial(computed, &name, registration);
}
}
apply_edge_var_delta(
&mut computed.margin,
&delta.margin,
&parent.margin,
&computed.custom_lengths,
&computed.custom_color_refs,
);
apply_logical_box_edge_var_delta(
&mut computed.margin,
&parent.margin,
&delta.margin_inline_start_var,
&delta.margin_inline_end_var,
&delta.margin_block_start_var,
&delta.margin_block_end_var,
computed.writing_mode,
computed.direction,
&computed.custom_lengths,
&computed.custom_color_refs,
);
apply_edge_var_delta(
&mut computed.padding,
&delta.padding,
&parent.padding,
&computed.custom_lengths,
&computed.custom_color_refs,
);
apply_logical_box_edge_var_delta(
&mut computed.padding,
&parent.padding,
&delta.padding_inline_start_var,
&delta.padding_inline_end_var,
&delta.padding_block_start_var,
&delta.padding_block_end_var,
computed.writing_mode,
computed.direction,
&computed.custom_lengths,
&computed.custom_color_refs,
);
apply_edge_var_delta(
&mut computed.border_width,
&delta.border_width,
&parent.border_width,
&computed.custom_lengths,
&computed.custom_color_refs,
);
apply_logical_box_edge_var_delta(
&mut computed.border_width,
&parent.border_width,
&delta.border_inline_start_width_var,
&delta.border_inline_end_width_var,
&delta.border_block_start_width_var,
&delta.border_block_end_width_var,
computed.writing_mode,
computed.direction,
&computed.custom_lengths,
&computed.custom_color_refs,
);
if computed.pagination.orphans == 0 {
computed.pagination.orphans = 1;
}
if computed.pagination.widows == 0 {
computed.pagination.widows = 1;
}
// Zero is a valid CSS font-size and is commonly used to remove the
// whitespace advance between inline-block boxes. Parsing already rejects
// negative sizes, so only an actually negative computed value is invalid.
if computed.font_size < Pt::ZERO {
computed.font_size = root_font_size;
}
}
fn apply_revert_layer_delta(
computed: &mut ComputedStyle,
delta: &RevertLayerDelta,
layer_base: &ComputedStyle,
) {
if delta.is_empty() {
return;
}
if delta.color {
computed.color = layer_base.color;
computed.pending_color_var = layer_base.pending_color_var.clone();
sync_text_decoration_current_color(computed);
sync_text_emphasis_current_color(computed);
}
if delta.background_color {
computed.background_color = layer_base.background_color;
computed.background_source_color = layer_base.background_source_color;
computed.background_alpha = layer_base.background_alpha;
computed.pending_background_color_var = layer_base.pending_background_color_var.clone();
}
if delta.width {
computed.width = layer_base.width;
computed.pending_width_var = layer_base.pending_width_var.clone();
}
if delta.height {
computed.height = layer_base.height;
computed.pending_height_var = layer_base.pending_height_var.clone();
}
if delta.box_sizing {
computed.box_sizing = layer_base.box_sizing;
}
if delta.display {
computed.display = layer_base.display;
}
if delta.visibility {
computed.visibility = layer_base.visibility;
}
if delta.position {
computed.position = layer_base.position;
computed.running_name = layer_base.running_name.clone();
}
if delta.float_mode {
computed.float_mode = layer_base.float_mode;
}
if delta.footnote_policy {
computed.footnote_policy = layer_base.footnote_policy;
}
if delta.footnote_display {
computed.footnote_display = layer_base.footnote_display;
}
if delta.clear_mode {
computed.clear_mode = layer_base.clear_mode;
}
if delta.z_index {
computed.z_index = layer_base.z_index;
computed.z_index_auto = layer_base.z_index_auto;
}
if delta.perspective {
computed.perspective = layer_base.perspective;
}
if delta.opacity {
computed.opacity = layer_base.opacity;
computed.pending_opacity_var = layer_base.pending_opacity_var.clone();
}
if delta.paint_filter {
computed.paint_filter = layer_base.paint_filter.clone();
computed.paint_filter_stacking_context = layer_base.paint_filter_stacking_context;
}
if delta.backdrop_filter {
computed.backdrop_filter = layer_base.backdrop_filter.clone();
}
if delta.will_change_backdrop_root {
computed.will_change_backdrop_root = layer_base.will_change_backdrop_root;
}
if delta.mask_backdrop_root {
computed.mask_backdrop_root = layer_base.mask_backdrop_root;
}
if delta.clip_path {
computed.clip_path = layer_base.clip_path.clone();
computed.clip_path_reference_box = layer_base.clip_path_reference_box;
computed.clip_path_inset = layer_base.clip_path_inset;
}
if delta.legacy_clip {
computed.legacy_clip = layer_base.legacy_clip;
}
if delta.box_shadow {
computed.box_shadow = layer_base.box_shadow.clone();
computed.box_shadows = layer_base.box_shadows.clone();
}
if delta.text_shadow {
computed.text_shadows = layer_base.text_shadows.clone();
computed.pending_text_shadow_var = layer_base.pending_text_shadow_var.clone();
}
if delta.text_decoration_line {
computed.text_decoration = layer_base.text_decoration;
computed.text_decoration_propagated = layer_base.text_decoration_propagated;
}
if delta.text_decoration_color {
computed.text_decoration_color = layer_base.text_decoration_color;
computed.text_decoration_color_tracks_current =
layer_base.text_decoration_color_tracks_current;
computed.pending_text_decoration_color_var =
layer_base.pending_text_decoration_color_var.clone();
sync_text_decoration_current_color(computed);
}
if delta.text_decoration_thickness {
computed.text_decoration_thickness = layer_base.text_decoration_thickness;
computed.pending_text_decoration_thickness_var =
layer_base.pending_text_decoration_thickness_var.clone();
}
if delta.text_decoration_style {
computed.text_decoration_style = layer_base.text_decoration_style;
}
if delta.text_decoration_skip_ink {
computed.text_decoration_skip_ink = layer_base.text_decoration_skip_ink;
}
if delta.text_emphasis_style {
computed.text_emphasis_style = layer_base.text_emphasis_style;
}
if delta.text_emphasis_color {
computed.text_emphasis_color = layer_base.text_emphasis_color;
computed.text_emphasis_color_tracks_current = layer_base.text_emphasis_color_tracks_current;
sync_text_emphasis_current_color(computed);
}
if delta.text_emphasis_position {
computed.text_emphasis_position = layer_base.text_emphasis_position;
}
if delta.text_underline_offset {
computed.text_underline_offset = layer_base.text_underline_offset;
computed.pending_text_underline_offset_var =
layer_base.pending_text_underline_offset_var.clone();
}
if delta.text_underline_position {
computed.text_underline_position = layer_base.text_underline_position;
computed.pending_text_underline_position_var =
layer_base.pending_text_underline_position_var.clone();
}
if delta.text_overflow {
computed.text_overflow = layer_base.text_overflow;
}
if delta.text_indent {
computed.text_indent = layer_base.text_indent;
computed.text_indent_hanging = layer_base.text_indent_hanging;
computed.text_indent_each_line = layer_base.text_indent_each_line;
}
if delta.letter_spacing {
computed.letter_spacing = layer_base.letter_spacing;
}
if delta.word_spacing {
computed.word_spacing = layer_base.word_spacing;
}
if delta.tab_size {
computed.tab_size = layer_base.tab_size;
}
if delta.text_align {
computed.text_align = layer_base.text_align;
}
if delta.text_align_last {
computed.text_align_last = layer_base.text_align_last;
}
if delta.text_justify {
computed.text_justify = layer_base.text_justify;
computed.pending_text_justify_var = layer_base.pending_text_justify_var.clone();
}
if delta.text_transform {
computed.text_transform = layer_base.text_transform;
}
if delta.white_space {
computed.white_space = layer_base.white_space;
computed.pending_white_space_var = layer_base.pending_white_space_var.clone();
}
if delta.text_wrap_mode {
computed.text_wrap_mode = layer_base.text_wrap_mode;
computed.pending_text_wrap_mode_var = layer_base.pending_text_wrap_mode_var.clone();
}
if delta.text_wrap_style {
computed.text_wrap_style = layer_base.text_wrap_style;
computed.pending_text_wrap_style_var = layer_base.pending_text_wrap_style_var.clone();
}
if delta.vertical_align {
computed.vertical_align = layer_base.vertical_align;
computed.pending_vertical_align_var = layer_base.pending_vertical_align_var.clone();
}
if delta.list_style_type {
computed.list_style_type = layer_base.list_style_type;
computed.list_style_marker = layer_base.list_style_marker.clone();
computed.list_style_symbols = layer_base.list_style_symbols.clone();
}
if delta.list_style_image {
computed.list_style_image = layer_base.list_style_image.clone();
computed.list_style_image_paint = layer_base.list_style_image_paint.clone();
}
if delta.list_style_position {
computed.list_style_position = layer_base.list_style_position;
}
if delta.word_break {
computed.word_break = layer_base.word_break;
}
if delta.overflow_wrap {
computed.overflow_wrap = layer_base.overflow_wrap;
}
if delta.direction {
computed.direction = layer_base.direction;
}
if delta.unicode_bidi {
computed.unicode_bidi = layer_base.unicode_bidi;
}
if delta.writing_mode {
computed.writing_mode = layer_base.writing_mode;
}
if delta.text_orientation {
computed.text_orientation = layer_base.text_orientation;
}
if delta.line_break {
computed.line_break = layer_base.line_break;
computed.pending_line_break_var = layer_base.pending_line_break_var.clone();
}
if delta.hyphens {
computed.hyphens = layer_base.hyphens;
computed.pending_hyphens_var = layer_base.pending_hyphens_var.clone();
}
if delta.hyphenate_character {
computed.hyphenate_character = layer_base.hyphenate_character.clone();
computed.pending_hyphenate_character_var =
layer_base.pending_hyphenate_character_var.clone();
}
if delta.mix_blend_mode {
computed.mix_blend_mode = layer_base.mix_blend_mode;
}
if delta.isolation {
computed.isolation = layer_base.isolation;
}
let mut overflow_reverted = false;
if delta.overflow_x {
computed.overflow_x = layer_base.overflow_x;
overflow_reverted = true;
}
if delta.overflow_y {
computed.overflow_y = layer_base.overflow_y;
overflow_reverted = true;
}
let vertical_writing_mode = is_vertical_writing_mode(computed.writing_mode);
if delta.overflow_inline {
if vertical_writing_mode {
computed.overflow_y = layer_base.overflow_y;
} else {
computed.overflow_x = layer_base.overflow_x;
}
overflow_reverted = true;
}
if delta.overflow_block {
if vertical_writing_mode {
computed.overflow_x = layer_base.overflow_x;
} else {
computed.overflow_y = layer_base.overflow_y;
}
overflow_reverted = true;
}
if overflow_reverted {
computed.overflow = combine_overflow_modes(computed.overflow_x, computed.overflow_y);
}
apply_revert_layer_insets(computed, &delta.inset, layer_base);
apply_revert_layer_logical_insets(
computed,
&delta.inset_logical,
layer_base,
computed.writing_mode,
computed.direction,
);
if delta.outline_width {
computed.outline_width = layer_base.outline_width;
}
if delta.outline_style {
computed.outline_style = layer_base.outline_style;
computed.outline_visible = layer_base.outline_visible;
}
if delta.outline_color {
computed.outline_color = layer_base.outline_color;
}
if delta.outline_offset {
computed.outline_offset = layer_base.outline_offset;
computed.pending_outline_offset_var = layer_base.pending_outline_offset_var.clone();
}
apply_revert_layer_edges(&mut computed.margin, &delta.margin, &layer_base.margin);
apply_revert_layer_edges(&mut computed.padding, &delta.padding, &layer_base.padding);
apply_revert_layer_logical_edges(
&mut computed.margin,
&delta.margin_logical,
&layer_base.margin,
computed.writing_mode,
computed.direction,
);
apply_revert_layer_logical_edges(
&mut computed.padding,
&delta.padding_logical,
&layer_base.padding,
computed.writing_mode,
computed.direction,
);
apply_revert_layer_border_colors(computed, &delta.border_color, layer_base);
apply_revert_layer_logical_border_colors(
computed,
&delta.border_color_logical,
layer_base,
computed.writing_mode,
computed.direction,
);
let border_width_reverted =
!delta.border_width.is_empty() || !delta.border_width_logical.is_empty();
let border_style_reverted =
!delta.border_style.is_empty() || !delta.border_style_logical.is_empty();
apply_revert_layer_edges(
&mut computed.border_width,
&delta.border_width,
&layer_base.border_width,
);
apply_revert_layer_logical_edges(
&mut computed.border_width,
&delta.border_width_logical,
&layer_base.border_width,
computed.writing_mode,
computed.direction,
);
apply_revert_layer_border_styles(
&mut computed.border_style,
&delta.border_style,
&layer_base.border_style,
);
apply_revert_layer_logical_border_styles(
&mut computed.border_style,
&delta.border_style_logical,
&layer_base.border_style,
computed.writing_mode,
computed.direction,
);
if border_width_reverted || border_style_reverted {
apply_border_style_mask(computed);
}
}
fn apply_revert_layer_edges(
computed: &mut EdgeSizes,
delta: &EdgeRevertDelta,
layer_base: &EdgeSizes,
) {
if delta.top {
computed.top = layer_base.top;
}
if delta.right {
computed.right = layer_base.right;
}
if delta.bottom {
computed.bottom = layer_base.bottom;
}
if delta.left {
computed.left = layer_base.left;
}
}
fn apply_revert_layer_logical_edges(
computed: &mut EdgeSizes,
delta: &LogicalEdgeRevertDelta,
layer_base: &EdgeSizes,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if delta.inline_start {
copy_revert_layer_edge(
computed,
inline_start_side(writing_mode, direction),
layer_base,
);
}
if delta.inline_end {
copy_revert_layer_edge(
computed,
inline_end_side(writing_mode, direction),
layer_base,
);
}
if delta.block_start {
copy_revert_layer_edge(computed, block_start_side(writing_mode), layer_base);
}
if delta.block_end {
copy_revert_layer_edge(computed, block_end_side(writing_mode), layer_base);
}
}
fn copy_revert_layer_edge(computed: &mut EdgeSizes, side: PhysicalSide, layer_base: &EdgeSizes) {
match side {
PhysicalSide::Left => computed.left = layer_base.left,
PhysicalSide::Top => computed.top = layer_base.top,
PhysicalSide::Right => computed.right = layer_base.right,
PhysicalSide::Bottom => computed.bottom = layer_base.bottom,
}
}
fn apply_revert_layer_insets(
computed: &mut ComputedStyle,
delta: &EdgeRevertDelta,
layer_base: &ComputedStyle,
) {
if delta.top {
copy_revert_layer_inset(computed, PhysicalSide::Top, layer_base);
}
if delta.right {
copy_revert_layer_inset(computed, PhysicalSide::Right, layer_base);
}
if delta.bottom {
copy_revert_layer_inset(computed, PhysicalSide::Bottom, layer_base);
}
if delta.left {
copy_revert_layer_inset(computed, PhysicalSide::Left, layer_base);
}
}
fn apply_revert_layer_logical_insets(
computed: &mut ComputedStyle,
delta: &LogicalEdgeRevertDelta,
layer_base: &ComputedStyle,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if delta.inline_start {
copy_revert_layer_inset(
computed,
inline_start_side(writing_mode, direction),
layer_base,
);
}
if delta.inline_end {
copy_revert_layer_inset(
computed,
inline_end_side(writing_mode, direction),
layer_base,
);
}
if delta.block_start {
copy_revert_layer_inset(computed, block_start_side(writing_mode), layer_base);
}
if delta.block_end {
copy_revert_layer_inset(computed, block_end_side(writing_mode), layer_base);
}
}
fn copy_revert_layer_inset(
computed: &mut ComputedStyle,
side: PhysicalSide,
layer_base: &ComputedStyle,
) {
match side {
PhysicalSide::Left => {
computed.inset_left = layer_base.inset_left;
computed.pending_inset_left_var = layer_base.pending_inset_left_var.clone();
}
PhysicalSide::Top => {
computed.inset_top = layer_base.inset_top;
computed.pending_inset_top_var = layer_base.pending_inset_top_var.clone();
}
PhysicalSide::Right => {
computed.inset_right = layer_base.inset_right;
computed.pending_inset_right_var = layer_base.pending_inset_right_var.clone();
}
PhysicalSide::Bottom => {
computed.inset_bottom = layer_base.inset_bottom;
computed.pending_inset_bottom_var = layer_base.pending_inset_bottom_var.clone();
}
}
}
fn apply_revert_layer_border_colors(
computed: &mut ComputedStyle,
delta: &EdgeRevertDelta,
layer_base: &ComputedStyle,
) {
if delta.top && delta.right && delta.bottom && delta.left {
computed.border_color = layer_base.border_color;
computed.pending_border_color_var = layer_base.pending_border_color_var.clone();
}
if delta.top {
copy_revert_layer_border_color(computed, PhysicalSide::Top, layer_base);
}
if delta.right {
copy_revert_layer_border_color(computed, PhysicalSide::Right, layer_base);
}
if delta.bottom {
copy_revert_layer_border_color(computed, PhysicalSide::Bottom, layer_base);
}
if delta.left {
copy_revert_layer_border_color(computed, PhysicalSide::Left, layer_base);
}
}
fn apply_revert_layer_logical_border_colors(
computed: &mut ComputedStyle,
delta: &LogicalEdgeRevertDelta,
layer_base: &ComputedStyle,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if delta.inline_start {
copy_revert_layer_border_color(
computed,
inline_start_side(writing_mode, direction),
layer_base,
);
}
if delta.inline_end {
copy_revert_layer_border_color(
computed,
inline_end_side(writing_mode, direction),
layer_base,
);
}
if delta.block_start {
copy_revert_layer_border_color(computed, block_start_side(writing_mode), layer_base);
}
if delta.block_end {
copy_revert_layer_border_color(computed, block_end_side(writing_mode), layer_base);
}
}
fn copy_revert_layer_border_color(
computed: &mut ComputedStyle,
side: PhysicalSide,
layer_base: &ComputedStyle,
) {
let colors = layer_base.resolved_border_colors(layer_base.color);
let opacities = layer_base.resolved_border_opacities();
match side {
PhysicalSide::Left => {
computed.border_left_color = Some(colors.left);
computed.border_left_alpha = opacities.left;
computed.pending_border_left_color_var = layer_base
.pending_border_left_color_var
.clone()
.or_else(|| layer_base.pending_border_color_var.clone());
}
PhysicalSide::Top => {
computed.border_top_color = Some(colors.top);
computed.border_top_alpha = opacities.top;
computed.pending_border_top_color_var = layer_base
.pending_border_top_color_var
.clone()
.or_else(|| layer_base.pending_border_color_var.clone());
}
PhysicalSide::Right => {
computed.border_right_color = Some(colors.right);
computed.border_right_alpha = opacities.right;
computed.pending_border_right_color_var = layer_base
.pending_border_right_color_var
.clone()
.or_else(|| layer_base.pending_border_color_var.clone());
}
PhysicalSide::Bottom => {
computed.border_bottom_color = Some(colors.bottom);
computed.border_bottom_alpha = opacities.bottom;
computed.pending_border_bottom_color_var = layer_base
.pending_border_bottom_color_var
.clone()
.or_else(|| layer_base.pending_border_color_var.clone());
}
}
}
fn apply_revert_layer_border_styles(
computed: &mut BorderStyleState,
delta: &EdgeRevertDelta,
layer_base: &BorderStyleState,
) {
if delta.top {
computed.top = layer_base.top;
}
if delta.right {
computed.right = layer_base.right;
}
if delta.bottom {
computed.bottom = layer_base.bottom;
}
if delta.left {
computed.left = layer_base.left;
}
}
fn apply_revert_layer_logical_border_styles(
computed: &mut BorderStyleState,
delta: &LogicalEdgeRevertDelta,
layer_base: &BorderStyleState,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if delta.inline_start {
copy_revert_layer_border_style(
computed,
inline_start_side(writing_mode, direction),
layer_base,
);
}
if delta.inline_end {
copy_revert_layer_border_style(
computed,
inline_end_side(writing_mode, direction),
layer_base,
);
}
if delta.block_start {
copy_revert_layer_border_style(computed, block_start_side(writing_mode), layer_base);
}
if delta.block_end {
copy_revert_layer_border_style(computed, block_end_side(writing_mode), layer_base);
}
}
fn copy_revert_layer_border_style(
computed: &mut BorderStyleState,
side: PhysicalSide,
layer_base: &BorderStyleState,
) {
match side {
PhysicalSide::Left => computed.left = layer_base.left,
PhysicalSide::Top => computed.top = layer_base.top,
PhysicalSide::Right => computed.right = layer_base.right,
PhysicalSide::Bottom => computed.bottom = layer_base.bottom,
}
}
fn apply_border_style_mask(style: &mut ComputedStyle) {
if matches!(
style.border_style.top,
BorderLineStyle::None | BorderLineStyle::Hidden
) {
style.border_width.top = LengthSpec::Absolute(Pt::ZERO);
}
if matches!(
style.border_style.right,
BorderLineStyle::None | BorderLineStyle::Hidden
) {
style.border_width.right = LengthSpec::Absolute(Pt::ZERO);
}
if matches!(
style.border_style.bottom,
BorderLineStyle::None | BorderLineStyle::Hidden
) {
style.border_width.bottom = LengthSpec::Absolute(Pt::ZERO);
}
if matches!(
style.border_style.left,
BorderLineStyle::None | BorderLineStyle::Hidden
) {
style.border_width.left = LengthSpec::Absolute(Pt::ZERO);
}
if !style.outline_visible {
style.outline_width = LengthSpec::Absolute(Pt::ZERO);
}
promote_uniform_border_color_from_active_edges(style);
}
fn is_effective_zero_border_width(spec: LengthSpec) -> bool {
match spec {
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => true,
LengthSpec::Absolute(value) => value == Pt::ZERO,
LengthSpec::Percent(value) => value.abs() <= f32::EPSILON,
LengthSpec::Em(value) => value.abs() <= f32::EPSILON,
LengthSpec::Rem(value) => value.abs() <= f32::EPSILON,
LengthSpec::Calc(calc) => {
calc.abs == Pt::ZERO
&& calc.percent.abs() <= f32::EPSILON
&& calc.em.abs() <= f32::EPSILON
&& calc.rem.abs() <= f32::EPSILON
}
LengthSpec::Clamped(calc) => calc.resolve(Pt::ZERO, Pt::ZERO, Pt::ZERO) == Pt::ZERO,
LengthSpec::FontRelative(calc) => {
calc.base.abs == Pt::ZERO
&& calc.base.percent.abs() <= f32::EPSILON
&& calc.base.em.abs() <= f32::EPSILON
&& calc.base.rem.abs() <= f32::EPSILON
&& calc.ex.abs() <= f32::EPSILON
&& calc.ch.abs() <= f32::EPSILON
&& calc.cap.abs() <= f32::EPSILON
&& calc.lh.abs() <= f32::EPSILON
&& calc.rlh.abs() <= f32::EPSILON
}
}
}
fn promote_uniform_border_color_from_active_edges(style: &mut ComputedStyle) {
if style.border_color.is_some() {
return;
}
let mut candidate: Option<Color> = None;
let mut active_edges = 0usize;
let edges = [
(style.border_width.top, style.border_top_color),
(style.border_width.right, style.border_right_color),
(style.border_width.bottom, style.border_bottom_color),
(style.border_width.left, style.border_left_color),
];
for (width, color) in edges {
if is_effective_zero_border_width(width) {
continue;
}
active_edges += 1;
let Some(edge_color) = color else {
// Another active side still relies on fallback; keep border_color unset.
return;
};
match candidate {
Some(existing) if existing != edge_color => {
// Active edges disagree; shorthand color is not uniform.
return;
}
None => {
candidate = Some(edge_color);
}
_ => {}
}
}
if active_edges > 0 {
style.border_color = candidate;
}
}
fn resolve_custom_color(style: &ComputedStyle, expr: &str) -> Option<Color> {
resolve_custom_color_expr_with_alpha(style, expr).map(|(color, _)| color)
}
fn resolve_text_decoration_thickness_expr(
style: &ComputedStyle,
expr: &str,
) -> Option<TextDecorationThicknessMode> {
resolve_text_decoration_thickness_expr_inner(style, expr, 0)
}
fn resolve_text_decoration_thickness_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextDecorationThicknessMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = text_decoration_thickness_value_from_raw(expr) {
return Some(value);
}
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, expr)
{
return Some(TextDecorationThicknessMode::Length(spec));
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_decoration_thickness_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_decoration_thickness_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_decoration_thickness_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_decoration_thickness_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_underline_offset_expr(
style: &ComputedStyle,
expr: &str,
) -> Option<TextUnderlineOffsetMode> {
resolve_text_underline_offset_expr_inner(style, expr, 0)
}
fn resolve_text_underline_offset_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextUnderlineOffsetMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = text_underline_offset_value_from_raw(expr) {
return Some(value);
}
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, expr)
{
return Some(TextUnderlineOffsetMode::Length(spec));
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_underline_offset_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_underline_offset_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_underline_offset_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_underline_offset_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_underline_position_expr(
style: &ComputedStyle,
expr: &str,
) -> Option<TextUnderlinePositionMode> {
resolve_text_underline_position_expr_inner(style, expr, 0)
}
fn resolve_text_underline_position_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextUnderlinePositionMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = text_underline_position_value_from_raw(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_underline_position_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_underline_position_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_underline_position_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_underline_position_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_shadow_expr(style: &ComputedStyle, expr: &str) -> Option<Vec<BoxShadowSpec>> {
resolve_text_shadow_expr_inner(style, expr, 0)
}
fn resolve_text_shadow_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<Vec<BoxShadowSpec>> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_text_shadow_list_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_shadow_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_shadow_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_shadow_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_shadow_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_line_break_expr(style: &ComputedStyle, expr: &str) -> Option<LineBreakMode> {
resolve_line_break_expr_inner(style, expr, 0)
}
fn resolve_text_justify_expr(style: &ComputedStyle, expr: &str) -> Option<TextJustifyMode> {
resolve_text_justify_expr_inner(style, expr, 0)
}
fn resolve_hyphens_expr(style: &ComputedStyle, expr: &str) -> Option<HyphensMode> {
resolve_hyphens_expr_inner(style, expr, 0)
}
fn resolve_hyphenate_character_expr(style: &ComputedStyle, expr: &str) -> Option<Option<String>> {
resolve_hyphenate_character_expr_inner(style, expr, 0)
}
const TEXT_WRAP_SHORTHAND_VAR_PREFIX: &str = "__fullbleed_text_wrap_shorthand__:";
const WHITE_SPACE_SHORTHAND_VAR_PREFIX: &str = "__fullbleed_white_space_shorthand__:";
fn text_wrap_shorthand_var_expr(expr: &str) -> String {
format!("{TEXT_WRAP_SHORTHAND_VAR_PREFIX}{}", expr.trim())
}
fn strip_text_wrap_shorthand_marker(expr: &str) -> Option<&str> {
expr.strip_prefix(TEXT_WRAP_SHORTHAND_VAR_PREFIX)
}
fn white_space_shorthand_var_expr(expr: &str) -> String {
format!("{WHITE_SPACE_SHORTHAND_VAR_PREFIX}{}", expr.trim())
}
fn strip_white_space_shorthand_marker(expr: &str) -> Option<&str> {
expr.strip_prefix(WHITE_SPACE_SHORTHAND_VAR_PREFIX)
}
fn resolve_text_wrap_mode_expr(style: &ComputedStyle, expr: &str) -> Option<TextWrapMode> {
resolve_text_wrap_mode_expr_inner(style, expr, 0)
}
fn resolve_text_wrap_style_expr(style: &ComputedStyle, expr: &str) -> Option<TextWrapStyleMode> {
resolve_text_wrap_style_expr_inner(style, expr, 0)
}
fn resolve_white_space_expr(style: &ComputedStyle, expr: &str) -> Option<WhiteSpaceMode> {
resolve_white_space_expr_inner(style, expr, 0)
}
fn resolve_vertical_align_expr(style: &ComputedStyle, expr: &str) -> Option<VerticalAlignMode> {
resolve_vertical_align_expr_inner(style, expr, 0)
}
fn resolve_vertical_align_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<VerticalAlignMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if expr.eq_ignore_ascii_case("inherit") {
return Some(style.vertical_align_inherit_base);
}
if let Some(value) = parse_vertical_align_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_vertical_align_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_vertical_align_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_vertical_align_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_vertical_align_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_font_shorthand_expr(
style: &ComputedStyle,
expr: &str,
) -> Option<(
FontStyleMode,
FontWeightSpec,
FontSizeSpec,
LineHeightSpec,
FontSpec,
)> {
let mut stack = Vec::new();
resolve_font_shorthand_expr_inner(style, expr, 0, &mut stack)
}
fn resolve_font_shorthand_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<(
FontStyleMode,
FontWeightSpec,
FontSizeSpec,
LineHeightSpec,
FontSpec,
)> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_font_shorthand_raw(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_font_shorthand_expr_inner(style, &name, depth + 1, stack) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_font_shorthand_expr_inner(style, &fallback_expr, depth + 1, stack);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(raw) = style.custom_raw_values.get(&key) {
resolve_font_shorthand_expr_inner(style, raw, depth + 1, stack)
} else if let Some(next) = style.custom_color_refs.get(&key) {
resolve_font_shorthand_expr_inner(style, next, depth + 1, stack)
} else {
None
};
stack.pop();
return resolved;
}
None
}
fn resolve_font_size_expr(style: &ComputedStyle, expr: &str, viewport: Size) -> Option<Pt> {
resolve_font_size_expr_inner(style, expr, viewport, 0)
}
fn resolve_font_size_expr_inner(
style: &ComputedStyle,
expr: &str,
viewport: Size,
depth: usize,
) -> Option<Pt> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(raw) = font_shorthand_var_raw(expr) {
let (_, _, size, _, _) = resolve_font_shorthand_expr(style, raw)?;
return resolve_font_size_spec(
&size,
style.font_size,
style.root_font_size,
viewport,
font_size_relative_metrics(style, None),
);
}
if let Some(spec) = parse_font_size_spec_str(expr) {
return resolve_font_size_spec(
&spec,
style.font_size,
style.root_font_size,
viewport,
font_size_relative_metrics(style, None),
);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_font_size_expr_inner(style, &name, viewport, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_font_size_expr_inner(style, &fallback_expr, viewport, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_font_size_expr_inner(style, raw, viewport, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_font_size_expr_inner(style, raw, viewport, depth + 1);
}
}
None
}
fn resolve_line_height_expr(
style: &ComputedStyle,
expr: &str,
viewport: Size,
) -> Option<LineHeightSpec> {
resolve_line_height_expr_inner(style, expr, viewport, 0)
}
fn resolve_line_height_expr_inner(
style: &ComputedStyle,
expr: &str,
viewport: Size,
depth: usize,
) -> Option<LineHeightSpec> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(raw) = font_shorthand_var_raw(expr) {
let (_, _, _, line_height, _) = resolve_font_shorthand_expr(style, raw)?;
return match line_height {
LineHeightSpec::Inherit => Some(style.line_height.clone()),
LineHeightSpec::Initial => Some(LineHeightSpec::Normal),
_ => Some(line_height),
};
}
if let Some(value) = parse_line_height_spec_str(expr) {
return match value {
LineHeightSpec::Inherit => Some(style.line_height.clone()),
LineHeightSpec::Initial => Some(LineHeightSpec::Normal),
_ => Some(value),
};
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_line_height_expr_inner(style, &name, viewport, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_line_height_expr_inner(style, &fallback_expr, viewport, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_line_height_expr_inner(style, raw, viewport, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_line_height_expr_inner(style, raw, viewport, depth + 1);
}
}
None
}
fn resolve_font_weight_expr(style: &ComputedStyle, expr: &str) -> Option<u16> {
resolve_font_weight_expr_inner(style, expr, 0)
}
fn resolve_font_weight_expr_inner(style: &ComputedStyle, expr: &str, depth: usize) -> Option<u16> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(raw) = font_shorthand_var_raw(expr) {
let (_, weight, _, _, _) = resolve_font_shorthand_expr(style, raw)?;
return match weight {
FontWeightSpec::Value(weight) => Some(weight),
FontWeightSpec::Inherit => Some(style.font_weight),
FontWeightSpec::Initial => Some(400),
FontWeightSpec::Bolder => Some(relative_bolder_font_weight(style.font_weight)),
FontWeightSpec::Lighter => Some(relative_lighter_font_weight(style.font_weight)),
};
}
if let Some(value) = parse_font_weight_spec_str(expr) {
return match value {
FontWeightSpec::Value(weight) => Some(weight),
FontWeightSpec::Inherit => Some(style.font_weight),
FontWeightSpec::Initial => Some(400),
FontWeightSpec::Bolder => Some(relative_bolder_font_weight(style.font_weight)),
FontWeightSpec::Lighter => Some(relative_lighter_font_weight(style.font_weight)),
};
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_font_weight_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_font_weight_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_font_weight_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_font_weight_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_font_style_expr(style: &ComputedStyle, expr: &str) -> Option<FontStyleMode> {
resolve_font_style_expr_inner(style, expr, 0)
}
fn resolve_font_style_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<FontStyleMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(raw) = font_shorthand_var_raw(expr) {
let (style, _, _, _, _) = resolve_font_shorthand_expr(style, raw)?;
return Some(style);
}
if let Some(value) = parse_font_style_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_font_style_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_font_style_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_font_style_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_font_style_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_wrap_shorthand_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(TextWrapMode, TextWrapStyleMode)> {
if depth > 12 {
return None;
}
let expr = strip_text_wrap_shorthand_marker(expr)
.unwrap_or(expr)
.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_text_wrap_shorthand_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_wrap_shorthand_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_wrap_shorthand_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_wrap_shorthand_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_wrap_shorthand_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_white_space_shorthand_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(WhiteSpaceMode, TextWrapMode)> {
if depth > 12 {
return None;
}
let expr = strip_white_space_shorthand_marker(expr)
.unwrap_or(expr)
.trim();
if expr.is_empty() {
return None;
}
if expr.eq_ignore_ascii_case("inherit")
|| expr.eq_ignore_ascii_case("unset")
|| expr.eq_ignore_ascii_case("revert")
|| expr.eq_ignore_ascii_case("revert-layer")
{
return Some((style.white_space, style.text_wrap_mode));
}
if expr.eq_ignore_ascii_case("initial") {
return Some((WhiteSpaceMode::Normal, TextWrapMode::Wrap));
}
if let Some(value) = parse_white_space_shorthand_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_white_space_shorthand_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_white_space_shorthand_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_white_space_shorthand_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_white_space_shorthand_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_wrap_mode_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextWrapMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if strip_text_wrap_shorthand_marker(expr).is_some() {
return resolve_text_wrap_shorthand_expr_inner(style, expr, depth + 1).map(|value| value.0);
}
if strip_white_space_shorthand_marker(expr).is_some() {
return resolve_white_space_shorthand_expr_inner(style, expr, depth + 1)
.map(|value| value.1);
}
if let Some(value) = parse_text_wrap_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_wrap_mode_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_wrap_mode_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_wrap_mode_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_wrap_mode_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_wrap_style_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextWrapStyleMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if strip_text_wrap_shorthand_marker(expr).is_some() {
return resolve_text_wrap_shorthand_expr_inner(style, expr, depth + 1).map(|value| value.1);
}
if let Some(value) = parse_text_wrap_style_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_wrap_style_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_wrap_style_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_wrap_style_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_wrap_style_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_white_space_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<WhiteSpaceMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if strip_white_space_shorthand_marker(expr).is_some() {
return resolve_white_space_shorthand_expr_inner(style, expr, depth + 1)
.map(|value| value.0);
}
if expr.eq_ignore_ascii_case("inherit")
|| expr.eq_ignore_ascii_case("unset")
|| expr.eq_ignore_ascii_case("revert")
|| expr.eq_ignore_ascii_case("revert-layer")
{
return Some(style.white_space);
}
if expr.eq_ignore_ascii_case("initial") {
return Some(WhiteSpaceMode::Normal);
}
if let Some(value) = parse_white_space_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_white_space_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_white_space_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_white_space_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_white_space_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_hyphenate_character_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<Option<String>> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_hyphenate_character_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_hyphenate_character_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_hyphenate_character_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_hyphenate_character_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_hyphenate_character_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_hyphens_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<HyphensMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_hyphens_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_hyphens_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_hyphens_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_hyphens_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_hyphens_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_text_justify_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<TextJustifyMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_text_justify_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_text_justify_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_text_justify_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_text_justify_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_text_justify_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_line_break_expr_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<LineBreakMode> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some(value) = parse_line_break_mode_str(expr) {
return Some(value);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_line_break_expr_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_line_break_expr_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(raw) = style.custom_raw_values.get(&key) {
return resolve_line_break_expr_inner(style, raw, depth + 1);
}
if let Some(raw) = style.custom_color_refs.get(&key) {
return resolve_line_break_expr_inner(style, raw, depth + 1);
}
}
None
}
fn resolve_custom_color_with_alpha(style: &ComputedStyle, name: &str) -> Option<(Color, f32)> {
let mut current = name.trim().to_ascii_lowercase();
if current.is_empty() {
return None;
}
if let Some(color) = style.custom_colors.get(¤t).copied() {
let alpha = style
.custom_color_alpha
.get(¤t)
.copied()
.unwrap_or(1.0);
return Some((color, alpha.clamp(0.0, 1.0)));
}
let mut hops = 0usize;
while let Some(next) = style.custom_color_refs.get(¤t) {
if let Some(color) = style.custom_colors.get(next).copied() {
let alpha = style.custom_color_alpha.get(next).copied().unwrap_or(1.0);
return Some((color, alpha.clamp(0.0, 1.0)));
}
if !next.starts_with("--") {
return resolve_custom_color_expr_with_alpha_inner(style, next, hops + 1);
}
current = next.clone();
hops += 1;
if hops > 8 {
break;
}
}
None
}
fn resolve_custom_color_expr_with_alpha(style: &ComputedStyle, expr: &str) -> Option<(Color, f32)> {
resolve_custom_color_expr_with_alpha_inner(style, expr, 0)
}
fn resolve_custom_color_expr_with_alpha_inner(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if expr.eq_ignore_ascii_case("currentcolor") {
return Some((style.color, 1.0));
}
if expr.starts_with("--") {
return resolve_custom_color_with_alpha(style, expr);
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(color) = resolve_custom_color_with_alpha(style, &name) {
return Some(color);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_color_expr_with_alpha_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if let Some((color, alpha)) = resolve_rgb_color_function_with_vars(style, expr, depth + 1) {
return Some((color, alpha.clamp(0.0, 1.0)));
}
if let Some((color, alpha)) = resolve_hsl_color_function_with_vars(style, expr, depth + 1) {
return Some((color, alpha.clamp(0.0, 1.0)));
}
if let Some((color, alpha)) = resolve_hwb_color_function_with_vars(style, expr, depth + 1) {
return Some((color, alpha.clamp(0.0, 1.0)));
}
if let Some((color, alpha)) = resolve_color_mix_expr_with_alpha(style, expr, depth + 1) {
return Some((color, alpha.clamp(0.0, 1.0)));
}
if let Some((color, alpha)) = parse_color_string(expr) {
return Some((color, alpha.clamp(0.0, 1.0)));
}
if expr.to_ascii_lowercase().contains("var(") {
if let Some(resolved) = substitute_vars_in_color_expr(style, expr, depth + 1) {
if resolved.trim() != expr {
return resolve_custom_color_expr_with_alpha_inner(style, &resolved, depth + 1);
}
}
}
if let Some(color) = parse_rgb_triplet_string(expr) {
return Some((color, 1.0));
}
None
}
fn substitute_vars_in_color_expr(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<String> {
if depth > 12 {
return None;
}
let lower = expr.to_ascii_lowercase();
let mut cursor = 0usize;
let mut out = String::new();
let mut changed = false;
while let Some(rel) = lower[cursor..].find("var(") {
let start = cursor + rel;
let end = find_function_call_end(expr, start)?;
out.push_str(&expr[cursor..start]);
let func = &expr[start..end];
let (name, fallback) = parse_var_function(func)?;
let mut replacement = if let Some(raw) = style.custom_raw_values.get(&name) {
raw.clone()
} else if let Some(next) = style.custom_color_refs.get(&name) {
next.clone()
} else if let Some((color, alpha)) = resolve_custom_color_with_alpha(style, &name) {
color_to_rgb_function(color, alpha)
} else {
fallback?
};
if replacement.to_ascii_lowercase().contains("var(") {
replacement = substitute_vars_in_color_expr(style, &replacement, depth + 1)?;
}
out.push_str(replacement.trim());
cursor = end;
changed = true;
}
if !changed {
return None;
}
out.push_str(&expr[cursor..]);
Some(out)
}
fn find_function_call_end(raw: &str, start: usize) -> Option<usize> {
let open = raw[start..].find('(')? + start;
let mut depth = 0usize;
for (idx, ch) in raw[open..].char_indices() {
let absolute = open + idx;
match ch {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(absolute + ch.len_utf8());
}
}
_ => {}
}
}
None
}
fn color_to_rgb_function(color: Color, alpha: f32) -> String {
let r = (color.r.clamp(0.0, 1.0) * 255.0).round();
let g = (color.g.clamp(0.0, 1.0) * 255.0).round();
let b = (color.b.clamp(0.0, 1.0) * 255.0).round();
format!("rgb({r} {g} {b} / {})", alpha.clamp(0.0, 1.0))
}
fn resolve_rgb_color_function_with_vars(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
if depth > 12 {
return None;
}
let expr = expr.trim();
let lower = expr.to_ascii_lowercase();
if !(lower.starts_with("rgb(") || lower.starts_with("rgba(")) || !expr.ends_with(')') {
return None;
}
let open = expr.find('(')?;
let inner = &expr[open + 1..expr.len() - 1];
let args = split_args(inner);
if args.is_empty() {
return None;
}
// Bootstrap utility pattern: rgba(var(--bs-success-rgb), var(--bs-text-opacity))
if args.len() == 2 && args[0].trim().to_ascii_lowercase().starts_with("var(") {
let (color, color_alpha) =
resolve_custom_color_expr_with_alpha_inner(style, args[0].trim(), depth + 1)?;
let alpha = resolve_custom_number_expr(style, args[1].trim(), depth + 1).unwrap_or(1.0);
return Some((color, (alpha * color_alpha).clamp(0.0, 1.0)));
}
// Accept direct rgb(var(--triplet)) form by resolving the var to a color.
if args.len() == 1 && parse_var_function(args[0].trim()).is_some() {
return resolve_custom_color_expr_with_alpha_inner(style, args[0].trim(), depth + 1);
}
if args.len() == 1 {
let (components_raw, alpha_raw) = split_top_level_slash_once(args[0].trim())
.map(|(before, after)| (before, Some(after)))
.unwrap_or((args[0].trim(), None));
let components = split_top_level_whitespace(components_raw);
if components.len() == 1 && components[0].to_ascii_lowercase().starts_with("var(") {
let (color, color_alpha) =
resolve_custom_color_expr_with_alpha_inner(style, &components[0], depth + 1)?;
let alpha = alpha_raw
.map(|raw| resolve_alpha_component(style, raw, depth + 1))
.unwrap_or(Some(1.0))?;
return Some((color, (alpha * color_alpha).clamp(0.0, 1.0)));
}
if components.len() != 3 {
return None;
}
let r = resolve_rgb_component(style, &components[0], depth + 1)?;
let g = resolve_rgb_component(style, &components[1], depth + 1)?;
let b = resolve_rgb_component(style, &components[2], depth + 1)?;
let alpha = alpha_raw
.map(|raw| resolve_alpha_component(style, raw, depth + 1))
.unwrap_or(Some(1.0))?;
return Some((
Color::rgb(
(r / 255.0).clamp(0.0, 1.0),
(g / 255.0).clamp(0.0, 1.0),
(b / 255.0).clamp(0.0, 1.0),
),
alpha.clamp(0.0, 1.0),
));
}
if args.len() < 3 {
return None;
}
let r = resolve_rgb_component(style, args[0].trim(), depth + 1)?;
let g = resolve_rgb_component(style, args[1].trim(), depth + 1)?;
let b = resolve_rgb_component(style, args[2].trim(), depth + 1)?;
let alpha = if args.len() >= 4 {
resolve_alpha_component(style, args[3].trim(), depth + 1).unwrap_or(1.0)
} else {
1.0
};
Some((
Color::rgb(
(r / 255.0).clamp(0.0, 1.0),
(g / 255.0).clamp(0.0, 1.0),
(b / 255.0).clamp(0.0, 1.0),
),
alpha.clamp(0.0, 1.0),
))
}
fn resolve_hsl_color_function_with_vars(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
if depth > 12 {
return None;
}
let expr = expr.trim();
let lower = expr.to_ascii_lowercase();
if !(lower.starts_with("hsl(") || lower.starts_with("hsla(")) || !expr.ends_with(')') {
return None;
}
let open = expr.find('(')?;
let inner = &expr[open + 1..expr.len() - 1];
if inner.trim().to_ascii_lowercase().starts_with("from ") {
return None;
}
let args = split_args(inner);
if args.is_empty() {
return None;
}
if args.len() == 1 {
let (components_raw, alpha_raw) = split_top_level_slash_once(args[0].trim())
.map(|(before, after)| (before, Some(after)))
.unwrap_or((args[0].trim(), None));
let mut components = split_top_level_whitespace(components_raw);
if components.len() == 1 {
if let Some((name, fallback)) = parse_var_function(&components[0]) {
let raw = style.custom_raw_values.get(&name).cloned().or(fallback)?;
components = split_top_level_whitespace(&raw);
}
}
if components.len() != 3 {
return None;
}
let hue_deg = resolve_hsl_hue_component(style, &components[0], depth + 1)?;
let saturation = resolve_hsl_percentage_component(style, &components[1], depth + 1)?;
let lightness = resolve_hsl_percentage_component(style, &components[2], depth + 1)?;
let alpha = alpha_raw
.map(|raw| resolve_alpha_component(style, raw, depth + 1))
.unwrap_or(Some(1.0))?;
return Some((
hsl_to_rgb(hue_deg, saturation, lightness),
alpha.clamp(0.0, 1.0),
));
}
if args.len() < 3 || args.len() > 4 {
return None;
}
let hue_deg = resolve_hsl_hue_component(style, args[0].trim(), depth + 1)?;
let saturation = resolve_hsl_percentage_component(style, args[1].trim(), depth + 1)?;
let lightness = resolve_hsl_percentage_component(style, args[2].trim(), depth + 1)?;
let alpha = if args.len() >= 4 {
resolve_alpha_component(style, args[3].trim(), depth + 1).unwrap_or(1.0)
} else {
1.0
};
Some((
hsl_to_rgb(hue_deg, saturation, lightness),
alpha.clamp(0.0, 1.0),
))
}
fn resolve_hsl_hue_component(style: &ComputedStyle, expr: &str, depth: usize) -> Option<f32> {
if depth > 12 {
return None;
}
if let Some(value) = parse_hsl_hue_component(expr) {
return Some(value);
}
resolve_custom_number_expr(style, expr, depth + 1)
}
fn resolve_hsl_percentage_component(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<f32> {
if depth > 12 {
return None;
}
if let Some(value) = parse_hsl_percentage_component(expr) {
return Some(value);
}
let value = resolve_custom_number_expr(style, expr, depth + 1)?;
Some(if value <= 1.0 {
value.clamp(0.0, 1.0)
} else {
(value / 100.0).clamp(0.0, 1.0)
})
}
fn resolve_hwb_color_function_with_vars(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
if depth > 12 {
return None;
}
let expr = expr.trim();
let lower = expr.to_ascii_lowercase();
if !lower.starts_with("hwb(") || !expr.ends_with(')') {
return None;
}
let inner = expr[4..expr.len() - 1].trim();
if inner.to_ascii_lowercase().starts_with("from ") || split_args(inner).len() != 1 {
return None;
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let mut components = split_top_level_whitespace(components_raw);
if components.len() == 1 {
if let Some((name, fallback)) = parse_var_function(&components[0]) {
let raw = style.custom_raw_values.get(&name).cloned().or(fallback)?;
components = split_top_level_whitespace(&raw);
}
}
if components.len() != 3 {
return None;
}
let hue_deg = resolve_hsl_hue_component(style, &components[0], depth + 1)?;
let whiteness = resolve_hsl_percentage_component(style, &components[1], depth + 1)?;
let blackness = resolve_hsl_percentage_component(style, &components[2], depth + 1)?;
let alpha = alpha_raw
.map(|raw| resolve_alpha_component(style, raw, depth + 1))
.unwrap_or(Some(1.0))?;
Some((
hwb_to_rgb(hue_deg, whiteness, blackness),
alpha.clamp(0.0, 1.0),
))
}
fn resolve_alpha_component(style: &ComputedStyle, expr: &str, depth: usize) -> Option<f32> {
if depth > 12 {
return None;
}
if let Some(value) = parse_alpha_component(expr) {
return Some(value.clamp(0.0, 1.0));
}
resolve_custom_number_expr(style, expr, depth + 1).map(|value| value.clamp(0.0, 1.0))
}
fn resolve_rgb_component(style: &ComputedStyle, expr: &str, depth: usize) -> Option<f32> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.ends_with('%') {
let pct = expr.trim_end_matches('%').trim().parse::<f32>().ok()?;
return Some((pct.clamp(0.0, 100.0) / 100.0) * 255.0);
}
if let Ok(value) = expr.parse::<f32>() {
return Some(value.clamp(0.0, 255.0));
}
if let Some(value) = resolve_custom_number_expr(style, expr, depth + 1) {
// Preserve historical behavior where 0..1 can still be used as normalized values.
return Some(if value <= 1.0 { value * 255.0 } else { value });
}
None
}
fn resolve_custom_number_expr(style: &ComputedStyle, expr: &str, depth: usize) -> Option<f32> {
resolve_custom_number_inner(style, expr, depth)
}
fn resolve_custom_number_inner(style: &ComputedStyle, expr: &str, depth: usize) -> Option<f32> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_custom_number_inner(style, &name, depth + 1) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_number_inner(style, &fallback_expr, depth + 1);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if let Some(value) = style
.custom_lengths
.get(&key)
.and_then(length_spec_to_scalar)
{
return Some(value);
}
if let Some(next) = style.custom_color_refs.get(&key) {
return resolve_custom_number_inner(style, next, depth + 1);
}
return None;
}
if expr.ends_with('%') {
let pct = expr.trim_end_matches('%').trim().parse::<f32>().ok()?;
return Some((pct / 100.0).clamp(0.0, 1.0));
}
expr.parse::<f32>().ok()
}
fn split_color_mix_stop(expr: &str) -> Option<(String, Option<f32>)> {
let trimmed = expr.trim();
if trimmed.is_empty() {
return None;
}
let mut depth = 0usize;
let mut split_idx: Option<usize> = None;
for (idx, ch) in trimmed.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
c if c.is_whitespace() && depth == 0 => split_idx = Some(idx),
_ => {}
}
}
if let Some(idx) = split_idx {
let color_part = trimmed[..idx].trim();
let weight_part = trimmed[idx..].trim();
if !color_part.is_empty() {
if let Some(weight) = parse_percent_scalar(weight_part) {
return Some((color_part.to_string(), Some(weight.clamp(0.0, 1.0))));
}
}
}
Some((trimmed.to_string(), None))
}
fn resolve_color_mix_component(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
let trimmed = expr.trim();
if trimmed.eq_ignore_ascii_case("currentcolor") {
return Some((style.color, 1.0));
}
resolve_custom_color_expr_with_alpha_inner(style, trimmed, depth + 1)
}
fn resolve_color_mix_expr_with_alpha(
style: &ComputedStyle,
expr: &str,
depth: usize,
) -> Option<(Color, f32)> {
if depth > 12 {
return None;
}
let expr = expr.trim();
if !(expr.to_ascii_lowercase().starts_with("color-mix(") && expr.ends_with(')')) {
return None;
}
let inside = extract_function_args(expr, "color-mix(")?;
let args = split_args(inside);
if args.len() != 3 {
return None;
}
let (space, hue_method) = parse_color_interpolation_space(&args[0])?;
let (left_expr, left_weight) = split_color_mix_stop(&args[1])?;
let (right_expr, right_weight) = split_color_mix_stop(&args[2])?;
let (left_color, left_alpha) = resolve_color_mix_component(style, &left_expr, depth + 1)?;
let (right_color, right_alpha) = resolve_color_mix_component(style, &right_expr, depth + 1)?;
mix_colors_in_space(
&space,
hue_method,
(left_color, left_alpha, left_weight),
(right_color, right_alpha, right_weight),
)
}
fn length_spec_to_scalar(spec: &LengthSpec) -> Option<f32> {
match spec {
LengthSpec::Absolute(value) => Some(value.to_f32() / 0.75),
LengthSpec::Percent(value) => Some(*value),
LengthSpec::Em(value) => Some(*value),
LengthSpec::Rem(value) => Some(*value),
_ => None,
}
}
fn parse_var_function(raw: &str) -> Option<(String, Option<String>)> {
let raw = raw.trim();
if !raw.to_ascii_lowercase().starts_with("var(") || !raw.ends_with(')') {
return None;
}
let inside = &raw[4..raw.len() - 1];
let args = split_args(inside);
if args.is_empty() {
return None;
}
let name = args[0].trim().to_ascii_lowercase();
if !name.starts_with("--") {
return None;
}
let fallback = if args.len() > 1 {
let joined = args[1..].join(",");
let trimmed = joined.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
} else {
None
};
Some((name, fallback))
}
fn strip_calc_wrapper(expr: &str) -> &str {
let trimmed = expr.trim();
let lowered = trimmed.to_ascii_lowercase();
if lowered.starts_with("calc(") && trimmed.ends_with(')') && trimmed.len() > 6 {
&trimmed[5..trimmed.len() - 1]
} else {
trimmed
}
}
fn split_top_level_mul_div_once(raw: &str) -> Option<(&str, char, &str)> {
let mut depth = 0usize;
for (idx, ch) in raw.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
'*' | '/' if depth == 0 => {
let left = raw[..idx].trim();
let right = raw[idx + ch.len_utf8()..].trim();
if !left.is_empty() && !right.is_empty() {
return Some((left, ch, right));
}
return None;
}
_ => {}
}
}
None
}
fn split_top_level_add_sub_terms(raw: &str) -> Option<Vec<(f32, String)>> {
let mut terms: Vec<(f32, String)> = Vec::new();
let mut depth = 0usize;
let mut sign = 1.0f32;
let mut start = 0usize;
for (idx, ch) in raw.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
'+' | '-' if depth == 0 => {
if idx == start {
sign = if ch == '-' { -1.0 } else { 1.0 };
start = idx + ch.len_utf8();
continue;
}
let part = raw[start..idx].trim();
if part.is_empty() {
return None;
}
terms.push((sign, part.to_string()));
sign = if ch == '-' { -1.0 } else { 1.0 };
start = idx + ch.len_utf8();
}
_ => {}
}
}
if start < raw.len() {
let part = raw[start..].trim();
if part.is_empty() {
return None;
}
terms.push((sign, part.to_string()));
}
if terms.len() >= 2 { Some(terms) } else { None }
}
fn parse_unitless_scalar(raw: &str) -> Option<f32> {
raw.trim().parse::<f32>().ok()
}
fn parse_percent_scalar(raw: &str) -> Option<f32> {
let value = raw.trim().strip_suffix('%')?.trim();
value.parse::<f32>().ok().map(|v| v / 100.0)
}
fn resolve_custom_scalar_from_maps_inner(
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<f32> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_custom_scalar_from_maps_inner(
custom_lengths,
custom_refs,
&name,
depth + 1,
stack,
) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_scalar_from_maps_inner(
custom_lengths,
custom_refs,
&fallback_expr,
depth + 1,
stack,
);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(value) = custom_lengths.get(&key).and_then(length_spec_to_scalar)
{
Some(value)
} else if let Some(next) = custom_refs.get(&key) {
resolve_custom_scalar_from_maps_inner(
custom_lengths,
custom_refs,
next,
depth + 1,
stack,
)
} else {
None
};
stack.pop();
return resolved;
}
if let Some(value) = parse_percent_scalar(expr) {
return Some(value);
}
parse_unitless_scalar(expr)
}
fn resolve_custom_calc_scaled_length_from_maps_inner(
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<LengthSpec> {
if depth > 16 {
return None;
}
let inner = strip_calc_wrapper(expr);
let (left, op, right) = split_top_level_mul_div_once(inner)?;
match op {
'*' => {
if let Some(scale) = parse_percent_scalar(right) {
if let Some(value) = resolve_custom_scalar_from_maps_inner(
custom_lengths,
custom_refs,
left,
depth + 1,
stack,
) {
return Some(LengthSpec::Percent(value * scale));
}
}
if let Some(scale) = parse_percent_scalar(left) {
if let Some(value) = resolve_custom_scalar_from_maps_inner(
custom_lengths,
custom_refs,
right,
depth + 1,
stack,
) {
return Some(LengthSpec::Percent(value * scale));
}
}
if let Some(scale) = parse_unitless_scalar(right) {
if let Some(spec) = resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
left,
depth + 1,
stack,
) {
return Some(scale_length_spec(spec, scale));
}
}
if let Some(scale) = parse_unitless_scalar(left) {
if let Some(spec) = resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
right,
depth + 1,
stack,
) {
return Some(scale_length_spec(spec, scale));
}
}
}
'/' => {
if let Some(divisor) = parse_unitless_scalar(right) {
if divisor.abs() <= CALC_LENGTH_EPSILON {
return None;
}
if let Some(spec) = resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
left,
depth + 1,
stack,
) {
return Some(scale_length_spec(spec, 1.0 / divisor));
}
}
}
_ => {}
}
None
}
fn calc_length_from_length_spec_for_custom_eval(spec: LengthSpec) -> Option<CalcLength> {
match spec {
LengthSpec::Absolute(value) => Some(CalcLength {
abs: value,
percent: 0.0,
em: 0.0,
rem: 0.0,
}),
LengthSpec::Percent(value) => Some(CalcLength {
abs: Pt::ZERO,
percent: value,
em: 0.0,
rem: 0.0,
}),
LengthSpec::Em(value) => Some(CalcLength {
abs: Pt::ZERO,
percent: 0.0,
em: value,
rem: 0.0,
}),
LengthSpec::Rem(value) => Some(CalcLength {
abs: Pt::ZERO,
percent: 0.0,
em: 0.0,
rem: value,
}),
LengthSpec::Calc(calc) => Some(calc),
LengthSpec::Clamped(_) | LengthSpec::FontRelative(_) => None,
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => None,
}
}
fn resolve_custom_calc_additive_length_from_maps_inner(
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<LengthSpec> {
if depth > 16 {
return None;
}
let inner = strip_calc_wrapper(expr);
let terms = split_top_level_add_sub_terms(inner)?;
let mut combined = CalcLength::zero();
for (sign, term) in terms {
let spec = resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
&term,
depth + 1,
stack,
)?;
let value = calc_length_from_length_spec_for_custom_eval(spec)?;
combined.abs = combined.abs + (value.abs * sign);
combined.percent += value.percent * sign;
combined.em += value.em * sign;
combined.rem += value.rem * sign;
}
Some(length_spec_from_calc_length(combined))
}
fn resolve_custom_length_from_maps(
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
expr: &str,
) -> Option<LengthSpec> {
let mut stack: Vec<String> = Vec::new();
resolve_custom_length_from_maps_inner(custom_lengths, custom_refs, expr, 0, &mut stack)
}
fn resolve_custom_length_from_maps_inner(
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<LengthSpec> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(spec) = resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
&name,
depth + 1,
stack,
) {
return Some(spec);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
&fallback_expr,
depth + 1,
stack,
);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(spec) = custom_lengths.get(&key).copied() {
Some(spec)
} else if let Some(next) = custom_refs.get(&key) {
resolve_custom_length_from_maps_inner(
custom_lengths,
custom_refs,
next,
depth + 1,
stack,
)
} else {
None
};
stack.pop();
return resolved;
}
if let Some(spec) = resolve_custom_calc_additive_length_from_maps_inner(
custom_lengths,
custom_refs,
expr,
depth + 1,
stack,
) {
return Some(spec);
}
if let Some(spec) = resolve_custom_calc_scaled_length_from_maps_inner(
custom_lengths,
custom_refs,
expr,
depth + 1,
stack,
) {
return Some(spec);
}
length_spec_from_string(expr)
}
fn resolve_custom_font_spec_from_maps(
custom_fonts: &HashMap<String, Vec<Arc<str>>>,
custom_refs: &HashMap<String, String>,
expr: &str,
) -> Option<FontSpec> {
let mut stack: Vec<String> = Vec::new();
resolve_custom_font_spec_from_maps_inner(custom_fonts, custom_refs, expr, 0, &mut stack)
}
fn resolve_custom_font_spec_from_maps_inner(
custom_fonts: &HashMap<String, Vec<Arc<str>>>,
custom_refs: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<FontSpec> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_custom_font_spec_from_maps_inner(
custom_fonts,
custom_refs,
&name,
depth + 1,
stack,
) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_font_spec_from_maps_inner(
custom_fonts,
custom_refs,
&fallback_expr,
depth + 1,
stack,
);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(value) = custom_fonts.get(&key).cloned() {
Some(FontSpec::Value(value))
} else if let Some(next) = custom_refs.get(&key) {
resolve_custom_font_spec_from_maps_inner(
custom_fonts,
custom_refs,
next,
depth + 1,
stack,
)
} else {
None
};
stack.pop();
return resolved;
}
font_spec_from_raw_string(expr)
}
fn resolve_font_name_expr(style: &ComputedStyle, expr: &str) -> Option<FontSpec> {
if let Some(raw) = font_shorthand_var_raw(expr) {
let (_, _, _, _, family) = resolve_font_shorthand_expr(style, raw)?;
return Some(family);
}
resolve_custom_font_spec_from_maps(&style.custom_font_stacks, &style.custom_color_refs, expr)
}
fn resolve_custom_transform_ops_from_maps(
custom_refs: &HashMap<String, String>,
custom_raw_values: &HashMap<String, String>,
expr: &str,
parse_fn: fn(&str) -> Option<Vec<CssTransformOp>>,
) -> Option<Vec<CssTransformOp>> {
let mut stack: Vec<String> = Vec::new();
resolve_custom_transform_ops_from_maps_inner(
custom_refs,
custom_raw_values,
expr,
parse_fn,
0,
&mut stack,
)
}
fn resolve_custom_border_radius_from_maps(
custom_refs: &HashMap<String, String>,
custom_raw_values: &HashMap<String, String>,
expr: &str,
) -> Option<BorderRadiiSpec> {
let mut stack: Vec<String> = Vec::new();
resolve_custom_border_radius_from_maps_inner(
custom_refs,
custom_raw_values,
expr,
0,
&mut stack,
)
}
fn resolve_custom_border_radius_from_maps_inner(
custom_refs: &HashMap<String, String>,
custom_raw_values: &HashMap<String, String>,
expr: &str,
depth: usize,
stack: &mut Vec<String>,
) -> Option<BorderRadiiSpec> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_custom_border_radius_from_maps_inner(
custom_refs,
custom_raw_values,
&name,
depth + 1,
stack,
) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_border_radius_from_maps_inner(
custom_refs,
custom_raw_values,
&fallback_expr,
depth + 1,
stack,
);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(raw) = custom_raw_values.get(&key) {
resolve_custom_border_radius_from_maps_inner(
custom_refs,
custom_raw_values,
raw,
depth + 1,
stack,
)
} else if let Some(next) = custom_refs.get(&key) {
resolve_custom_border_radius_from_maps_inner(
custom_refs,
custom_raw_values,
next,
depth + 1,
stack,
)
} else {
None
};
stack.pop();
return resolved;
}
parse_border_radius_str(expr)
}
fn resolve_custom_transform_ops_from_maps_inner(
custom_refs: &HashMap<String, String>,
custom_raw_values: &HashMap<String, String>,
expr: &str,
parse_fn: fn(&str) -> Option<Vec<CssTransformOp>>,
depth: usize,
stack: &mut Vec<String>,
) -> Option<Vec<CssTransformOp>> {
if depth > 16 {
return None;
}
let expr = expr.trim();
if expr.is_empty() {
return None;
}
if let Some((name, fallback)) = parse_var_function(expr) {
if let Some(value) = resolve_custom_transform_ops_from_maps_inner(
custom_refs,
custom_raw_values,
&name,
parse_fn,
depth + 1,
stack,
) {
return Some(value);
}
if let Some(fallback_expr) = fallback {
return resolve_custom_transform_ops_from_maps_inner(
custom_refs,
custom_raw_values,
&fallback_expr,
parse_fn,
depth + 1,
stack,
);
}
return None;
}
if expr.starts_with("--") {
let key = expr.to_ascii_lowercase();
if stack.iter().any(|entry| entry == &key) {
return None;
}
stack.push(key.clone());
let resolved = if let Some(raw) = custom_raw_values.get(&key) {
resolve_custom_transform_ops_from_maps_inner(
custom_refs,
custom_raw_values,
raw,
parse_fn,
depth + 1,
stack,
)
} else if let Some(next) = custom_refs.get(&key) {
resolve_custom_transform_ops_from_maps_inner(
custom_refs,
custom_raw_values,
next,
parse_fn,
depth + 1,
stack,
)
} else {
None
};
stack.pop();
return resolved;
}
parse_fn(expr)
}
fn resolve_pending_font_size_var(style: &mut ComputedStyle, viewport: Size) {
let Some(name) = style.pending_font_size_var.clone() else {
return;
};
if let Some(value) = resolve_font_size_expr(style, &name, viewport) {
style.font_size = value;
style.pending_font_size_var = None;
}
}
fn resolve_pending_line_height_var(style: &mut ComputedStyle, viewport: Size) {
let Some(name) = style.pending_line_height_var.clone() else {
return;
};
if let Some(value) = resolve_line_height_expr(style, &name, viewport) {
style.line_height = value;
style.pending_line_height_var = None;
}
}
fn resolve_pending_vars(style: &mut ComputedStyle) {
if let Some(raw) = style.pending_grid_column_tracks_var.take() {
if let Some(resolved) = substitute_vars_in_color_expr(style, &raw, 0) {
if let Some(tracks) = parse_grid_track_list(&resolved) {
style.grid_columns = (!tracks.is_empty()).then_some(tracks.len());
style.grid_column_tracks = tracks;
style.grid_column_line_names = parse_grid_track_line_names(&resolved);
style.grid_column_auto_repeat = parse_grid_auto_repeat(&resolved);
}
}
}
if let Some(raw) = style.pending_grid_row_tracks_var.take() {
if let Some(resolved) = substitute_vars_in_color_expr(style, &raw, 0) {
if let Some(tracks) = parse_grid_track_list(&resolved) {
style.grid_rows = (!tracks.is_empty()).then_some(tracks.len());
style.grid_row_tracks = tracks;
style.grid_row_line_names = parse_grid_track_line_names(&resolved);
style.grid_row_auto_repeat = parse_grid_auto_repeat(&resolved);
}
}
}
if let Some(name) = style.pending_width_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.width = spec;
}
}
if let Some(name) = style.pending_max_width_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.max_width = spec;
}
}
if let Some(name) = style.pending_height_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.height = spec;
}
}
if let Some(name) = style.pending_min_width_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.min_width = spec;
}
}
if let Some(name) = style.pending_min_height_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.min_height = spec;
}
}
if let Some(name) = style.pending_max_height_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.max_height = spec;
}
}
if let Some(name) = style.pending_inset_left_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.inset_left = spec;
}
}
if let Some(name) = style.pending_inset_top_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.inset_top = spec;
}
}
if let Some(name) = style.pending_inset_right_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.inset_right = spec;
}
}
if let Some(name) = style.pending_inset_bottom_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.inset_bottom = spec;
}
}
if let Some(name) = style.pending_flex_basis_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.flex_basis = spec;
}
}
if let Some(name) = style.pending_gap_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.gap = spec;
}
}
if let Some(name) = style.pending_row_gap_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.row_gap = spec;
}
}
if let Some(name) = style.pending_outline_offset_var.take() {
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, &name)
{
style.outline_offset = normalize_length_spec(spec, style.outline_offset);
}
}
if let Some(name) = style.pending_border_radius_var.take() {
if let Some(radius) = resolve_custom_border_radius_from_maps(
&style.custom_color_refs,
&style.custom_raw_values,
&name,
) {
style.border_radius = radius;
}
}
let mut transforms_dirty = false;
if let Some(name) = style.pending_translate_var.take() {
if let Some(ops) = resolve_custom_transform_ops_from_maps(
&style.custom_color_refs,
&style.custom_raw_values,
&name,
transform_ops_from_translate_string,
) {
style.translate = ops;
transforms_dirty = true;
}
}
if let Some(name) = style.pending_rotate_var.take() {
if let Some(ops) = resolve_custom_transform_ops_from_maps(
&style.custom_color_refs,
&style.custom_raw_values,
&name,
transform_ops_from_rotate_string,
) {
style.rotate = ops;
transforms_dirty = true;
}
}
if let Some(name) = style.pending_scale_var.take() {
if let Some(ops) = resolve_custom_transform_ops_from_maps(
&style.custom_color_refs,
&style.custom_raw_values,
&name,
transform_ops_from_scale_string,
) {
style.scale = ops;
transforms_dirty = true;
}
}
if let Some(name) = style.pending_transform_var.take() {
if let Some(ops) = resolve_custom_transform_ops_from_maps(
&style.custom_color_refs,
&style.custom_raw_values,
&name,
transform_ops_from_string,
) {
style.transform_list = ops;
transforms_dirty = true;
}
}
if transforms_dirty {
recompose_transform_ops(style);
}
if let Some(name) = style.pending_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.color = blend_over_white(color, alpha);
}
}
if let Some(name) = style.pending_text_decoration_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.text_decoration_color = blend_over_white(color, alpha);
style.text_decoration_color_tracks_current = false;
}
}
if let Some(name) = style.pending_text_decoration_thickness_var.take() {
if let Some(value) = resolve_text_decoration_thickness_expr(style, &name) {
style.text_decoration_thickness = normalize_text_decoration_thickness(value);
}
}
if let Some(name) = style.pending_text_underline_offset_var.take() {
if let Some(value) = resolve_text_underline_offset_expr(style, &name) {
style.text_underline_offset = value;
}
}
if let Some(name) = style.pending_text_underline_position_var.take() {
if let Some(value) = resolve_text_underline_position_expr(style, &name) {
style.text_underline_position = value;
}
}
if let Some(name) = style.pending_text_shadow_var.take() {
if let Some(value) = resolve_text_shadow_expr(style, &name) {
style.text_shadows = value;
}
}
if let Some(name) = style.pending_line_break_var.take() {
if let Some(value) = resolve_line_break_expr(style, &name) {
style.line_break = value;
}
}
if let Some(name) = style.pending_text_justify_var.take() {
if let Some(value) = resolve_text_justify_expr(style, &name) {
style.text_justify = value;
}
}
if let Some(name) = style.pending_hyphens_var.take() {
if let Some(value) = resolve_hyphens_expr(style, &name) {
style.hyphens = value;
}
}
if let Some(name) = style.pending_hyphenate_character_var.take() {
if let Some(value) = resolve_hyphenate_character_expr(style, &name) {
style.hyphenate_character = value;
}
}
if let Some(name) = style.pending_text_wrap_mode_var.take() {
if let Some(value) = resolve_text_wrap_mode_expr(style, &name) {
style.text_wrap_mode = value;
}
}
if let Some(name) = style.pending_text_wrap_style_var.take() {
if let Some(value) = resolve_text_wrap_style_expr(style, &name) {
style.text_wrap_style = value;
}
}
if let Some(name) = style.pending_white_space_var.take() {
if let Some(value) = resolve_white_space_expr(style, &name) {
style.white_space = value;
}
}
if let Some(name) = style.pending_vertical_align_var.take() {
if let Some(value) = resolve_vertical_align_expr(style, &name) {
style.vertical_align = value;
}
}
if let Some(name) = style.pending_font_weight_var.take() {
if let Some(value) = resolve_font_weight_expr(style, &name) {
style.font_weight = value;
}
}
if let Some(name) = style.pending_font_style_var.take() {
if let Some(value) = resolve_font_style_expr(style, &name) {
style.font_style = value;
}
}
if let Some(expr) = style.pending_opacity_var.take() {
if let Some(value) = resolve_custom_number_expr(style, &expr, 0) {
style.opacity = value.clamp(0.0, 1.0);
}
}
sync_text_decoration_current_color(style);
sync_text_emphasis_current_color(style);
if let Some(name) = style.pending_background_color_var.take() {
let paints = parse_background_paints_with_style(style, &name);
if !paints.is_empty() {
style.background_paint = paints.first().cloned();
style.background_paints = paints;
} else if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.background_color = (alpha > 0.0).then(|| blend_over_white(color, alpha));
style.background_source_color = (alpha > 0.0).then_some(color);
style.background_alpha = alpha.clamp(0.0, 1.0);
}
}
if let Some(name) = style.pending_border_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
let alpha = alpha.clamp(0.0, 1.0);
style.border_color = Some(color);
style.border_top_color = Some(color);
style.border_right_color = Some(color);
style.border_bottom_color = Some(color);
style.border_left_color = Some(color);
style.border_top_alpha = alpha;
style.border_right_alpha = alpha;
style.border_bottom_alpha = alpha;
style.border_left_alpha = alpha;
}
}
if let Some(name) = style.pending_border_top_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.border_top_color = Some(color);
style.border_top_alpha = alpha.clamp(0.0, 1.0);
}
}
if let Some(name) = style.pending_border_right_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.border_right_color = Some(color);
style.border_right_alpha = alpha.clamp(0.0, 1.0);
}
}
if let Some(name) = style.pending_border_bottom_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.border_bottom_color = Some(color);
style.border_bottom_alpha = alpha.clamp(0.0, 1.0);
}
}
if let Some(name) = style.pending_border_left_color_var.take() {
if let Some((color, alpha)) = resolve_custom_color_expr_with_alpha(style, &name) {
style.border_left_color = Some(color);
style.border_left_alpha = alpha.clamp(0.0, 1.0);
}
}
if let Some(name) = style.pending_font_name_var.take() {
if let Some(spec) = resolve_font_name_expr(style, &name) {
match spec {
FontSpec::Value(stack) => {
if !stack.is_empty() {
style.font_stack = stack.clone();
style.font_name = stack[0].clone();
style.font_unicode_ranges = vec![None; stack.len()];
style.font_face_family = None;
style.font_face_source_count = 0;
style.font_face_size_adjust = I32F32::from_num(1.0);
style.font_face_satisfies_weight = false;
style.font_face_satisfies_style = false;
}
}
FontSpec::Inherit => {}
FontSpec::Initial => {
style.font_stack = vec![Arc::<str>::from("Helvetica")];
style.font_name = Arc::<str>::from("Helvetica");
style.font_unicode_ranges = vec![None];
style.font_face_family = None;
style.font_face_source_count = 0;
style.font_face_size_adjust = I32F32::from_num(1.0);
style.font_face_satisfies_weight = false;
style.font_face_satisfies_style = false;
}
}
}
}
let shadow_expr = style
.box_shadow
.as_ref()
.and_then(|shadow| shadow.color_var.clone());
let shadow_resolved = shadow_expr
.as_ref()
.and_then(|expr| resolve_custom_color_expr_with_alpha(style, expr));
if let Some(shadow) = style.box_shadow.as_mut() {
if shadow_expr.is_some() {
if let Some((color, alpha)) = shadow_resolved {
shadow.color = color;
shadow.opacity = (shadow.opacity * alpha).clamp(0.0, 1.0);
shadow.color_var = None;
} else {
// Unresolved color var should not silently paint opaque black.
shadow.opacity = 0.0;
}
}
}
let shadow_resolutions = style
.box_shadows
.iter()
.enumerate()
.filter_map(|(idx, shadow)| {
shadow
.color_var
.as_ref()
.map(|expr| (idx, resolve_custom_color_expr_with_alpha(style, expr)))
})
.collect::<Vec<_>>();
for (idx, resolved) in shadow_resolutions {
if let Some(shadow) = style.box_shadows.get_mut(idx) {
if let Some((color, alpha)) = resolved {
shadow.color = color;
shadow.opacity = (shadow.opacity * alpha).clamp(0.0, 1.0);
shadow.color_var = None;
} else {
shadow.opacity = 0.0;
}
}
}
if !style.box_shadows.is_empty() {
style.box_shadow = style.box_shadows.first().cloned();
}
let text_shadow_resolutions = style
.text_shadows
.iter()
.enumerate()
.filter_map(|(idx, shadow)| {
shadow
.color_var
.as_ref()
.map(|expr| (idx, resolve_custom_color_expr_with_alpha(style, expr)))
})
.collect::<Vec<_>>();
for (idx, resolved) in text_shadow_resolutions {
if let Some(shadow) = style.text_shadows.get_mut(idx) {
if let Some((color, alpha)) = resolved {
shadow.color = color;
shadow.opacity = (shadow.opacity * alpha).clamp(0.0, 1.0);
shadow.color_var = None;
} else {
shadow.opacity = 0.0;
}
}
}
}
fn parse_font_size_spec_str(raw: &str) -> Option<FontSizeSpec> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => Some(FontSizeSpec::Inherit),
"initial" => Some(FontSizeSpec::Initial),
"xx-small" => Some(FontSizeSpec::AbsolutePt(px_to_pt(9.0))),
"x-small" => Some(FontSizeSpec::AbsolutePt(px_to_pt(10.0))),
"small" => Some(FontSizeSpec::AbsolutePt(px_to_pt(13.0))),
"medium" => Some(FontSizeSpec::AbsolutePt(px_to_pt(16.0))),
"large" => Some(FontSizeSpec::AbsolutePt(px_to_pt(18.0))),
"x-large" => Some(FontSizeSpec::AbsolutePt(px_to_pt(24.0))),
"xx-large" => Some(FontSizeSpec::AbsolutePt(px_to_pt(32.0))),
"xxx-large" => Some(FontSizeSpec::AbsolutePt(px_to_pt(40.0))),
"smaller" => Some(FontSizeSpec::RelativeScale(I32F32::from_num(0.8))),
"larger" => Some(FontSizeSpec::RelativeScale(I32F32::from_num(1.2))),
"0" | "+0" => Some(FontSizeSpec::AbsolutePt(Pt::ZERO)),
_ => font_size_from_native_length(&raw),
}
}
fn native_font_length_math(raw: &str) -> Option<NativeLengthMath> {
match parse_native_length_math(raw, false)? {
NativeMathValue::Number(value) if native_math_component_is_zero(value) => {
Some(NativeLengthMath::zero())
}
NativeMathValue::Length(value) => Some(value),
NativeMathValue::Number(_) | NativeMathValue::Clamped(_) | NativeMathValue::Typed(_) => {
None
}
}
}
fn native_length_math_is_finite(value: NativeLengthMath) -> bool {
[
value.abs_pt,
value.percent,
value.em,
value.rem,
value.vw,
value.vh,
value.vmin,
value.vmax,
value.ex,
value.ch,
value.cap,
value.lh,
value.rlh,
]
.into_iter()
.all(f32::is_finite)
}
fn native_length_math_is_nonnegative(value: NativeLengthMath) -> bool {
value.abs_pt >= 0.0
&& value.percent >= 0.0
&& value.em >= 0.0
&& value.rem >= 0.0
&& value.vw >= 0.0
&& value.vh >= 0.0
&& value.vmin >= 0.0
&& value.vmax >= 0.0
&& value.ex >= 0.0
&& value.ch >= 0.0
&& value.cap >= 0.0
&& value.lh >= 0.0
&& value.rlh >= 0.0
}
fn native_length_math_has_only_abs(value: NativeLengthMath) -> bool {
native_math_component_is_zero(value.percent)
&& native_math_component_is_zero(value.em)
&& native_math_component_is_zero(value.rem)
&& native_math_component_is_zero(value.vw)
&& native_math_component_is_zero(value.vh)
&& native_math_component_is_zero(value.vmin)
&& native_math_component_is_zero(value.vmax)
&& native_math_component_is_zero(value.ex)
&& native_math_component_is_zero(value.ch)
&& native_math_component_is_zero(value.cap)
&& native_math_component_is_zero(value.lh)
&& native_math_component_is_zero(value.rlh)
}
fn font_size_from_native_length(raw: &str) -> Option<FontSizeSpec> {
let value = native_font_length_math(raw)?;
if !native_length_math_is_finite(value) {
return None;
}
let has_font_relative = [value.ex, value.ch, value.cap, value.lh, value.rlh]
.into_iter()
.any(|component| !native_math_component_is_zero(component));
let lowered = raw.trim().to_ascii_lowercase();
let is_math_function = lowered.contains('(');
if !is_math_function && !native_length_math_is_nonnegative(value) {
return None;
}
if has_font_relative {
let LengthSpec::FontRelative(value) = value.to_length_spec()? else {
return None;
};
return Some(FontSizeSpec::FontRelative(value));
}
if is_math_function {
return Some(FontSizeSpec::Calc(value.to_font_calc()));
}
if lowered.ends_with('%') {
return Some(FontSizeSpec::RelativeScale(I32F32::from_num(value.percent)));
}
if lowered.ends_with("em") && !lowered.ends_with("rem") {
return Some(FontSizeSpec::RelativeScale(I32F32::from_num(value.em)));
}
if native_length_math_has_only_abs(value) {
return Some(FontSizeSpec::AbsolutePt(Pt::from_f32(value.abs_pt)));
}
Some(FontSizeSpec::Calc(value.to_font_calc()))
}
fn parse_line_height_spec_str(raw: &str) -> Option<LineHeightSpec> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"normal" => return Some(LineHeightSpec::Normal),
"inherit" | "unset" | "revert" | "revert-layer" => return Some(LineHeightSpec::Inherit),
"initial" => return Some(LineHeightSpec::Initial),
_ => {}
}
if let Ok(value) = raw.parse::<f32>() {
return (value >= 0.0).then_some(LineHeightSpec::Number(value));
}
line_height_from_native_length(&raw)
}
fn line_height_from_native_length(raw: &str) -> Option<LineHeightSpec> {
let value = native_font_length_math(raw)?;
if !native_length_math_is_finite(value) {
return None;
}
if [value.ex, value.ch, value.cap, value.lh, value.rlh]
.into_iter()
.any(|component| !native_math_component_is_zero(component))
{
return None;
}
let lowered = raw.trim().to_ascii_lowercase();
let is_math_function = lowered.contains('(');
if !is_math_function && !native_length_math_is_nonnegative(value) {
return None;
}
if is_math_function {
return Some(LineHeightSpec::Calc(value.to_font_calc()));
}
if lowered.ends_with('%') {
return Some(LineHeightSpec::RelativeScale(I32F32::from_num(
value.percent,
)));
}
if lowered.ends_with("em") && !lowered.ends_with("rem") {
return Some(LineHeightSpec::RelativeScale(I32F32::from_num(value.em)));
}
if native_length_math_has_only_abs(value) {
return Some(LineHeightSpec::AbsolutePt(Pt::from_f32(value.abs_pt)));
}
Some(LineHeightSpec::Calc(value.to_font_calc()))
}
fn parse_flex_flow_str(raw: &str) -> Option<(FlexDirectionMode, FlexWrapMode)> {
let mut direction = FlexDirectionMode::Row;
let mut wrap = FlexWrapMode::NoWrap;
let mut saw_component = false;
for token in raw.split_ascii_whitespace() {
let token = token.to_ascii_lowercase();
match token.as_str() {
"row" => {
direction = FlexDirectionMode::Row;
saw_component = true;
}
"row-reverse" => {
direction = FlexDirectionMode::RowReverse;
saw_component = true;
}
"column" => {
direction = FlexDirectionMode::Column;
saw_component = true;
}
"column-reverse" => {
direction = FlexDirectionMode::ColumnReverse;
saw_component = true;
}
"nowrap" => {
wrap = FlexWrapMode::NoWrap;
saw_component = true;
}
"wrap" => {
wrap = FlexWrapMode::Wrap;
saw_component = true;
}
"wrap-reverse" => {
wrap = FlexWrapMode::WrapReverse;
saw_component = true;
}
_ => return None,
}
}
if saw_component {
Some((direction, wrap))
} else {
None
}
}
fn justify_content_mode_from_keyword(keyword: &str) -> Option<JustifyContentMode> {
match keyword {
"flex-start" | "start" | "normal" | "stretch" => Some(JustifyContentMode::FlexStart),
"flex-end" | "end" => Some(JustifyContentMode::FlexEnd),
"center" => Some(JustifyContentMode::Center),
"space-between" => Some(JustifyContentMode::SpaceBetween),
"space-around" => Some(JustifyContentMode::SpaceAround),
"space-evenly" => Some(JustifyContentMode::SpaceEvenly),
_ => None,
}
}
fn parse_aspect_ratio_spec(raw: &str) -> Option<AspectRatioSpec> {
let normalized = raw.trim().to_ascii_lowercase();
match normalized.as_str() {
"auto" => return Some(AspectRatioSpec::Auto),
"inherit" => return Some(AspectRatioSpec::Inherit),
"initial" => return Some(AspectRatioSpec::Initial),
_ => {}
}
let ratio = normalized.strip_prefix("auto ").unwrap_or(&normalized);
let mut components = ratio.split('/').map(str::trim);
let numerator = components.next()?.parse::<f32>().ok()?;
let denominator = components
.next()
.map(str::parse::<f32>)
.transpose()
.ok()?
.unwrap_or(1.0);
if components.next().is_some()
|| !numerator.is_finite()
|| !denominator.is_finite()
|| numerator <= 0.0
|| denominator <= 0.0
{
return None;
}
Some(AspectRatioSpec::Ratio(numerator / denominator))
}
fn justify_content_mode_from_value(raw: &str) -> Option<JustifyContentMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"safe center" => Some(JustifyContentMode::SafeCenter),
"unsafe center" => Some(JustifyContentMode::Center),
_ => last_css_keyword(raw).and_then(justify_content_mode_from_keyword),
}
}
fn align_items_mode_from_keyword(keyword: &str) -> Option<AlignItemsMode> {
match keyword {
"flex-start" | "start" | "normal" => Some(AlignItemsMode::FlexStart),
"baseline" => Some(AlignItemsMode::FirstBaseline),
"flex-end" | "end" => Some(AlignItemsMode::FlexEnd),
"center" => Some(AlignItemsMode::Center),
"stretch" => Some(AlignItemsMode::Stretch),
_ => None,
}
}
fn align_items_mode_from_value(raw: &str) -> Option<AlignItemsMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"first baseline" => Some(AlignItemsMode::FirstBaseline),
"last baseline" => Some(AlignItemsMode::LastBaseline),
_ => last_css_keyword(raw).and_then(align_items_mode_from_keyword),
}
}
fn align_content_mode_from_keyword(keyword: &str) -> Option<AlignContentMode> {
match keyword {
"flex-start" | "start" => Some(AlignContentMode::FlexStart),
"flex-end" | "end" => Some(AlignContentMode::FlexEnd),
"center" => Some(AlignContentMode::Center),
"normal" | "stretch" => Some(AlignContentMode::Stretch),
"space-between" => Some(AlignContentMode::SpaceBetween),
"space-around" => Some(AlignContentMode::SpaceAround),
"space-evenly" => Some(AlignContentMode::SpaceEvenly),
_ => None,
}
}
fn align_self_mode_from_keyword(keyword: &str) -> Option<AlignSelfMode> {
match keyword {
"auto" | "normal" => Some(AlignSelfMode::Auto),
"flex-end" | "end" | "self-end" => Some(AlignSelfMode::FlexEnd),
"center" => Some(AlignSelfMode::Center),
"stretch" => Some(AlignSelfMode::Stretch),
"flex-start" | "start" | "self-start" => Some(AlignSelfMode::FlexStart),
"baseline" => Some(AlignSelfMode::FirstBaseline),
_ => None,
}
}
fn align_self_mode_from_value(raw: &str) -> Option<AlignSelfMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"first baseline" => Some(AlignSelfMode::FirstBaseline),
"last baseline" => Some(AlignSelfMode::LastBaseline),
_ => last_css_keyword(raw).and_then(align_self_mode_from_keyword),
}
}
fn parse_place_content_str(raw: &str) -> Option<(AlignContentMode, JustifyContentMode)> {
let mut tokens = raw.split_ascii_whitespace().map(|v| v.to_ascii_lowercase());
let first = tokens.next()?;
let align = align_content_mode_from_keyword(first.as_str())?;
let justify = if let Some(second) = tokens.next() {
justify_content_mode_from_keyword(second.as_str())?
} else {
justify_content_mode_from_keyword(first.as_str()).unwrap_or(JustifyContentMode::FlexStart)
};
if tokens.next().is_some() {
return None;
}
Some((align, justify))
}
fn parse_place_items_str(raw: &str) -> Option<(AlignItemsMode, AlignItemsMode)> {
let mut tokens = raw.split_ascii_whitespace().map(|v| v.to_ascii_lowercase());
let first = tokens.next()?;
let align = align_items_mode_from_keyword(first.as_str())?;
let justify = if let Some(second) = tokens.next() {
align_items_mode_from_keyword(second.as_str())?
} else {
align
};
if tokens.next().is_some() {
return None;
}
Some((align, justify))
}
fn parse_place_self_str(raw: &str) -> Option<(AlignSelfMode, AlignSelfMode)> {
let mut tokens = raw.split_ascii_whitespace().map(|v| v.to_ascii_lowercase());
let first = tokens.next()?;
let align = align_self_mode_from_keyword(first.as_str())?;
let justify = if let Some(second) = tokens.next() {
align_self_mode_from_keyword(second.as_str())?
} else {
align
};
if tokens.next().is_some() {
return None;
}
Some((align, justify))
}
fn parse_font_weight_str(raw: &str) -> Option<u16> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"normal" => Some(400),
"bold" => Some(700),
_ => raw
.parse::<u16>()
.ok()
.filter(|weight| (1..=1000).contains(weight)),
}
}
fn parse_font_weight_spec_str(raw: &str) -> Option<FontWeightSpec> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => Some(FontWeightSpec::Inherit),
"initial" => Some(FontWeightSpec::Initial),
"bolder" => Some(FontWeightSpec::Bolder),
"lighter" => Some(FontWeightSpec::Lighter),
_ => parse_font_weight_str(&raw).map(FontWeightSpec::Value),
}
}
fn relative_bolder_font_weight(parent: u16) -> u16 {
match parent {
0..=349 => 400,
350..=549 => 700,
_ => 900,
}
}
fn relative_lighter_font_weight(parent: u16) -> u16 {
match parent {
0..=549 => 100,
550..=749 => 400,
_ => 700,
}
}
fn oblique_font_style(degrees: f32) -> FontStyleMode {
let clamped = degrees.clamp(-90.0, 90.0);
FontStyleMode::Oblique((clamped * 100.0).round() as i16)
}
#[cfg(test)]
fn oblique_font_style_degrees(mode: FontStyleMode) -> Option<f32> {
match mode {
FontStyleMode::Oblique(centideg) => Some((centideg as f32) / 100.0),
_ => None,
}
}
fn font_style_debug_string(mode: FontStyleMode) -> String {
match mode {
FontStyleMode::Normal => "Normal".to_string(),
FontStyleMode::Italic => "Italic".to_string(),
FontStyleMode::Oblique(centideg) => format!("Oblique({:.2}deg)", (centideg as f32) / 100.0),
}
}
pub(crate) fn parse_font_style_str(raw: &str) -> Option<FontStyleMode> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"normal" => return Some(FontStyleMode::Normal),
"italic" => return Some(FontStyleMode::Italic),
"oblique" => return Some(oblique_font_style(14.0)),
_ => {}
}
let angle = raw.strip_prefix("oblique ")?;
let degrees = parse_angle_radians(angle)?.to_degrees();
Some(oblique_font_style(degrees))
}
fn parse_text_transform_str(raw: &str) -> Option<TextTransformMode> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"uppercase" => Some(TextTransformMode::Uppercase),
"lowercase" => Some(TextTransformMode::Lowercase),
"capitalize" => Some(TextTransformMode::Capitalize),
"none" => Some(TextTransformMode::None),
_ => None,
}
}
fn parse_text_align_last_str(raw: &str) -> Option<TextAlignLastMode> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"auto" => Some(TextAlignLastMode::Auto),
"start" | "match-parent" => Some(TextAlignLastMode::Start),
"end" => Some(TextAlignLastMode::End),
"left" => Some(TextAlignLastMode::Left),
"right" => Some(TextAlignLastMode::Right),
"center" => Some(TextAlignLastMode::Center),
"justify" => Some(TextAlignLastMode::Justify),
_ => None,
}
}
fn parse_text_justify_mode_str(raw: &str) -> Option<TextJustifyMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Some(TextJustifyMode::Auto),
"none" => Some(TextJustifyMode::None),
"inter-word" => Some(TextJustifyMode::InterWord),
"inter-character" | "distribute" => Some(TextJustifyMode::InterCharacter),
_ => None,
}
}
fn parse_hyphens_mode_str(raw: &str) -> Option<HyphensMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" => Some(HyphensMode::None),
"manual" => Some(HyphensMode::Manual),
"auto" => Some(HyphensMode::Auto),
_ => None,
}
}
fn parse_text_wrap_mode_str(raw: &str) -> Option<TextWrapMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"wrap" => Some(TextWrapMode::Wrap),
"nowrap" => Some(TextWrapMode::NoWrap),
_ => None,
}
}
fn parse_text_wrap_style_str(raw: &str) -> Option<TextWrapStyleMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Some(TextWrapStyleMode::Auto),
"balance" => Some(TextWrapStyleMode::Balance),
"stable" => Some(TextWrapStyleMode::Stable),
"pretty" => Some(TextWrapStyleMode::Pretty),
"avoid-orphans" => Some(TextWrapStyleMode::AvoidOrphans),
_ => None,
}
}
fn parse_text_wrap_shorthand_str(raw: &str) -> Option<(TextWrapMode, TextWrapStyleMode)> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() || lowered.contains("var(") {
return None;
}
let mut mode = None;
let mut style = None;
for part in lowered.split_whitespace() {
if mode.is_none() {
if let Some(value) = parse_text_wrap_mode_str(part) {
mode = Some(value);
continue;
}
}
if style.is_none() {
if let Some(value) = parse_text_wrap_style_str(part) {
style = Some(value);
continue;
}
}
return None;
}
Some((
mode.unwrap_or(TextWrapMode::Wrap),
style.unwrap_or(TextWrapStyleMode::Auto),
))
}
fn parse_white_space_collapse_mode_str(raw: &str) -> Option<WhiteSpaceMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"collapse" => Some(WhiteSpaceMode::Normal),
"preserve" => Some(WhiteSpaceMode::PreWrap),
"preserve-breaks" => Some(WhiteSpaceMode::PreLine),
"preserve-spaces" => Some(WhiteSpaceMode::PreserveSpaces),
"break-spaces" => Some(WhiteSpaceMode::BreakSpaces),
_ => None,
}
}
fn white_space_mode_from_collapse_and_wrap(
collapse: WhiteSpaceMode,
wrap: TextWrapMode,
) -> WhiteSpaceMode {
match (collapse, wrap) {
(WhiteSpaceMode::Normal, TextWrapMode::NoWrap) => WhiteSpaceMode::NoWrap,
(WhiteSpaceMode::PreWrap, TextWrapMode::NoWrap) => WhiteSpaceMode::Pre,
_ => collapse,
}
}
fn parse_white_space_shorthand_str(raw: &str) -> Option<(WhiteSpaceMode, TextWrapMode)> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() || lowered.contains("var(") {
return None;
}
match lowered.as_str() {
"normal" => return Some((WhiteSpaceMode::Normal, TextWrapMode::Wrap)),
"pre" => return Some((WhiteSpaceMode::Pre, TextWrapMode::NoWrap)),
"pre-wrap" => return Some((WhiteSpaceMode::PreWrap, TextWrapMode::Wrap)),
"pre-line" => return Some((WhiteSpaceMode::PreLine, TextWrapMode::Wrap)),
_ => {}
}
let mut collapse = None;
let mut wrap = None;
for part in lowered.split_whitespace() {
if collapse.is_none() {
if let Some(value) = parse_white_space_collapse_mode_str(part) {
collapse = Some(value);
continue;
}
}
if wrap.is_none() {
if let Some(value) = parse_text_wrap_mode_str(part) {
wrap = Some(value);
continue;
}
}
return None;
}
let wrap = wrap.unwrap_or(TextWrapMode::Wrap);
let collapse = collapse.unwrap_or(WhiteSpaceMode::Normal);
Some((
white_space_mode_from_collapse_and_wrap(collapse, wrap),
wrap,
))
}
fn parse_white_space_mode_str(raw: &str) -> Option<WhiteSpaceMode> {
parse_white_space_collapse_mode_str(raw)
}
fn parse_hyphenate_character_str(raw: &str) -> Option<Option<String>> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("auto") {
return Some(None);
}
let (value, trailing) = parse_css_string_literal(raw)?;
trailing.trim().is_empty().then_some(Some(value))
}
fn matrix_op_from_components(
a: f32,
b: f32,
c: f32,
d: f32,
e_px: f32,
f_px: f32,
) -> Option<CssTransformOp> {
if !a.is_finite()
|| !b.is_finite()
|| !c.is_finite()
|| !d.is_finite()
|| !e_px.is_finite()
|| !f_px.is_finite()
{
return None;
}
Some(CssTransformOp::Matrix {
a,
b,
c,
d,
e: px_to_pt(e_px),
f: px_to_pt(f_px),
})
}
fn transform_origin_component_from_string(
token: &str,
prefer_x: bool,
) -> Option<(Option<LengthSpec>, Option<LengthSpec>)> {
let lower = token.trim().to_ascii_lowercase();
if lower.is_empty() {
return None;
}
match lower.as_str() {
"left" => Some((Some(LengthSpec::Percent(0.0)), None)),
"right" => Some((Some(LengthSpec::Percent(1.0)), None)),
"top" => Some((None, Some(LengthSpec::Percent(0.0)))),
"bottom" => Some((None, Some(LengthSpec::Percent(1.0)))),
"center" => {
if prefer_x {
Some((Some(LengthSpec::Percent(0.5)), None))
} else {
Some((None, Some(LengthSpec::Percent(0.5))))
}
}
_ => {
let length = parse_single_length_spec(lower.as_str())?;
if prefer_x {
Some((Some(length), None))
} else {
Some((None, Some(length)))
}
}
}
}
fn transform_origin_from_string(raw: &str) -> Option<CssTransformOrigin> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let parts = split_top_level_whitespace(raw);
if parts.is_empty() || parts.len() > 5 {
return None;
}
if let Some((x, y)) = parse_shape_position_pair(&parts) {
return Some(CssTransformOrigin {
x,
y,
z: LengthSpec::Absolute(Pt::ZERO),
});
}
// `transform-origin` extends the ordinary 2D position grammar with one
// trailing, non-percentage z length. Try the complete position first so
// far-edge offset forms such as `right 10px bottom 4px` stay unambiguous.
if parts.len() >= 2 {
let position = &parts[..parts.len() - 1];
let z = parse_non_percentage_length_spec(&parts[parts.len() - 1]);
if let (Some((x, y)), Some(z)) = (parse_shape_position_pair(position), z) {
return Some(CssTransformOrigin { x, y, z });
}
}
let mut x: Option<LengthSpec> = None;
let mut y: Option<LengthSpec> = None;
for part in parts {
let prefer_x = x.is_none();
let (candidate_x, candidate_y) = transform_origin_component_from_string(&part, prefer_x)?;
if let Some(cx) = candidate_x {
if x.is_some() {
return None;
}
x = Some(cx);
continue;
}
if let Some(cy) = candidate_y {
if y.is_some() {
return None;
}
y = Some(cy);
}
}
Some(CssTransformOrigin {
x: x.unwrap_or(LengthSpec::Percent(0.5)),
y: y.unwrap_or(LengthSpec::Percent(0.5)),
z: LengthSpec::Absolute(Pt::ZERO),
})
}
fn transform_ops_from_string(raw: &str) -> Option<Vec<CssTransformOp>> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let calls = parse_transform_calls(raw)?;
let mut ops = Vec::new();
for (name, args) in calls {
let op = match name.as_str() {
"translate" => {
let (x, y) = parse_translate_components(&args)?;
CssTransformOp::Translate { x, y }
}
"translatex" => CssTransformOp::Translate {
x: parse_single_length_spec(&args)?,
y: LengthSpec::Absolute(Pt::ZERO),
},
"translatey" => CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::ZERO),
y: parse_single_length_spec(&args)?,
},
"translatez" => CssTransformOp::Translate3d {
x: LengthSpec::Absolute(Pt::ZERO),
y: LengthSpec::Absolute(Pt::ZERO),
z: parse_non_percentage_length_spec(&args)?,
},
"translate3d" => {
let (x, y, z) = parse_translate_3d_components(&args)?;
CssTransformOp::Translate3d { x, y, z }
}
"scale" => {
let (x, y) = parse_scale_components(&args)?;
CssTransformOp::Scale { x, y }
}
"scale3d" => {
let (x, y, z) = parse_scale_3d_components(&args)?;
CssTransformOp::Scale3d { x, y, z }
}
"scalex" => CssTransformOp::Scale {
x: parse_single_scale_factor(&args)?,
y: 1.0,
},
"scaley" => CssTransformOp::Scale {
x: 1.0,
y: parse_single_scale_factor(&args)?,
},
"scalez" => CssTransformOp::Scale3d {
x: 1.0,
y: 1.0,
z: parse_single_scale_factor(&args)?,
},
"rotate" | "rotatez" => CssTransformOp::Rotate {
radians: parse_angle_radians(&args)?,
},
"rotatex" => CssTransformOp::Rotate3d {
x: 1.0,
y: 0.0,
z: 0.0,
radians: parse_angle_radians(&args)?,
},
"rotatey" => CssTransformOp::Rotate3d {
x: 0.0,
y: 1.0,
z: 0.0,
radians: parse_angle_radians(&args)?,
},
"rotate3d" => {
let (x, y, z, radians) = parse_rotate_3d_components(&args)?;
CssTransformOp::Rotate3d { x, y, z, radians }
}
"perspective" => CssTransformOp::Perspective {
distance: parse_non_percentage_length_spec(&args)?,
},
"skew" => {
let (x_radians, y_radians) = parse_skew_components(&args)?;
CssTransformOp::Skew {
x_radians,
y_radians,
}
}
"skewx" => CssTransformOp::Skew {
x_radians: parse_angle_radians(&args)?,
y_radians: 0.0,
},
"skewy" => CssTransformOp::Skew {
x_radians: 0.0,
y_radians: parse_angle_radians(&args)?,
},
"matrix" => {
let (a, b, c, d, e_px, f_px) = parse_matrix_components(&args)?;
matrix_op_from_components(a, b, c, d, e_px, f_px)?
}
"matrix3d" => {
let values = parse_matrix3d_components(&args)?;
matrix_op_from_matrix_3d_components(values)?
}
_ => return None,
};
ops.push(op);
}
Some(ops)
}
fn transform_ops_from_translate_string(raw: &str) -> Option<Vec<CssTransformOp>> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let parts = split_top_level_whitespace(raw);
if parts.len() == 3 {
let x = parse_single_length_spec(&parts[0])?;
let y = parse_single_length_spec(&parts[1])?;
let z = parse_non_percentage_length_spec(&parts[2])?;
return Some(vec![CssTransformOp::Translate3d { x, y, z }]);
}
let (x, y) = parse_translate_components(raw)?;
Some(vec![CssTransformOp::Translate { x, y }])
}
fn transform_ops_from_rotate_string(raw: &str) -> Option<Vec<CssTransformOp>> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
Some(vec![CssTransformOp::Rotate {
radians: parse_angle_radians(raw)?,
}])
}
fn transform_ops_from_scale_string(raw: &str) -> Option<Vec<CssTransformOp>> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let parts = split_top_level_whitespace(raw);
if parts.len() == 3 {
let x = parse_single_scale_factor(&parts[0])?;
let y = parse_single_scale_factor(&parts[1])?;
let z = parse_single_scale_factor(&parts[2])?;
return Some(vec![CssTransformOp::Scale3d { x, y, z }]);
}
let (x, y) = parse_scale_components(raw)?;
Some(vec![CssTransformOp::Scale { x, y }])
}
fn parse_transform_calls(raw: &str) -> Option<Vec<(String, String)>> {
let chars: Vec<char> = raw.chars().collect();
let mut out: Vec<(String, String)> = Vec::new();
let mut i = 0usize;
while i < chars.len() {
while i < chars.len() && chars[i].is_ascii_whitespace() {
i += 1;
}
if i >= chars.len() {
break;
}
let start = i;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '-') {
i += 1;
}
if i == start || i >= chars.len() || chars[i] != '(' {
return None;
}
let name = chars[start..i]
.iter()
.collect::<String>()
.to_ascii_lowercase();
i += 1;
let args_start = i;
let mut depth = 1usize;
while i < chars.len() {
match chars[i] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
if depth != 0 || i >= chars.len() {
return None;
}
let args = chars[args_start..i].iter().collect::<String>();
out.push((name, args));
i += 1;
}
if out.is_empty() { None } else { Some(out) }
}
fn parse_single_length_spec(raw: &str) -> Option<LengthSpec> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
match parse_native_length_math(raw, true)? {
NativeMathValue::Number(value) if value.abs() <= f32::EPSILON => {
Some(LengthSpec::Absolute(Pt::ZERO))
}
NativeMathValue::Number(_) => None,
NativeMathValue::Length(value) => value.to_length_spec(),
NativeMathValue::Clamped(value) => value.to_length_spec(),
NativeMathValue::Typed(_) => None,
}
}
fn parse_non_percentage_length_spec(raw: &str) -> Option<LengthSpec> {
let value = parse_single_length_spec(raw)?;
let contains_percentage = match value {
LengthSpec::Percent(_) => true,
LengthSpec::Calc(calc) => calc.percent.abs() > f32::EPSILON,
LengthSpec::Clamped(calc) => calc.value.percent.abs() > f32::EPSILON,
LengthSpec::FontRelative(calc) => calc.base.percent.abs() > f32::EPSILON,
_ => false,
};
(!contains_percentage).then_some(value)
}
fn parse_translate_components(raw: &str) -> Option<(LengthSpec, LengthSpec)> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
if comma_parts.len() > 1 {
if comma_parts.len() > 2 {
return None;
}
let x = parse_single_length_spec(comma_parts[0].as_str())?;
let y = parse_single_length_spec(comma_parts[1].as_str())?;
return Some((x, y));
}
let ws_parts = split_top_level_whitespace(raw);
if ws_parts.is_empty() || ws_parts.len() > 2 {
return None;
}
let x = parse_single_length_spec(ws_parts[0].as_str())?;
let y = if ws_parts.len() == 2 {
parse_single_length_spec(ws_parts[1].as_str())?
} else {
LengthSpec::Absolute(Pt::ZERO)
};
Some((x, y))
}
fn parse_translate_3d_components(raw: &str) -> Option<(LengthSpec, LengthSpec, LengthSpec)> {
let comma_parts = split_top_level_delimited(raw.trim(), ',');
let parts = if comma_parts.len() > 1 {
comma_parts
} else {
split_top_level_whitespace(raw.trim())
};
if parts.len() != 3 {
return None;
}
Some((
parse_single_length_spec(&parts[0])?,
parse_single_length_spec(&parts[1])?,
parse_non_percentage_length_spec(&parts[2])?,
))
}
fn parse_scale_components(raw: &str) -> Option<(f32, f32)> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
if comma_parts.len() > 1 {
if comma_parts.len() > 2 {
return None;
}
let x = parse_single_scale_factor(comma_parts[0].as_str())?;
let y = parse_single_scale_factor(comma_parts[1].as_str())?;
return Some((x, y));
}
let ws_parts = split_top_level_whitespace(raw);
if ws_parts.is_empty() || ws_parts.len() > 2 {
return None;
}
let x = parse_single_scale_factor(ws_parts[0].as_str())?;
let y = if ws_parts.len() == 2 {
parse_single_scale_factor(ws_parts[1].as_str())?
} else {
x
};
Some((x, y))
}
fn parse_scale_3d_components(raw: &str) -> Option<(f32, f32, f32)> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
if comma_parts.len() == 3 {
let x = parse_single_scale_factor(comma_parts[0].as_str())?;
let y = parse_single_scale_factor(comma_parts[1].as_str())?;
let z = parse_single_scale_factor(comma_parts[2].as_str())?;
return Some((x, y, z));
}
let ws_parts = split_top_level_whitespace(raw);
if ws_parts.len() != 3 {
return None;
}
let x = parse_single_scale_factor(ws_parts[0].as_str())?;
let y = parse_single_scale_factor(ws_parts[1].as_str())?;
let z = parse_single_scale_factor(ws_parts[2].as_str())?;
Some((x, y, z))
}
fn parse_rotate_3d_components(raw: &str) -> Option<(f32, f32, f32, f32)> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
if comma_parts.len() == 4 {
let x = parse_single_scale_factor(comma_parts[0].as_str())?;
let y = parse_single_scale_factor(comma_parts[1].as_str())?;
let z = parse_single_scale_factor(comma_parts[2].as_str())?;
let angle = parse_angle_radians(comma_parts[3].as_str())?;
return Some((x, y, z, angle));
}
let ws_parts = split_top_level_whitespace(raw);
if ws_parts.len() != 4 {
return None;
}
let x = parse_single_scale_factor(ws_parts[0].as_str())?;
let y = parse_single_scale_factor(ws_parts[1].as_str())?;
let z = parse_single_scale_factor(ws_parts[2].as_str())?;
let angle = parse_angle_radians(ws_parts[3].as_str())?;
Some((x, y, z, angle))
}
fn parse_skew_components(raw: &str) -> Option<(f32, f32)> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
if comma_parts.len() > 1 {
if comma_parts.len() > 2 {
return None;
}
let x = parse_angle_radians(comma_parts[0].as_str())?;
let y = parse_angle_radians(comma_parts[1].as_str())?;
return Some((x, y));
}
let ws_parts = split_top_level_whitespace(raw);
if ws_parts.is_empty() || ws_parts.len() > 2 {
return None;
}
let x = parse_angle_radians(ws_parts[0].as_str())?;
let y = if ws_parts.len() == 2 {
parse_angle_radians(ws_parts[1].as_str())?
} else {
0.0
};
Some((x, y))
}
fn parse_matrix_components(raw: &str) -> Option<(f32, f32, f32, f32, f32, f32)> {
let values = parse_numeric_components(raw, 6)?;
Some((
values[0], values[1], values[2], values[3], values[4], values[5],
))
}
fn parse_matrix3d_components(raw: &str) -> Option<[f32; 16]> {
let values = parse_numeric_components(raw, 16)?;
let mut out = [0.0f32; 16];
out.copy_from_slice(values.as_slice());
Some(out)
}
fn parse_numeric_components(raw: &str, expected: usize) -> Option<Vec<f32>> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let comma_parts = split_top_level_delimited(raw, ',');
let parts = if comma_parts.len() > 1 {
comma_parts
} else {
split_top_level_whitespace(raw)
};
if parts.len() != expected {
return None;
}
let mut out = Vec::with_capacity(expected);
for part in parts {
out.push(parse_single_number(&part)?);
}
Some(out)
}
fn parse_single_number(raw: &str) -> Option<f32> {
let value = raw.trim().parse::<f32>().ok()?;
if value.is_finite() { Some(value) } else { None }
}
fn matrix_op_from_matrix_3d_components(values: [f32; 16]) -> Option<CssTransformOp> {
let eps = 1.0e-6f32;
let near = |value: f32, target: f32| (value - target).abs() <= eps;
if !near(values[2], 0.0)
|| !near(values[3], 0.0)
|| !near(values[6], 0.0)
|| !near(values[7], 0.0)
|| !near(values[8], 0.0)
|| !near(values[9], 0.0)
|| !near(values[11], 0.0)
|| !near(values[14], 0.0)
|| !near(values[10], 1.0)
|| !near(values[15], 1.0)
{
return Some(CssTransformOp::Matrix3d { values });
}
matrix_op_from_components(
values[0], values[1], values[4], values[5], values[12], values[13],
)
}
fn parse_single_scale_factor(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if let Some(value) = raw.strip_suffix('%') {
let parsed = value.trim().parse::<f32>().ok()?;
if !parsed.is_finite() {
return None;
}
return Some(parsed / 100.0);
}
let parsed = raw.parse::<f32>().ok()?;
if !parsed.is_finite() {
return None;
}
Some(parsed)
}
fn parse_angle_radians(raw: &str) -> Option<f32> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
if raw == "0" {
return Some(0.0);
}
let parse_value = |value: &str| -> Option<f32> {
let parsed = value.trim().parse::<f32>().ok()?;
if parsed.is_finite() {
Some(parsed)
} else {
None
}
};
if let Some(value) = raw.strip_suffix("deg") {
return parse_value(value).map(|v| v.to_radians());
}
if let Some(value) = raw.strip_suffix("grad") {
return parse_value(value).map(|v| v * (std::f32::consts::PI / 200.0));
}
if let Some(value) = raw.strip_suffix("rad") {
return parse_value(value);
}
if let Some(value) = raw.strip_suffix("turn") {
return parse_value(value).map(|v| v * (2.0 * std::f32::consts::PI));
}
None
}
fn split_top_level_delimited(raw: &str, delimiter: char) -> Vec<String> {
let mut parts: Vec<String> = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
for ch in raw.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
if depth > 0 {
depth -= 1;
}
current.push(ch);
}
c if c == delimiter && depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
fn split_top_level_whitespace(raw: &str) -> Vec<String> {
let mut parts: Vec<String> = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
for ch in raw.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
if depth > 0 {
depth -= 1;
}
current.push(ch);
}
c if c.is_ascii_whitespace() && depth == 0 => {
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
current.clear();
}
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
fn set_delta_text_decoration_spec(delta: &mut StyleDelta, spec: TextDecorationSpec) {
delta.text_decoration = Some(spec);
delta.revert_layer.text_decoration_line = false;
}
fn set_delta_text_decoration_color_spec(delta: &mut StyleDelta, spec: ColorSpec) {
delta.text_decoration_color = Some(spec);
delta.text_decoration_color_var = None;
delta.revert_layer.text_decoration_color = false;
}
fn set_delta_text_decoration_color_var(delta: &mut StyleDelta, var: String) {
delta.text_decoration_color_var = Some(var);
delta.text_decoration_color = None;
delta.revert_layer.text_decoration_color = false;
}
fn set_delta_text_decoration_thickness_spec(
delta: &mut StyleDelta,
spec: TextDecorationThicknessSpec,
) {
delta.text_decoration_thickness = Some(spec);
delta.text_decoration_thickness_var = None;
delta.revert_layer.text_decoration_thickness = false;
}
fn set_delta_text_decoration_thickness_var(delta: &mut StyleDelta, var: String) {
delta.text_decoration_thickness_var = Some(var);
delta.text_decoration_thickness = None;
delta.revert_layer.text_decoration_thickness = false;
}
fn set_delta_text_decoration_style_spec(delta: &mut StyleDelta, spec: TextDecorationStyleSpec) {
delta.text_decoration_style = Some(spec);
delta.revert_layer.text_decoration_style = false;
}
fn set_delta_text_decoration_skip_ink_spec(
delta: &mut StyleDelta,
spec: TextDecorationSkipInkSpec,
) {
delta.text_decoration_skip_ink = Some(spec);
delta.revert_layer.text_decoration_skip_ink = false;
}
fn set_delta_text_emphasis_style(delta: &mut StyleDelta, spec: TextEmphasisStyleSpec) {
delta.text_emphasis_style = Some(spec);
delta.revert_layer.text_emphasis_style = false;
}
fn set_delta_text_emphasis_color(delta: &mut StyleDelta, spec: ColorSpec) {
delta.text_emphasis_color = Some(spec);
delta.revert_layer.text_emphasis_color = false;
}
fn set_delta_text_emphasis_position(delta: &mut StyleDelta, spec: TextEmphasisPositionSpec) {
delta.text_emphasis_position = Some(spec);
delta.revert_layer.text_emphasis_position = false;
}
fn set_delta_text_underline_offset_spec(delta: &mut StyleDelta, spec: TextUnderlineOffsetSpec) {
delta.text_underline_offset = Some(spec);
delta.text_underline_offset_var = None;
delta.revert_layer.text_underline_offset = false;
}
fn set_delta_text_underline_offset_var(delta: &mut StyleDelta, var: String) {
delta.text_underline_offset_var = Some(var);
delta.text_underline_offset = None;
delta.revert_layer.text_underline_offset = false;
}
fn set_delta_text_underline_position_spec(delta: &mut StyleDelta, spec: TextUnderlinePositionSpec) {
delta.text_underline_position = Some(spec);
delta.text_underline_position_var = None;
delta.revert_layer.text_underline_position = false;
}
fn set_delta_text_underline_position_var(delta: &mut StyleDelta, var: String) {
delta.text_underline_position_var = Some(var);
delta.text_underline_position = None;
delta.revert_layer.text_underline_position = false;
}
fn set_delta_text_overflow_spec(delta: &mut StyleDelta, spec: TextOverflowSpec) {
delta.text_overflow = Some(spec);
delta.revert_layer.text_overflow = false;
}
fn set_delta_text_indent_value(delta: &mut StyleDelta, value: TextIndentValue) {
delta.text_indent = Some(value);
delta.revert_layer.text_indent = false;
}
fn set_delta_letter_spacing(delta: &mut StyleDelta, value: LengthSpec) {
delta.letter_spacing = Some(value);
delta.revert_layer.letter_spacing = false;
}
fn set_delta_word_spacing(delta: &mut StyleDelta, value: LengthSpec) {
delta.word_spacing = Some(value);
delta.revert_layer.word_spacing = false;
}
fn set_delta_tab_size(delta: &mut StyleDelta, value: TabSizeSpec) {
delta.tab_size = Some(value);
delta.revert_layer.tab_size = false;
}
fn set_delta_text_align(delta: &mut StyleDelta, value: TextAlignMode) {
delta.text_align = Some(value);
delta.revert_layer.text_align = false;
}
fn set_delta_text_align_last(delta: &mut StyleDelta, value: TextAlignLastSpec) {
delta.text_align_last = Some(value);
delta.revert_layer.text_align_last = false;
}
fn set_delta_text_justify(delta: &mut StyleDelta, value: TextJustifySpec) {
delta.text_justify_var = None;
delta.text_justify = Some(value);
delta.revert_layer.text_justify = false;
}
fn set_delta_text_justify_var(delta: &mut StyleDelta, value: String) {
delta.text_justify = None;
delta.text_justify_var = Some(value);
delta.revert_layer.text_justify = false;
}
fn set_delta_text_transform(delta: &mut StyleDelta, value: TextTransformSpec) {
delta.text_transform = Some(value);
delta.revert_layer.text_transform = false;
}
fn set_delta_word_break(delta: &mut StyleDelta, value: WordBreakSpec) {
delta.word_break = Some(value);
delta.revert_layer.word_break = false;
}
fn set_delta_overflow_wrap(delta: &mut StyleDelta, value: OverflowWrapSpec) {
delta.overflow_wrap = Some(value);
delta.revert_layer.overflow_wrap = false;
}
fn set_delta_line_break(delta: &mut StyleDelta, value: LineBreakSpec) {
delta.line_break_var = None;
delta.line_break = Some(value);
delta.revert_layer.line_break = false;
}
fn set_delta_line_break_var(delta: &mut StyleDelta, value: String) {
delta.line_break = None;
delta.line_break_var = Some(value);
delta.revert_layer.line_break = false;
}
fn set_delta_hyphens(delta: &mut StyleDelta, value: HyphensSpec) {
delta.hyphens_var = None;
delta.hyphens = Some(value);
delta.revert_layer.hyphens = false;
}
fn set_delta_hyphens_var(delta: &mut StyleDelta, value: String) {
delta.hyphens = None;
delta.hyphens_var = Some(value);
delta.revert_layer.hyphens = false;
}
fn set_delta_hyphenate_character(delta: &mut StyleDelta, value: HyphenateCharacterSpec) {
delta.hyphenate_character_var = None;
delta.hyphenate_character = Some(value);
delta.revert_layer.hyphenate_character = false;
}
fn set_delta_hyphenate_character_var(delta: &mut StyleDelta, value: String) {
delta.hyphenate_character = None;
delta.hyphenate_character_var = Some(value);
delta.revert_layer.hyphenate_character = false;
}
fn set_delta_text_shadow_spec(delta: &mut StyleDelta, spec: TextShadowSpec) {
delta.text_shadow = Some(spec);
delta.text_shadow_var = None;
delta.revert_layer.text_shadow = false;
}
fn set_delta_text_shadow_var(delta: &mut StyleDelta, var: String) {
delta.text_shadow_var = Some(var);
delta.text_shadow = None;
delta.revert_layer.text_shadow = false;
}
fn apply_text_decoration_color_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() {
return false;
}
match lowered.as_str() {
"inherit" => {
set_delta_text_decoration_color_spec(delta, ColorSpec::Inherit);
return true;
}
"initial" | "unset" | "revert" => {
set_delta_text_decoration_color_spec(delta, ColorSpec::Initial);
return true;
}
"revert-layer" => return set_revert_layer_text_decoration_color(delta),
"currentcolor" => {
set_delta_text_decoration_color_spec(delta, ColorSpec::CurrentColor);
return true;
}
_ => {}
}
if should_defer_color_expr(&lowered) {
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_text_decoration_color_spec(
delta,
ColorSpec::Value(blend_over_white(color, alpha)),
);
} else {
set_delta_text_decoration_color_var(delta, lowered);
}
return true;
}
if let Some((color, alpha)) = parse_color_string(&lowered) {
set_delta_text_decoration_color_spec(
delta,
ColorSpec::Value(blend_over_white(color, alpha)),
);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_decoration_color_var(delta, var);
return true;
}
false
}
fn apply_text_decoration_color_from_shorthand(delta: &mut StyleDelta, raw: &str) -> bool {
for token in split_top_level_whitespace(raw) {
if apply_text_decoration_color_from_raw(delta, &token) {
return true;
}
}
false
}
fn text_decoration_thickness_value_from_raw(raw: &str) -> Option<TextDecorationThicknessMode> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"auto" => return Some(TextDecorationThicknessMode::Auto),
"from-font" => return Some(TextDecorationThicknessMode::FromFont),
"hairline" => {
return Some(TextDecorationThicknessMode::Length(LengthSpec::Absolute(
Pt::from_f32(0.5),
)));
}
"thin" => {
return Some(TextDecorationThicknessMode::Length(LengthSpec::Absolute(
Pt::from_f32(0.75),
)));
}
"medium" => {
return Some(TextDecorationThicknessMode::Length(LengthSpec::Absolute(
Pt::from_f32(2.25),
)));
}
"thick" => {
return Some(TextDecorationThicknessMode::Length(LengthSpec::Absolute(
Pt::from_f32(3.75),
)));
}
_ => {}
}
let spec = length_spec_from_string(&lowered)?;
match spec {
LengthSpec::Absolute(value) if value >= Pt::ZERO => Some(
TextDecorationThicknessMode::Length(LengthSpec::Absolute(value)),
),
LengthSpec::Percent(value) if value >= 0.0 => Some(TextDecorationThicknessMode::Length(
LengthSpec::Percent(value),
)),
LengthSpec::Em(value) if value >= 0.0 => {
Some(TextDecorationThicknessMode::Length(LengthSpec::Em(value)))
}
LengthSpec::Rem(value) if value >= 0.0 => {
Some(TextDecorationThicknessMode::Length(LengthSpec::Rem(value)))
}
LengthSpec::Calc(calc) => Some(TextDecorationThicknessMode::Length(LengthSpec::Calc(calc))),
_ => None,
}
}
fn apply_text_decoration_thickness_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => {
set_delta_text_decoration_thickness_spec(delta, TextDecorationThicknessSpec::Inherit);
return true;
}
"initial" | "unset" | "revert" => {
set_delta_text_decoration_thickness_spec(delta, TextDecorationThicknessSpec::Initial);
return true;
}
"revert-layer" => return set_revert_layer_text_decoration_thickness(delta),
_ => {}
}
if let Some(value) = text_decoration_thickness_value_from_raw(&lowered) {
set_delta_text_decoration_thickness_spec(delta, TextDecorationThicknessSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_decoration_thickness_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_decoration_thickness_var(delta, var);
return true;
}
false
}
fn apply_text_decoration_thickness_from_shorthand(delta: &mut StyleDelta, raw: &str) -> bool {
for token in split_top_level_whitespace(raw) {
if apply_text_decoration_thickness_from_raw(delta, &token) {
return true;
}
}
false
}
fn text_decoration_style_value_from_raw(raw: &str) -> Option<TextDecorationStyleMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"solid" => Some(TextDecorationStyleMode::Solid),
"double" => Some(TextDecorationStyleMode::Double),
"dotted" => Some(TextDecorationStyleMode::Dotted),
"dashed" => Some(TextDecorationStyleMode::Dashed),
"wavy" => Some(TextDecorationStyleMode::Wavy),
_ => None,
}
}
fn apply_text_decoration_style_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" => {
set_delta_text_decoration_style_spec(delta, TextDecorationStyleSpec::Inherit);
return true;
}
"initial" | "unset" | "revert" => {
set_delta_text_decoration_style_spec(delta, TextDecorationStyleSpec::Initial);
return true;
}
"revert-layer" => return set_revert_layer_text_decoration_style(delta),
_ => {}
}
if let Some(value) = text_decoration_style_value_from_raw(&lowered) {
set_delta_text_decoration_style_spec(delta, TextDecorationStyleSpec::Value(value));
return true;
}
false
}
fn apply_text_decoration_skip_ink_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"auto" => TextDecorationSkipInkSpec::Value(TextDecorationSkipInkMode::Auto),
"none" => TextDecorationSkipInkSpec::Value(TextDecorationSkipInkMode::None),
"all" => TextDecorationSkipInkSpec::Value(TextDecorationSkipInkMode::All),
"inherit" | "unset" | "revert" => TextDecorationSkipInkSpec::Inherit,
"initial" => TextDecorationSkipInkSpec::Initial,
"revert-layer" => return set_revert_layer_text_decoration_skip_ink(delta),
_ => return false,
};
set_delta_text_decoration_skip_ink_spec(delta, spec);
true
}
fn apply_text_decoration_style_from_shorthand(delta: &mut StyleDelta, raw: &str) -> bool {
for token in split_top_level_whitespace(raw) {
if apply_text_decoration_style_from_raw(delta, &token) {
return true;
}
}
false
}
fn text_underline_offset_value_from_raw(raw: &str) -> Option<TextUnderlineOffsetMode> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "auto" {
return Some(TextUnderlineOffsetMode::Auto);
}
let spec = length_spec_from_string(&lowered)?;
match spec {
LengthSpec::Absolute(value) => {
Some(TextUnderlineOffsetMode::Length(LengthSpec::Absolute(value)))
}
LengthSpec::Percent(value) => {
Some(TextUnderlineOffsetMode::Length(LengthSpec::Percent(value)))
}
LengthSpec::Em(value) => Some(TextUnderlineOffsetMode::Length(LengthSpec::Em(value))),
LengthSpec::Rem(value) => Some(TextUnderlineOffsetMode::Length(LengthSpec::Rem(value))),
LengthSpec::Calc(calc) => Some(TextUnderlineOffsetMode::Length(LengthSpec::Calc(calc))),
_ => None,
}
}
fn apply_text_underline_offset_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_underline_offset_spec(delta, TextUnderlineOffsetSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_underline_offset(delta),
"initial" => {
set_delta_text_underline_offset_spec(delta, TextUnderlineOffsetSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = text_underline_offset_value_from_raw(&lowered) {
set_delta_text_underline_offset_spec(delta, TextUnderlineOffsetSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_underline_offset_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_underline_offset_var(delta, var);
return true;
}
false
}
fn text_underline_position_value_from_raw(raw: &str) -> Option<TextUnderlinePositionMode> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "auto" {
return Some(TextUnderlinePositionMode::auto());
}
let mut line = TextUnderlinePositionLine::Auto;
let mut side = None;
let parts = split_top_level_whitespace(&lowered);
if parts.is_empty() || parts.iter().any(|part| part == "auto") {
return None;
}
for part in parts {
match part.as_str() {
"from-font" if line == TextUnderlinePositionLine::Auto => {
line = TextUnderlinePositionLine::FromFont;
}
"under" if line == TextUnderlinePositionLine::Auto => {
line = TextUnderlinePositionLine::Under;
}
"left" if side.is_none() => {
side = Some(TextUnderlinePositionSide::Left);
}
"right" if side.is_none() => {
side = Some(TextUnderlinePositionSide::Right);
}
_ => return None,
}
}
Some(TextUnderlinePositionMode { line, side })
}
fn apply_text_underline_position_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_underline_position_spec(delta, TextUnderlinePositionSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_underline_position(delta),
"initial" => {
set_delta_text_underline_position_spec(delta, TextUnderlinePositionSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = text_underline_position_value_from_raw(&lowered) {
set_delta_text_underline_position_spec(delta, TextUnderlinePositionSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_underline_position_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_underline_position_var(delta, var);
return true;
}
false
}
fn apply_text_shadow_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_shadow_spec(delta, TextShadowSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_shadow(delta),
"initial" => {
set_delta_text_shadow_spec(delta, TextShadowSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_text_shadow_list_str(raw) {
set_delta_text_shadow_spec(delta, TextShadowSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_shadow_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_shadow_var(delta, var);
return true;
}
false
}
fn apply_line_break_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_line_break(delta, LineBreakSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_line_break(delta),
"initial" => {
set_delta_line_break(delta, LineBreakSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_line_break_mode_str(&lowered) {
set_delta_line_break(delta, LineBreakSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_line_break_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_line_break_var(delta, var);
return true;
}
false
}
fn apply_word_break_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_word_break(delta, WordBreakSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_word_break(delta),
"initial" => {
set_delta_word_break(delta, WordBreakSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_word_break_mode_str(&lowered) {
set_delta_word_break(delta, WordBreakSpec::Value(value));
return true;
}
false
}
fn apply_overflow_wrap_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_overflow_wrap(delta, OverflowWrapSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_overflow_wrap(delta),
"initial" => {
set_delta_overflow_wrap(delta, OverflowWrapSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_overflow_wrap_mode_str(&lowered) {
set_delta_overflow_wrap(delta, OverflowWrapSpec::Value(value));
return true;
}
false
}
fn apply_text_justify_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_justify(delta, TextJustifySpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_justify(delta),
"initial" => {
set_delta_text_justify(delta, TextJustifySpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_text_justify_mode_str(&lowered) {
set_delta_text_justify(delta, TextJustifySpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_justify_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_justify_var(delta, var);
return true;
}
false
}
fn set_delta_text_wrap_mode(delta: &mut StyleDelta, spec: TextWrapModeSpec) {
delta.text_wrap_mode_var = None;
delta.text_wrap_mode = Some(spec);
delta.revert_layer.text_wrap_mode = false;
}
fn set_delta_text_wrap_mode_var(delta: &mut StyleDelta, value: String) {
delta.text_wrap_mode = None;
delta.text_wrap_mode_var = Some(value);
delta.revert_layer.text_wrap_mode = false;
}
fn set_delta_text_wrap_style(delta: &mut StyleDelta, spec: TextWrapStyleSpec) {
delta.text_wrap_style_var = None;
delta.text_wrap_style = Some(spec);
delta.revert_layer.text_wrap_style = false;
}
fn set_delta_text_wrap_style_var(delta: &mut StyleDelta, value: String) {
delta.text_wrap_style = None;
delta.text_wrap_style_var = Some(value);
delta.revert_layer.text_wrap_style = false;
}
fn set_delta_white_space(delta: &mut StyleDelta, spec: WhiteSpaceSpec) {
delta.white_space_var = None;
delta.white_space = Some(spec);
delta.revert_layer.white_space = false;
}
fn set_delta_white_space_var(delta: &mut StyleDelta, value: String) {
delta.white_space = None;
delta.white_space_var = Some(value);
delta.revert_layer.white_space = false;
}
fn apply_text_wrap_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Inherit);
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_wrap_shorthand(delta),
"initial" => {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Initial);
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Initial);
return true;
}
_ => {}
}
if let Some((mode, style)) = parse_text_wrap_shorthand_str(&lowered) {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Value(mode));
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Value(style));
return true;
}
if lowered.contains("var(") {
let expr = text_wrap_shorthand_var_expr(&lowered);
set_delta_text_wrap_mode_var(delta, expr.clone());
set_delta_text_wrap_style_var(delta, expr);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
let expr = text_wrap_shorthand_var_expr(&var);
set_delta_text_wrap_mode_var(delta, expr.clone());
set_delta_text_wrap_style_var(delta, expr);
return true;
}
false
}
fn apply_text_wrap_mode_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_wrap_mode(delta),
"initial" => {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_text_wrap_mode_str(&lowered) {
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_wrap_mode_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_wrap_mode_var(delta, var);
return true;
}
false
}
fn apply_text_wrap_style_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_text_wrap_style(delta),
"initial" => {
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_text_wrap_style_str(&lowered) {
set_delta_text_wrap_style(delta, TextWrapStyleSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_text_wrap_style_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_text_wrap_style_var(delta, var);
return true;
}
false
}
fn apply_hyphens_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_hyphens(delta, HyphensSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_hyphens(delta),
"initial" => {
set_delta_hyphens(delta, HyphensSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_hyphens_mode_str(&lowered) {
set_delta_hyphens(delta, HyphensSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_hyphens_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_hyphens_var(delta, var);
return true;
}
false
}
fn apply_hyphenate_character_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_hyphenate_character(delta, HyphenateCharacterSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_hyphenate_character(delta),
"initial" => {
set_delta_hyphenate_character(delta, HyphenateCharacterSpec::Initial);
return true;
}
"auto" => {
set_delta_hyphenate_character(delta, HyphenateCharacterSpec::Auto);
return true;
}
_ => {}
}
if let Some(Some(value)) = parse_hyphenate_character_str(trimmed) {
set_delta_hyphenate_character(delta, HyphenateCharacterSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_hyphenate_character_var(delta, trimmed.to_string());
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_hyphenate_character_var(delta, var);
return true;
}
false
}
fn parse_text_decoration_str(raw: &str) -> Option<TextDecorationMode> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
let mut mode = TextDecorationMode::default();
let mut saw = false;
for token in raw.split_whitespace() {
match token {
"none" => return Some(TextDecorationMode::default()),
"underline" => {
mode.underline = true;
saw = true;
}
"overline" => {
mode.overline = true;
saw = true;
}
"line-through" => {
mode.line_through = true;
saw = true;
}
_ => {}
}
}
if saw { Some(mode) } else { None }
}
fn parse_letter_spacing_str(raw: &str) -> Option<LengthSpec> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
if raw == "normal" {
return Some(LengthSpec::Absolute(Pt::ZERO));
}
length_spec_from_string(&raw)
}
fn parse_word_spacing_str(raw: &str) -> Option<LengthSpec> {
parse_letter_spacing_str(raw)
}
fn parse_line_break_mode_str(raw: &str) -> Option<LineBreakMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Some(LineBreakMode::Auto),
"loose" => Some(LineBreakMode::Loose),
"normal" => Some(LineBreakMode::Normal),
"strict" => Some(LineBreakMode::Strict),
"anywhere" => Some(LineBreakMode::Anywhere),
_ => None,
}
}
fn parse_word_break_mode_str(raw: &str) -> Option<WordBreakMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"normal" => Some(WordBreakMode::Normal),
"break-word" => Some(WordBreakMode::BreakWord),
"break-all" => Some(WordBreakMode::BreakAll),
"keep-all" => Some(WordBreakMode::KeepAll),
_ => None,
}
}
fn parse_overflow_wrap_mode_str(raw: &str) -> Option<OverflowWrapMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"normal" => Some(OverflowWrapMode::Normal),
"break-word" => Some(OverflowWrapMode::BreakWord),
"anywhere" => Some(OverflowWrapMode::Anywhere),
_ => None,
}
}
fn parse_tab_size_str(raw: &str) -> Option<TabSizeSpec> {
let raw = raw.trim().to_ascii_lowercase();
if raw.is_empty() {
return None;
}
match raw.as_str() {
"inherit" | "unset" | "revert" => return Some(TabSizeSpec::Inherit),
"initial" => return Some(TabSizeSpec::Initial),
_ => {}
}
if let Ok(spaces) = raw.parse::<f32>() {
return if spaces.is_finite() && spaces >= 0.0 {
Some(TabSizeSpec::Spaces(spaces))
} else {
None
};
}
let length = length_spec_from_string(&raw)?;
if tab_size_length_is_non_negative(length) {
Some(TabSizeSpec::Length(length))
} else {
None
}
}
fn tab_size_length_is_non_negative(length: LengthSpec) -> bool {
match length {
LengthSpec::Absolute(value) => value >= Pt::ZERO,
LengthSpec::Em(value) | LengthSpec::Rem(value) => value >= 0.0,
LengthSpec::Calc(calc) => {
calc.percent == 0.0 && calc.abs >= Pt::ZERO && calc.em >= 0.0 && calc.rem >= 0.0
}
LengthSpec::Clamped(calc) => calc.min.is_some_and(|minimum| minimum >= Pt::ZERO),
LengthSpec::FontRelative(calc) => {
calc.base.percent == 0.0
&& calc.base.abs >= Pt::ZERO
&& calc.base.em >= 0.0
&& calc.base.rem >= 0.0
&& calc.ex >= 0.0
&& calc.ch >= 0.0
&& calc.cap >= 0.0
&& calc.lh >= 0.0
&& calc.rlh >= 0.0
}
LengthSpec::Percent(_)
| LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => false,
}
}
fn parse_text_indent_str(raw: &str) -> Option<TextIndentValue> {
let parts: Vec<String> = split_ws_preserve_parens(raw.trim())
.into_iter()
.map(|part| part.trim().to_string())
.filter(|part| !part.is_empty())
.collect();
if parts.is_empty() {
return None;
}
if parts.len() == 1 {
let value = parts[0].to_ascii_lowercase();
match value.as_str() {
"inherit" | "unset" | "revert" => {
return Some(TextIndentValue {
value: LengthSpec::Inherit,
hanging: false,
each_line: false,
});
}
"initial" => {
return Some(TextIndentValue {
value: LengthSpec::Initial,
hanging: false,
each_line: false,
});
}
_ => {}
}
}
let mut value = None;
let mut hanging = false;
let mut each_line = false;
for part in parts {
let lower = part.to_ascii_lowercase();
match lower.as_str() {
"hanging" if !hanging => hanging = true,
"each-line" if !each_line => each_line = true,
"inherit" | "initial" | "unset" | "revert" | "revert-layer" => return None,
_ if value.is_none() => {
value = length_spec_from_string(&lower);
value?;
}
_ => return None,
}
}
Some(TextIndentValue {
value: value?,
hanging,
each_line,
})
}
fn length_spec_from_string(raw: &str) -> Option<LengthSpec> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
match parse_native_length_math(raw, true)? {
NativeMathValue::Number(value) => Some(LengthSpec::Absolute(px_to_pt(value))),
NativeMathValue::Length(value) => value.to_length_spec(),
NativeMathValue::Clamped(value) => value.to_length_spec(),
NativeMathValue::Typed(_) => None,
}
}
fn parse_length_list(raw: &str) -> Vec<LengthSpec> {
raw.split_whitespace()
.filter_map(length_spec_from_string)
.collect()
}
fn parse_grid_auto_repeat(raw: &str) -> Option<GridAutoRepeatSpec> {
let tokens = split_top_level_whitespace_tokens(&raw.trim().to_ascii_lowercase());
if tokens.len() != 1 {
return None;
}
let token = tokens.first()?.trim();
if !token.starts_with("repeat(") || !token.ends_with(')') {
return None;
}
let inner = &token[7..token.len().saturating_sub(1)];
let (count_raw, segment_raw) = split_top_level_comma_once(inner)?;
let mode = match count_raw.trim() {
"auto-fill" => GridAutoRepeatMode::Fill,
"auto-fit" => GridAutoRepeatMode::Fit,
_ => return None,
};
let mut tracks = Vec::new();
parse_grid_track_segment(segment_raw.trim(), 1, &mut tracks)?;
(!tracks.is_empty()).then_some(GridAutoRepeatSpec { mode, tracks })
}
fn parse_grid_track_list(raw: &str) -> Option<Vec<GridTrackSize>> {
let normalized = raw.trim().to_ascii_lowercase();
if normalized.is_empty() {
return None;
}
if normalized == "none" {
return Some(Vec::new());
}
let mut tracks = Vec::new();
parse_grid_track_segment(&normalized, 0, &mut tracks)?;
(!tracks.is_empty()).then_some(tracks)
}
fn parse_subgrid_line_names(raw: &str) -> Option<Vec<Vec<String>>> {
let tokens = split_top_level_whitespace_tokens(&raw.trim().to_ascii_lowercase());
if tokens.first().map(String::as_str) != Some("subgrid") {
return None;
}
let mut lines = Vec::new();
for token in tokens.iter().skip(1) {
if !(token.starts_with('[') && token.ends_with(']')) {
return None;
}
lines.push(
token[1..token.len().saturating_sub(1)]
.split_ascii_whitespace()
.filter(|name| !name.is_empty())
.map(ToString::to_string)
.collect(),
);
}
Some(lines)
}
fn parse_grid_track_line_names(raw: &str) -> Vec<Vec<String>> {
fn segment(raw: &str, depth: usize) -> Option<Vec<Vec<String>>> {
if depth > 8 {
return None;
}
let mut lines = vec![Vec::new()];
for token in split_top_level_whitespace_tokens(raw) {
let token = token.trim();
if token.is_empty() {
continue;
}
if token.starts_with('[') && token.ends_with(']') {
lines.last_mut()?.extend(
token[1..token.len().saturating_sub(1)]
.split_ascii_whitespace()
.filter(|name| !name.is_empty())
.map(ToString::to_string),
);
continue;
}
if token.starts_with("repeat(") && token.ends_with(')') {
let inner = &token[7..token.len().saturating_sub(1)];
let (repeat_raw, repeated_raw) = split_top_level_comma_once(inner)?;
let repeat_count = repeat_raw
.trim()
.parse::<usize>()
.ok()
.filter(|count| *count > 0)
.unwrap_or(1);
let repeated = segment(repeated_raw, depth + 1)?;
for _ in 0..repeat_count {
if let Some(first) = repeated.first() {
lines.last_mut()?.extend(first.iter().cloned());
}
lines.extend(repeated.iter().skip(1).cloned());
}
continue;
}
if parse_grid_track_size(token).is_some() {
lines.push(Vec::new());
}
}
Some(lines)
}
segment(&raw.trim().to_ascii_lowercase(), 0).unwrap_or_default()
}
fn parse_grid_line_spec(raw: &str) -> GridLineSpec {
let normalized = raw.trim().to_ascii_lowercase();
if normalized.is_empty() || normalized == "auto" {
return GridLineSpec::Auto;
}
let parts = split_top_level_whitespace_tokens(&normalized);
if parts.first().is_some_and(|part| part == "span") {
let mut count = 1usize;
let mut name = None;
for part in parts.iter().skip(1) {
if let Ok(value) = part.parse::<usize>() {
if value == 0 {
return GridLineSpec::Auto;
}
count = value;
} else if name.replace(part.clone()).is_some() {
return GridLineSpec::Auto;
}
}
return name.map_or(GridLineSpec::Span(count), |name| GridLineSpec::SpanNamed {
count,
name,
});
}
if parts.len() == 2 {
if let Ok(occurrence) = parts[0].parse::<i32>() {
if occurrence != 0 {
return GridLineSpec::NamedOccurrence {
occurrence,
name: parts[1].clone(),
};
}
}
if let Ok(occurrence) = parts[1].parse::<i32>() {
if occurrence != 0 {
return GridLineSpec::NamedOccurrence {
occurrence,
name: parts[0].clone(),
};
}
}
}
if let Ok(line) = normalized.parse::<i32>() {
return if line == 0 {
GridLineSpec::Auto
} else {
GridLineSpec::Line(line)
};
}
GridLineSpec::Named(normalized)
}
fn parse_grid_placement_shorthand(raw: &str) -> (GridLineSpec, GridLineSpec) {
if let Some((start, end)) = raw.split_once('/') {
(parse_grid_line_spec(start), parse_grid_line_spec(end))
} else {
(parse_grid_line_spec(raw), GridLineSpec::Auto)
}
}
fn parse_grid_area_lines(raw: &str) -> (GridLineSpec, GridLineSpec, GridLineSpec, GridLineSpec) {
let parts: Vec<&str> = raw.split('/').collect();
let get = |index: usize| {
parts
.get(index)
.map(|value| parse_grid_line_spec(value))
.unwrap_or_default()
};
(get(0), get(1), get(2), get(3))
}
fn legacy_positive_grid_start(line: &GridLineSpec) -> usize {
match line {
GridLineSpec::Line(line) if *line > 0 => *line as usize,
_ => 0,
}
}
fn parse_grid_template_areas(raw: &str) -> Option<Vec<Vec<Option<String>>>> {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let mut rows = Vec::new();
let mut quote = None;
let mut current = String::new();
for character in trimmed.chars() {
match quote {
Some(delimiter) if character == delimiter => {
let row: Vec<Option<String>> = current
.split_ascii_whitespace()
.map(|cell| {
(!cell.chars().all(|character| character == '.'))
.then(|| cell.to_ascii_lowercase())
})
.collect();
if row.is_empty() {
return None;
}
rows.push(row);
current.clear();
quote = None;
}
Some(_) => current.push(character),
None if matches!(character, '\'' | '"') => quote = Some(character),
None if character.is_whitespace() => {}
None => return None,
}
}
if quote.is_some() || rows.is_empty() {
return None;
}
let columns = rows[0].len();
if rows.iter().any(|row| row.len() != columns) {
return None;
}
let mut bounds: HashMap<&str, (usize, usize, usize, usize, usize)> = HashMap::new();
for (row_index, row) in rows.iter().enumerate() {
for (column_index, cell) in row.iter().enumerate() {
if let Some(name) = cell.as_deref() {
let entry = bounds.entry(name).or_insert((
row_index,
row_index,
column_index,
column_index,
0,
));
entry.0 = entry.0.min(row_index);
entry.1 = entry.1.max(row_index);
entry.2 = entry.2.min(column_index);
entry.3 = entry.3.max(column_index);
entry.4 += 1;
}
}
}
if bounds
.values()
.any(|(r0, r1, c0, c1, count)| (r1 - r0 + 1).saturating_mul(c1 - c0 + 1) != *count)
{
return None;
}
Some(rows)
}
struct ParsedGridTemplateShorthand {
column_tracks: Vec<GridTrackSize>,
column_line_names: Vec<Vec<String>>,
row_tracks: Vec<GridTrackSize>,
row_line_names: Vec<Vec<String>>,
areas: Vec<Vec<Option<String>>>,
}
fn parse_grid_template_shorthand(raw: &str) -> Option<ParsedGridTemplateShorthand> {
let (rows_raw, columns_raw) = split_top_level_slash_once(raw)?;
let column_tracks = parse_grid_track_list(columns_raw)?;
let column_line_names = parse_grid_track_line_names(columns_raw);
if !rows_raw.contains(['\'', '"']) {
let row_tracks = parse_grid_track_list(rows_raw)?;
return Some(ParsedGridTemplateShorthand {
column_tracks,
column_line_names,
row_line_names: parse_grid_track_line_names(rows_raw),
row_tracks,
areas: Vec::new(),
});
}
let mut quoted_rows = Vec::new();
let mut row_ranges = Vec::new();
let mut quote = None;
let mut row_start = 0usize;
let mut escaped = false;
for (index, character) in rows_raw.char_indices() {
if let Some(delimiter) = quote {
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == delimiter {
quoted_rows.push(rows_raw[row_start..index].to_string());
row_ranges.push((row_start.saturating_sub(1), index + character.len_utf8()));
quote = None;
}
} else if matches!(character, '\'' | '"') {
quote = Some(character);
row_start = index + character.len_utf8();
}
}
if quote.is_some() || quoted_rows.is_empty() {
return None;
}
let areas_raw = quoted_rows
.iter()
.map(|row| format!("\"{row}\""))
.collect::<Vec<_>>()
.join(" ");
let areas = parse_grid_template_areas(&areas_raw)?;
if !column_tracks.is_empty()
&& areas
.first()
.is_some_and(|row| row.len() != column_tracks.len())
{
return None;
}
let mut row_tracks = Vec::with_capacity(row_ranges.len());
for (index, (_, close)) in row_ranges.iter().copied().enumerate() {
let next_open = row_ranges
.get(index + 1)
.map(|(open, _)| *open)
.unwrap_or(rows_raw.len());
let tail = rows_raw.get(close..next_open)?.trim();
let sizes: Vec<GridTrackSize> = split_top_level_whitespace_tokens(tail)
.into_iter()
.filter(|token| !(token.starts_with('[') && token.ends_with(']')))
.filter_map(|token| parse_grid_track_size(&token))
.collect();
if sizes.len() > 1 {
return None;
}
row_tracks.push(sizes.first().copied().unwrap_or_else(GridTrackSize::auto));
}
let row_line_names = vec![Vec::new(); row_tracks.len().saturating_add(1)];
Some(ParsedGridTemplateShorthand {
column_tracks,
column_line_names,
row_tracks,
row_line_names,
areas,
})
}
fn set_grid_template_delta(delta: &mut StyleDelta, parsed: ParsedGridTemplateShorthand) {
delta.grid_columns = Some(parsed.column_tracks.len());
delta.grid_rows = Some(parsed.row_tracks.len());
delta.grid_column_tracks = Some(parsed.column_tracks);
delta.grid_row_tracks = Some(parsed.row_tracks);
delta.grid_subgrid_columns = Some(false);
delta.grid_subgrid_rows = Some(false);
delta.grid_column_line_names = Some(parsed.column_line_names);
delta.grid_row_line_names = Some(parsed.row_line_names);
delta.grid_template_areas = Some(parsed.areas);
delta.grid_column_auto_repeat = Some(None);
delta.grid_row_auto_repeat = Some(None);
delta.grid_column_tracks_var = Some(None);
delta.grid_row_tracks_var = Some(None);
}
fn apply_native_grid_template_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(parsed) = parse_grid_template_shorthand(raw) else {
return false;
};
set_grid_template_delta(delta, parsed);
true
}
fn parse_grid_auto_flow_side(raw: &str) -> Option<(GridAutoFlowMode, Vec<GridTrackSize>)> {
let tokens = split_top_level_whitespace_tokens(raw);
if !tokens
.iter()
.any(|token| token.eq_ignore_ascii_case("auto-flow"))
{
return None;
}
let dense = tokens
.iter()
.any(|token| token.eq_ignore_ascii_case("dense"));
let track_raw = tokens
.iter()
.filter(|token| {
!token.eq_ignore_ascii_case("auto-flow") && !token.eq_ignore_ascii_case("dense")
})
.cloned()
.collect::<Vec<_>>()
.join(" ");
let tracks = if track_raw.is_empty() {
vec![GridTrackSize::auto()]
} else {
parse_grid_track_list(&track_raw)?
};
Some((
if dense {
GridAutoFlowMode::RowDense
} else {
GridAutoFlowMode::Row
},
tracks,
))
}
fn apply_native_grid_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let Some((rows_raw, columns_raw)) = split_top_level_slash_once(raw) else {
return false;
};
let row_auto_flow = parse_grid_auto_flow_side(rows_raw);
let column_auto_flow = parse_grid_auto_flow_side(columns_raw);
if row_auto_flow.is_some() && column_auto_flow.is_some() {
return false;
}
if let Some((flow, auto_rows)) = row_auto_flow {
let Some(column_tracks) = parse_grid_track_list(columns_raw) else {
return false;
};
delta.grid_columns = Some(column_tracks.len());
delta.grid_rows = Some(0);
delta.grid_column_line_names = Some(parse_grid_track_line_names(columns_raw));
delta.grid_row_line_names = Some(Vec::new());
delta.grid_column_tracks = Some(column_tracks);
delta.grid_row_tracks = Some(Vec::new());
delta.grid_subgrid_columns = Some(false);
delta.grid_subgrid_rows = Some(false);
delta.grid_template_areas = Some(Vec::new());
delta.grid_auto_flow = Some(flow);
delta.grid_auto_row_tracks = Some(auto_rows);
delta.grid_auto_column_tracks = Some(Vec::new());
delta.grid_column_auto_repeat = Some(None);
delta.grid_row_auto_repeat = Some(None);
delta.grid_column_tracks_var = Some(None);
delta.grid_row_tracks_var = Some(None);
return true;
}
if let Some((flow, auto_columns)) = column_auto_flow {
let Some(row_tracks) = parse_grid_track_list(rows_raw) else {
return false;
};
delta.grid_columns = Some(0);
delta.grid_rows = Some(row_tracks.len());
delta.grid_column_line_names = Some(Vec::new());
delta.grid_row_line_names = Some(parse_grid_track_line_names(rows_raw));
delta.grid_column_tracks = Some(Vec::new());
delta.grid_row_tracks = Some(row_tracks);
delta.grid_subgrid_columns = Some(false);
delta.grid_subgrid_rows = Some(false);
delta.grid_template_areas = Some(Vec::new());
delta.grid_auto_flow = Some(if matches!(flow, GridAutoFlowMode::RowDense) {
GridAutoFlowMode::ColumnDense
} else {
GridAutoFlowMode::Column
});
delta.grid_auto_column_tracks = Some(auto_columns);
delta.grid_auto_row_tracks = Some(Vec::new());
delta.grid_column_auto_repeat = Some(None);
delta.grid_row_auto_repeat = Some(None);
delta.grid_column_tracks_var = Some(None);
delta.grid_row_tracks_var = Some(None);
return true;
}
let Some(parsed) = parse_grid_template_shorthand(raw) else {
return false;
};
set_grid_template_delta(delta, parsed);
delta.grid_auto_flow = Some(GridAutoFlowMode::Row);
delta.grid_auto_column_tracks = Some(Vec::new());
delta.grid_auto_row_tracks = Some(Vec::new());
true
}
fn parse_grid_auto_flow(raw: &str) -> Option<GridAutoFlowMode> {
let mut row = false;
let mut column = false;
let mut dense = false;
for token in raw.split_ascii_whitespace() {
match token.to_ascii_lowercase().as_str() {
"row" if !row && !column => row = true,
"column" if !row && !column => column = true,
"dense" if !dense => dense = true,
_ => return None,
}
}
if column {
Some(if dense {
GridAutoFlowMode::ColumnDense
} else {
GridAutoFlowMode::Column
})
} else {
Some(if dense {
GridAutoFlowMode::RowDense
} else {
GridAutoFlowMode::Row
})
}
}
fn parse_grid_track_segment(
raw: &str,
depth: usize,
tracks: &mut Vec<GridTrackSize>,
) -> Option<()> {
if depth > 8 {
return None;
}
for token in split_top_level_whitespace_tokens(raw) {
let token = token.trim();
if token.is_empty() || (token.starts_with('[') && token.ends_with(']')) {
continue;
}
if token.starts_with("repeat(") && token.ends_with(')') {
let inner = &token[7..token.len().saturating_sub(1)];
let (repeat_raw, segment_raw) = split_top_level_comma_once(inner)?;
if let Ok(repeat_count) = repeat_raw.trim().parse::<usize>() {
if repeat_count == 0 || repeat_count > 4096 {
return None;
}
let mut repeated = Vec::new();
parse_grid_track_segment(segment_raw.trim(), depth + 1, &mut repeated)?;
if repeated.is_empty() {
return None;
}
tracks.reserve(repeated.len().saturating_mul(repeat_count));
for _ in 0..repeat_count {
tracks.extend(repeated.iter().copied());
}
continue;
}
// The number of auto-fill/auto-fit tracks depends on the eventual
// containing block. Preserve one semantic pattern here; layout can
// still use it as a safe intrinsic fallback.
if matches!(repeat_raw.trim(), "auto-fill" | "auto-fit") {
parse_grid_track_segment(segment_raw.trim(), depth + 1, tracks)?;
continue;
}
return None;
}
tracks.push(parse_grid_track_size(token)?);
}
Some(())
}
fn parse_grid_track_size(raw: &str) -> Option<GridTrackSize> {
let raw = raw.trim();
if raw.starts_with("minmax(") && raw.ends_with(')') {
let inner = &raw[7..raw.len().saturating_sub(1)];
let (min_raw, max_raw) = split_top_level_comma_once(inner)?;
return Some(GridTrackSize {
min: parse_grid_track_breadth(min_raw.trim())?,
max: parse_grid_track_breadth(max_raw.trim())?,
});
}
if raw.starts_with("fit-content(") && raw.ends_with(')') {
let inner = &raw[12..raw.len().saturating_sub(1)];
return Some(GridTrackSize {
min: GridTrackBreadth::Auto,
max: GridTrackBreadth::FitContent(length_spec_from_string(inner.trim())?),
});
}
match parse_grid_track_breadth(raw)? {
GridTrackBreadth::Auto => Some(GridTrackSize::auto()),
GridTrackBreadth::Length(length) => Some(GridTrackSize::fixed(length)),
GridTrackBreadth::Fraction(factor) => Some(GridTrackSize::fraction(factor)),
breadth => Some(GridTrackSize {
min: breadth,
max: breadth,
}),
}
}
fn parse_grid_track_breadth(raw: &str) -> Option<GridTrackBreadth> {
match raw.trim() {
"auto" => Some(GridTrackBreadth::Auto),
"min-content" => Some(GridTrackBreadth::MinContent),
"max-content" => Some(GridTrackBreadth::MaxContent),
raw => {
if let Some(number) = raw.strip_suffix("fr") {
let factor = number.trim().parse::<f32>().ok()?;
return (factor.is_finite() && factor >= 0.0)
.then_some(GridTrackBreadth::Fraction(factor));
}
length_spec_from_string(raw).map(GridTrackBreadth::Length)
}
}
}
fn split_top_level_comma_once(raw: &str) -> Option<(&str, &str)> {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
for (idx, ch) in raw.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
',' if paren_depth == 0 && bracket_depth == 0 => {
let head = raw[..idx].trim();
let tail = raw[idx + ch.len_utf8()..].trim();
if !head.is_empty() && !tail.is_empty() {
return Some((head, tail));
}
return None;
}
_ => {}
}
}
None
}
fn split_top_level_whitespace_tokens(raw: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut buf = String::new();
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
for ch in raw.chars() {
match ch {
'(' => {
paren_depth += 1;
buf.push(ch);
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
buf.push(ch);
}
'[' => {
bracket_depth += 1;
buf.push(ch);
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
buf.push(ch);
}
_ if ch.is_whitespace() && paren_depth == 0 && bracket_depth == 0 => {
if !buf.trim().is_empty() {
out.push(buf.trim().to_string());
}
buf.clear();
}
_ => buf.push(ch),
}
}
if !buf.trim().is_empty() {
out.push(buf.trim().to_string());
}
out
}
fn parse_border_spacing_str(raw: &str) -> Option<BorderSpacingSpec> {
let values = parse_length_list(raw);
if values.is_empty() {
return None;
}
let horizontal = values[0];
let vertical = if values.len() > 1 {
values[1]
} else {
values[0]
};
Some(BorderSpacingSpec {
horizontal,
vertical,
})
}
fn parse_border_radius_axis(raw: &str) -> Option<BorderRadiusSpec> {
let tokens = split_top_level_whitespace(raw);
if tokens.is_empty() || tokens.len() > 4 {
return None;
}
let mut values = Vec::with_capacity(tokens.len());
for token in tokens {
let value = parse_border_radius_component(&token)?;
if !border_radius_component_is_non_negative(value) {
return None;
}
values.push(value);
}
let (tl, tr, br, bl) = match values.len() {
1 => (values[0], values[0], values[0], values[0]),
2 => (values[0], values[1], values[0], values[1]),
3 => (values[0], values[1], values[2], values[1]),
_ => (values[0], values[1], values[2], values[3]),
};
Some(BorderRadiusSpec {
top_left: tl,
top_right: tr,
bottom_right: br,
bottom_left: bl,
})
}
fn parse_border_radius_component(raw: &str) -> Option<LengthSpec> {
match parse_native_length_math(raw.trim(), true)? {
// CSS permits a unitless zero in a length slot, but nonzero numbers are
// invalid and must not replace an earlier valid declaration.
NativeMathValue::Number(value) if value.abs() <= f32::EPSILON => {
Some(LengthSpec::Absolute(Pt::ZERO))
}
NativeMathValue::Number(_) | NativeMathValue::Typed(_) => None,
NativeMathValue::Length(value) => value.to_length_spec(),
NativeMathValue::Clamped(value) => value.to_length_spec(),
}
}
fn parse_border_radius_corner_str(raw: &str) -> Option<BorderRadiusCornerValue> {
let tokens = split_top_level_whitespace(raw);
if tokens.is_empty() || tokens.len() > 2 {
return None;
}
let horizontal = parse_border_radius_component(&tokens[0])?;
let vertical = if tokens.len() == 2 {
parse_border_radius_component(&tokens[1])?
} else {
horizontal
};
if !border_radius_component_is_non_negative(horizontal)
|| !border_radius_component_is_non_negative(vertical)
{
return None;
}
Some(BorderRadiusCornerValue {
horizontal,
vertical,
})
}
fn apply_border_radius_corner_longhand(
property_name: &str,
raw: &str,
delta: &mut StyleDelta,
) -> bool {
let Some(value) = parse_border_radius_corner_str(raw) else {
return false;
};
let target = match property_name {
"border-top-left-radius" => BorderRadiusCornerTarget::Physical(PhysicalCorner::TopLeft),
"border-top-right-radius" => BorderRadiusCornerTarget::Physical(PhysicalCorner::TopRight),
"border-bottom-right-radius" => {
BorderRadiusCornerTarget::Physical(PhysicalCorner::BottomRight)
}
"border-bottom-left-radius" => {
BorderRadiusCornerTarget::Physical(PhysicalCorner::BottomLeft)
}
"border-start-start-radius" => BorderRadiusCornerTarget::StartStart,
"border-start-end-radius" => BorderRadiusCornerTarget::StartEnd,
"border-end-start-radius" => BorderRadiusCornerTarget::EndStart,
"border-end-end-radius" => BorderRadiusCornerTarget::EndEnd,
_ => return false,
};
delta.border_radius_var = None;
delta
.border_radius_ops
.push(BorderRadiusOp::Corner(target, value));
true
}
fn border_radius_component_is_non_negative(value: LengthSpec) -> bool {
match value {
LengthSpec::Absolute(value) => value >= Pt::ZERO,
LengthSpec::Percent(value) | LengthSpec::Em(value) | LengthSpec::Rem(value) => value >= 0.0,
LengthSpec::Calc(_) | LengthSpec::Clamped(_) | LengthSpec::FontRelative(_) => true,
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => false,
}
}
fn parse_border_radius_str(raw: &str) -> Option<BorderRadiiSpec> {
let parts = split_top_level_delimited(raw, '/');
if parts.is_empty() || parts.len() > 2 {
return None;
}
let horizontal = parse_border_radius_axis(&parts[0])?;
let vertical = if parts.len() == 2 {
parse_border_radius_axis(&parts[1])?
} else {
horizontal
};
Some(BorderRadiiSpec {
horizontal,
vertical,
})
}
fn apply_background_from_string(raw: &str, delta: &mut StyleDelta) {
if raw.trim().eq_ignore_ascii_case("currentcolor") {
set_delta_background_color(delta, BackgroundSpec::CurrentColor);
return;
}
if let Some((color, alpha)) = parse_color_string(raw) {
set_delta_background_color(delta, background_spec_from_color_alpha(color, alpha));
return;
}
let layers = split_args(raw);
let mut paints = Vec::new();
let mut positions = Vec::new();
let mut repeats = Vec::new();
let mut attachments = Vec::new();
let mut sizes = Vec::new();
let mut origins = Vec::new();
let mut clips = Vec::new();
for layer in &layers {
let slash_parts = split_top_level_delimited(layer, '/');
if slash_parts.len() > 2 {
continue;
}
let before = split_ws_preserve_parens(&slash_parts[0]);
let after = slash_parts
.get(1)
.map(|value| split_ws_preserve_parens(value))
.unwrap_or_default();
let mut position_parts = Vec::new();
let mut size_parts = Vec::new();
let mut repeat_parts = Vec::new();
let mut boxes = Vec::new();
let mut attachment = BackgroundAttachment::Scroll;
for (is_after_slash, token) in before
.iter()
.map(|token| (false, token))
.chain(after.iter().map(|token| (true, token)))
{
let lowered = token.trim().to_ascii_lowercase();
if let Some(paint) = parse_background_layer_paint_str(token) {
paints.push(paint);
continue;
}
if lowered == "currentcolor" {
set_delta_background_color(delta, BackgroundSpec::CurrentColor);
continue;
}
if let Some((color, alpha)) = parse_color_string(token) {
set_delta_background_color(delta, background_spec_from_color_alpha(color, alpha));
continue;
}
if let Some(origin) = parse_background_origin_layer(&lowered) {
boxes.push(origin);
continue;
}
if matches!(
lowered.as_str(),
"repeat"
| "no-repeat"
| "space"
| "round"
| "repeat-x"
| "repeat-y"
| "repeat-inline"
| "repeat-block"
) {
repeat_parts.push(lowered);
continue;
}
if let Some(value) = parse_background_attachment_layer(&lowered) {
attachment = value;
continue;
}
if is_after_slash {
size_parts.push(token.clone());
} else {
position_parts.push(token.clone());
}
}
positions.push(
(!position_parts.is_empty())
.then(|| parse_background_position_layer(&position_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
sizes.push(
(!size_parts.is_empty())
.then(|| parse_background_size_layer(&size_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
repeats.push(
(!repeat_parts.is_empty())
.then(|| parse_background_repeat_layer(&repeat_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
attachments.push(attachment);
let origin = boxes.first().copied().unwrap_or_default();
let clip = boxes
.get(1)
.copied()
.or_else(|| boxes.first().copied())
.map(|value| match value {
BackgroundBox::Border => BackgroundClipBox::Border,
BackgroundBox::Padding => BackgroundClipBox::Padding,
BackgroundBox::Content => BackgroundClipBox::Content,
})
.unwrap_or_default();
origins.push(origin);
clips.push(clip);
}
if !paints.is_empty() {
delta.background_paint = paints.first().cloned();
delta.background_paints = Some(paints);
}
if !layers.is_empty() {
delta.background_positions = Some(positions);
delta.background_sizes = Some(sizes);
delta.background_repeats = Some(repeats);
delta.background_attachments = Some(attachments);
delta.background_origins = Some(origins);
delta.background_clips = Some(clips);
}
if has_dynamic_gradient_expression(raw) {
// Preserve unresolved gradients so custom properties and currentColor are substituted
// against the eventual computed style rather than discarded during specified-value parse.
set_delta_background_color_var(delta, raw.trim().to_ascii_lowercase());
} else if let Some(var) = var_name_from_string(raw) {
set_delta_background_color_var(delta, var);
}
}
fn apply_background_image_from_string(raw: &str, delta: &mut StyleDelta) {
let paints = parse_background_paints_str(raw);
if !paints.is_empty() {
delta.background_paint = paints.first().cloned();
delta.background_paints = Some(paints);
} else if has_dynamic_gradient_expression(raw) {
set_delta_background_color_var(delta, raw.trim().to_ascii_lowercase());
}
}
fn apply_background_size_from_string(raw: &str, delta: &mut StyleDelta) {
let sizes = parse_background_size_list(raw);
if !sizes.is_empty() {
delta.background_sizes = Some(sizes);
}
}
fn apply_background_position_from_string(raw: &str, delta: &mut StyleDelta) {
let positions = parse_background_position_list(raw);
if !positions.is_empty() {
delta.background_positions = Some(positions);
delta.background_position_x = None;
delta.background_position_y = None;
}
}
fn apply_background_position_axis_from_string(
raw: &str,
axis: BackgroundPositionAxis,
delta: &mut StyleDelta,
) {
let components = split_args(raw)
.into_iter()
.filter_map(|layer| {
let combined = match axis {
BackgroundPositionAxis::X => format!("{layer} top"),
BackgroundPositionAxis::Y => format!("left {layer}"),
};
let position = parse_background_position_layer(&combined)?;
Some(match axis {
BackgroundPositionAxis::X => position.x,
BackgroundPositionAxis::Y => position.y,
})
})
.collect::<Vec<_>>();
if !components.is_empty() {
match axis {
BackgroundPositionAxis::X => delta.background_position_x = Some(components),
BackgroundPositionAxis::Y => delta.background_position_y = Some(components),
}
}
}
fn merge_background_position_axis(
positions: &mut Vec<BackgroundPositionSpec>,
components: &[BackgroundPositionComponent],
axis: BackgroundPositionAxis,
) {
if components.is_empty() {
return;
}
let previous = positions.clone();
let count = previous.len().max(components.len()).max(1);
positions.clear();
positions.reserve(count);
for index in 0..count {
let mut position = if previous.is_empty() {
BackgroundPositionSpec::default()
} else {
previous[index % previous.len()]
};
match axis {
BackgroundPositionAxis::X => position.x = components[index % components.len()],
BackgroundPositionAxis::Y => position.y = components[index % components.len()],
}
positions.push(position);
}
}
fn apply_background_repeat_from_string(raw: &str, delta: &mut StyleDelta) {
let repeats = parse_background_repeat_list(raw);
if !repeats.is_empty() {
delta.background_repeats = Some(repeats);
}
}
fn apply_background_attachment_from_string(raw: &str, delta: &mut StyleDelta) {
let attachments = parse_background_attachment_list(raw);
if !attachments.is_empty() {
delta.background_attachments = Some(attachments);
}
}
fn apply_background_blend_mode_from_string(raw: &str, delta: &mut StyleDelta) {
let modes = parse_background_blend_mode_list(raw);
if !modes.is_empty() {
delta.background_blend_modes = Some(modes);
}
}
fn apply_background_origin_from_string(raw: &str, delta: &mut StyleDelta) {
let origins = parse_background_origin_list(raw);
if !origins.is_empty() {
delta.background_origins = Some(origins);
}
}
fn apply_background_clip_from_string(raw: &str, delta: &mut StyleDelta) {
let clips = parse_background_clip_list(raw);
if !clips.is_empty() {
delta.background_clips = Some(clips);
}
}
fn reset_mask_shorthand(delta: &mut StyleDelta) {
delta.mask_paints = Some(vec![BackgroundPaint::None]);
delta.mask_sizes = Some(vec![BackgroundSizeSpec::default()]);
delta.mask_positions = Some(vec![BackgroundPositionSpec::default()]);
delta.mask_repeats = Some(vec![BackgroundRepeatSpec::default()]);
delta.mask_origins = Some(vec![BackgroundBox::Border]);
delta.mask_clips = Some(vec![BackgroundClipBox::Border]);
delta.mask_modes = Some(vec![MaskMode::MatchSource]);
delta.mask_composites = Some(vec![MaskComposite::Add]);
delta.mask_backdrop_root = Some(false);
delta.revert_layer.mask_backdrop_root = false;
}
fn apply_mask_shorthand_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let raw = raw.trim();
reset_mask_shorthand(delta);
if matches!(
raw.to_ascii_lowercase().as_str(),
"none" | "initial" | "unset" | "revert"
) {
return true;
}
let layers = split_args(raw);
if layers.is_empty() {
return false;
}
let mut paints = Vec::with_capacity(layers.len());
let mut positions = Vec::with_capacity(layers.len());
let mut sizes = Vec::with_capacity(layers.len());
let mut repeats = Vec::with_capacity(layers.len());
let mut origins = Vec::with_capacity(layers.len());
let mut clips = Vec::with_capacity(layers.len());
let mut modes = Vec::with_capacity(layers.len());
let mut composites = Vec::with_capacity(layers.len());
for layer in layers {
let slash_parts = split_top_level_delimited(&layer, '/');
if slash_parts.len() > 2 {
return false;
}
let before = split_ws_preserve_parens(&slash_parts[0]);
let after = slash_parts
.get(1)
.map(|value| split_ws_preserve_parens(value))
.unwrap_or_default();
let mut paint = BackgroundPaint::None;
let mut position_parts = Vec::new();
let mut size_parts = Vec::new();
let mut repeat_parts = Vec::new();
let mut boxes = Vec::new();
let mut mode = MaskMode::MatchSource;
let mut composite = MaskComposite::Add;
for (after_slash, token) in before
.iter()
.map(|token| (false, token))
.chain(after.iter().map(|token| (true, token)))
{
let lower = token.trim().to_ascii_lowercase();
if let Some(value) = parse_background_layer_paint_str(token) {
paint = value;
continue;
}
if let Some(value) = parse_background_origin_layer(&lower) {
boxes.push(value);
continue;
}
if let Some(value) = parse_mask_mode(&lower) {
mode = value;
continue;
}
if let Some(value) = parse_mask_composite(&lower, false) {
composite = value;
continue;
}
if matches!(
lower.as_str(),
"repeat"
| "no-repeat"
| "space"
| "round"
| "repeat-x"
| "repeat-y"
| "repeat-inline"
| "repeat-block"
) {
repeat_parts.push(lower);
continue;
}
if after_slash {
size_parts.push(token.clone());
} else {
position_parts.push(token.clone());
}
}
paints.push(paint);
positions.push(
(!position_parts.is_empty())
.then(|| parse_background_position_layer(&position_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
sizes.push(
(!size_parts.is_empty())
.then(|| parse_background_size_layer(&size_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
repeats.push(
(!repeat_parts.is_empty())
.then(|| parse_background_repeat_layer(&repeat_parts.join(" ")))
.flatten()
.unwrap_or_default(),
);
let origin = boxes.first().copied().unwrap_or(BackgroundBox::Border);
let clip = boxes
.get(1)
.copied()
.or_else(|| boxes.first().copied())
.map(|value| match value {
BackgroundBox::Border => BackgroundClipBox::Border,
BackgroundBox::Padding => BackgroundClipBox::Padding,
BackgroundBox::Content => BackgroundClipBox::Content,
})
.unwrap_or(BackgroundClipBox::Border);
origins.push(origin);
clips.push(clip);
modes.push(mode);
composites.push(composite);
}
delta.mask_backdrop_root = Some(
paints
.iter()
.any(|paint| !matches!(paint, BackgroundPaint::None)),
);
delta.mask_paints = Some(paints);
delta.mask_sizes = Some(sizes);
delta.mask_positions = Some(positions);
delta.mask_repeats = Some(repeats);
delta.mask_origins = Some(origins);
delta.mask_clips = Some(clips);
delta.mask_modes = Some(modes);
delta.mask_composites = Some(composites);
true
}
fn apply_mask_image_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let paints = parse_background_paints_str(raw);
if paints.is_empty() {
return false;
}
delta.mask_backdrop_root = Some(
paints
.iter()
.any(|paint| !matches!(paint, BackgroundPaint::None)),
);
delta.revert_layer.mask_backdrop_root = false;
delta.mask_paints = Some(paints);
true
}
fn apply_mask_size_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = parse_background_size_list(raw);
if values.is_empty() {
return false;
}
delta.mask_sizes = Some(values);
true
}
fn apply_mask_position_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = parse_background_position_list(raw);
if values.is_empty() {
return false;
}
delta.mask_positions = Some(values);
true
}
fn apply_mask_repeat_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = parse_background_repeat_list(raw);
if values.is_empty() {
return false;
}
delta.mask_repeats = Some(values);
true
}
fn apply_mask_origin_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = parse_background_origin_list(raw);
if values.is_empty() {
return false;
}
delta.mask_origins = Some(values);
true
}
fn apply_mask_clip_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = parse_background_clip_list(raw);
if values.is_empty() {
return false;
}
delta.mask_clips = Some(values);
true
}
fn parse_mask_mode(raw: &str) -> Option<MaskMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"match-source" => Some(MaskMode::MatchSource),
"alpha" => Some(MaskMode::Alpha),
"luminance" => Some(MaskMode::Luminance),
_ => None,
}
}
fn apply_mask_mode_from_string(raw: &str, delta: &mut StyleDelta) -> bool {
let values = split_args(raw)
.into_iter()
.filter_map(|value| parse_mask_mode(&value))
.collect::<Vec<_>>();
if values.is_empty() {
return false;
}
delta.mask_modes = Some(values);
true
}
fn parse_mask_composite(raw: &str, webkit: bool) -> Option<MaskComposite> {
match raw.trim().to_ascii_lowercase().as_str() {
"add" | "source-over" => Some(MaskComposite::Add),
"subtract" => Some(MaskComposite::Subtract),
"source-out" if webkit => Some(MaskComposite::DestinationOut),
"intersect" | "source-in" => Some(MaskComposite::Intersect),
"exclude" | "xor" => Some(MaskComposite::Exclude),
"destination-over" if webkit => Some(MaskComposite::Add),
"destination-in" if webkit => Some(MaskComposite::Intersect),
"destination-out" if webkit => Some(MaskComposite::Subtract),
_ => None,
}
}
fn apply_mask_composite_from_string(raw: &str, webkit: bool, delta: &mut StyleDelta) -> bool {
let values = split_args(raw)
.into_iter()
.filter_map(|value| parse_mask_composite(&value, webkit))
.collect::<Vec<_>>();
if values.is_empty() {
return false;
}
delta.mask_composites = Some(values);
true
}
fn apply_mask_border_source(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(source) = parse_border_image_source_value(raw) else {
return false;
};
delta.mask_border.source = Some(source);
true
}
fn apply_mask_border_slice(raw: &str, delta: &mut StyleDelta) -> bool {
let Some((slice, fill)) = parse_border_image_slice(raw) else {
return false;
};
delta.mask_border.slice = Some(slice);
delta.mask_border.fill = Some(fill);
true
}
fn apply_mask_border_width(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(width) = parse_border_image_width(raw) else {
return false;
};
delta.mask_border.width = Some(width);
true
}
fn apply_mask_border_outset(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(outset) = parse_border_image_outset(raw) else {
return false;
};
delta.mask_border.outset = Some(outset);
true
}
fn apply_mask_border_repeat(raw: &str, delta: &mut StyleDelta) -> bool {
let Some(repeat) = parse_border_image_repeat(raw) else {
return false;
};
delta.mask_border.repeat = Some(repeat);
true
}
fn apply_mask_border_shorthand(raw: &str, delta: &mut StyleDelta) -> bool {
let mut parsed = StyleDelta::default();
if !apply_border_image_shorthand(raw, &mut parsed) {
return false;
}
delta.mask_border = parsed.border_image;
true
}
fn parse_background_size_list(raw: &str) -> Vec<BackgroundSizeSpec> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_size_layer(&layer))
.collect()
}
fn parse_background_size_layer(raw: &str) -> Option<BackgroundSizeSpec> {
let lower = raw.trim().to_ascii_lowercase();
if lower.is_empty() {
return None;
}
if lower == "cover" {
return Some(BackgroundSizeSpec {
mode: BackgroundSizeMode::Cover,
..BackgroundSizeSpec::default()
});
}
if lower == "contain" {
return Some(BackgroundSizeSpec {
mode: BackgroundSizeMode::Contain,
..BackgroundSizeSpec::default()
});
}
let parts: Vec<&str> = lower.split_whitespace().collect();
if parts.is_empty() || parts.len() > 2 {
return None;
}
let width = parse_background_size_component(parts[0])?;
let height = if parts.len() > 1 {
parse_background_size_component(parts[1])?
} else {
LengthSpec::Auto
};
Some(BackgroundSizeSpec {
mode: BackgroundSizeMode::Explicit,
width,
height,
})
}
fn parse_background_size_component(raw: &str) -> Option<LengthSpec> {
let value = if raw.eq_ignore_ascii_case("auto") {
LengthSpec::Auto
} else {
length_spec_from_string(raw)?
};
if background_size_component_is_non_negative(value) {
Some(value)
} else {
None
}
}
fn background_size_component_is_non_negative(value: LengthSpec) -> bool {
match value {
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => true,
LengthSpec::Absolute(value) => value >= Pt::ZERO,
LengthSpec::Percent(value) | LengthSpec::Em(value) | LengthSpec::Rem(value) => value >= 0.0,
LengthSpec::Calc(_) | LengthSpec::Clamped(_) | LengthSpec::FontRelative(_) => true,
}
}
fn parse_background_position_list(raw: &str) -> Vec<BackgroundPositionSpec> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_position_layer(&layer))
.collect()
}
fn parse_background_position_layer(raw: &str) -> Option<BackgroundPositionSpec> {
let parts: Vec<String> = raw
.split_whitespace()
.map(|part| part.trim().to_ascii_lowercase())
.filter(|part| !part.is_empty())
.collect();
if parts.is_empty() {
return None;
}
if parts.len() == 1 && parts[0] == "center" {
return Some(BackgroundPositionSpec {
x: BackgroundPositionComponent::Center,
y: BackgroundPositionComponent::Center,
});
}
if parts.len() == 1 {
let zero = LengthSpec::Absolute(Pt::ZERO);
return match background_position_axis_edge(parts[0].as_str()) {
Some((BackgroundPositionAxis::X, BackgroundPositionEdge::Start)) => {
Some(BackgroundPositionSpec {
x: BackgroundPositionComponent::Start(zero),
y: BackgroundPositionComponent::Center,
})
}
Some((BackgroundPositionAxis::X, BackgroundPositionEdge::End)) => {
Some(BackgroundPositionSpec {
x: BackgroundPositionComponent::End(zero),
y: BackgroundPositionComponent::Center,
})
}
Some((BackgroundPositionAxis::Y, BackgroundPositionEdge::Start)) => {
Some(BackgroundPositionSpec {
x: BackgroundPositionComponent::Center,
y: BackgroundPositionComponent::Start(zero),
})
}
Some((BackgroundPositionAxis::Y, BackgroundPositionEdge::End)) => {
Some(BackgroundPositionSpec {
x: BackgroundPositionComponent::Center,
y: BackgroundPositionComponent::End(zero),
})
}
_ => length_spec_from_string(&parts[0]).map(|spec| BackgroundPositionSpec {
x: BackgroundPositionComponent::Start(spec),
y: BackgroundPositionComponent::Center,
}),
};
}
let mut x: Option<BackgroundPositionComponent> = None;
let mut y: Option<BackgroundPositionComponent> = None;
let mut idx = 0usize;
while idx < parts.len() {
let part = parts[idx].as_str();
if let Some((axis, edge)) = background_position_axis_edge(part) {
let (offset, consumed) = parse_optional_edge_position_offset(&parts, idx);
let component = match edge {
BackgroundPositionEdge::Start => BackgroundPositionComponent::Start(offset),
BackgroundPositionEdge::End => BackgroundPositionComponent::End(offset),
};
match axis {
BackgroundPositionAxis::X => x = Some(component),
BackgroundPositionAxis::Y => y = Some(component),
}
idx += consumed;
} else {
match part {
"center" => {
if x.is_none() {
x = Some(BackgroundPositionComponent::Center);
} else if y.is_none() {
y = Some(BackgroundPositionComponent::Center);
}
}
_ => {
let spec = length_spec_from_string(part)?;
if x.is_none() {
x = Some(BackgroundPositionComponent::Start(spec));
} else if y.is_none() {
y = Some(BackgroundPositionComponent::Start(spec));
}
}
}
}
idx += 1;
}
Some(BackgroundPositionSpec {
x: x.unwrap_or_default(),
y: y.unwrap_or_default(),
})
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BackgroundPositionAxis {
X,
Y,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BackgroundPositionEdge {
Start,
End,
}
fn background_position_axis_edge(
raw: &str,
) -> Option<(BackgroundPositionAxis, BackgroundPositionEdge)> {
match raw {
"left" | "x-start" | "inline-start" => {
Some((BackgroundPositionAxis::X, BackgroundPositionEdge::Start))
}
"right" | "x-end" | "inline-end" => {
Some((BackgroundPositionAxis::X, BackgroundPositionEdge::End))
}
"top" | "y-start" | "block-start" => {
Some((BackgroundPositionAxis::Y, BackgroundPositionEdge::Start))
}
"bottom" | "y-end" | "block-end" => {
Some((BackgroundPositionAxis::Y, BackgroundPositionEdge::End))
}
_ => None,
}
}
fn parse_optional_edge_position_offset(parts: &[String], idx: usize) -> (LengthSpec, usize) {
if parts.len() >= 3 {
parse_optional_position_offset(parts, idx + 1)
} else {
(LengthSpec::Absolute(Pt::ZERO), 0)
}
}
fn parse_optional_position_offset(parts: &[String], idx: usize) -> (LengthSpec, usize) {
if idx < parts.len() {
if let Some(spec) = length_spec_from_string(&parts[idx]) {
return (spec, 1);
}
}
(LengthSpec::Absolute(Pt::ZERO), 0)
}
fn parse_background_repeat_list(raw: &str) -> Vec<BackgroundRepeatSpec> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_repeat_layer(&layer))
.collect()
}
fn parse_background_repeat_layer(raw: &str) -> Option<BackgroundRepeatSpec> {
let lower = raw.trim().to_ascii_lowercase();
if lower.is_empty() {
return None;
}
match lower.as_str() {
"repeat-x" => Some(BackgroundRepeatSpec {
x: BackgroundRepeatMode::Repeat,
y: BackgroundRepeatMode::NoRepeat,
}),
"repeat-y" => Some(BackgroundRepeatSpec {
x: BackgroundRepeatMode::NoRepeat,
y: BackgroundRepeatMode::Repeat,
}),
"repeat-inline" => Some(BackgroundRepeatSpec {
x: BackgroundRepeatMode::Repeat,
y: BackgroundRepeatMode::NoRepeat,
}),
"repeat-block" => Some(BackgroundRepeatSpec {
x: BackgroundRepeatMode::NoRepeat,
y: BackgroundRepeatMode::Repeat,
}),
_ => {
let parts: Vec<&str> = lower.split_whitespace().collect();
if parts.is_empty() || parts.len() > 2 {
return None;
}
let x = parse_background_repeat_mode(parts[0])?;
let y = if parts.len() > 1 {
parse_background_repeat_mode(parts[1])?
} else {
x
};
Some(BackgroundRepeatSpec { x, y })
}
}
}
fn parse_background_repeat_mode(raw: &str) -> Option<BackgroundRepeatMode> {
match raw {
"repeat" => Some(BackgroundRepeatMode::Repeat),
"no-repeat" => Some(BackgroundRepeatMode::NoRepeat),
"space" => Some(BackgroundRepeatMode::Space),
"round" => Some(BackgroundRepeatMode::Round),
_ => None,
}
}
fn parse_background_attachment_list(raw: &str) -> Vec<BackgroundAttachment> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_attachment_layer(&layer))
.collect()
}
fn parse_background_attachment_layer(raw: &str) -> Option<BackgroundAttachment> {
match raw.trim().to_ascii_lowercase().as_str() {
"scroll" | "initial" | "unset" | "revert" | "revert-layer" => {
Some(BackgroundAttachment::Scroll)
}
"fixed" => Some(BackgroundAttachment::Fixed),
"local" => Some(BackgroundAttachment::Local),
_ => None,
}
}
fn parse_background_blend_mode_list(raw: &str) -> Vec<MixBlendMode> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("initial")
|| raw.eq_ignore_ascii_case("unset")
|| raw.eq_ignore_ascii_case("revert")
|| raw.eq_ignore_ascii_case("revert-layer")
{
return vec![MixBlendMode::Normal];
}
if raw.eq_ignore_ascii_case("inherit") {
return Vec::new();
}
split_top_level_delimited(raw, ',')
.into_iter()
.filter_map(|part| parse_mix_blend_mode_str(&part))
.collect()
}
fn parse_background_origin_list(raw: &str) -> Vec<BackgroundBox> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_origin_layer(&layer))
.collect()
}
fn parse_background_origin_layer(raw: &str) -> Option<BackgroundBox> {
match raw.trim().to_ascii_lowercase().as_str() {
"border-box" => Some(BackgroundBox::Border),
"padding-box" => Some(BackgroundBox::Padding),
"content-box" => Some(BackgroundBox::Content),
_ => None,
}
}
fn parse_background_clip_list(raw: &str) -> Vec<BackgroundClipBox> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_clip_layer(&layer))
.collect()
}
fn parse_background_clip_layer(raw: &str) -> Option<BackgroundClipBox> {
match raw.trim().to_ascii_lowercase().as_str() {
"border-box" => Some(BackgroundClipBox::Border),
"padding-box" => Some(BackgroundClipBox::Padding),
"content-box" => Some(BackgroundClipBox::Content),
_ => None,
}
}
fn var_name_from_string(raw: &str) -> Option<String> {
let lower = raw.to_ascii_lowercase();
let start = lower.find("var(")?;
let after = &raw[start + 4..];
let end = after.find(')')?;
let inside = &after[..end];
for part in inside.split(',') {
let name = part.trim();
if name.starts_with("--") {
return Some(name.to_ascii_lowercase());
}
}
None
}
fn parse_box_shadow_list_str(raw: &str) -> Option<Vec<BoxShadowSpec>> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let mut shadows = Vec::new();
for shadow in split_args(raw) {
shadows.push(parse_single_box_shadow_str(&shadow)?);
}
if shadows.is_empty() {
None
} else {
Some(shadows)
}
}
fn parse_shadow_length_str(raw: &str) -> Option<LengthSpec> {
match parse_native_length_math(raw.trim(), true)? {
NativeMathValue::Number(value) if value.abs() <= f32::EPSILON => {
Some(LengthSpec::Absolute(Pt::ZERO))
}
NativeMathValue::Number(_) => None,
NativeMathValue::Length(value) => value.to_length_spec(),
NativeMathValue::Clamped(value) => value.to_length_spec(),
NativeMathValue::Typed(_) => None,
}
}
fn parse_single_box_shadow_str(shadow: &str) -> Option<BoxShadowSpec> {
let tokens = split_ws_preserve_parens(&shadow);
let mut lengths: Vec<LengthSpec> = Vec::new();
let mut color: Option<(Color, f32)> = None;
let mut color_var: Option<String> = None;
let mut inset = false;
let mut saw_position_keyword = false;
for token in tokens {
let t = token.trim();
if t.is_empty() {
continue;
}
if t.eq_ignore_ascii_case("inset") || t.eq_ignore_ascii_case("outset") {
if saw_position_keyword {
return None;
}
saw_position_keyword = true;
inset = t.eq_ignore_ascii_case("inset");
continue;
}
if let Some(len) = parse_shadow_length_str(t) {
if !box_shadow_length_is_supported(len) {
return None;
}
lengths.push(len);
continue;
}
if let Some(c) = parse_color_string(t) {
if color.is_some() || color_var.is_some() {
return None;
}
color = Some(c);
continue;
}
if t.eq_ignore_ascii_case("currentcolor") {
if color.is_some() || color_var.is_some() {
return None;
}
color_var = Some("currentcolor".to_string());
continue;
}
if color_var.is_none() {
if t.to_ascii_lowercase().contains("var(") {
if color.is_some() {
return None;
}
color_var = Some(t.to_ascii_lowercase());
continue;
}
if let Some(var) = var_name_from_string(t) {
if color.is_some() {
return None;
}
color_var = Some(var);
continue;
}
}
return None;
}
if lengths.len() < 2 || lengths.len() > 4 {
return None;
}
let offset_x = lengths[0];
let offset_y = lengths[1];
let blur = lengths
.get(2)
.copied()
.unwrap_or(LengthSpec::Absolute(Pt::ZERO));
if length_spec_definitely_negative(blur) {
return None;
}
let spread = lengths
.get(3)
.copied()
.unwrap_or(LengthSpec::Absolute(Pt::ZERO));
if color.is_none() && color_var.is_none() {
color_var = Some("currentcolor".to_string());
}
let (color, opacity) = if let Some(color) = color {
color
} else {
(Color::BLACK, 1.0)
};
Some(BoxShadowSpec {
offset_x,
offset_y,
blur,
spread,
color,
opacity,
inset,
color_var,
})
}
fn parse_text_shadow_list_str(raw: &str) -> Option<Vec<BoxShadowSpec>> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(Vec::new());
}
let mut shadows = Vec::new();
for shadow in split_args(raw) {
shadows.push(parse_single_text_shadow_str(&shadow)?);
}
if shadows.is_empty() {
None
} else {
Some(shadows)
}
}
fn parse_single_text_shadow_str(shadow: &str) -> Option<BoxShadowSpec> {
let tokens = split_ws_preserve_parens(shadow);
let mut lengths: Vec<LengthSpec> = Vec::new();
let mut color: Option<(Color, f32)> = None;
let mut color_var: Option<String> = None;
for token in tokens {
let t = token.trim();
if t.is_empty() {
continue;
}
if let Some(len) = parse_shadow_length_str(t) {
if !box_shadow_length_is_supported(len) {
return None;
}
lengths.push(len);
continue;
}
if let Some(c) = parse_color_string(t) {
if color.is_some() || color_var.is_some() {
return None;
}
color = Some(c);
continue;
}
if t.eq_ignore_ascii_case("currentcolor") {
if color.is_some() || color_var.is_some() {
return None;
}
color_var = Some("currentcolor".to_string());
continue;
}
if color_var.is_none() {
if t.to_ascii_lowercase().contains("var(") {
if color.is_some() {
return None;
}
color_var = Some(t.to_ascii_lowercase());
continue;
}
if let Some(var) = var_name_from_string(t) {
if color.is_some() {
return None;
}
color_var = Some(var);
continue;
}
}
return None;
}
if lengths.len() < 2 || lengths.len() > 3 {
return None;
}
let offset_x = lengths[0];
let offset_y = lengths[1];
let blur = lengths
.get(2)
.copied()
.unwrap_or(LengthSpec::Absolute(Pt::ZERO));
if length_spec_definitely_negative(blur) {
return None;
}
if color.is_none() && color_var.is_none() {
color_var = Some("currentcolor".to_string());
}
let (color, opacity) = color.unwrap_or((Color::BLACK, 1.0));
Some(BoxShadowSpec {
offset_x,
offset_y,
blur,
spread: LengthSpec::Absolute(Pt::ZERO),
color,
opacity,
inset: false,
color_var,
})
}
fn box_shadow_length_is_supported(spec: LengthSpec) -> bool {
match spec {
LengthSpec::Absolute(_) | LengthSpec::Em(_) | LengthSpec::Rem(_) => true,
LengthSpec::Calc(calc) => calc.percent.abs() <= 1.0e-6,
_ => false,
}
}
fn length_spec_definitely_negative(spec: LengthSpec) -> bool {
match spec {
LengthSpec::Absolute(value) => value < Pt::ZERO,
LengthSpec::Em(value) | LengthSpec::Rem(value) => value < 0.0,
LengthSpec::Calc(calc) => {
calc.percent.abs() <= 1.0e-6
&& calc.em.abs() <= 1.0e-6
&& calc.rem.abs() <= 1.0e-6
&& calc.abs < Pt::ZERO
}
_ => false,
}
}
fn parse_clip_path_spec_str(raw: &str) -> Option<ClipPathSpec> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let lower = raw.to_ascii_lowercase();
if lower == "none" {
return Some(ClipPathSpec::None);
}
if lower == "inherit" {
return Some(ClipPathSpec::Inherit);
}
if lower == "initial" || lower == "unset" {
return Some(ClipPathSpec::Initial);
}
let (shape, reference_box) = parse_clip_path_shape_and_reference_box(raw)?;
Some(ClipPathSpec::Shape {
shape,
reference_box,
})
}
fn parse_legacy_clip_spec_str(raw: &str) -> Option<LegacyClipSpec> {
let raw = raw.trim();
match raw.to_ascii_lowercase().as_str() {
"auto" | "none" => return Some(LegacyClipSpec::None),
"inherit" => return Some(LegacyClipSpec::Inherit),
"initial" | "unset" | "revert" => return Some(LegacyClipSpec::Initial),
_ => {}
}
let rect = parse_clip_path_rect_str(raw)?;
if rect.radius.is_some() {
return None;
}
Some(LegacyClipSpec::Rect(rect))
}
fn parse_clip_path_shape_and_reference_box(
raw: &str,
) -> Option<(ClipPathShapeSpec, ClipPathReferenceBox)> {
let mut reference_box = ClipPathReferenceBox::Border;
let mut saw_reference_box = false;
let mut shape_tokens = Vec::new();
for token in split_ws_preserve_parens(raw) {
if let Some(parsed_box) = parse_clip_path_reference_box_str(&token) {
if saw_reference_box {
return None;
}
reference_box = parsed_box;
saw_reference_box = true;
} else {
shape_tokens.push(token);
}
}
if shape_tokens.is_empty() {
return saw_reference_box.then_some((ClipPathShapeSpec::ReferenceBox, reference_box));
}
let shape_raw = shape_tokens.join(" ");
let shape = if let Some(source) = parse_clip_path_url_str(&shape_raw) {
ClipPathShapeSpec::Url(source)
} else if let Some(inset) = parse_clip_path_inset_str(&shape_raw) {
ClipPathShapeSpec::Inset(inset)
} else if let Some(circle) = parse_clip_path_circle_str(&shape_raw) {
ClipPathShapeSpec::Circle(circle)
} else if let Some(ellipse) = parse_clip_path_ellipse_str(&shape_raw) {
ClipPathShapeSpec::Ellipse(ellipse)
} else if let Some(xywh) = parse_clip_path_xywh_str(&shape_raw) {
ClipPathShapeSpec::Xywh(xywh)
} else if let Some(rect) = parse_clip_path_rect_str(&shape_raw) {
ClipPathShapeSpec::Rect(rect)
} else if let Some(polygon) = parse_clip_path_polygon_str(&shape_raw) {
ClipPathShapeSpec::Polygon(polygon)
} else if let Some(path) = parse_clip_path_path_str(&shape_raw) {
ClipPathShapeSpec::Path(path)
} else if let Some(shape) = parse_clip_path_shape_function_str(&shape_raw) {
ClipPathShapeSpec::ShapeFunction(shape)
} else {
return None;
};
Some((shape, reference_box))
}
fn parse_clip_path_url_str(raw: &str) -> Option<String> {
match parse_background_image_url_str(raw)? {
BackgroundPaint::Image { source } => Some(source),
_ => None,
}
}
fn parse_clip_path_reference_box_str(raw: &str) -> Option<ClipPathReferenceBox> {
match raw.trim().to_ascii_lowercase().as_str() {
"margin-box" => Some(ClipPathReferenceBox::Margin),
"border-box" => Some(ClipPathReferenceBox::Border),
"half-border-box" => Some(ClipPathReferenceBox::HalfBorder),
"padding-box" => Some(ClipPathReferenceBox::Padding),
"content-box" => Some(ClipPathReferenceBox::Content),
_ => None,
}
}
fn parse_clip_path_inset_str(raw: &str) -> Option<ClipPathInsetSpec> {
let inside = extract_function_args(raw.trim(), "inset(")?;
let tokens = split_ws_preserve_parens(inside)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let (value_tokens, radius) = split_clip_path_round_radius(tokens)?;
let values = value_tokens
.iter()
.map(|token| length_spec_from_string(token))
.collect::<Option<Vec<_>>>()?;
if values.is_empty() || values.len() > 4 {
return None;
}
let (top, right, bottom, left) = match values.len() {
1 => (values[0], values[0], values[0], values[0]),
2 => (values[0], values[1], values[0], values[1]),
3 => (values[0], values[1], values[2], values[1]),
_ => (values[0], values[1], values[2], values[3]),
};
Some(ClipPathInsetSpec {
top,
right,
bottom,
left,
radius,
})
}
fn parse_clip_path_circle_str(raw: &str) -> Option<ClipPathCircleSpec> {
let inside = extract_function_args(raw.trim(), "circle(")?;
let tokens = split_ws_preserve_parens(inside)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let at_index = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("at"));
let (radius_tokens, position_tokens) = if let Some(index) = at_index {
(&tokens[..index], &tokens[index + 1..])
} else {
(&tokens[..], &tokens[0..0])
};
let radius = match radius_tokens.len() {
0 => ClipPathShapeRadius::ClosestSide,
1 => parse_clip_path_shape_radius(&radius_tokens[0])?,
_ => return None,
};
let (center_x, center_y) = parse_clip_path_circle_position(position_tokens)?;
Some(ClipPathCircleSpec {
radius,
center_x,
center_y,
})
}
fn parse_clip_path_circle_position(tokens: &[String]) -> Option<(LengthSpec, LengthSpec)> {
if tokens.is_empty() {
Some((LengthSpec::Percent(0.5), LengthSpec::Percent(0.5)))
} else {
parse_shape_position_pair(tokens)
}
}
fn parse_clip_path_ellipse_str(raw: &str) -> Option<ClipPathEllipseSpec> {
let inside = extract_function_args(raw.trim(), "ellipse(")?;
let tokens = split_ws_preserve_parens(inside)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let at_index = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("at"));
let (radius_tokens, position_tokens) = if let Some(index) = at_index {
(&tokens[..index], &tokens[index + 1..])
} else {
(&tokens[..], &tokens[0..0])
};
let (radius_x, radius_y) = match radius_tokens.len() {
0 => (
ClipPathShapeRadius::ClosestSide,
ClipPathShapeRadius::ClosestSide,
),
1 => {
let radius = parse_clip_path_shape_radius(&radius_tokens[0])?;
(radius, radius)
}
2 => (
parse_clip_path_shape_radius(&radius_tokens[0])?,
parse_clip_path_shape_radius(&radius_tokens[1])?,
),
_ => return None,
};
let (center_x, center_y) = parse_clip_path_circle_position(position_tokens)?;
Some(ClipPathEllipseSpec {
radius_x,
radius_y,
center_x,
center_y,
})
}
fn parse_clip_path_shape_radius(raw: &str) -> Option<ClipPathShapeRadius> {
match raw.trim().to_ascii_lowercase().as_str() {
"closest-side" => Some(ClipPathShapeRadius::ClosestSide),
"farthest-side" => Some(ClipPathShapeRadius::FarthestSide),
"closest-corner" => Some(ClipPathShapeRadius::ClosestCorner),
"farthest-corner" => Some(ClipPathShapeRadius::FarthestCorner),
_ => length_spec_from_string(raw).map(ClipPathShapeRadius::Length),
}
}
fn parse_clip_path_xywh_str(raw: &str) -> Option<ClipPathXywhSpec> {
let inside = extract_function_args(raw.trim(), "xywh(")?;
let tokens = split_ws_preserve_parens(inside)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let (tokens, radius) = split_clip_path_round_radius(tokens)?;
if tokens.len() != 4 {
return None;
}
Some(ClipPathXywhSpec {
x: length_spec_from_string(&tokens[0])?,
y: length_spec_from_string(&tokens[1])?,
width: length_spec_from_string(&tokens[2])?,
height: length_spec_from_string(&tokens[3])?,
radius,
})
}
fn parse_clip_path_rect_str(raw: &str) -> Option<ClipPathRectSpec> {
let inside = extract_function_args(raw.trim(), "rect(")?;
let ws_tokens = split_ws_preserve_parens(inside);
let has_round = ws_tokens
.iter()
.any(|token| token.eq_ignore_ascii_case("round"));
let comma_parts = split_top_level_delimited(inside, ',');
let tokens = if comma_parts.len() > 1 && !has_round {
comma_parts
} else {
ws_tokens
}
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let (tokens, radius) = split_clip_path_round_radius(tokens)?;
if tokens.len() != 4 {
return None;
}
Some(ClipPathRectSpec {
top: parse_clip_path_rect_edge(&tokens[0])?,
right: parse_clip_path_rect_edge(&tokens[1])?,
bottom: parse_clip_path_rect_edge(&tokens[2])?,
left: parse_clip_path_rect_edge(&tokens[3])?,
radius,
})
}
fn split_clip_path_round_radius(
tokens: Vec<String>,
) -> Option<(Vec<String>, Option<ClipPathRadiusSpec>)> {
let Some(round_index) = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("round"))
else {
return Some((tokens, None));
};
let radius_tokens = &tokens[round_index + 1..];
if radius_tokens.is_empty() {
return None;
}
let radius_raw = radius_tokens.join(" ");
let slash_parts = split_top_level_delimited(&radius_raw, '/');
let (horizontal_raw, vertical_raw) = match slash_parts.len() {
1 => (slash_parts[0].as_str(), slash_parts[0].as_str()),
2 => (slash_parts[0].as_str(), slash_parts[1].as_str()),
_ => return None,
};
let horizontal = parse_clip_path_radius_axis(horizontal_raw)?;
let vertical = parse_clip_path_radius_axis(vertical_raw)?;
Some((
tokens[..round_index].to_vec(),
Some(ClipPathRadiusSpec {
horizontal,
vertical,
}),
))
}
fn parse_clip_path_radius_axis(raw: &str) -> Option<BorderRadiusSpec> {
let values = split_ws_preserve_parens(raw)
.into_iter()
.map(|token| length_spec_from_string(token.trim()))
.collect::<Option<Vec<_>>>()?;
if values.is_empty() || values.len() > 4 {
return None;
}
let (top_left, top_right, bottom_right, bottom_left) = match values.len() {
1 => (values[0], values[0], values[0], values[0]),
2 => (values[0], values[1], values[0], values[1]),
3 => (values[0], values[1], values[2], values[1]),
_ => (values[0], values[1], values[2], values[3]),
};
Some(BorderRadiusSpec {
top_left,
top_right,
bottom_right,
bottom_left,
})
}
fn parse_clip_path_rect_edge(raw: &str) -> Option<LengthSpec> {
if raw.trim().eq_ignore_ascii_case("auto") {
Some(LengthSpec::Auto)
} else {
length_spec_from_string(raw)
}
}
fn parse_clip_path_polygon_str(raw: &str) -> Option<ClipPathPolygonSpec> {
let inside = extract_function_args(raw.trim(), "polygon(")?;
let mut parts = split_top_level_delimited(inside, ',')
.into_iter()
.map(|part| part.trim().to_string())
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
if parts.is_empty() {
return None;
}
let mut evenodd = false;
let mut fill_rule_explicit = false;
let first = parts[0].to_ascii_lowercase();
if first == "evenodd" || first == "nonzero" {
evenodd = first == "evenodd";
fill_rule_explicit = true;
parts.remove(0);
}
if parts.len() < 3 {
return None;
}
let mut points = Vec::with_capacity(parts.len());
for part in parts {
let coords = split_ws_preserve_parens(&part)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
if coords.len() != 2 {
return None;
}
let x = length_spec_from_string(&coords[0])?;
let y = length_spec_from_string(&coords[1])?;
points.push((x, y));
}
Some(ClipPathPolygonSpec {
evenodd,
fill_rule_explicit,
points,
})
}
fn parse_clip_path_path_str(raw: &str) -> Option<ClipPathPathSpec> {
let inside = extract_function_args(raw.trim(), "path(")?;
let mut rest = inside.trim_start();
let mut evenodd = false;
let mut fill_rule_explicit = false;
for (rule, is_evenodd) in [("evenodd", true), ("nonzero", false)] {
if let Some(after_rule) = consume_css_ident(rest, rule) {
evenodd = is_evenodd;
fill_rule_explicit = true;
rest = after_rule.trim_start();
if let Some(after_comma) = rest.strip_prefix(',') {
rest = after_comma.trim_start();
}
break;
}
}
let data = if let Some((data, trailing)) = parse_css_string_literal(rest) {
if !trailing.trim().is_empty() {
return None;
}
data
} else {
let data = rest.trim();
if data.is_empty() {
return None;
}
data.to_string()
};
let commands = svg::parse_svg_path_data(&data);
if commands.is_empty() {
return None;
}
let commands = commands
.into_iter()
.map(|command| match command {
svg::SvgPathSegment::MoveTo(x, y) => ClipPathPathCommand::MoveTo {
x: px_to_pt(x),
y: px_to_pt(y),
},
svg::SvgPathSegment::LineTo(x, y) => ClipPathPathCommand::LineTo {
x: px_to_pt(x),
y: px_to_pt(y),
},
svg::SvgPathSegment::CurveTo(x1, y1, x2, y2, x, y) => ClipPathPathCommand::CurveTo {
x1: px_to_pt(x1),
y1: px_to_pt(y1),
x2: px_to_pt(x2),
y2: px_to_pt(y2),
x: px_to_pt(x),
y: px_to_pt(y),
},
svg::SvgPathSegment::Close => ClipPathPathCommand::Close,
})
.collect();
Some(ClipPathPathSpec {
evenodd,
fill_rule_explicit,
data,
commands,
})
}
fn parse_clip_path_shape_function_str(raw: &str) -> Option<ClipPathShapeFunctionSpec> {
let inside = extract_function_args(raw.trim(), "shape(")?;
let mut parts = split_top_level_delimited(inside, ',')
.into_iter()
.map(|part| part.trim().to_string())
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
if parts.len() < 2 {
return None;
}
let mut header = split_ws_preserve_parens(&parts.remove(0))
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let mut evenodd = false;
let mut fill_rule_explicit = false;
if let Some(first) = header.first().map(|token| token.to_ascii_lowercase()) {
if first == "evenodd" || first == "nonzero" {
evenodd = first == "evenodd";
fill_rule_explicit = true;
header.remove(0);
}
}
if header
.first()
.is_none_or(|token| !token.eq_ignore_ascii_case("from"))
{
return None;
}
let (start_x, start_y) = parse_shape_position_pair(&header[1..])?;
let mut commands = Vec::with_capacity(parts.len());
for part in parts {
let tokens = split_ws_preserve_parens(&part)
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let first = tokens.first()?.to_ascii_lowercase();
match first.as_str() {
"close" if tokens.len() == 1 => commands.push(ClipPathShapeFunctionCommand::Close),
"move" | "line" if tokens.len() >= 3 => {
let relative = parse_shape_command_relative(&tokens[1])?;
let (x, y) = parse_shape_endpoint(&tokens[2..], relative)?;
if first == "move" {
commands.push(ClipPathShapeFunctionCommand::MoveTo { x, y, relative });
} else {
commands.push(ClipPathShapeFunctionCommand::LineTo { x, y, relative });
}
}
"hline" if tokens.len() == 3 => {
let relative = parse_shape_command_relative(&tokens[1])?;
commands.push(ClipPathShapeFunctionCommand::HLine {
x: if relative {
length_spec_from_string(&tokens[2])?
} else {
parse_shape_x_position_value(&tokens[2])?
},
relative,
});
}
"vline" if tokens.len() == 3 => {
let relative = parse_shape_command_relative(&tokens[1])?;
commands.push(ClipPathShapeFunctionCommand::VLine {
y: if relative {
length_spec_from_string(&tokens[2])?
} else {
parse_shape_y_position_value(&tokens[2])?
},
relative,
});
}
"curve" if tokens.len() >= 6 => {
let relative = parse_shape_command_relative(&tokens[1])?;
let with_index = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("with"))?;
if with_index <= 2 {
return None;
}
let (x, y) = parse_shape_endpoint(&tokens[2..with_index], relative)?;
let controls = parse_shape_curve_controls(&tokens[with_index + 1..])?;
commands.push(ClipPathShapeFunctionCommand::CurveTo {
x,
y,
relative,
control1_x: controls.0,
control1_y: controls.1,
control1_anchor: controls.2,
control2_x: controls.3,
control2_y: controls.4,
control2_anchor: controls.5,
});
}
"smooth" if tokens.len() >= 3 => {
let relative = parse_shape_command_relative(&tokens[1])?;
let with_index = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("with"));
let endpoint_end = with_index.unwrap_or(tokens.len());
if endpoint_end <= 2 {
return None;
}
let (x, y) = parse_shape_endpoint(&tokens[2..endpoint_end], relative)?;
let (control_x, control_y, control_anchor) = if let Some(with_index) = with_index {
let (control_x, control_y, control_anchor) =
parse_shape_control_point(&tokens[with_index + 1..])?;
(Some(control_x), Some(control_y), Some(control_anchor))
} else {
(None, None, None)
};
commands.push(ClipPathShapeFunctionCommand::SmoothTo {
x,
y,
relative,
control_x,
control_y,
control_anchor,
});
}
"arc" if tokens.len() >= 6 => {
let relative = parse_shape_command_relative(&tokens[1])?;
let of_index = tokens
.iter()
.position(|token| token.eq_ignore_ascii_case("of"))?;
if of_index <= 2 {
return None;
}
let (x, y) = parse_shape_endpoint(&tokens[2..of_index], relative)?;
let (radius_x, radius_y, large_arc, sweep, rotation_deg) =
parse_shape_arc_args(&tokens[of_index + 1..])?;
commands.push(ClipPathShapeFunctionCommand::ArcTo {
x,
y,
relative,
radius_x,
radius_y,
large_arc,
sweep,
rotation_deg,
});
}
_ => return None,
}
}
Some(ClipPathShapeFunctionSpec {
evenodd,
fill_rule_explicit,
start_x,
start_y,
commands,
})
}
fn parse_shape_command_relative(raw: &str) -> Option<bool> {
match raw.to_ascii_lowercase().as_str() {
"by" => Some(true),
"to" => Some(false),
_ => None,
}
}
fn parse_shape_coordinate_pair(tokens: &[String]) -> Option<(LengthSpec, LengthSpec)> {
if tokens.len() != 2 {
return None;
}
Some((
length_spec_from_string(&tokens[0])?,
length_spec_from_string(&tokens[1])?,
))
}
fn parse_shape_endpoint(tokens: &[String], relative: bool) -> Option<(LengthSpec, LengthSpec)> {
if relative {
parse_shape_coordinate_pair(tokens)
} else {
parse_shape_position_pair(tokens)
}
}
fn parse_shape_position_pair(tokens: &[String]) -> Option<(LengthSpec, LengthSpec)> {
match tokens.len() {
1 => {
if let Some(value) = length_spec_from_string(&tokens[0]) {
return Some((value, LengthSpec::Percent(0.5)));
}
let lower = tokens[0].to_ascii_lowercase();
if lower == "center" {
return Some((LengthSpec::Percent(0.5), LengthSpec::Percent(0.5)));
}
if let Some(x) = parse_shape_x_position_keyword(&lower) {
return Some((x, LengthSpec::Percent(0.5)));
}
if let Some(y) = parse_shape_y_position_keyword(&lower) {
return Some((LengthSpec::Percent(0.5), y));
}
None
}
2 => {
if let Some(pair) = parse_shape_coordinate_pair(tokens) {
return Some(pair);
}
if let (Some(x), Some(y)) = (
parse_shape_x_position_value(&tokens[0]),
parse_shape_y_position_value(&tokens[1]),
) {
return Some((x, y));
}
if let (Some(x), Some(y)) = (
parse_shape_x_position_value(&tokens[1]),
parse_shape_y_position_value(&tokens[0]),
) {
return Some((x, y));
}
None
}
4 => parse_shape_position_offset_pair(tokens),
_ => None,
}
}
fn parse_shape_position_offset_pair(tokens: &[String]) -> Option<(LengthSpec, LengthSpec)> {
let first_offset = length_spec_from_string(&tokens[1])?;
let second_offset = length_spec_from_string(&tokens[3])?;
if let (Some(x), Some(y)) = (
parse_shape_x_position_offset(&tokens[0], first_offset),
parse_shape_y_position_offset(&tokens[2], second_offset),
) {
return Some((x, y));
}
if let (Some(x), Some(y)) = (
parse_shape_x_position_offset(&tokens[2], second_offset),
parse_shape_y_position_offset(&tokens[0], first_offset),
) {
return Some((x, y));
}
None
}
fn parse_shape_x_position_offset(raw: &str, offset: LengthSpec) -> Option<LengthSpec> {
match raw.to_ascii_lowercase().as_str() {
"left" | "x-start" | "inline-start" | "start" => Some(offset),
"right" | "x-end" | "inline-end" | "end" => Some(length_spec_from_end_offset(offset)),
_ => None,
}
}
fn parse_shape_y_position_offset(raw: &str, offset: LengthSpec) -> Option<LengthSpec> {
match raw.to_ascii_lowercase().as_str() {
"top" | "y-start" | "block-start" | "start" => Some(offset),
"bottom" | "y-end" | "block-end" | "end" => Some(length_spec_from_end_offset(offset)),
_ => None,
}
}
fn length_spec_from_end_offset(offset: LengthSpec) -> LengthSpec {
match offset {
LengthSpec::Absolute(value) => length_spec_from_calc_length(CalcLength {
abs: -value,
percent: 1.0,
em: 0.0,
rem: 0.0,
}),
LengthSpec::Percent(value) => LengthSpec::Percent(1.0 - value),
LengthSpec::Em(value) => length_spec_from_calc_length(CalcLength {
abs: Pt::ZERO,
percent: 1.0,
em: -value,
rem: 0.0,
}),
LengthSpec::Rem(value) => length_spec_from_calc_length(CalcLength {
abs: Pt::ZERO,
percent: 1.0,
em: 0.0,
rem: -value,
}),
LengthSpec::Calc(calc) => length_spec_from_calc_length(CalcLength {
abs: -calc.abs,
percent: 1.0 - calc.percent,
em: -calc.em,
rem: -calc.rem,
}),
LengthSpec::FontRelative(calc) => LengthSpec::FontRelative(FontRelativeLength {
base: CalcLength {
abs: -calc.base.abs,
percent: 1.0 - calc.base.percent,
em: -calc.base.em,
rem: -calc.base.rem,
},
ex: -calc.ex,
ch: -calc.ch,
cap: -calc.cap,
lh: -calc.lh,
rlh: -calc.rlh,
}),
LengthSpec::Clamped(_) => offset,
LengthSpec::Auto
| LengthSpec::Content
| LengthSpec::MinContent
| LengthSpec::MaxContent
| LengthSpec::FitContent
| LengthSpec::Inherit
| LengthSpec::Initial => offset,
}
}
fn parse_shape_x_position_value(raw: &str) -> Option<LengthSpec> {
length_spec_from_string(raw)
.or_else(|| parse_shape_x_position_keyword(&raw.to_ascii_lowercase()))
}
fn parse_shape_y_position_value(raw: &str) -> Option<LengthSpec> {
length_spec_from_string(raw)
.or_else(|| parse_shape_y_position_keyword(&raw.to_ascii_lowercase()))
}
fn parse_shape_x_position_keyword(raw: &str) -> Option<LengthSpec> {
match raw {
"left" | "x-start" | "inline-start" | "start" => Some(LengthSpec::Percent(0.0)),
"center" => Some(LengthSpec::Percent(0.5)),
"right" | "x-end" | "inline-end" | "end" => Some(LengthSpec::Percent(1.0)),
_ => None,
}
}
fn parse_shape_y_position_keyword(raw: &str) -> Option<LengthSpec> {
match raw {
"top" | "y-start" | "block-start" | "start" => Some(LengthSpec::Percent(0.0)),
"center" => Some(LengthSpec::Percent(0.5)),
"bottom" | "y-end" | "block-end" | "end" => Some(LengthSpec::Percent(1.0)),
_ => None,
}
}
fn parse_shape_curve_controls(
tokens: &[String],
) -> Option<(
LengthSpec,
LengthSpec,
ClipPathShapeControlAnchor,
Option<LengthSpec>,
Option<LengthSpec>,
Option<ClipPathShapeControlAnchor>,
)> {
let raw = tokens.join(" ");
let parts = split_top_level_delimited(&raw, '/');
match parts.len() {
1 => {
let tokens = split_ws_preserve_parens(&parts[0])
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let (x, y, anchor) = parse_shape_control_point(&tokens)?;
Some((x, y, anchor, None, None, None))
}
2 => {
let first_tokens = split_ws_preserve_parens(&parts[0])
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let second_tokens = split_ws_preserve_parens(&parts[1])
.into_iter()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect::<Vec<_>>();
let (x1, y1, anchor1) = parse_shape_control_point(&first_tokens)?;
let (x2, y2, anchor2) = parse_shape_control_point(&second_tokens)?;
Some((x1, y1, anchor1, Some(x2), Some(y2), Some(anchor2)))
}
_ => None,
}
}
fn parse_shape_control_point(
tokens: &[String],
) -> Option<(LengthSpec, LengthSpec, ClipPathShapeControlAnchor)> {
match tokens.len() {
2 => {
if let Some((x, y)) = parse_shape_coordinate_pair(tokens) {
Some((x, y, ClipPathShapeControlAnchor::Start))
} else {
parse_shape_position_pair(tokens)
.map(|(x, y)| (x, y, ClipPathShapeControlAnchor::Origin))
}
}
4 if tokens[2].eq_ignore_ascii_case("from") => {
let (x, y) = parse_shape_coordinate_pair(&tokens[..2])?;
let anchor = parse_shape_control_anchor(&tokens[3])?;
Some((x, y, anchor))
}
_ => parse_shape_position_pair(tokens)
.map(|(x, y)| (x, y, ClipPathShapeControlAnchor::Origin)),
}
}
fn parse_shape_control_anchor(raw: &str) -> Option<ClipPathShapeControlAnchor> {
match raw.to_ascii_lowercase().as_str() {
"start" => Some(ClipPathShapeControlAnchor::Start),
"end" => Some(ClipPathShapeControlAnchor::End),
"origin" => Some(ClipPathShapeControlAnchor::Origin),
_ => None,
}
}
fn parse_shape_arc_args(tokens: &[String]) -> Option<(LengthSpec, LengthSpec, bool, bool, f32)> {
if tokens.is_empty() {
return None;
}
let radius_x = length_spec_from_string(&tokens[0])?;
let mut radius_y = radius_x;
let mut index = 1usize;
if let Some(token) = tokens.get(index) {
if !is_shape_arc_option_keyword(token) {
radius_y = length_spec_from_string(token)?;
index += 1;
}
}
let mut large_arc = false;
let mut sweep = false;
let mut saw_size = false;
let mut saw_sweep = false;
let mut saw_rotation = false;
let mut rotation_deg = 0.0f32;
while let Some(token) = tokens.get(index) {
match token.to_ascii_lowercase().as_str() {
"small" | "large" => {
if saw_size {
return None;
}
saw_size = true;
large_arc = token.eq_ignore_ascii_case("large");
index += 1;
}
"ccw" | "cw" => {
if saw_sweep {
return None;
}
saw_sweep = true;
sweep = token.eq_ignore_ascii_case("cw");
index += 1;
}
"rotate" => {
if saw_rotation {
return None;
}
let angle = tokens.get(index + 1)?;
rotation_deg = parse_angle_radians(angle)?.to_degrees();
saw_rotation = true;
index += 2;
}
_ => return None,
}
}
Some((radius_x, radius_y, large_arc, sweep, rotation_deg))
}
fn is_shape_arc_option_keyword(token: &str) -> bool {
matches!(
token.to_ascii_lowercase().as_str(),
"small" | "large" | "ccw" | "cw" | "rotate"
)
}
fn consume_css_ident<'a>(raw: &'a str, ident: &str) -> Option<&'a str> {
let trimmed = raw.trim_start();
let prefix = trimmed.get(..ident.len())?;
if !prefix.eq_ignore_ascii_case(ident) {
return None;
}
let rest = &trimmed[ident.len()..];
if rest
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
{
return None;
}
Some(rest)
}
fn parse_css_string_literal(raw: &str) -> Option<(String, &str)> {
let trimmed = raw.trim_start();
let mut chars = trimmed.char_indices();
let (_, quote) = chars.next()?;
if quote != '"' && quote != '\'' {
return None;
}
let mut out = String::new();
let mut escaped = false;
for (idx, ch) in chars {
if escaped {
out.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == quote {
let rest_start = idx + ch.len_utf8();
return Some((out, &trimmed[rest_start..]));
}
out.push(ch);
}
None
}
fn default_filter_font_size() -> Pt {
TextStyle::default().font_size
}
fn parse_filter_spec_str(raw: &str) -> Option<PaintFilterSpec> {
let font_size = default_filter_font_size();
parse_filter_spec_str_with_mode(raw, true, true, font_size, font_size)
}
fn parse_filter_spec_str_for_computed(
raw: &str,
font_size: Pt,
root_font_size: Pt,
) -> Option<PaintFilterSpec> {
parse_filter_spec_str_with_mode(raw, true, true, font_size, root_font_size)
}
fn parse_backdrop_filter_spec_str(raw: &str) -> Option<PaintFilterSpec> {
let font_size = default_filter_font_size();
parse_filter_spec_str_with_mode(raw, true, true, font_size, font_size)
}
fn parse_backdrop_filter_spec_str_for_computed(
raw: &str,
font_size: Pt,
root_font_size: Pt,
) -> Option<PaintFilterSpec> {
parse_filter_spec_str_with_mode(raw, true, true, font_size, root_font_size)
}
fn will_change_backdrop_root_from_raw(raw: &str) -> Option<bool> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered.is_empty() || lowered.contains("var(") {
return None;
}
if lowered == "auto" {
return Some(false);
}
let mut saw_ident = false;
let mut establishes_root = false;
for part in lowered.split(',') {
let ident = part.trim();
if ident.is_empty() {
return None;
}
saw_ident = true;
if matches!(
ident,
"opacity"
| "filter"
| "backdrop-filter"
| "mask"
| "mask-image"
| "mask-border"
| "clip-path"
| "mix-blend-mode"
) {
establishes_root = true;
}
}
saw_ident.then_some(establishes_root)
}
fn set_delta_will_change_backdrop_root_from_raw(delta: &mut StyleDelta, raw: &str) {
if let Some(root) = will_change_backdrop_root_from_raw(raw) {
delta.will_change_backdrop_root = Some(root);
delta.revert_layer.will_change_backdrop_root = false;
}
}
fn clip_path_spec_or_revert_layer_is_supported(raw: &str) -> bool {
raw.trim().eq_ignore_ascii_case("revert-layer") || parse_clip_path_spec_str(raw).is_some()
}
fn set_delta_paint_filter_from_raw(delta: &mut StyleDelta, raw: &str) {
if let Some(filter) = parse_filter_spec_str(raw) {
delta.paint_filter = Some(filter);
delta.paint_filter_raw = Some(raw.trim().to_string());
delta.revert_layer.paint_filter = false;
}
}
fn set_delta_backdrop_filter_from_raw(delta: &mut StyleDelta, raw: &str) {
if let Some(filter) = parse_backdrop_filter_spec_str(raw) {
delta.backdrop_filter = Some(filter);
delta.backdrop_filter_raw = Some(raw.trim().to_string());
delta.revert_layer.backdrop_filter = false;
}
}
fn parse_filter_spec_str_with_mode(
raw: &str,
allow_blur: bool,
allow_drop_shadow: bool,
font_size: Pt,
root_font_size: Pt,
) -> Option<PaintFilterSpec> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let lower = raw.to_ascii_lowercase();
if lower == "none" {
return Some(PaintFilterSpec::identity());
}
let mut saw_component = false;
let mut saturate = 1.0_f32;
let mut brightness = 1.0_f32;
let mut contrast = 1.0_f32;
let mut invert = 0.0_f32;
let mut sepia = 0.0_f32;
let mut hue_rotate = 0.0_f32;
let mut opacity = 1.0_f32;
let mut blur_radius = Pt::ZERO;
let mut drop_shadows: Vec<FilterDropShadowSpec> = Vec::new();
let mut operations = Vec::new();
for token in split_ws_preserve_parens(&lower) {
let token = token.trim();
if token.is_empty() {
continue;
}
if token.starts_with("url(") {
let BackgroundPaint::Image { source } = parse_background_image_url_str(token)? else {
return None;
};
saw_component = true;
operations.push(PaintFilterOperation::Url(source));
continue;
}
if token.starts_with("saturate(") {
let arg = extract_function_args(token, "saturate(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.max(0.0);
saturate *= value;
operations.push(PaintFilterOperation::Saturate(value));
continue;
}
if token.starts_with("grayscale(") {
let arg = extract_function_args(token, "grayscale(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = (1.0 - value.clamp(0.0, 1.0)).max(0.0);
saturate *= value;
operations.push(PaintFilterOperation::Saturate(value));
continue;
}
if token.starts_with("brightness(") {
let arg = extract_function_args(token, "brightness(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.max(0.0);
brightness *= value;
operations.push(PaintFilterOperation::Brightness(value));
continue;
}
if token.starts_with("contrast(") {
let arg = extract_function_args(token, "contrast(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.max(0.0);
contrast *= value;
operations.push(PaintFilterOperation::Contrast(value));
continue;
}
if token.starts_with("invert(") {
let arg = extract_function_args(token, "invert(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.clamp(0.0, 1.0);
invert = 1.0 - ((1.0 - invert) * (1.0 - value));
operations.push(PaintFilterOperation::Invert(value));
continue;
}
if token.starts_with("sepia(") {
let arg = extract_function_args(token, "sepia(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.clamp(0.0, 1.0);
sepia = 1.0 - ((1.0 - sepia) * (1.0 - value));
operations.push(PaintFilterOperation::Sepia(value));
continue;
}
if token.starts_with("hue-rotate(") {
let arg = extract_function_args(token, "hue-rotate(")?.trim();
let value = if arg.is_empty() {
0.0
} else {
parse_angle_radians(arg)?
};
if !value.is_finite() {
return None;
}
saw_component = true;
hue_rotate += value;
operations.push(PaintFilterOperation::HueRotate(value));
continue;
}
if token.starts_with("opacity(") {
let arg = extract_function_args(token, "opacity(")?.trim();
let value = if arg.is_empty() {
1.0
} else if let Some(pct) = arg.strip_suffix('%') {
let pct = pct.trim().parse::<f32>().ok()?;
pct / 100.0
} else {
arg.parse::<f32>().ok()?
};
if !value.is_finite() {
return None;
}
saw_component = true;
let value = value.clamp(0.0, 1.0);
opacity *= value;
operations.push(PaintFilterOperation::Opacity(value));
continue;
}
if allow_blur && token.starts_with("blur(") {
let arg = extract_function_args(token, "blur(")?.trim();
let length = if arg.is_empty() {
LengthSpec::Absolute(Pt::ZERO)
} else {
length_spec_from_string(arg)?
};
let radius = filter_shadow_length_to_pt(length, font_size, root_font_size)?;
if radius < Pt::ZERO {
return None;
}
saw_component = true;
blur_radius = blur_radius + radius;
operations.push(PaintFilterOperation::Blur(radius));
continue;
}
if token.starts_with("drop-shadow(") {
if !allow_drop_shadow {
return None;
}
let arg = extract_function_args(token, "drop-shadow(")?.trim();
let shadow = parse_filter_drop_shadow_str(arg, font_size, root_font_size)?;
saw_component = true;
drop_shadows.push(shadow);
operations.push(PaintFilterOperation::DropShadow(shadow));
continue;
}
return None;
}
if !saw_component {
return None;
}
Some(PaintFilterSpec {
saturate,
brightness,
contrast,
invert,
sepia,
hue_rotate,
opacity,
blur_radius,
drop_shadows,
operations,
})
}
fn filter_shadow_length_to_pt(spec: LengthSpec, font_size: Pt, root_font_size: Pt) -> Option<Pt> {
match spec {
LengthSpec::Absolute(value) => Some(value),
LengthSpec::Em(value) => Some(font_size * value),
LengthSpec::Rem(value) => Some(root_font_size * value),
LengthSpec::Calc(calc) => {
if calc.percent.abs() > 1.0e-6 {
None
} else {
Some(calc.resolve(Pt::ZERO, font_size, root_font_size))
}
}
_ => None,
}
}
fn resolve_filter_current_color(filter: &mut PaintFilterSpec, current_color: Color) {
for shadow in &mut filter.drop_shadows {
if shadow.color_is_current_color {
shadow.color = current_color;
shadow.opacity = 1.0;
shadow.color_is_current_color = false;
}
}
for operation in &mut filter.operations {
if let PaintFilterOperation::DropShadow(shadow) = operation {
if shadow.color_is_current_color {
shadow.color = current_color;
shadow.opacity = 1.0;
shadow.color_is_current_color = false;
}
}
}
}
fn parse_filter_drop_shadow_str(
raw: &str,
font_size: Pt,
root_font_size: Pt,
) -> Option<FilterDropShadowSpec> {
let tokens = split_ws_preserve_parens(raw);
let mut lengths: Vec<LengthSpec> = Vec::new();
let mut color: Option<(Color, f32)> = None;
let mut color_is_current_color = false;
for token in tokens {
let t = token.trim();
if t.is_empty() {
continue;
}
if t.eq_ignore_ascii_case("inset") {
return None;
}
if let Some(len) = length_spec_from_string(t) {
lengths.push(len);
continue;
}
if t.eq_ignore_ascii_case("currentcolor") {
if color.is_some() || color_is_current_color {
return None;
}
color_is_current_color = true;
continue;
}
if let Some(c) = parse_color_string(t) {
if color.is_some() || color_is_current_color {
return None;
}
color = Some(c);
continue;
}
if t.to_ascii_lowercase().contains("var(") || var_name_from_string(t).is_some() {
return None;
}
return None;
}
if lengths.len() < 2 || lengths.len() > 3 {
return None;
}
if color.is_some() && color_is_current_color {
return None;
}
let offset_x = filter_shadow_length_to_pt(lengths[0], font_size, root_font_size)?;
let offset_y = filter_shadow_length_to_pt(lengths[1], font_size, root_font_size)?;
let blur_radius = lengths
.get(2)
.copied()
.map(|length| filter_shadow_length_to_pt(length, font_size, root_font_size))
.unwrap_or(Some(Pt::ZERO))?;
if blur_radius < Pt::ZERO {
return None;
}
let (color, opacity, color_is_current_color) = if let Some((color, opacity)) = color {
(color, opacity.clamp(0.0, 1.0), false)
} else {
(Color::BLACK, 1.0, true)
};
Some(FilterDropShadowSpec {
offset_x,
offset_y,
blur_radius,
color,
opacity,
color_is_current_color,
})
}
fn parse_mix_blend_mode_str(raw: &str) -> Option<MixBlendMode> {
let lower = raw.trim().to_ascii_lowercase();
match lower.as_str() {
"normal" => Some(MixBlendMode::Normal),
"multiply" => Some(MixBlendMode::Multiply),
"screen" => Some(MixBlendMode::Screen),
"overlay" => Some(MixBlendMode::Overlay),
"darken" => Some(MixBlendMode::Darken),
"lighten" => Some(MixBlendMode::Lighten),
"color-dodge" => Some(MixBlendMode::ColorDodge),
"color-burn" => Some(MixBlendMode::ColorBurn),
"hard-light" => Some(MixBlendMode::HardLight),
"soft-light" => Some(MixBlendMode::SoftLight),
"difference" => Some(MixBlendMode::Difference),
"exclusion" => Some(MixBlendMode::Exclusion),
"hue" => Some(MixBlendMode::Hue),
"saturation" => Some(MixBlendMode::Saturation),
"color" => Some(MixBlendMode::Color),
"luminosity" => Some(MixBlendMode::Luminosity),
"plus-lighter" => Some(MixBlendMode::PlusLighter),
"plus-darker" => Some(MixBlendMode::PlusDarker),
_ => None,
}
}
fn parse_isolation_spec_str(raw: &str) -> Option<IsolationSpec> {
let lower = raw.trim().to_ascii_lowercase();
match lower.as_str() {
"isolate" => Some(IsolationSpec::Value(true)),
"auto" => Some(IsolationSpec::Value(false)),
"inherit" => Some(IsolationSpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" => Some(IsolationSpec::Initial),
_ => None,
}
}
fn parse_opacity_spec_str(raw: &str) -> Option<OpacitySpec> {
let lower = raw.trim().to_ascii_lowercase();
match lower.as_str() {
"inherit" => Some(OpacitySpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" => Some(OpacitySpec::Initial),
_ => parse_alpha_component(&lower).map(OpacitySpec::Value),
}
}
fn apply_container_shorthand_from_raw(raw: &str, delta: &mut StyleDelta) {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("inherit") {
delta.container_names = Some(ContainerNamesSpec::Inherit);
delta.container_type = Some(ContainerQueryTypeSpec::Inherit);
return;
}
if matches!(
trimmed.to_ascii_lowercase().as_str(),
"initial" | "unset" | "revert" | "revert-layer"
) {
delta.container_names = Some(ContainerNamesSpec::Initial);
delta.container_type = Some(ContainerQueryTypeSpec::Initial);
return;
}
let (name_part, type_part) = trimmed
.split_once('/')
.map(|(name, ty)| (name.trim(), Some(ty.trim())))
.unwrap_or((trimmed, None));
if let Some(names) = parse_container_names_spec_str(name_part) {
delta.container_names = Some(names);
}
delta.container_type = type_part
.and_then(parse_container_query_type_spec_str)
.or(Some(ContainerQueryTypeSpec::Value(
ContainerQueryType::Normal,
)));
}
fn parse_container_names_spec_str(raw: &str) -> Option<ContainerNamesSpec> {
let trimmed = raw.trim();
let lower = trimmed.to_ascii_lowercase();
match lower.as_str() {
"inherit" => Some(ContainerNamesSpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" | "none" => {
Some(ContainerNamesSpec::Initial)
}
_ => {
let names: Vec<String> = trimmed
.split_whitespace()
.filter(|part| !part.is_empty())
.map(|part| part.to_string())
.collect();
if names.is_empty() {
None
} else {
Some(ContainerNamesSpec::Value(names))
}
}
}
}
fn parse_container_query_type_spec_str(raw: &str) -> Option<ContainerQueryTypeSpec> {
let lower = raw.trim().to_ascii_lowercase();
match lower.as_str() {
"normal" => Some(ContainerQueryTypeSpec::Value(ContainerQueryType::Normal)),
"inline-size" => Some(ContainerQueryTypeSpec::Value(
ContainerQueryType::InlineSize,
)),
"size" => Some(ContainerQueryTypeSpec::Value(ContainerQueryType::Size)),
"scroll-state" => Some(ContainerQueryTypeSpec::Value(
ContainerQueryType::ScrollState,
)),
"inherit" => Some(ContainerQueryTypeSpec::Inherit),
"initial" | "unset" | "revert" | "revert-layer" => Some(ContainerQueryTypeSpec::Initial),
_ => None,
}
}
fn parse_background_paint_str(raw: &str) -> Option<BackgroundPaint> {
parse_background_paints_str(raw).into_iter().next()
}
fn parse_background_paints_str(raw: &str) -> Vec<BackgroundPaint> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_layer_paint_str(&layer))
.collect()
}
fn parse_background_layer_paint_str(layer: &str) -> Option<BackgroundPaint> {
if layer.trim().eq_ignore_ascii_case("none") {
return Some(BackgroundPaint::None);
}
parse_background_image_url_str(layer)
.or_else(|| parse_background_image_set_str(layer))
.or_else(|| parse_linear_gradient_str(layer))
.or_else(|| parse_radial_gradient_str(layer))
.or_else(|| parse_conic_gradient_str(layer))
}
fn parse_background_paint_with_style(style: &ComputedStyle, raw: &str) -> Option<BackgroundPaint> {
parse_background_paints_with_style(style, raw)
.into_iter()
.next()
}
fn parse_background_paints_with_style(style: &ComputedStyle, raw: &str) -> Vec<BackgroundPaint> {
split_args(raw)
.into_iter()
.filter_map(|layer| parse_background_layer_paint_with_style(style, &layer))
.collect()
}
fn parse_background_layer_paint_with_style(
style: &ComputedStyle,
layer: &str,
) -> Option<BackgroundPaint> {
{
if layer.trim().eq_ignore_ascii_case("none") {
return Some(BackgroundPaint::None);
}
if let Some(paint) = parse_background_image_url_str(layer) {
return Some(paint);
}
if let Some(paint) = parse_background_image_set_str(layer) {
return Some(paint);
}
if has_dynamic_gradient_expression(layer) {
if let Some(paint) = parse_linear_gradient_str_with_style(style, layer, 0) {
return Some(paint);
}
if let Some(paint) = parse_radial_gradient_str_with_style(style, layer, 0) {
return Some(paint);
}
if let Some(paint) = parse_conic_gradient_str_with_style(style, layer, 0) {
return Some(paint);
}
}
if let Some(paint) = parse_linear_gradient_str(layer) {
return Some(paint);
}
if let Some(paint) = parse_radial_gradient_str(layer) {
return Some(paint);
}
if let Some(paint) = parse_conic_gradient_str(layer) {
return Some(paint);
}
}
None
}
fn parse_background_image_url_str(raw: &str) -> Option<BackgroundPaint> {
let value = raw.trim();
let lower = value.to_ascii_lowercase();
if lower == "none" {
return None;
}
if !lower.starts_with("url(") || !value.ends_with(')') {
return None;
}
let inner = value[4..value.len().saturating_sub(1)].trim();
if inner.is_empty() {
return None;
}
let source = inner
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.or_else(|| inner.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
.unwrap_or(inner)
.trim();
if source.is_empty() {
return None;
}
Some(BackgroundPaint::Image {
source: source.to_string(),
})
}
fn parse_image_set_resolution(raw: &str) -> Option<f32> {
let value = raw.trim().to_ascii_lowercase();
let (number, scale) = if let Some(number) = value.strip_suffix("dppx") {
(number, 1.0)
} else if let Some(number) = value.strip_suffix('x') {
(number, 1.0)
} else if let Some(number) = value.strip_suffix("dpi") {
(number, 1.0 / 96.0)
} else if let Some(number) = value.strip_suffix("dpcm") {
(number, 2.54 / 96.0)
} else {
return None;
};
let density = number.trim().parse::<f32>().ok()? * scale;
(density.is_finite() && density > 0.0).then_some(density)
}
fn parse_background_image_set_str(raw: &str) -> Option<BackgroundPaint> {
let value = raw.trim();
let lower = value.to_ascii_lowercase();
let prefix_len = if lower.starts_with("image-set(") {
"image-set(".len()
} else if lower.starts_with("-webkit-image-set(") {
"-webkit-image-set(".len()
} else {
return None;
};
if !value.ends_with(')') {
return None;
}
let inner = &value[prefix_len..value.len().saturating_sub(1)];
let mut best: Option<(f32, BackgroundPaint)> = None;
for candidate in split_args(inner) {
let tokens = split_ws_preserve_parens(&candidate);
let Some(source_token) = tokens.first() else {
continue;
};
let paint = parse_background_image_url_str(source_token).or_else(|| {
let source = source_token
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| {
source_token
.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
})?;
(!source.is_empty()).then(|| BackgroundPaint::Image {
source: source.to_string(),
})
});
let Some(paint) = paint else {
continue;
};
let density = tokens
.iter()
.skip(1)
.find_map(|token| parse_image_set_resolution(token))
.unwrap_or(1.0);
let distance = (density - 1.0).abs();
if best
.as_ref()
.is_none_or(|(best_distance, _)| distance < *best_distance)
{
best = Some((distance, paint));
}
}
best.map(|(_, paint)| paint)
}
fn has_dynamic_gradient_expression(raw: &str) -> bool {
let lower = raw.to_ascii_lowercase();
lower.contains("var(") || lower.contains("currentcolor") || lower.contains("color-mix(")
}
fn extract_function_args<'a>(raw: &'a str, function: &str) -> Option<&'a str> {
let lower = raw.to_ascii_lowercase();
let start = lower.find(function)?;
let open_idx = start + function.len() - 1;
let mut depth = 0usize;
for (idx, ch) in raw[open_idx..].char_indices() {
let absolute = open_idx + idx;
match ch {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(raw[open_idx + 1..absolute].trim());
}
}
_ => {}
}
}
None
}
fn gradient_stop_position_from_length_spec(
spec: LengthSpec,
font_size: Pt,
root_font_size: Pt,
) -> Option<GradientStopPosition> {
let (fraction, length) = match spec {
LengthSpec::Absolute(value) => (0.0, value),
LengthSpec::Percent(value) => (value, Pt::ZERO),
LengthSpec::Em(value) => (0.0, font_size * value),
LengthSpec::Rem(value) => (0.0, root_font_size * value),
LengthSpec::Calc(calc) => (
calc.percent,
calc.abs + font_size * calc.em + root_font_size * calc.rem,
),
_ => return None,
};
(fraction.is_finite() && length.to_f32().is_finite())
.then_some(GradientStopPosition { fraction, length })
}
fn parse_gradient_stop_position(
raw: &str,
font_size: Pt,
root_font_size: Pt,
) -> Option<GradientStopPosition> {
gradient_stop_position_from_length_spec(
parse_single_length_spec(raw)?,
font_size,
root_font_size,
)
}
fn parse_linear_gradient_color_stop(
part: &str,
) -> Option<Vec<(Color, f32, Option<GradientStopPosition>)>> {
let tokens = split_top_level_whitespace(part);
if tokens.is_empty() || tokens.len() > 3 {
return None;
}
let (color, alpha) = parse_color_string(&tokens[0])?;
let default_font_size = TextStyle::default().font_size;
if tokens.len() == 1 {
return Some(vec![(color, alpha, None)]);
}
tokens
.iter()
.skip(1)
.map(|token| {
parse_gradient_stop_position(token, default_font_size, default_font_size)
.map(|position| (color, alpha, Some(position)))
})
.collect()
}
fn parse_linear_gradient_color_stop_with_style(
style: &ComputedStyle,
part: &str,
depth: usize,
) -> Option<Vec<(Color, f32, Option<GradientStopPosition>)>> {
if depth > 12 {
return None;
}
let tokens = split_top_level_whitespace(part);
if tokens.is_empty() || tokens.len() > 3 {
return None;
}
let color_part = tokens[0].trim();
let (color, alpha) = if color_part.eq_ignore_ascii_case("currentcolor") {
(style.color, 1.0)
} else {
resolve_custom_color_expr_with_alpha_inner(style, color_part, depth + 1)
.or_else(|| parse_color_string(color_part))?
};
if tokens.len() == 1 {
return Some(vec![(color, alpha, None)]);
}
tokens
.iter()
.skip(1)
.map(|token| {
let spec = parse_single_length_spec(token).or_else(|| {
resolve_custom_length_from_maps(
&style.custom_lengths,
&style.custom_color_refs,
token,
)
})?;
gradient_stop_position_from_length_spec(spec, style.font_size, style.root_font_size)
.map(|position| (color, alpha, Some(position)))
})
.collect()
}
fn parse_linear_gradient_str(raw: &str) -> Option<BackgroundPaint> {
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-linear-gradient(");
let inside = extract_function_args(raw.trim(), "linear-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut angle_deg = 180.0;
let mut stop_start = 0usize;
if let Some(angle) = parse_gradient_angle(&parts[0]) {
angle_deg = angle;
stop_start = 1;
}
let mut positioned_stops = Vec::new();
let mut hints = Vec::new();
let mut length_stops = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(parsed) = parse_gradient_length_stops(part) {
length_stops.extend(parsed);
}
if let Some(stops) = parse_linear_gradient_color_stop(part) {
positioned_stops.extend(stops);
} else if let Some(hint) = parse_percentage_unit(part.trim()) {
hints.push(hint);
}
}
let stops = positioned_stops
.iter()
.map(|(color, alpha, position)| {
(*color, *alpha, position.map(|position| position.fraction))
})
.collect::<Vec<_>>();
let geometry_positions = !repeating
&& hints.is_empty()
&& positioned_stops
.iter()
.any(|(_, _, position)| position.is_some_and(|position| position.length != Pt::ZERO));
let (shading_stops, repeat) = if geometry_positions {
let denominator = positioned_stops.len().saturating_sub(1).max(1) as f32;
(
positioned_stops
.iter()
.enumerate()
.map(|(index, (color, alpha, _))| ShadingStop {
offset: index as f32 / denominator,
color: *color,
alpha: *alpha,
})
.collect(),
GradientRepeat::None,
)
} else {
finalize_gradient_stops(&stops, &hints, &length_stops, repeating)
};
if shading_stops.len() < 2 {
return None;
}
let stop_positions = geometry_positions.then(|| {
positioned_stops
.iter()
.map(|(_, _, position)| *position)
.collect()
});
Some(BackgroundPaint::LinearGradient {
angle_deg,
repeat,
stops: shading_stops,
stop_positions,
})
}
fn parse_linear_gradient_str_with_style(
style: &ComputedStyle,
raw: &str,
depth: usize,
) -> Option<BackgroundPaint> {
if depth > 12 {
return None;
}
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-linear-gradient(");
let inside = extract_function_args(raw.trim(), "linear-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut angle_deg = 180.0;
let mut stop_start = 0usize;
if let Some(angle) = parse_gradient_angle(&parts[0]) {
angle_deg = angle;
stop_start = 1;
}
let mut positioned_stops = Vec::new();
let mut hints = Vec::new();
let mut length_stops = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(parsed) = parse_gradient_length_stops(part) {
length_stops.extend(parsed);
}
if let Some(stops) = parse_linear_gradient_color_stop_with_style(style, part, depth + 1) {
positioned_stops.extend(stops);
} else if let Some(hint) = parse_percentage_unit(part.trim()) {
hints.push(hint);
} else {
return None;
}
}
let stops = positioned_stops
.iter()
.map(|(color, alpha, position)| {
(*color, *alpha, position.map(|position| position.fraction))
})
.collect::<Vec<_>>();
let geometry_positions = !repeating
&& hints.is_empty()
&& positioned_stops
.iter()
.any(|(_, _, position)| position.is_some_and(|position| position.length != Pt::ZERO));
let (shading_stops, repeat) = if geometry_positions {
let denominator = positioned_stops.len().saturating_sub(1).max(1) as f32;
(
positioned_stops
.iter()
.enumerate()
.map(|(index, (color, alpha, _))| ShadingStop {
offset: index as f32 / denominator,
color: *color,
alpha: *alpha,
})
.collect(),
GradientRepeat::None,
)
} else {
finalize_gradient_stops(&stops, &hints, &length_stops, repeating)
};
if shading_stops.len() < 2 {
return None;
}
let stop_positions = geometry_positions.then(|| {
positioned_stops
.iter()
.map(|(_, _, position)| *position)
.collect()
});
Some(BackgroundPaint::LinearGradient {
angle_deg,
repeat,
stops: shading_stops,
stop_positions,
})
}
fn parse_radial_gradient_str(raw: &str) -> Option<BackgroundPaint> {
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-radial-gradient(");
let inside = extract_function_args(raw.trim(), "radial-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut shape = RadialGradientShape::Ellipse;
let mut size = RadialGradientSize::FarthestCorner;
let mut center_x = GradientPosition::Percent(0.5);
let mut center_y = GradientPosition::Percent(0.5);
let mut stop_start = 0usize;
if let Some((parsed_shape, parsed_size, cx, cy)) = parse_radial_gradient_preamble(&parts[0]) {
shape = parsed_shape;
size = parsed_size;
center_x = cx;
center_y = cy;
stop_start = 1;
}
let mut positioned_stops = Vec::new();
let mut hints = Vec::new();
let mut length_stops = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(parsed) = parse_gradient_length_stops(part) {
length_stops.extend(parsed);
}
if let Some(stops) = parse_linear_gradient_color_stop(part) {
positioned_stops.extend(stops);
} else if let Some(hint) = parse_percentage_unit(part.trim()) {
hints.push(hint);
}
}
let stops = positioned_stops
.iter()
.map(|(color, alpha, position)| {
(*color, *alpha, position.map(|position| position.fraction))
})
.collect::<Vec<_>>();
let geometry_positions = !repeating
&& hints.is_empty()
&& positioned_stops
.iter()
.any(|(_, _, position)| position.is_some_and(|position| position.length != Pt::ZERO));
let (shading_stops, repeat) = if geometry_positions {
let denominator = positioned_stops.len().saturating_sub(1).max(1) as f32;
(
positioned_stops
.iter()
.enumerate()
.map(|(index, (color, alpha, _))| ShadingStop {
offset: index as f32 / denominator,
color: *color,
alpha: *alpha,
})
.collect(),
GradientRepeat::None,
)
} else {
finalize_gradient_stops(&stops, &hints, &length_stops, repeating)
};
if shading_stops.len() < 2 {
return None;
}
let stop_positions = geometry_positions.then(|| {
positioned_stops
.iter()
.map(|(_, _, position)| *position)
.collect()
});
Some(BackgroundPaint::RadialGradient {
shape,
size,
center_x,
center_y,
repeat,
stops: shading_stops,
stop_positions,
})
}
fn parse_radial_gradient_str_with_style(
style: &ComputedStyle,
raw: &str,
depth: usize,
) -> Option<BackgroundPaint> {
if depth > 12 {
return None;
}
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-radial-gradient(");
let inside = extract_function_args(raw.trim(), "radial-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut shape = RadialGradientShape::Ellipse;
let mut size = RadialGradientSize::FarthestCorner;
let mut center_x = GradientPosition::Percent(0.5);
let mut center_y = GradientPosition::Percent(0.5);
let mut stop_start = 0usize;
if let Some((parsed_shape, parsed_size, cx, cy)) = parse_radial_gradient_preamble(&parts[0]) {
shape = parsed_shape;
size = parsed_size;
center_x = cx;
center_y = cy;
stop_start = 1;
}
let mut positioned_stops = Vec::new();
let mut hints = Vec::new();
let mut length_stops = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(parsed) = parse_gradient_length_stops(part) {
length_stops.extend(parsed);
}
if let Some(stops) = parse_linear_gradient_color_stop_with_style(style, part, depth + 1) {
positioned_stops.extend(stops);
} else if let Some(hint) = parse_percentage_unit(part.trim()) {
hints.push(hint);
} else {
return None;
}
}
let stops = positioned_stops
.iter()
.map(|(color, alpha, position)| {
(*color, *alpha, position.map(|position| position.fraction))
})
.collect::<Vec<_>>();
let geometry_positions = !repeating
&& hints.is_empty()
&& positioned_stops
.iter()
.any(|(_, _, position)| position.is_some_and(|position| position.length != Pt::ZERO));
let (shading_stops, repeat) = if geometry_positions {
let denominator = positioned_stops.len().saturating_sub(1).max(1) as f32;
(
positioned_stops
.iter()
.enumerate()
.map(|(index, (color, alpha, _))| ShadingStop {
offset: index as f32 / denominator,
color: *color,
alpha: *alpha,
})
.collect(),
GradientRepeat::None,
)
} else {
finalize_gradient_stops(&stops, &hints, &length_stops, repeating)
};
if shading_stops.len() < 2 {
return None;
}
let stop_positions = geometry_positions.then(|| {
positioned_stops
.iter()
.map(|(_, _, position)| *position)
.collect()
});
Some(BackgroundPaint::RadialGradient {
shape,
size,
center_x,
center_y,
repeat,
stops: shading_stops,
stop_positions,
})
}
fn parse_conic_gradient_str(raw: &str) -> Option<BackgroundPaint> {
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-conic-gradient(");
let inside = extract_function_args(raw.trim(), "conic-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut start_angle_deg = 0.0f32;
let mut center_x = GradientPosition::Percent(0.5);
let mut center_y = GradientPosition::Percent(0.5);
let mut stop_start = 0usize;
if let Some((angle_deg, cx, cy)) = parse_conic_gradient_preamble(&parts[0]) {
start_angle_deg = angle_deg;
center_x = cx;
center_y = cy;
stop_start = 1;
}
let mut stops: Vec<(Color, f32, Option<f32>)> = Vec::new();
let mut hints = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(stop) = parse_conic_gradient_stop(part) {
stops.push(stop);
if let Some(second_offset) = parse_second_conic_gradient_stop_offset(part) {
stops.push((stop.0, stop.1, Some(second_offset)));
}
} else if let Some(hint) = parse_conic_stop_offset(part.trim()) {
hints.push(hint);
}
}
let (shading_stops, repeat) = finalize_gradient_stops(&stops, &hints, &[], repeating);
if shading_stops.len() < 2 {
return None;
}
Some(BackgroundPaint::ConicGradient {
start_angle_deg,
center_x,
center_y,
repeat,
stops: shading_stops,
})
}
fn parse_conic_gradient_str_with_style(
style: &ComputedStyle,
raw: &str,
depth: usize,
) -> Option<BackgroundPaint> {
if depth > 12 {
return None;
}
let repeating = raw
.trim()
.to_ascii_lowercase()
.starts_with("repeating-conic-gradient(");
let inside = extract_function_args(raw.trim(), "conic-gradient(")?;
let parts = split_args(inside);
if parts.len() < 2 {
return None;
}
let mut start_angle_deg = 0.0f32;
let mut center_x = GradientPosition::Percent(0.5);
let mut center_y = GradientPosition::Percent(0.5);
let mut stop_start = 0usize;
if let Some((angle_deg, cx, cy)) = parse_conic_gradient_preamble(&parts[0]) {
start_angle_deg = angle_deg;
center_x = cx;
center_y = cy;
stop_start = 1;
}
let mut stops: Vec<(Color, f32, Option<f32>)> = Vec::new();
let mut hints = Vec::new();
for part in parts.iter().skip(stop_start) {
if let Some(stop) = parse_conic_gradient_stop_with_style(style, part, depth + 1) {
stops.push(stop);
if let Some(second_offset) = parse_second_conic_gradient_stop_offset(part) {
stops.push((stop.0, stop.1, Some(second_offset)));
}
} else if let Some(hint) = parse_conic_stop_offset(part.trim()) {
hints.push(hint);
} else {
return None;
}
}
let (shading_stops, repeat) = finalize_gradient_stops(&stops, &hints, &[], repeating);
if shading_stops.len() < 2 {
return None;
}
Some(BackgroundPaint::ConicGradient {
start_angle_deg,
center_x,
center_y,
repeat,
stops: shading_stops,
})
}
fn parse_conic_gradient_preamble(part: &str) -> Option<(f32, GradientPosition, GradientPosition)> {
let part_trimmed = part.trim();
let lower = part_trimmed.to_ascii_lowercase();
let has_from = lower.contains("from ");
let has_at = lower.starts_with("at ") || lower.contains(" at ");
if !has_from && !has_at {
return None;
}
let mut start_angle_deg = 0.0f32;
let mut center_x = GradientPosition::Percent(0.5);
let mut center_y = GradientPosition::Percent(0.5);
if has_from {
let from_index = lower.find("from ")?;
let after = part_trimmed[from_index + 5..].trim();
let token = after.split_whitespace().next()?;
start_angle_deg = parse_angle_to_degrees(token)?;
}
if has_at {
let after = if lower.starts_with("at ") {
&part_trimmed[3..]
} else if let Some(idx) = lower.find(" at ") {
&part_trimmed[idx + 4..]
} else {
return None;
};
(center_x, center_y) = parse_gradient_position(after.trim())?;
}
Some((start_angle_deg, center_x, center_y))
}
fn parse_radial_gradient_preamble(
part: &str,
) -> Option<(
RadialGradientShape,
RadialGradientSize,
GradientPosition,
GradientPosition,
)> {
let part_trimmed = part.trim();
if part_trimmed.is_empty() {
return None;
}
let lower = part_trimmed.to_ascii_lowercase();
let has_at = lower.starts_with("at ") || lower.contains(" at ");
let (geometry, position) = if lower.starts_with("at ") {
("", Some(&part_trimmed[3..]))
} else if let Some(index) = lower.find(" at ") {
(&part_trimmed[..index], Some(&part_trimmed[index + 4..]))
} else {
(part_trimmed, None)
};
let geometry_lower = geometry.trim().to_ascii_lowercase();
let mut shape = if geometry_lower.contains("circle") {
RadialGradientShape::Circle
} else {
RadialGradientShape::Ellipse
};
let mut size = if geometry_lower.contains("closest-side") {
RadialGradientSize::ClosestSide
} else if geometry_lower.contains("closest-corner") {
RadialGradientSize::ClosestCorner
} else if geometry_lower.contains("farthest-side") {
RadialGradientSize::FarthestSide
} else {
RadialGradientSize::FarthestCorner
};
let explicit = geometry
.split_ascii_whitespace()
.filter_map(parse_gradient_absolute_length)
.collect::<Vec<_>>();
if explicit.len() >= 2 {
shape = RadialGradientShape::Ellipse;
size = RadialGradientSize::Explicit(explicit[0], Some(explicit[1]));
} else if let Some(value) = explicit.first().copied() {
shape = RadialGradientShape::Circle;
size = RadialGradientSize::Explicit(value, None);
}
let recognized_geometry = geometry_lower.is_empty()
|| geometry_lower.contains("circle")
|| geometry_lower.contains("ellipse")
|| geometry_lower.contains("closest-")
|| geometry_lower.contains("farthest-")
|| !explicit.is_empty();
if !recognized_geometry || (!has_at && geometry_lower.is_empty()) {
return None;
}
let (center_x, center_y) = if let Some(position) = position {
parse_gradient_position(position)?
} else {
(
GradientPosition::Percent(0.5),
GradientPosition::Percent(0.5),
)
};
Some((shape, size, center_x, center_y))
}
fn parse_gradient_absolute_length(raw: &str) -> Option<Pt> {
let lower = raw.trim().to_ascii_lowercase();
if let Some(value) = lower.strip_suffix("px") {
return value
.trim()
.parse::<f32>()
.ok()
.filter(|value| value.is_finite() && *value >= 0.0)
.map(|value| Pt::from_f32(value * 0.75));
}
if let Some(value) = lower.strip_suffix("pt") {
return value
.trim()
.parse::<f32>()
.ok()
.filter(|value| value.is_finite() && *value >= 0.0)
.map(Pt::from_f32);
}
(lower == "0").then_some(Pt::ZERO)
}
fn parse_gradient_position(raw: &str) -> Option<(GradientPosition, GradientPosition)> {
let tokens = split_ws_preserve_parens(raw);
if tokens.is_empty() {
return None;
}
let mut x = GradientPosition::Percent(0.5);
let mut y = GradientPosition::Percent(0.5);
let mut numeric = Vec::new();
let mut index = 0usize;
while index < tokens.len() {
let lower = tokens[index].trim().to_ascii_lowercase();
let edge_with_offset = tokens
.get(index + 1)
.and_then(|next| parse_gradient_absolute_length(next));
match lower.as_str() {
"left" => {
x = edge_with_offset
.map(GradientPosition::Length)
.unwrap_or(GradientPosition::Percent(0.0));
}
"right" => {
x = edge_with_offset
.map(GradientPosition::EndLength)
.unwrap_or(GradientPosition::Percent(1.0));
}
"top" => {
y = edge_with_offset
.map(GradientPosition::Length)
.unwrap_or(GradientPosition::Percent(0.0));
}
"bottom" => {
y = edge_with_offset
.map(GradientPosition::EndLength)
.unwrap_or(GradientPosition::Percent(1.0));
}
"center" => {}
_ if parse_percentage_unit(&lower).is_some() => {
numeric.push(GradientPosition::Percent(parse_percentage_unit(&lower)?));
}
_ if parse_gradient_absolute_length(&lower).is_some() => {
numeric.push(GradientPosition::Length(parse_gradient_absolute_length(
&lower,
)?));
}
_ => return None,
}
if matches!(lower.as_str(), "left" | "right" | "top" | "bottom")
&& edge_with_offset.is_some()
{
index += 1;
}
index += 1;
}
if let Some(first) = numeric.first().copied() {
x = first;
}
if let Some(second) = numeric.get(1).copied() {
y = second;
}
Some((x, y))
}
fn parse_conic_gradient_stop(part: &str) -> Option<(Color, f32, Option<f32>)> {
let part = part.trim();
if part.is_empty() {
return None;
}
let mut split_at = None;
let mut depth = 0usize;
for (idx, ch) in part.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
' ' if depth == 0 => {
split_at = Some(idx);
break;
}
_ => {}
}
}
let (color_part, offset_part) = if let Some(idx) = split_at {
(&part[..idx], part[idx + 1..].trim())
} else {
(part, "")
};
let (color, alpha) = parse_color_string(color_part)?;
let offset_token = offset_part.split_whitespace().next().unwrap_or("").trim();
let offset = if offset_token.is_empty() {
None
} else {
parse_conic_stop_offset(offset_token)
};
Some((color, alpha, offset))
}
fn parse_conic_gradient_stop_with_style(
style: &ComputedStyle,
part: &str,
depth: usize,
) -> Option<(Color, f32, Option<f32>)> {
if depth > 12 {
return None;
}
let part = part.trim();
if part.is_empty() {
return None;
}
let mut split_at = None;
let mut paren_depth = 0usize;
for (idx, ch) in part.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
' ' if paren_depth == 0 => {
split_at = Some(idx);
break;
}
_ => {}
}
}
let (color_part, offset_part) = if let Some(idx) = split_at {
(&part[..idx], part[idx + 1..].trim())
} else {
(part, "")
};
let color_part = color_part.trim();
let (color, alpha) = if color_part.eq_ignore_ascii_case("currentcolor") {
(style.color, 1.0)
} else {
resolve_custom_color_expr_with_alpha_inner(style, color_part, depth + 1)
.or_else(|| parse_color_string(color_part))?
};
let offset_token = split_top_level_whitespace(offset_part)
.into_iter()
.next()
.unwrap_or_default();
let offset = if offset_token.is_empty() {
None
} else {
parse_conic_stop_offset_with_style(style, &offset_token, depth + 1)
};
Some((color, alpha, offset))
}
fn parse_conic_stop_offset(raw: &str) -> Option<f32> {
if let Some(value) = parse_percentage_unit(raw) {
return Some(value.clamp(0.0, 1.0));
}
if let Some(angle_deg) = parse_angle_to_degrees(raw) {
let turn = angle_deg / 360.0;
let wrapped = turn.rem_euclid(1.0);
if wrapped == 0.0 && angle_deg > 0.0 {
return Some(1.0);
}
return Some(wrapped);
}
if let Ok(value) = raw.trim().parse::<f32>() {
if (0.0..=1.0).contains(&value) {
return Some(value);
}
}
None
}
fn parse_second_conic_gradient_stop_offset(part: &str) -> Option<f32> {
split_top_level_whitespace(part)
.get(2)
.and_then(|token| parse_conic_stop_offset(token))
}
fn parse_conic_stop_offset_with_style(
style: &ComputedStyle,
raw: &str,
depth: usize,
) -> Option<f32> {
if depth > 12 {
return None;
}
if let Some(value) = parse_conic_stop_offset(raw) {
return Some(value);
}
if let Some(spec) =
resolve_custom_length_from_maps(&style.custom_lengths, &style.custom_color_refs, raw)
{
let percent = match spec {
LengthSpec::Percent(value) => Some(value),
LengthSpec::Calc(calc)
if calc.abs == Pt::ZERO
&& calc.em.abs() <= f32::EPSILON
&& calc.rem.abs() <= f32::EPSILON =>
{
Some(calc.percent)
}
_ => None,
};
if let Some(value) = percent {
return Some(value.clamp(0.0, 1.0));
}
}
let resolved = resolve_custom_number_expr(style, raw, depth + 1)?;
if (0.0..=1.0).contains(&resolved) {
Some(resolved)
} else {
None
}
}
fn parse_percentage_unit(raw: &str) -> Option<f32> {
let trimmed = raw.trim();
if !trimmed.ends_with('%') {
return None;
}
let value = trimmed.trim_end_matches('%').trim().parse::<f32>().ok()?;
Some(value / 100.0)
}
fn parse_angle_to_degrees(raw: &str) -> Option<f32> {
let lower = raw.trim().to_ascii_lowercase();
if let Some(value) = lower.strip_suffix("deg") {
return value.trim().parse::<f32>().ok();
}
if let Some(value) = lower.strip_suffix("turn") {
return value.trim().parse::<f32>().ok().map(|v| v * 360.0);
}
if let Some(value) = lower.strip_suffix("grad") {
return value.trim().parse::<f32>().ok().map(|v| v * 0.9);
}
if let Some(value) = lower.strip_suffix("rad") {
return value
.trim()
.parse::<f32>()
.ok()
.map(|v| v * (180.0 / std::f32::consts::PI));
}
None
}
fn normalize_gradient_stops(stops: &[(Color, f32, Option<f32>)]) -> Vec<ShadingStop> {
clip_gradient_stops_to_unit(&normalize_gradient_stops_unclipped(stops))
}
fn normalize_gradient_stops_unclipped(stops: &[(Color, f32, Option<f32>)]) -> Vec<ShadingStop> {
if stops.len() < 2 {
return Vec::new();
}
let mut offsets: Vec<Option<f32>> = stops.iter().map(|(_, _, off)| *off).collect();
if offsets.iter().all(|off| off.is_none()) {
let denom = (stops.len() - 1) as f32;
return stops
.iter()
.enumerate()
.map(|(idx, (color, alpha, _))| ShadingStop {
offset: idx as f32 / denom,
color: *color,
alpha: *alpha,
})
.collect();
}
if offsets[0].is_none() {
offsets[0] = Some(0.0);
}
let last_idx = offsets.len() - 1;
if offsets[last_idx].is_none() {
offsets[last_idx] = Some(1.0);
}
let mut idx = 0usize;
while idx < offsets.len() {
if offsets[idx].is_some() {
idx += 1;
continue;
}
let left = idx.saturating_sub(1);
let mut right = idx;
while right < offsets.len() && offsets[right].is_none() {
right += 1;
}
let left_value = offsets[left].unwrap_or(0.0);
let right_value = offsets
.get(right)
.and_then(|v| *v)
.unwrap_or(left_value)
.max(left_value);
let span = (right - left) as f32;
for fill in idx..right {
let t = (fill - left) as f32 / span;
offsets[fill] = Some(left_value + (right_value - left_value) * t);
}
idx = right;
}
let mut previous = f32::NEG_INFINITY;
for off in &mut offsets {
let mut value = off.unwrap_or(previous);
if value < previous {
value = previous;
}
*off = Some(value);
previous = value;
}
let normalized = stops
.iter()
.enumerate()
.map(|(idx, (color, alpha, _))| ShadingStop {
offset: offsets[idx].unwrap_or(0.0),
color: *color,
alpha: *alpha,
})
.collect::<Vec<_>>();
normalized
}
fn clip_gradient_stops_to_unit(stops: &[ShadingStop]) -> Vec<ShadingStop> {
if stops.len() < 2 {
return stops.to_vec();
}
let sample = |position: f32| {
let mut left = stops[0];
for stop in stops.iter().copied().skip(1) {
if stop.offset <= position {
left = stop;
continue;
}
let span = stop.offset - left.offset;
if span <= 1.0e-6 {
return stop;
}
let t = ((position - left.offset) / span).clamp(0.0, 1.0);
return ShadingStop {
offset: position,
color: Color::rgb(
left.color.r + (stop.color.r - left.color.r) * t,
left.color.g + (stop.color.g - left.color.g) * t,
left.color.b + (stop.color.b - left.color.b) * t,
),
alpha: left.alpha + (stop.alpha - left.alpha) * t,
};
}
ShadingStop {
offset: position,
..left
}
};
let mut clipped = Vec::with_capacity(stops.len() + 2);
if !stops.iter().any(|stop| stop.offset == 0.0) {
clipped.push(sample(0.0));
}
clipped.extend(
stops
.iter()
.copied()
.filter(|stop| (0.0..=1.0).contains(&stop.offset)),
);
if !stops.iter().any(|stop| stop.offset == 1.0) {
clipped.push(sample(1.0));
}
clipped
}
fn expand_gradient_color_hints(stops: &[ShadingStop], hints: &[f32]) -> Vec<ShadingStop> {
const HINT_APPROXIMATION_STOPS: usize = 9;
if stops.len() < 2 || hints.is_empty() {
return stops.to_vec();
}
let mut expanded = Vec::with_capacity(stops.len() + hints.len() * HINT_APPROXIMATION_STOPS);
expanded.push(stops[0]);
for window in stops.windows(2) {
let start = window[0];
let end = window[1];
let hint = hints
.iter()
.copied()
.find(|hint| *hint > start.offset && *hint < end.offset);
let Some(hint) = hint else {
expanded.push(end);
continue;
};
let span = end.offset - start.offset;
let midpoint = ((hint - start.offset) / span).clamp(1.0e-5, 1.0 - 1.0e-5);
if (midpoint - 0.5).abs() <= 1.0e-6 {
expanded.push(end);
continue;
}
let exponent = 0.5f32.ln() / midpoint.ln();
// PDF shadings interpolate linearly between stops. Match Chromium's
// focused nine-stop approximation of the CSS exponential hint curve:
// two samples on the short side and seven concentrated around the hint.
let mut positions = [0.0f32; HINT_APPROXIMATION_STOPS];
if midpoint > 0.5 {
for (index, position) in positions.iter_mut().take(7).enumerate() {
*position = midpoint * (7 + index) as f32 / 13.0;
}
positions[7] = midpoint + (1.0 - midpoint) / 3.0;
positions[8] = midpoint + (1.0 - midpoint) * 2.0 / 3.0;
} else {
positions[0] = midpoint / 3.0;
positions[1] = midpoint * 2.0 / 3.0;
for (index, position) in positions.iter_mut().skip(2).enumerate() {
*position = midpoint + (1.0 - midpoint) * index as f32 / 13.0;
}
}
for position_t in positions {
let color_t = position_t.powf(exponent).clamp(0.0, 1.0);
expanded.push(ShadingStop {
offset: start.offset + span * position_t,
color: Color::rgb(
start.color.r + (end.color.r - start.color.r) * color_t,
start.color.g + (end.color.g - start.color.g) * color_t,
start.color.b + (end.color.b - start.color.b) * color_t,
),
alpha: start.alpha + (end.alpha - start.alpha) * color_t,
});
}
expanded.push(end);
}
expanded
}
fn parse_gradient_length_stops(part: &str) -> Option<Vec<(Color, f32, Pt)>> {
let tokens = split_top_level_whitespace(part);
let color_token = tokens.first()?;
let (color, alpha) = parse_color_string(color_token)?;
let positions = tokens
.iter()
.skip(1)
.map(|token| parse_gradient_absolute_length(token))
.collect::<Option<Vec<_>>>()?;
(!positions.is_empty()).then(|| {
positions
.into_iter()
.map(|position| (color, alpha, position))
.collect()
})
}
fn finalize_gradient_stops(
stops: &[(Color, f32, Option<f32>)],
hints: &[f32],
length_stops: &[(Color, f32, Pt)],
repeating: bool,
) -> (Vec<ShadingStop>, GradientRepeat) {
if repeating && length_stops.len() >= 2 {
let start = length_stops[0].2;
let end = length_stops[length_stops.len() - 1].2.max(start);
let period = end - start;
if period > Pt::ZERO {
let mut previous = Pt::ZERO;
let normalized = length_stops
.iter()
.map(|(color, alpha, position)| {
let position = (*position - start).max(previous);
previous = position;
ShadingStop {
offset: position.to_f32() / period.to_f32(),
color: *color,
alpha: *alpha,
}
})
.collect();
return (normalized, GradientRepeat::Length(period));
}
}
let normalized = if repeating {
expand_gradient_color_hints(&normalize_gradient_stops_unclipped(stops), hints)
} else {
expand_gradient_color_hints(&normalize_gradient_stops(stops), hints)
};
let repeat = if repeating && normalized.len() >= 2 {
GradientRepeat::Fraction(
(normalized[normalized.len() - 1].offset - normalized[0].offset).max(0.0),
)
} else {
GradientRepeat::None
};
(normalized, repeat)
}
fn parse_gradient_angle(part: &str) -> Option<f32> {
let part = part.trim().to_ascii_lowercase();
if let Some(angle) = part
.split_ascii_whitespace()
.next()
.and_then(parse_angle_to_degrees)
{
return Some(angle);
}
if part.ends_with("deg") {
let value = part.trim_end_matches("deg").trim();
return value.parse::<f32>().ok();
}
if let Some(rest) = part.strip_prefix("to ") {
let mut dx = 0;
let mut dy = 0;
for dir in rest.split_whitespace() {
match dir {
"left" => dx -= 1,
"right" => dx += 1,
"top" => dy -= 1,
"bottom" => dy += 1,
_ => {}
}
}
if dx == 0 && dy == 0 {
return None;
}
let angle = match (dx, dy) {
(0, -1) => 0.0,
(1, -1) => 45.0,
(1, 0) => 90.0,
(1, 1) => 135.0,
(0, 1) => 180.0,
(-1, 1) => 225.0,
(-1, 0) => 270.0,
(-1, -1) => 315.0,
_ => 180.0,
};
return Some(angle);
}
None
}
fn parse_hex_color_with_alpha(value: &str) -> Option<(Color, f32)> {
let raw = value.trim();
let hex = raw.strip_prefix('#').unwrap_or(raw);
let expanded = match hex.len() {
3 | 4 => {
let mut out = String::with_capacity(hex.len() * 2);
for ch in hex.chars() {
out.push(ch);
out.push(ch);
}
out
}
6 | 8 => hex.to_string(),
_ => return None,
};
let r = u8::from_str_radix(&expanded[0..2], 16).ok()?;
let g = u8::from_str_radix(&expanded[2..4], 16).ok()?;
let b = u8::from_str_radix(&expanded[4..6], 16).ok()?;
let alpha = if expanded.len() == 8 {
u8::from_str_radix(&expanded[6..8], 16).ok()? as f32 / 255.0
} else {
1.0
};
Some((
Color::rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0),
alpha,
))
}
fn parse_named_css_color(name: &str) -> Option<Color> {
let (r, g, b) = match name {
"black" => (0, 0, 0),
"silver" => (192, 192, 192),
"gray" | "grey" => (128, 128, 128),
"white" => (255, 255, 255),
"maroon" => (128, 0, 0),
"red" => (255, 0, 0),
"purple" => (128, 0, 128),
"fuchsia" | "magenta" => (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" | "cyan" => (0, 255, 255),
"aliceblue" => (240, 248, 255),
"antiquewhite" => (250, 235, 215),
"aquamarine" => (127, 255, 212),
"azure" => (240, 255, 255),
"beige" => (245, 245, 220),
"bisque" => (255, 228, 196),
"blanchedalmond" => (255, 235, 205),
"blueviolet" => (138, 43, 226),
"brown" => (165, 42, 42),
"burlywood" => (222, 184, 135),
"cadetblue" => (95, 158, 160),
"chartreuse" => (127, 255, 0),
"chocolate" => (210, 105, 30),
"coral" => (255, 127, 80),
"cornflowerblue" => (100, 149, 237),
"cornsilk" => (255, 248, 220),
"crimson" => (220, 20, 60),
"darkblue" => (0, 0, 139),
"darkcyan" => (0, 139, 139),
"darkgoldenrod" => (184, 134, 11),
"darkgray" | "darkgrey" => (169, 169, 169),
"darkgreen" => (0, 100, 0),
"darkkhaki" => (189, 183, 107),
"darkmagenta" => (139, 0, 139),
"darkolivegreen" => (85, 107, 47),
"darkorange" => (255, 140, 0),
"darkorchid" => (153, 50, 204),
"darkred" => (139, 0, 0),
"darksalmon" => (233, 150, 122),
"darkseagreen" => (143, 188, 143),
"darkslateblue" => (72, 61, 139),
"darkslategray" | "darkslategrey" => (47, 79, 79),
"darkturquoise" => (0, 206, 209),
"darkviolet" => (148, 0, 211),
"deeppink" => (255, 20, 147),
"deepskyblue" => (0, 191, 255),
"dimgray" | "dimgrey" => (105, 105, 105),
"dodgerblue" => (30, 144, 255),
"firebrick" => (178, 34, 34),
"floralwhite" => (255, 250, 240),
"forestgreen" => (34, 139, 34),
"gainsboro" => (220, 220, 220),
"ghostwhite" => (248, 248, 255),
"gold" => (255, 215, 0),
"goldenrod" => (218, 165, 32),
"greenyellow" => (173, 255, 47),
"honeydew" => (240, 255, 240),
"hotpink" => (255, 105, 180),
"indianred" => (205, 92, 92),
"indigo" => (75, 0, 130),
"ivory" => (255, 255, 240),
"khaki" => (240, 230, 140),
"lavender" => (230, 230, 250),
"lavenderblush" => (255, 240, 245),
"lawngreen" => (124, 252, 0),
"lemonchiffon" => (255, 250, 205),
"lightblue" => (173, 216, 230),
"lightcoral" => (240, 128, 128),
"lightcyan" => (224, 255, 255),
"lightgoldenrodyellow" => (250, 250, 210),
"lightgray" | "lightgrey" => (211, 211, 211),
"lightgreen" => (144, 238, 144),
"lightpink" => (255, 182, 193),
"lightsalmon" => (255, 160, 122),
"lightseagreen" => (32, 178, 170),
"lightskyblue" => (135, 206, 250),
"lightslategray" | "lightslategrey" => (119, 136, 153),
"lightsteelblue" => (176, 196, 222),
"lightyellow" => (255, 255, 224),
"limegreen" => (50, 205, 50),
"linen" => (250, 240, 230),
"mediumaquamarine" => (102, 205, 170),
"mediumblue" => (0, 0, 205),
"mediumorchid" => (186, 85, 211),
"mediumpurple" => (147, 112, 219),
"mediumseagreen" => (60, 179, 113),
"mediumslateblue" => (123, 104, 238),
"mediumspringgreen" => (0, 250, 154),
"mediumturquoise" => (72, 209, 204),
"mediumvioletred" => (199, 21, 133),
"midnightblue" => (25, 25, 112),
"mintcream" => (245, 255, 250),
"mistyrose" => (255, 228, 225),
"moccasin" => (255, 228, 181),
"navajowhite" => (255, 222, 173),
"oldlace" => (253, 245, 230),
"olivedrab" => (107, 142, 35),
"orange" => (255, 165, 0),
"orangered" => (255, 69, 0),
"orchid" => (218, 112, 214),
"palegoldenrod" => (238, 232, 170),
"palegreen" => (152, 251, 152),
"paleturquoise" => (175, 238, 238),
"palevioletred" => (219, 112, 147),
"papayawhip" => (255, 239, 213),
"peachpuff" => (255, 218, 185),
"peru" => (205, 133, 63),
"pink" => (255, 192, 203),
"plum" => (221, 160, 221),
"powderblue" => (176, 224, 230),
"rebeccapurple" => (102, 51, 153),
"rosybrown" => (188, 143, 143),
"royalblue" => (65, 105, 225),
"saddlebrown" => (139, 69, 19),
"salmon" => (250, 128, 114),
"sandybrown" => (244, 164, 96),
"seagreen" => (46, 139, 87),
"seashell" => (255, 245, 238),
"sienna" => (160, 82, 45),
"skyblue" => (135, 206, 235),
"slateblue" => (106, 90, 205),
"slategray" | "slategrey" => (112, 128, 144),
"snow" => (255, 250, 250),
"springgreen" => (0, 255, 127),
"steelblue" => (70, 130, 180),
"tan" => (210, 180, 140),
"thistle" => (216, 191, 216),
"tomato" => (255, 99, 71),
"turquoise" => (64, 224, 208),
"violet" => (238, 130, 238),
"wheat" => (245, 222, 179),
"whitesmoke" => (245, 245, 245),
"yellowgreen" => (154, 205, 50),
_ => return None,
};
Some(Color::rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
pub(crate) fn parse_color_string(raw: &str) -> Option<(Color, f32)> {
let s = raw.trim().trim_end_matches(',');
if s.is_empty() {
return None;
}
if let Some(color) = parse_hex_color_with_alpha(s) {
return Some(color);
}
let lower = s.to_ascii_lowercase();
if lower == "transparent" {
return Some((Color::BLACK, 0.0));
}
if let Some(color) = parse_named_css_color(&lower) {
return Some((color, 1.0));
}
if lower.starts_with("rgb(") || lower.starts_with("rgba(") {
if let Some(color) = parse_rgb_color_function(s) {
return Some(color);
}
}
if lower.starts_with("hsl(") || lower.starts_with("hsla(") {
if let Some(color) = parse_hsl_color_function(s) {
return Some(color);
}
}
if lower.starts_with("hwb(") {
if let Some(color) = parse_hwb_color_function(s) {
return Some(color);
}
}
if lower.starts_with("lab(") || lower.starts_with("lch(") {
if let Some(color) = parse_lab_family_color_function(s, false) {
return Some(color);
}
}
if lower.starts_with("oklab(") || lower.starts_with("oklch(") {
if let Some(color) = parse_lab_family_color_function(s, true) {
return Some(color);
}
}
if lower.starts_with("color(") {
if let Some(color) = parse_css_color_function(s) {
return Some(color);
}
}
if lower.starts_with("color-mix(") {
if let Some(color) = parse_static_color_mix(s) {
return Some(color);
}
}
None
}
fn parse_rgb_color_function(raw: &str) -> Option<(Color, f32)> {
let raw = raw.trim();
let lower = raw.to_ascii_lowercase();
let inner = if lower.starts_with("rgba(") && raw.ends_with(')') {
&raw[5..raw.len().saturating_sub(1)]
} else if lower.starts_with("rgb(") && raw.ends_with(')') {
&raw[4..raw.len().saturating_sub(1)]
} else {
return None;
};
let inner = inner.trim();
if inner.to_ascii_lowercase().starts_with("from ") {
return parse_relative_rgb_color(inner);
}
let comma_parts = split_args(inner);
if comma_parts.len() > 1 {
if comma_parts.len() < 3 || comma_parts.len() > 4 {
return None;
}
let r = parse_rgb_absolute_component(&comma_parts[0])?;
let g = parse_rgb_absolute_component(&comma_parts[1])?;
let b = parse_rgb_absolute_component(&comma_parts[2])?;
let a = comma_parts
.get(3)
.map(|part| parse_alpha_component(part))
.unwrap_or(Some(1.0))?;
return Some((Color::rgb(r, g, b), a));
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 3 {
return None;
}
let r = parse_rgb_absolute_component(&components[0])?;
let g = parse_rgb_absolute_component(&components[1])?;
let b = parse_rgb_absolute_component(&components[2])?;
let a = alpha_raw.map(parse_alpha_component).unwrap_or(Some(1.0))?;
Some((Color::rgb(r, g, b), a))
}
fn parse_rgb_absolute_component(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
let value = if let Some(percent) = parse_percentage_unit(raw) {
percent
} else {
raw.parse::<f32>().ok()? / 255.0
};
Some(value.clamp(0.0, 1.0))
}
fn parse_hsl_color_function(raw: &str) -> Option<(Color, f32)> {
let raw = raw.trim();
let lower = raw.to_ascii_lowercase();
let inner = if lower.starts_with("hsla(") && raw.ends_with(')') {
&raw[5..raw.len().saturating_sub(1)]
} else if lower.starts_with("hsl(") && raw.ends_with(')') {
&raw[4..raw.len().saturating_sub(1)]
} else {
return None;
};
let inner = inner.trim();
if inner.to_ascii_lowercase().starts_with("from ") {
return parse_relative_hsl_color(inner);
}
let comma_parts = split_args(inner);
if comma_parts.len() > 1 {
if comma_parts.len() < 3 || comma_parts.len() > 4 {
return None;
}
let hue_deg = parse_hsl_hue_component(&comma_parts[0])?;
let saturation = parse_hsl_percentage_component(&comma_parts[1])?;
let lightness = parse_hsl_percentage_component(&comma_parts[2])?;
let alpha = comma_parts
.get(3)
.map(|part| parse_alpha_component(part))
.unwrap_or(Some(1.0))?;
return Some((hsl_to_rgb(hue_deg, saturation, lightness), alpha));
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 3 {
return None;
}
let hue_deg = parse_hsl_hue_component(&components[0])?;
let saturation = parse_hsl_percentage_component(&components[1])?;
let lightness = parse_hsl_percentage_component(&components[2])?;
let alpha = alpha_raw.map(parse_alpha_component).unwrap_or(Some(1.0))?;
Some((hsl_to_rgb(hue_deg, saturation, lightness), alpha))
}
fn parse_hwb_color_function(raw: &str) -> Option<(Color, f32)> {
let raw = raw.trim();
let lower = raw.to_ascii_lowercase();
if !lower.starts_with("hwb(") || !raw.ends_with(')') {
return None;
}
let inner = raw[4..raw.len().saturating_sub(1)].trim();
if inner.to_ascii_lowercase().starts_with("from ") {
return parse_relative_hwb_color(inner);
}
if split_args(inner).len() != 1 {
return None;
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 3 {
return None;
}
let hue_deg = parse_hsl_hue_component(&components[0])?;
let whiteness = parse_hsl_percentage_component(&components[1])?;
let blackness = parse_hsl_percentage_component(&components[2])?;
let alpha = alpha_raw.map(parse_alpha_component).unwrap_or(Some(1.0))?;
Some((hwb_to_rgb(hue_deg, whiteness, blackness), alpha))
}
fn split_relative_color_origin(inner: &str) -> Option<(&str, &str)> {
let trimmed = inner.trim();
if !trimmed
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("from"))
{
return None;
}
let rest = trimmed.get(4..)?.trim_start();
if rest.is_empty() {
return None;
}
let mut end = 0usize;
if rest.starts_with('#') {
end = rest
.char_indices()
.find_map(|(idx, ch)| (idx > 0 && ch.is_whitespace()).then_some(idx))
.unwrap_or(rest.len());
} else {
for (idx, ch) in rest.char_indices() {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '\\') {
end = idx + ch.len_utf8();
} else {
break;
}
}
if end == 0 {
return None;
}
if rest[end..].starts_with('(') {
let mut depth = 0usize;
let mut quote = None;
let mut escaped = false;
let mut found = None;
for (offset, ch) in rest[end..].char_indices() {
if let Some(quote_char) = quote {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == quote_char {
quote = None;
}
continue;
}
match ch {
'\'' | '"' => quote = Some(ch),
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
found = Some(end + offset + ch.len_utf8());
break;
}
}
_ => {}
}
}
end = found?;
}
}
let origin = rest[..end].trim();
let components = rest[end..].trim();
(!origin.is_empty() && !components.is_empty()).then_some((origin, components))
}
fn split_relative_color_components(raw: &str) -> Option<(Vec<String>, Option<&str>)> {
let (components_raw, alpha_raw) = split_top_level_slash_once(raw)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((raw, None));
let components = split_top_level_whitespace(components_raw);
(components.len() == 3).then_some((components, alpha_raw))
}
fn replace_relative_color_identifiers(raw: &str, values: &[(&str, f32)]) -> String {
let mut out = String::with_capacity(raw.len() + 16);
let mut cursor = 0usize;
while cursor < raw.len() {
let byte = raw.as_bytes()[cursor];
if byte.is_ascii_alphabetic() {
let start = cursor;
cursor += 1;
while cursor < raw.len()
&& (raw.as_bytes()[cursor].is_ascii_alphanumeric()
|| raw.as_bytes()[cursor] == b'-')
{
cursor += 1;
}
let ident = &raw[start..cursor];
if let Some((_, value)) = values
.iter()
.find(|(name, _)| ident.eq_ignore_ascii_case(name))
{
out.push_str(&format!("{value}"));
} else {
out.push_str(ident);
}
} else {
out.push(byte as char);
cursor += 1;
}
}
out
}
fn parse_relative_number_expression(raw: &str, values: &[(&str, f32)]) -> Option<f32> {
let substituted = replace_relative_color_identifiers(raw, values);
match parse_native_length_math(&substituted, true)? {
NativeMathValue::Number(value) if value.is_finite() => Some(value),
_ => None,
}
}
fn parse_relative_rgb_component(raw: &str, origin: Color, alpha: f32) -> Option<f32> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"r" => return Some(origin.r),
"g" => return Some(origin.g),
"b" => return Some(origin.b),
"alpha" => return Some(alpha / 255.0),
"none" => return Some(0.0),
_ => {}
}
if let Some(percent) = parse_percentage_unit(&lowered) {
return Some(percent.clamp(0.0, 1.0));
}
if let Ok(value) = lowered.parse::<f32>() {
return Some((value / 255.0).clamp(0.0, 1.0));
}
parse_relative_number_expression(
&lowered,
&[
("r", origin.r * 255.0),
("g", origin.g * 255.0),
("b", origin.b * 255.0),
("alpha", alpha),
],
)
.map(|value| (value / 255.0).clamp(0.0, 1.0))
}
fn parse_relative_alpha(raw: &str, origin_alpha: f32, values: &[(&str, f32)]) -> Option<f32> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"alpha" => return Some(origin_alpha.clamp(0.0, 1.0)),
"none" => return Some(0.0),
_ => {}
}
if let Some(value) = parse_alpha_component(&lowered) {
return Some(value);
}
parse_relative_number_expression(&lowered, values).map(|value| value.clamp(0.0, 1.0))
}
fn parse_relative_rgb_color(inner: &str) -> Option<(Color, f32)> {
let (origin_raw, remainder) = split_relative_color_origin(inner)?;
let (origin, origin_alpha) = parse_color_string(origin_raw)?;
let (components, alpha_raw) = split_relative_color_components(remainder)?;
let r = parse_relative_rgb_component(&components[0], origin, origin_alpha)?;
let g = parse_relative_rgb_component(&components[1], origin, origin_alpha)?;
let b = parse_relative_rgb_component(&components[2], origin, origin_alpha)?;
let alpha = alpha_raw
.map(|raw| {
parse_relative_alpha(
raw,
origin_alpha,
&[
("r", origin.r * 255.0),
("g", origin.g * 255.0),
("b", origin.b * 255.0),
("alpha", origin_alpha),
],
)
})
.unwrap_or(Some(origin_alpha))?;
Some((Color::rgb(r, g, b), alpha))
}
fn rgb_to_hsl(color: Color) -> (f32, f32, f32) {
let max = color.r.max(color.g).max(color.b);
let min = color.r.min(color.g).min(color.b);
let delta = max - min;
let lightness = (max + min) * 0.5;
if delta <= f32::EPSILON {
return (0.0, 0.0, lightness);
}
let hue = if max == color.r {
60.0 * ((color.g - color.b) / delta).rem_euclid(6.0)
} else if max == color.g {
60.0 * (((color.b - color.r) / delta) + 2.0)
} else {
60.0 * (((color.r - color.g) / delta) + 4.0)
};
let saturation = delta / (1.0 - (2.0 * lightness - 1.0).abs());
(hue.rem_euclid(360.0), saturation, lightness)
}
fn parse_relative_hsl_component(
raw: &str,
component: &str,
hue: f32,
saturation: f32,
lightness: f32,
alpha: f32,
) -> Option<f32> {
let lowered = raw.trim().to_ascii_lowercase();
let direct = match lowered.as_str() {
"h" => Some(hue),
"s" => Some(saturation),
"l" => Some(lightness),
"alpha" if component == "h" => Some(alpha),
"none" => Some(0.0),
_ => None,
};
if let Some(value) = direct {
return Some(value);
}
if component == "h" {
if let Some(value) = parse_hsl_hue_component(&lowered) {
return Some(value);
}
} else if let Some(value) = parse_hsl_percentage_component(&lowered) {
return Some(value);
}
let value = parse_relative_number_expression(
&lowered,
&[
("h", hue),
("s", saturation * 100.0),
("l", lightness * 100.0),
("alpha", alpha),
],
)?;
Some(if component == "h" {
value
} else {
(value / 100.0).clamp(0.0, 1.0)
})
}
fn parse_relative_hsl_color(inner: &str) -> Option<(Color, f32)> {
let (origin_raw, remainder) = split_relative_color_origin(inner)?;
let (origin, origin_alpha) = parse_color_string(origin_raw)?;
let (hue, saturation, lightness) = rgb_to_hsl(origin);
let (components, alpha_raw) = split_relative_color_components(remainder)?;
let h = parse_relative_hsl_component(
&components[0],
"h",
hue,
saturation,
lightness,
origin_alpha,
)?;
let s = parse_relative_hsl_component(
&components[1],
"s",
hue,
saturation,
lightness,
origin_alpha,
)?;
let l = parse_relative_hsl_component(
&components[2],
"l",
hue,
saturation,
lightness,
origin_alpha,
)?;
let alpha = alpha_raw
.map(|raw| {
parse_relative_alpha(
raw,
origin_alpha,
&[
("h", hue),
("s", saturation * 100.0),
("l", lightness * 100.0),
("alpha", origin_alpha),
],
)
})
.unwrap_or(Some(origin_alpha))?;
Some((hsl_to_rgb(h, s, l), alpha))
}
fn rgb_to_hwb(color: Color) -> (f32, f32, f32) {
let (hue, _, _) = rgb_to_hsl(color);
(
hue,
color.r.min(color.g).min(color.b),
1.0 - color.r.max(color.g).max(color.b),
)
}
fn parse_relative_hwb_component(
raw: &str,
keyword: &str,
hue: f32,
whiteness: f32,
blackness: f32,
alpha: f32,
) -> Option<f32> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == keyword {
return Some(if keyword == "w" { whiteness } else { blackness });
}
if lowered == "none" {
return Some(0.0);
}
if let Some(value) = parse_hsl_percentage_component(&lowered) {
return Some(value);
}
parse_relative_number_expression(
&lowered,
&[
("h", hue),
("w", whiteness * 100.0),
("b", blackness * 100.0),
("alpha", alpha),
],
)
.map(|value| (value / 100.0).clamp(0.0, 1.0))
}
fn parse_relative_hwb_color(inner: &str) -> Option<(Color, f32)> {
let (origin_raw, remainder) = split_relative_color_origin(inner)?;
let (origin, origin_alpha) = parse_color_string(origin_raw)?;
let (hue, whiteness, blackness) = rgb_to_hwb(origin);
let (components, alpha_raw) = split_relative_color_components(remainder)?;
let h =
parse_relative_hsl_component(&components[0], "h", hue, whiteness, blackness, origin_alpha)?;
let w =
parse_relative_hwb_component(&components[1], "w", hue, whiteness, blackness, origin_alpha)?;
let b =
parse_relative_hwb_component(&components[2], "b", hue, whiteness, blackness, origin_alpha)?;
let alpha = alpha_raw
.map(|raw| {
parse_relative_alpha(
raw,
origin_alpha,
&[
("h", hue),
("w", whiteness * 100.0),
("b", blackness * 100.0),
("alpha", origin_alpha),
],
)
})
.unwrap_or(Some(origin_alpha))?;
Some((hwb_to_rgb(h, w, b), alpha))
}
fn parse_lab_family_color_function(raw: &str, ok_space: bool) -> Option<(Color, f32)> {
let lower = raw.trim().to_ascii_lowercase();
let (function, cylindrical) = if ok_space {
if lower.starts_with("oklch(") {
("oklch(", true)
} else if lower.starts_with("oklab(") {
("oklab(", false)
} else {
return None;
}
} else if lower.starts_with("lch(") {
("lch(", true)
} else if lower.starts_with("lab(") {
("lab(", false)
} else {
return None;
};
let inner = extract_function_args(raw.trim(), function)?;
if inner.to_ascii_lowercase().starts_with("from ") {
return parse_relative_lab_color(inner, ok_space, cylindrical);
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 3 {
return None;
}
let lightness = parse_lab_lightness(&components[0], ok_space)?;
let alpha = alpha_raw.map(parse_alpha_component).unwrap_or(Some(1.0))?;
let color = if cylindrical {
let chroma = parse_lab_chroma(&components[1], ok_space)?.max(0.0);
let hue = parse_hsl_hue_component(&components[2])?.to_radians();
let a = chroma * hue.cos();
let b = chroma * hue.sin();
if ok_space {
oklab_to_srgb(lightness, a, b)
} else {
lab_to_srgb(lightness, a, b)
}
} else {
let a = parse_lab_axis(&components[1], ok_space)?;
let b = parse_lab_axis(&components[2], ok_space)?;
if ok_space {
oklab_to_srgb(lightness, a, b)
} else {
lab_to_srgb(lightness, a, b)
}
};
Some((color, alpha))
}
fn parse_lab_lightness(raw: &str, ok_space: bool) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
if let Some(percent) = parse_percentage_unit(raw) {
return Some(if ok_space {
percent.clamp(0.0, 1.0)
} else {
(percent * 100.0).clamp(0.0, 100.0)
});
}
let value = raw.parse::<f32>().ok()?;
Some(if ok_space {
value.clamp(0.0, 1.0)
} else {
value.clamp(0.0, 100.0)
})
}
fn parse_lab_axis(raw: &str, ok_space: bool) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
if let Some(percent) = parse_percentage_unit(raw) {
return Some(percent * if ok_space { 0.4 } else { 125.0 });
}
raw.parse::<f32>().ok()
}
fn parse_lab_chroma(raw: &str, ok_space: bool) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
if let Some(percent) = parse_percentage_unit(raw) {
return Some(percent * if ok_space { 0.4 } else { 150.0 });
}
raw.parse::<f32>().ok()
}
fn lab_to_srgb(lightness: f32, a: f32, b: f32) -> Color {
let f1 = (lightness + 16.0) / 116.0;
let f0 = a / 500.0 + f1;
let f2 = f1 - b / 200.0;
let epsilon = 216.0 / 24389.0;
let kappa = 24389.0 / 27.0;
let finv = |value: f32| {
let cube = value * value * value;
if cube > epsilon {
cube
} else {
(116.0 * value - 16.0) / kappa
}
};
let xyz_d50 = [0.96422 * finv(f0), finv(f1), 0.82521 * finv(f2)];
xyz_d65_to_srgb(xyz_d50_to_d65(xyz_d50))
}
fn oklab_to_srgb(lightness: f32, a: f32, b: f32) -> Color {
let l = (lightness + 0.396_337_78 * a + 0.215_803_76 * b).powi(3);
let m = (lightness - 0.105_561_346 * a - 0.063_854_17 * b).powi(3);
let s = (lightness - 0.089_484_18 * a - 1.291_485_5 * b).powi(3);
linear_srgb_to_color([
4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s,
-1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s,
-0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s,
])
}
fn xyz_d50_to_d65([x, y, z]: [f32; 3]) -> [f32; 3] {
[
0.955_473_4 * x - 0.023_098_5 * y + 0.063_259_3 * z,
-0.028_369_7 * x + 1.009_995_5 * y + 0.021_041_4 * z,
0.012_314 * x - 0.020_507_7 * y + 1.330_365_9 * z,
]
}
fn xyz_d65_to_srgb([x, y, z]: [f32; 3]) -> Color {
linear_srgb_to_color([
3.240_97 * x - 1.537_383_2 * y - 0.498_610_76 * z,
-0.969_243_65 * x + 1.875_967_5 * y + 0.041_555_06 * z,
0.055_630_08 * x - 0.203_976_96 * y + 1.056_971_5 * z,
])
}
fn linear_srgb_to_color(rgb: [f32; 3]) -> Color {
let encode = |value: f32| {
let encoded = if value.abs() <= 0.003_130_8 {
12.92 * value
} else {
value.signum() * (1.055 * value.abs().powf(1.0 / 2.4) - 0.055)
};
encoded.clamp(0.0, 1.0)
};
Color::rgb(encode(rgb[0]), encode(rgb[1]), encode(rgb[2]))
}
fn decode_srgb_component(value: f32) -> f32 {
if value.abs() <= 0.04045 {
value / 12.92
} else {
value.signum() * ((value.abs() + 0.055) / 1.055).powf(2.4)
}
}
fn parse_css_color_function(raw: &str) -> Option<(Color, f32)> {
let inner = extract_function_args(raw.trim(), "color(")?;
if inner.to_ascii_lowercase().starts_with("from ") {
return parse_relative_css_color(inner);
}
let (components_raw, alpha_raw) = split_top_level_slash_once(inner)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((inner, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 4 {
return None;
}
let color_space = components[0].to_ascii_lowercase();
let coords = [
parse_css_color_space_component(&components[1])?,
parse_css_color_space_component(&components[2])?,
parse_css_color_space_component(&components[3])?,
];
let alpha = alpha_raw.map(parse_alpha_component).unwrap_or(Some(1.0))?;
let color = color_space_coordinates_to_srgb(&color_space, coords)?;
Some((color, alpha))
}
fn parse_css_color_space_component(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
if let Some(percent) = parse_percentage_unit(raw) {
return Some(percent);
}
raw.parse::<f32>().ok().filter(|value| value.is_finite())
}
fn color_space_coordinates_to_srgb(space: &str, [r, g, b]: [f32; 3]) -> Option<Color> {
let color = match space {
"srgb" => Color::rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)),
"srgb-linear" => linear_srgb_to_color([r, g, b]),
"display-p3" => {
let r = decode_srgb_component(r);
let g = decode_srgb_component(g);
let b = decode_srgb_component(b);
xyz_d65_to_srgb([
0.486_570_95 * r + 0.265_667_7 * g + 0.198_217_29 * b,
0.228_974_57 * r + 0.691_738_55 * g + 0.079_286_91 * b,
0.0 * r + 0.045_113_38 * g + 1.043_944_4 * b,
])
}
"a98-rgb" => {
let decode = |value: f32| value.signum() * value.abs().powf(563.0 / 256.0);
let r = decode(r);
let g = decode(g);
let b = decode(b);
xyz_d65_to_srgb([
0.576_730_9 * r + 0.185_554 * g + 0.188_185_2 * b,
0.297_376_9 * r + 0.627_349_1 * g + 0.075_274_1 * b,
0.027_034_3 * r + 0.070_687_2 * g + 0.991_108_5 * b,
])
}
"prophoto-rgb" => {
let decode = |value: f32| {
if value.abs() <= 16.0 / 512.0 {
value / 16.0
} else {
value.signum() * value.abs().powf(1.8)
}
};
let r = decode(r);
let g = decode(g);
let b = decode(b);
xyz_d65_to_srgb(xyz_d50_to_d65([
0.797_766_6 * r + 0.135_181_3 * g + 0.031_347_7 * b,
0.288_074_8 * r + 0.711_835_2 * g,
0.825_104_6 * b,
]))
}
"rec2020" => {
let decode = |value: f32| {
if value.abs() < 0.08145 {
value / 4.5
} else {
value.signum() * ((value.abs() + 0.0993) / 1.0993).powf(1.0 / 0.45)
}
};
let r = decode(r);
let g = decode(g);
let b = decode(b);
xyz_d65_to_srgb([
0.636_958_06 * r + 0.144_616_9 * g + 0.168_880_98 * b,
0.262_700_2 * r + 0.677_998_07 * g + 0.059_301_715 * b,
0.028_072_693 * g + 1.060_985_1 * b,
])
}
"xyz" | "xyz-d65" => xyz_d65_to_srgb([r, g, b]),
"xyz-d50" => xyz_d65_to_srgb(xyz_d50_to_d65([r, g, b])),
_ => return None,
};
Some(color)
}
fn color_to_linear_srgb(color: Color) -> [f32; 3] {
[
decode_srgb_component(color.r),
decode_srgb_component(color.g),
decode_srgb_component(color.b),
]
}
fn srgb_to_xyz_d65(color: Color) -> [f32; 3] {
let [r, g, b] = color_to_linear_srgb(color);
[
0.412_390_8 * r + 0.357_584_33 * g + 0.180_480_8 * b,
0.212_639 * r + 0.715_168_65 * g + 0.072_192_32 * b,
0.019_330_82 * r + 0.119_194_78 * g + 0.950_532_14 * b,
]
}
fn xyz_d65_to_d50([x, y, z]: [f32; 3]) -> [f32; 3] {
[
1.047_929_8 * x + 0.022_946_8 * y - 0.050_192_2 * z,
0.029_627_8 * x + 0.990_434_5 * y - 0.017_073_8 * z,
-0.009_243 * x + 0.015_055_2 * y + 0.751_874_3 * z,
]
}
fn srgb_to_lab(color: Color) -> [f32; 3] {
let [x, y, z] = xyz_d65_to_d50(srgb_to_xyz_d65(color));
let epsilon = 216.0 / 24389.0;
let kappa = 24389.0 / 27.0;
let f = |value: f32| {
if value > epsilon {
value.cbrt()
} else {
(kappa * value + 16.0) / 116.0
}
};
let fx = f(x / 0.96422);
let fy = f(y);
let fz = f(z / 0.82521);
[116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)]
}
fn srgb_to_oklab(color: Color) -> [f32; 3] {
let [r, g, b] = color_to_linear_srgb(color);
let l = (0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt();
let m = (0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b).cbrt();
let s = (0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b).cbrt();
[
0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s,
1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s,
0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s,
]
}
fn cartesian_to_cylindrical([lightness, a, b]: [f32; 3]) -> [f32; 3] {
[
lightness,
a.hypot(b),
b.atan2(a).to_degrees().rem_euclid(360.0),
]
}
fn parse_relative_lab_component(
raw: &str,
own_keyword: &str,
own_value: f32,
percent_scale: f32,
hue: bool,
values: &[(&str, f32)],
) -> Option<f32> {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == own_keyword {
return Some(own_value);
}
if lowered == "none" {
return Some(0.0);
}
if hue {
if let Some(value) = parse_hsl_hue_component(&lowered) {
return Some(value);
}
} else if let Some(value) = parse_percentage_unit(&lowered) {
return Some(value * percent_scale);
} else if let Ok(value) = lowered.parse::<f32>() {
return Some(value);
}
parse_relative_number_expression(&lowered, values)
}
fn parse_relative_lab_color(
inner: &str,
ok_space: bool,
cylindrical: bool,
) -> Option<(Color, f32)> {
let (origin_raw, remainder) = split_relative_color_origin(inner)?;
let (origin, origin_alpha) = parse_color_string(origin_raw)?;
let cartesian = if ok_space {
srgb_to_oklab(origin)
} else {
srgb_to_lab(origin)
};
let original = if cylindrical {
cartesian_to_cylindrical(cartesian)
} else {
cartesian
};
let (components, alpha_raw) = split_relative_color_components(remainder)?;
let lightness_scale = if ok_space { 1.0 } else { 100.0 };
let second_scale = if cylindrical {
if ok_space { 0.4 } else { 150.0 }
} else if ok_space {
0.4
} else {
125.0
};
let names = if cylindrical {
["l", "c", "h"]
} else {
["l", "a", "b"]
};
let values = [
(names[0], original[0]),
(names[1], original[1]),
(names[2], original[2]),
("alpha", origin_alpha),
];
let first = parse_relative_lab_component(
&components[0],
names[0],
original[0],
lightness_scale,
false,
&values,
)?;
let second = parse_relative_lab_component(
&components[1],
names[1],
original[1],
second_scale,
false,
&values,
)?;
let third = parse_relative_lab_component(
&components[2],
names[2],
original[2],
second_scale,
cylindrical,
&values,
)?;
let alpha = alpha_raw
.map(|raw| parse_relative_alpha(raw, origin_alpha, &values))
.unwrap_or(Some(origin_alpha))?;
let color = if cylindrical {
let hue = third.to_radians();
let a = second.max(0.0) * hue.cos();
let b = second.max(0.0) * hue.sin();
if ok_space {
oklab_to_srgb(first.clamp(0.0, 1.0), a, b)
} else {
lab_to_srgb(first.clamp(0.0, 100.0), a, b)
}
} else if ok_space {
oklab_to_srgb(first.clamp(0.0, 1.0), second, third)
} else {
lab_to_srgb(first.clamp(0.0, 100.0), second, third)
};
Some((color, alpha))
}
fn encode_srgb_component(value: f32) -> f32 {
if value.abs() <= 0.003_130_8 {
12.92 * value
} else {
value.signum() * (1.055 * value.abs().powf(1.0 / 2.4) - 0.055)
}
}
fn srgb_to_color_space_coordinates(space: &str, color: Color) -> Option<[f32; 3]> {
let xyz_d65 = srgb_to_xyz_d65(color);
let [x, y, z] = xyz_d65;
let coordinates = match space {
"srgb" => [color.r, color.g, color.b],
"srgb-linear" => color_to_linear_srgb(color),
"display-p3" => {
let linear = [
2.493_497 * x - 0.931_383_6 * y - 0.402_710_8 * z,
-0.829_489 * x + 1.762_664_1 * y + 0.023_624_686 * z,
0.035_845_83 * x - 0.076_172_39 * y + 0.956_884_5 * z,
];
linear.map(encode_srgb_component)
}
"a98-rgb" => {
let linear = [
2.041_369 * x - 0.564_946_4 * y - 0.344_694_4 * z,
-0.969_266 * x + 1.876_010_8 * y + 0.041_556 * z,
0.013_447_4 * x - 0.118_389_7 * y + 1.015_409_6 * z,
];
linear.map(|value| value.signum() * value.abs().powf(256.0 / 563.0))
}
"prophoto-rgb" => {
let [x, y, z] = xyz_d65_to_d50(xyz_d65);
let linear = [
1.345_943_3 * x - 0.255_607_5 * y - 0.051_111_8 * z,
-0.544_598_9 * x + 1.508_167_3 * y + 0.020_535_1 * z,
1.211_812_8 * z,
];
linear.map(|value| {
if value.abs() <= 1.0 / 512.0 {
value * 16.0
} else {
value.signum() * value.abs().powf(1.0 / 1.8)
}
})
}
"rec2020" => {
let linear = [
1.716_651_2 * x - 0.355_670_78 * y - 0.253_366_3 * z,
-0.666_684_3 * x + 1.616_481_2 * y + 0.015_768_546 * z,
0.017_639_857 * x - 0.042_770_613 * y + 0.942_103_15 * z,
];
linear.map(|value| {
if value.abs() < 0.0181 {
value * 4.5
} else {
value.signum() * (1.0993 * value.abs().powf(0.45) - 0.0993)
}
})
}
"xyz" | "xyz-d65" => xyz_d65,
"xyz-d50" => xyz_d65_to_d50(xyz_d65),
_ => return None,
};
Some(coordinates)
}
fn parse_relative_css_color(inner: &str) -> Option<(Color, f32)> {
let (origin_raw, remainder) = split_relative_color_origin(inner)?;
let (origin, origin_alpha) = parse_color_string(origin_raw)?;
let (components_raw, alpha_raw) = split_top_level_slash_once(remainder)
.map(|(before, after)| (before, Some(after)))
.unwrap_or((remainder, None));
let components = split_top_level_whitespace(components_raw);
if components.len() != 4 {
return None;
}
let space = components[0].to_ascii_lowercase();
let origin_coords = srgb_to_color_space_coordinates(&space, origin)?;
let names = if space.starts_with("xyz") || space == "xyz" {
["x", "y", "z"]
} else {
["r", "g", "b"]
};
let values = [
(names[0], origin_coords[0]),
(names[1], origin_coords[1]),
(names[2], origin_coords[2]),
("alpha", origin_alpha),
];
let mut coords = [0.0; 3];
for index in 0..3 {
let raw = components[index + 1].trim().to_ascii_lowercase();
coords[index] = if raw == names[index] {
origin_coords[index]
} else if raw == "none" {
0.0
} else if let Some(value) = parse_percentage_unit(&raw) {
value
} else if let Ok(value) = raw.parse::<f32>() {
value
} else {
parse_relative_number_expression(&raw, &values)?
};
}
let alpha = alpha_raw
.map(|raw| parse_relative_alpha(raw, origin_alpha, &values))
.unwrap_or(Some(origin_alpha))?;
Some((color_space_coordinates_to_srgb(&space, coords)?, alpha))
}
#[derive(Debug, Clone, Copy)]
enum HueInterpolationMethod {
Shorter,
Longer,
Increasing,
Decreasing,
}
fn parse_color_interpolation_space(raw: &str) -> Option<(String, HueInterpolationMethod)> {
let tokens = raw.split_ascii_whitespace().collect::<Vec<_>>();
if tokens.len() < 2 || !tokens[0].eq_ignore_ascii_case("in") {
return None;
}
let space = tokens[1].to_ascii_lowercase();
let hue = if tokens.len() == 2 {
HueInterpolationMethod::Shorter
} else if tokens.len() == 4 && tokens[3].eq_ignore_ascii_case("hue") {
match tokens[2].to_ascii_lowercase().as_str() {
"shorter" => HueInterpolationMethod::Shorter,
"longer" => HueInterpolationMethod::Longer,
"increasing" => HueInterpolationMethod::Increasing,
"decreasing" => HueInterpolationMethod::Decreasing,
_ => return None,
}
} else {
return None;
};
Some((space, hue))
}
fn color_to_interpolation_coordinates(space: &str, color: Color) -> Option<([f32; 3], bool)> {
let result = match space {
"lab" => (srgb_to_lab(color), false),
"lch" => (cartesian_to_cylindrical(srgb_to_lab(color)), true),
"oklab" => (srgb_to_oklab(color), false),
"oklch" => (cartesian_to_cylindrical(srgb_to_oklab(color)), true),
"hsl" => {
let (h, s, l) = rgb_to_hsl(color);
([s, l, h], true)
}
"hwb" => {
let (h, w, b) = rgb_to_hwb(color);
([w, b, h], true)
}
_ => (srgb_to_color_space_coordinates(space, color)?, false),
};
Some(result)
}
fn interpolation_coordinates_to_color(space: &str, coordinates: [f32; 3]) -> Option<Color> {
let [first, second, third] = coordinates;
let color = match space {
"lab" => lab_to_srgb(first.clamp(0.0, 100.0), second, third),
"lch" => {
let hue = third.to_radians();
lab_to_srgb(
first.clamp(0.0, 100.0),
second.max(0.0) * hue.cos(),
second.max(0.0) * hue.sin(),
)
}
"oklab" => oklab_to_srgb(first.clamp(0.0, 1.0), second, third),
"oklch" => {
let hue = third.to_radians();
oklab_to_srgb(
first.clamp(0.0, 1.0),
second.max(0.0) * hue.cos(),
second.max(0.0) * hue.sin(),
)
}
"hsl" => hsl_to_rgb(third, first.clamp(0.0, 1.0), second.clamp(0.0, 1.0)),
"hwb" => hwb_to_rgb(third, first.clamp(0.0, 1.0), second.clamp(0.0, 1.0)),
_ => return color_space_coordinates_to_srgb(space, coordinates),
};
Some(color)
}
fn interpolate_hue(left: f32, right: f32, factor: f32, method: HueInterpolationMethod) -> f32 {
let mut delta = (right - left).rem_euclid(360.0);
match method {
HueInterpolationMethod::Shorter => {
if delta > 180.0 {
delta -= 360.0;
}
}
HueInterpolationMethod::Longer => {
if delta > 0.0 && delta < 180.0 {
delta -= 360.0;
}
}
HueInterpolationMethod::Increasing => {}
HueInterpolationMethod::Decreasing => {
if delta > 0.0 {
delta -= 360.0;
}
}
}
(left + delta * factor).rem_euclid(360.0)
}
fn normalized_color_mix_weights(left: Option<f32>, right: Option<f32>) -> Option<(f32, f32, f32)> {
let (mut left, mut right, alpha_multiplier) = match (left, right) {
(Some(left), Some(right)) => {
let left = left.clamp(0.0, 1.0);
let right = right.clamp(0.0, 1.0);
(left, right, (left + right).clamp(0.0, 1.0))
}
(Some(left), None) => {
let left = left.clamp(0.0, 1.0);
(left, 1.0 - left, 1.0)
}
(None, Some(right)) => {
let right = right.clamp(0.0, 1.0);
(1.0 - right, right, 1.0)
}
(None, None) => (0.5, 0.5, 1.0),
};
let total = left + right;
if total <= f32::EPSILON {
return None;
}
left /= total;
right /= total;
Some((left, right, alpha_multiplier))
}
fn mix_colors_in_space(
space: &str,
hue_method: HueInterpolationMethod,
left: (Color, f32, Option<f32>),
right: (Color, f32, Option<f32>),
) -> Option<(Color, f32)> {
let (left_weight, right_weight, alpha_multiplier) =
normalized_color_mix_weights(left.2, right.2)?;
let mixed_alpha = (left.1 * left_weight + right.1 * right_weight).clamp(0.0, 1.0);
let output_alpha = (mixed_alpha * alpha_multiplier).clamp(0.0, 1.0);
let (left_coords, left_polar) = color_to_interpolation_coordinates(space, left.0)?;
let (right_coords, right_polar) = color_to_interpolation_coordinates(space, right.0)?;
if left_polar != right_polar {
return None;
}
if mixed_alpha <= f32::EPSILON {
return Some((Color::BLACK, output_alpha));
}
let left_factor = left.1 * left_weight;
let right_factor = right.1 * right_weight;
let mut coords = [0.0; 3];
for index in 0..if left_polar { 2 } else { 3 } {
coords[index] =
(left_coords[index] * left_factor + right_coords[index] * right_factor) / mixed_alpha;
}
if left_polar {
coords[2] = interpolate_hue(
left_coords[2],
right_coords[2],
right_factor / mixed_alpha,
hue_method,
);
}
Some((
interpolation_coordinates_to_color(space, coords)?,
output_alpha,
))
}
fn parse_static_color_mix(raw: &str) -> Option<(Color, f32)> {
let inner = extract_function_args(raw.trim(), "color-mix(")?;
let args = split_args(inner);
if args.len() != 3 {
return None;
}
let (space, hue_method) = parse_color_interpolation_space(&args[0])?;
let (left_raw, left_weight) = split_color_mix_stop(&args[1])?;
let (right_raw, right_weight) = split_color_mix_stop(&args[2])?;
let (left_color, left_alpha) = parse_color_string(&left_raw)?;
let (right_color, right_alpha) = parse_color_string(&right_raw)?;
mix_colors_in_space(
&space,
hue_method,
(left_color, left_alpha, left_weight),
(right_color, right_alpha, right_weight),
)
}
fn parse_hsl_hue_component(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
if let Some(value) = parse_angle_to_degrees(raw) {
return Some(value);
}
raw.parse::<f32>().ok()
}
fn parse_hsl_percentage_component(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(0.0);
}
let value = if let Some(percent) = parse_percentage_unit(raw) {
percent
} else {
raw.parse::<f32>().ok()? / 100.0
};
Some(value.clamp(0.0, 1.0))
}
fn hsl_to_rgb(hue_deg: f32, saturation: f32, lightness: f32) -> Color {
let h = (hue_deg / 360.0).rem_euclid(1.0);
let s = saturation.clamp(0.0, 1.0);
let l = lightness.clamp(0.0, 1.0);
if s <= f32::EPSILON {
return Color::rgb(l, l, l);
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - (l * s)
};
let p = (2.0 * l) - q;
Color::rgb(
hue_to_rgb_channel(p, q, h + (1.0 / 3.0)),
hue_to_rgb_channel(p, q, h),
hue_to_rgb_channel(p, q, h - (1.0 / 3.0)),
)
}
fn hue_to_rgb_channel(p: f32, q: f32, mut t: f32) -> f32 {
t = t.rem_euclid(1.0);
if t < (1.0 / 6.0) {
p + (q - p) * 6.0 * t
} else if t < 0.5 {
q
} else if t < (2.0 / 3.0) {
p + (q - p) * ((2.0 / 3.0) - t) * 6.0
} else {
p
}
}
fn hwb_to_rgb(hue_deg: f32, whiteness: f32, blackness: f32) -> Color {
let w = whiteness.clamp(0.0, 1.0);
let b = blackness.clamp(0.0, 1.0);
let sum = w + b;
if sum >= 1.0 {
let gray = if sum > 0.0 { w / sum } else { 0.0 };
return Color::rgb(gray, gray, gray);
}
let base = hsl_to_rgb(hue_deg, 1.0, 0.5);
let scale = 1.0 - w - b;
Color::rgb(base.r * scale + w, base.g * scale + w, base.b * scale + w)
}
fn parse_alpha_component(raw: &str) -> Option<f32> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("none") {
return Some(1.0);
}
let value = if let Some(percent) = parse_percentage_unit(raw) {
percent
} else {
raw.parse::<f32>().ok()?
};
Some(value.clamp(0.0, 1.0))
}
fn split_top_level_slash_once(raw: &str) -> Option<(&str, &str)> {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
for (idx, ch) in raw.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
'/' if paren_depth == 0 && bracket_depth == 0 => {
let before = raw[..idx].trim();
let after = raw[idx + ch.len_utf8()..].trim();
if before.is_empty() || after.is_empty() {
return None;
}
return Some((before, after));
}
_ => {}
}
}
None
}
fn split_args(raw: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
let mut quote: Option<char> = None;
let mut escaped = false;
for ch in raw.chars() {
if let Some(quote_char) = quote {
current.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == quote_char {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => {
quote = Some(ch);
current.push(ch);
}
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
',' if depth == 0 => {
if !current.trim().is_empty() {
out.push(current.trim().to_string());
}
current.clear();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
out.push(current.trim().to_string());
}
out
}
fn split_ws_preserve_parens(raw: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
for ch in raw.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
c if c.is_whitespace() && depth == 0 => {
if !current.trim().is_empty() {
out.push(current.trim().to_string());
}
current.clear();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
out.push(current.trim().to_string());
}
out
}
fn px_to_pt(px: f32) -> Pt {
Pt::from_f32(px * 0.75)
}
fn set_delta_direction(delta: &mut StyleDelta, spec: DirectionSpec) {
delta.direction = Some(spec);
delta.revert_layer.direction = false;
}
fn set_delta_unicode_bidi(delta: &mut StyleDelta, spec: UnicodeBidiSpec) {
delta.unicode_bidi = Some(spec);
delta.revert_layer.unicode_bidi = false;
}
fn set_delta_writing_mode(delta: &mut StyleDelta, spec: WritingModeSpec) {
delta.writing_mode = Some(spec);
delta.revert_layer.writing_mode = false;
}
fn set_delta_text_orientation(delta: &mut StyleDelta, spec: TextOrientationSpec) {
delta.text_orientation = Some(spec);
delta.revert_layer.text_orientation = false;
}
fn writing_mode_from_ident(value: &str) -> Option<WritingModeMode> {
match value.trim().to_ascii_lowercase().as_str() {
"horizontal-tb" | "lr" | "lr-tb" | "rl" => Some(WritingModeMode::HorizontalTb),
"vertical-rl" | "tb-rl" => Some(WritingModeMode::VerticalRl),
"vertical-lr" | "tb" | "tb-lr" => Some(WritingModeMode::VerticalLr),
"sideways-rl" => Some(WritingModeMode::SidewaysRl),
"sideways-lr" => Some(WritingModeMode::SidewaysLr),
_ => None,
}
}
fn apply_writing_mode_from_str(raw: &str, delta: &mut StyleDelta) {
match raw.trim().to_ascii_lowercase().as_str() {
"inherit" => set_delta_writing_mode(delta, WritingModeSpec::Inherit),
"initial" | "unset" | "revert" => set_delta_writing_mode(delta, WritingModeSpec::Initial),
"revert-layer" => {
set_revert_layer_writing_mode(delta);
}
value => {
if let Some(mode) = writing_mode_from_ident(value) {
set_delta_writing_mode(delta, WritingModeSpec::Value(mode));
}
}
}
}
fn apply_text_orientation_from_str(raw: &str, delta: &mut StyleDelta) -> bool {
let spec = match raw.trim().to_ascii_lowercase().as_str() {
"mixed" => TextOrientationSpec::Value(TextOrientationMode::Mixed),
"upright" => TextOrientationSpec::Value(TextOrientationMode::Upright),
"sideways" | "sideways-right" => TextOrientationSpec::Value(TextOrientationMode::Sideways),
"inherit" | "unset" => TextOrientationSpec::Inherit,
"initial" | "revert" => TextOrientationSpec::Initial,
"revert-layer" => return set_revert_layer_text_orientation(delta),
_ => return false,
};
set_delta_text_orientation(delta, spec);
true
}
fn is_vertical_writing_mode(mode: WritingModeMode) -> bool {
!matches!(mode, WritingModeMode::HorizontalTb)
}
fn set_delta_list_style_type(
delta: &mut StyleDelta,
value: (
ListStyleTypeMode,
Option<String>,
Option<AnonymousListStyleSymbols>,
),
) {
delta.list_style_type = Some(value);
delta.revert_layer.list_style_type = false;
}
fn is_supported_gradient_image_expression(raw: &str) -> bool {
let lower = raw.to_ascii_lowercase();
lower.contains("linear-gradient(")
|| lower.contains("radial-gradient(")
|| lower.contains("conic-gradient(")
}
fn set_delta_list_style_image(delta: &mut StyleDelta, spec: ListStyleImageSpec) {
delta.list_style_image = Some(spec);
delta.revert_layer.list_style_image = false;
}
fn resolve_named_counter_style(
computed: &mut ComputedStyle,
counter_styles: &HashMap<String, AnonymousListStyleSymbols>,
) {
if matches!(
computed.list_style_type,
ListStyleTypeMode::CustomCounterStyleName
) {
if let Some(symbols) = computed
.list_style_marker
.as_deref()
.and_then(|name| counter_styles.get(name))
{
computed.list_style_symbols = Some(symbols.clone());
}
}
let resolve_generated = |style: &mut GeneratedCounterStyle| {
if !matches!(
style.list_style_type,
ListStyleTypeMode::CustomCounterStyleName
) {
return;
}
if let Some(symbols) = style
.marker
.as_deref()
.and_then(|name| counter_styles.get(name))
{
style.symbols = Some(symbols.clone());
}
};
if let Some(parts) = computed.generated_content.as_mut() {
for part in parts {
match part {
GeneratedContentPart::Counter(counter) => resolve_generated(&mut counter.style),
GeneratedContentPart::Counters(counter) => resolve_generated(&mut counter.style),
GeneratedContentPart::TargetCounter(counter) => {
resolve_generated(&mut counter.style)
}
GeneratedContentPart::Text(_)
| GeneratedContentPart::Image(_)
| GeneratedContentPart::Leader(_)
| GeneratedContentPart::TargetText(_)
| GeneratedContentPart::OpenQuote
| GeneratedContentPart::CloseQuote
| GeneratedContentPart::NoOpenQuote
| GeneratedContentPart::NoCloseQuote => {}
}
}
}
}
fn set_delta_list_style_position(delta: &mut StyleDelta, spec: ListStylePositionSpec) {
delta.list_style_position = Some(spec);
delta.revert_layer.list_style_position = false;
}
fn parse_vertical_align_mode_str(raw: &str) -> Option<VerticalAlignMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"baseline" | "initial" | "unset" | "revert" => Some(VerticalAlignMode::Baseline),
"inherit" => None,
"sub" => Some(VerticalAlignMode::Sub),
"super" => Some(VerticalAlignMode::Super),
"text-top" => Some(VerticalAlignMode::TextTop),
"top" => Some(VerticalAlignMode::Top),
"middle" => Some(VerticalAlignMode::Middle),
"text-bottom" => Some(VerticalAlignMode::TextBottom),
"bottom" => Some(VerticalAlignMode::Bottom),
value => parse_single_length_spec(value).map(VerticalAlignMode::Length),
}
}
fn set_delta_vertical_align(delta: &mut StyleDelta, mode: VerticalAlignMode) {
delta.vertical_align = Some(mode);
delta.vertical_align_var = None;
delta.revert_layer.vertical_align = false;
}
fn set_delta_vertical_align_var(delta: &mut StyleDelta, var: String) {
delta.vertical_align = None;
delta.vertical_align_var = Some(var);
delta.revert_layer.vertical_align = false;
}
fn border_line_paint_style(value: BorderLineStyle) -> OutlineLineStyle {
match value {
BorderLineStyle::None | BorderLineStyle::Hidden => OutlineLineStyle::Solid,
BorderLineStyle::Style(style) => style,
}
}
fn border_line_style_from_ident(value: &str) -> Option<BorderLineStyle> {
match value {
"none" => Some(BorderLineStyle::None),
"hidden" => Some(BorderLineStyle::Hidden),
"solid" => Some(BorderLineStyle::Style(OutlineLineStyle::Solid)),
"dashed" => Some(BorderLineStyle::Style(OutlineLineStyle::Dashed)),
"dotted" => Some(BorderLineStyle::Style(OutlineLineStyle::Dotted)),
"double" => Some(BorderLineStyle::Style(OutlineLineStyle::Double)),
"groove" => Some(BorderLineStyle::Style(OutlineLineStyle::Groove)),
"ridge" => Some(BorderLineStyle::Style(OutlineLineStyle::Ridge)),
"inset" => Some(BorderLineStyle::Style(OutlineLineStyle::Inset)),
"outset" => Some(BorderLineStyle::Style(OutlineLineStyle::Outset)),
_ => None,
}
}
fn outline_line_style_from_ident(value: &str) -> Option<Option<OutlineLineStyle>> {
match value {
"none" => Some(None),
"auto" | "solid" => Some(Some(OutlineLineStyle::Solid)),
"dashed" => Some(Some(OutlineLineStyle::Dashed)),
"dotted" => Some(Some(OutlineLineStyle::Dotted)),
"double" => Some(Some(OutlineLineStyle::Double)),
"groove" => Some(Some(OutlineLineStyle::Groove)),
"ridge" => Some(Some(OutlineLineStyle::Ridge)),
"inset" => Some(Some(OutlineLineStyle::Inset)),
"outset" => Some(Some(OutlineLineStyle::Outset)),
_ => None,
}
}
fn parse_display_mode_str(raw: &str) -> Option<DisplayMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"flow-root" | "block flow-root" | "flow-root block" => Some(DisplayMode::FlowRoot),
"block"
| "ruby"
| "block ruby"
| "ruby block"
| "ruby-base"
| "ruby-text"
| "ruby-base-container"
| "ruby-text-container"
| "-webkit-box"
| "-moz-box"
| "block -webkit-box"
| "block -moz-box" => Some(DisplayMode::Block),
"inline" | "inline ruby" | "ruby inline" | "-webkit-inline-box" | "-moz-inline-box"
| "inline -webkit-box" | "inline -moz-box" => Some(DisplayMode::Inline),
"inline-block" => Some(DisplayMode::InlineBlock),
"list-item" | "block list-item" | "list-item block" | "flow list-item"
| "list-item flow" => Some(DisplayMode::ListItem),
"inline list-item" | "list-item inline" => Some(DisplayMode::InlineListItem),
"table" => Some(DisplayMode::Table),
"inline-table" => Some(DisplayMode::InlineTable),
"table-row-group" => Some(DisplayMode::TableRowGroup),
"table-header-group" => Some(DisplayMode::TableHeaderGroup),
"table-footer-group" => Some(DisplayMode::TableFooterGroup),
"table-row" => Some(DisplayMode::TableRow),
"table-cell" => Some(DisplayMode::TableCell),
"table-column-group" => Some(DisplayMode::TableColumnGroup),
"table-column" => Some(DisplayMode::TableColumn),
"table-caption" => Some(DisplayMode::TableCaption),
"flex" | "block flex" | "flex block" => Some(DisplayMode::Flex),
"inline-flex" | "inline flex" | "flex inline" => Some(DisplayMode::InlineFlex),
"grid" | "block grid" | "grid block" => Some(DisplayMode::Grid),
"inline-grid" | "inline grid" | "grid inline" => Some(DisplayMode::InlineGrid),
"none" => Some(DisplayMode::None),
"contents" => Some(DisplayMode::Contents),
_ => None,
}
}
fn parse_display_spec_str(raw: &str) -> Option<DisplaySpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"inherit" => Some(DisplaySpec::Inherit),
"initial" | "unset" | "revert" => Some(DisplaySpec::Initial),
"revert-layer" => None,
value => parse_display_mode_str(value).map(DisplaySpec::Value),
}
}
fn apply_display_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(spec) = parse_display_spec_str(raw) else {
return false;
};
set_delta_display(delta, spec);
true
}
fn parse_visibility_spec_str(raw: &str) -> Option<VisibilitySpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"visible" => Some(VisibilitySpec::Value(VisibilityMode::Visible)),
"hidden" | "force-hidden" => Some(VisibilitySpec::Value(VisibilityMode::Hidden)),
"collapse" => Some(VisibilitySpec::Value(VisibilityMode::Collapse)),
"inherit" | "unset" => Some(VisibilitySpec::Inherit),
"initial" | "revert" => Some(VisibilitySpec::Initial),
"revert-layer" => None,
_ => None,
}
}
fn apply_visibility_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
if raw.trim().eq_ignore_ascii_case("revert-layer") {
return set_revert_layer_visibility(delta);
}
let Some(spec) = parse_visibility_spec_str(raw) else {
return false;
};
set_delta_visibility(delta, spec);
true
}
fn parse_position_mode_str(raw: &str) -> Option<PositionMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"static" | "initial" | "unset" | "revert" => Some(PositionMode::Static),
"relative" => Some(PositionMode::Relative),
"sticky" => Some(PositionMode::Sticky),
"absolute" => Some(PositionMode::Absolute),
"fixed" => Some(PositionMode::Fixed),
_ => None,
}
}
fn parse_running_position_name(raw: &str) -> Option<Arc<str>> {
let mut tokens = crate::css_native::tokenize_component_values(raw)?;
if tokens.len() != 1 {
return None;
}
let ComponentValue::Function { name, arguments } = tokens.pop()? else {
return None;
};
if !name.eq_ignore_ascii_case("running") {
return None;
}
let names = crate::css_native::parse_string_or_ident_list(&arguments)?;
(names.len() == 1 && !names[0].trim().is_empty()).then(|| Arc::<str>::from(names[0].trim()))
}
fn apply_position_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
if let Some(name) = parse_running_position_name(raw) {
set_delta_running_position(delta, name);
return true;
}
let Some(mode) = parse_position_mode_str(raw) else {
return false;
};
set_delta_position(delta, mode);
true
}
fn parse_named_string_set_spec(raw: &str) -> Option<NamedStringSetSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" | "initial" | "unset" | "revert" => {
return Some(NamedStringSetSpec::Initial);
}
"inherit" => return Some(NamedStringSetSpec::Inherit),
_ => {}
}
let tokens = crate::css_native::tokenize_component_values(raw)?;
let mut cursor = 0usize;
let mut assignments = Vec::new();
while cursor < tokens.len() {
if matches!(tokens.get(cursor), Some(ComponentValue::Delim(','))) {
cursor += 1;
continue;
}
let ComponentValue::Ident(name) = tokens.get(cursor)? else {
return None;
};
let name = name.trim();
if name.is_empty() || name.eq_ignore_ascii_case("none") {
return None;
}
cursor += 1;
let source = match tokens.get(cursor)? {
ComponentValue::Function { name, arguments }
if name.eq_ignore_ascii_case("content")
&& (arguments.trim().is_empty()
|| arguments.trim().eq_ignore_ascii_case("text")) =>
{
NamedStringSource::Content
}
ComponentValue::Function { name, arguments } if name.eq_ignore_ascii_case("attr") => {
let attributes = crate::css_native::parse_string_or_ident_list(arguments)?;
if attributes.len() != 1 || attributes[0].trim().is_empty() {
return None;
}
NamedStringSource::Attribute(attributes[0].trim().to_string())
}
ComponentValue::String(value) => NamedStringSource::Text(value.clone()),
_ => return None,
};
assignments.push(NamedStringAssignment {
name: name.to_string(),
source,
});
cursor += 1;
}
(!assignments.is_empty()).then_some(NamedStringSetSpec::Value(assignments))
}
fn apply_named_string_set_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(spec) = parse_named_string_set_spec(raw) else {
return false;
};
delta.string_set = Some(spec);
true
}
fn parse_float_mode_str(raw: &str) -> Option<FloatMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" | "initial" | "unset" | "revert" => Some(FloatMode::None),
"left" | "inline-start" => Some(FloatMode::Left),
"right" | "inline-end" => Some(FloatMode::Right),
"footnote" => Some(FloatMode::Footnote),
_ => None,
}
}
fn apply_float_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(mode) = parse_float_mode_str(raw) else {
return false;
};
set_delta_float_mode(delta, mode);
true
}
fn apply_footnote_policy_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"auto" => FootnotePolicySpec::Value(FootnotePolicy::Auto),
"line" => FootnotePolicySpec::Value(FootnotePolicy::Line),
"block" => FootnotePolicySpec::Value(FootnotePolicy::Block),
"inherit" => FootnotePolicySpec::Inherit,
"initial" | "unset" | "revert" => FootnotePolicySpec::Initial,
_ => return false,
};
set_delta_footnote_policy(delta, value);
true
}
fn apply_footnote_display_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let value = match raw.trim().to_ascii_lowercase().as_str() {
"block" => FootnoteDisplaySpec::Value(FootnoteDisplay::Block),
"inline" => FootnoteDisplaySpec::Value(FootnoteDisplay::Inline),
"compact" => FootnoteDisplaySpec::Value(FootnoteDisplay::Compact),
"inherit" => FootnoteDisplaySpec::Inherit,
"initial" | "unset" | "revert" => FootnoteDisplaySpec::Initial,
_ => return false,
};
set_delta_footnote_display(delta, value);
true
}
fn parse_clear_mode_str(raw: &str) -> Option<ClearMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" | "initial" | "unset" | "revert" => Some(ClearMode::None),
"left" | "inline-start" => Some(ClearMode::Left),
"right" | "inline-end" => Some(ClearMode::Right),
"both" => Some(ClearMode::Both),
_ => None,
}
}
fn apply_clear_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(mode) = parse_clear_mode_str(raw) else {
return false;
};
set_delta_clear_mode(delta, mode);
true
}
fn parse_z_index_str(raw: &str) -> Option<ZIndexSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" | "initial" | "unset" | "revert" => Some(ZIndexSpec::Auto),
"inherit" => Some(ZIndexSpec::Inherit),
value => value.parse::<i32>().ok().map(ZIndexSpec::Integer),
}
}
fn apply_z_index_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let Some(value) = parse_z_index_str(raw) else {
return false;
};
set_delta_z_index(delta, value);
true
}
fn apply_perspective_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let value = match lowered.as_str() {
"revert-layer" => {
delta.perspective = None;
delta.revert_layer.perspective = true;
return true;
}
"none" | "initial" | "unset" | "revert" => PerspectiveSpec::None,
"inherit" => PerspectiveSpec::Inherit,
_ => {
let Some(length) = parse_single_length_spec(raw) else {
return false;
};
PerspectiveSpec::Length(length)
}
};
set_delta_perspective(delta, value);
true
}
fn apply_opacity_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
if let Some(spec) = parse_opacity_spec_str(raw) {
set_delta_opacity(delta, spec);
return true;
}
if raw.to_ascii_lowercase().contains("var(") {
delta.opacity = None;
delta.opacity_var = Some(raw.trim().to_ascii_lowercase());
delta.revert_layer.opacity = false;
return true;
}
false
}
fn display_mode_name(mode: DisplayMode) -> &'static str {
match mode {
DisplayMode::Inline => "inline",
DisplayMode::Block => "block",
DisplayMode::FlowRoot => "flow-root",
DisplayMode::ListItem => "list-item",
DisplayMode::InlineListItem => "inline list-item",
DisplayMode::Table => "table",
DisplayMode::InlineTable => "inline-table",
DisplayMode::TableRowGroup => "table-row-group",
DisplayMode::TableHeaderGroup => "table-header-group",
DisplayMode::TableFooterGroup => "table-footer-group",
DisplayMode::TableRow => "table-row",
DisplayMode::TableCell => "table-cell",
DisplayMode::TableColumnGroup => "table-column-group",
DisplayMode::TableColumn => "table-column",
DisplayMode::TableCaption => "table-caption",
DisplayMode::None => "none",
DisplayMode::Contents => "contents",
DisplayMode::Flex => "flex",
DisplayMode::InlineBlock => "inline-block",
DisplayMode::InlineFlex => "inline-flex",
DisplayMode::Grid => "grid",
DisplayMode::InlineGrid => "inline-grid",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NativeLengthZeroHint {
Absolute,
Percent,
Em,
Rem,
Viewport,
FontRelative,
Mixed,
}
impl NativeLengthZeroHint {
fn merge(left: Option<Self>, right: Option<Self>) -> Option<Self> {
match (left, right) {
(None, hint) | (hint, None) => hint,
(Some(left), Some(right)) if left == right => Some(left),
(Some(_), Some(_)) => Some(Self::Mixed),
}
}
}
#[derive(Debug, Clone, Copy)]
struct NativeLengthMath {
abs_pt: f32,
percent: f32,
em: f32,
rem: f32,
vw: f32,
vh: f32,
vmin: f32,
vmax: f32,
ex: f32,
ch: f32,
cap: f32,
lh: f32,
rlh: f32,
zero_hint: Option<NativeLengthZeroHint>,
}
impl NativeLengthMath {
fn zero() -> Self {
Self {
abs_pt: 0.0,
percent: 0.0,
em: 0.0,
rem: 0.0,
vw: 0.0,
vh: 0.0,
vmin: 0.0,
vmax: 0.0,
ex: 0.0,
ch: 0.0,
cap: 0.0,
lh: 0.0,
rlh: 0.0,
zero_hint: None,
}
}
fn from_dimension(value: f32, unit: &str) -> Option<Self> {
let mut result = Self::zero();
match unit {
"%" => {
result.percent = value / 100.0;
result.zero_hint = Some(NativeLengthZeroHint::Percent);
}
"px" | "pt" | "pc" | "in" | "cm" | "mm" | "q" => {
result.abs_pt = match unit {
"px" => value * 0.75,
"pt" => value,
"pc" => value * 12.0,
"in" => value * 72.0,
"cm" => value * (72.0 / 2.54),
"mm" => value * (72.0 / 25.4),
"q" => value * (72.0 / 101.6),
_ => unreachable!(),
};
result.zero_hint = Some(NativeLengthZeroHint::Absolute);
}
"em" => {
result.em = value;
result.zero_hint = Some(NativeLengthZeroHint::Em);
}
"rem" => {
result.rem = value;
result.zero_hint = Some(NativeLengthZeroHint::Rem);
}
"ex" | "ch" | "cap" | "lh" | "rlh" => {
match unit {
"ex" => result.ex = value,
"ch" => result.ch = value,
"cap" => result.cap = value,
"lh" => result.lh = value,
"rlh" => result.rlh = value,
_ => unreachable!(),
}
result.zero_hint = Some(NativeLengthZeroHint::FontRelative);
}
"vw" | "vh" | "vmin" | "vmax" => {
match unit {
"vw" => result.vw = value / 100.0,
"vh" => result.vh = value / 100.0,
"vmin" => result.vmin = value / 100.0,
"vmax" => result.vmax = value / 100.0,
_ => unreachable!(),
}
result.zero_hint = Some(NativeLengthZeroHint::Viewport);
}
_ => return None,
}
Some(result)
}
fn add(self, other: Self) -> Self {
Self {
abs_pt: self.abs_pt + other.abs_pt,
percent: self.percent + other.percent,
em: self.em + other.em,
rem: self.rem + other.rem,
vw: self.vw + other.vw,
vh: self.vh + other.vh,
vmin: self.vmin + other.vmin,
vmax: self.vmax + other.vmax,
ex: self.ex + other.ex,
ch: self.ch + other.ch,
cap: self.cap + other.cap,
lh: self.lh + other.lh,
rlh: self.rlh + other.rlh,
zero_hint: NativeLengthZeroHint::merge(self.zero_hint, other.zero_hint),
}
}
fn scale(self, factor: f32) -> Self {
Self {
abs_pt: self.abs_pt * factor,
percent: self.percent * factor,
em: self.em * factor,
rem: self.rem * factor,
vw: self.vw * factor,
vh: self.vh * factor,
vmin: self.vmin * factor,
vmax: self.vmax * factor,
ex: self.ex * factor,
ch: self.ch * factor,
cap: self.cap * factor,
lh: self.lh * factor,
rlh: self.rlh * factor,
zero_hint: self.zero_hint,
}
}
fn to_length_spec(self) -> Option<LengthSpec> {
if [self.ex, self.ch, self.cap, self.lh, self.rlh]
.into_iter()
.any(|value| !native_math_component_is_zero(value))
{
if [self.vw, self.vh, self.vmin, self.vmax]
.into_iter()
.any(|value| !native_math_component_is_zero(value))
{
return None;
}
return Some(LengthSpec::FontRelative(FontRelativeLength {
base: CalcLength {
abs: Pt::from_f32(self.abs_pt),
percent: self.percent,
em: self.em,
rem: self.rem,
},
ex: self.ex,
ch: self.ch,
cap: self.cap,
lh: self.lh,
rlh: self.rlh,
}));
}
let calc = self.to_calc_length()?;
if calc_length_component_is_zero(calc.abs.to_f32())
&& calc_length_component_is_zero(calc.percent)
&& calc_length_component_is_zero(calc.em)
&& calc_length_component_is_zero(calc.rem)
{
return Some(match self.zero_hint {
Some(NativeLengthZeroHint::Percent) => LengthSpec::Percent(0.0),
Some(NativeLengthZeroHint::Em) => LengthSpec::Em(0.0),
Some(NativeLengthZeroHint::Rem) => LengthSpec::Rem(0.0),
_ => LengthSpec::Absolute(Pt::ZERO),
});
}
Some(length_spec_from_calc_length(calc))
}
fn to_calc_length(self) -> Option<CalcLength> {
if !native_math_component_is_zero(self.vw)
|| !native_math_component_is_zero(self.vh)
|| !native_math_component_is_zero(self.vmin)
|| !native_math_component_is_zero(self.vmax)
|| !native_math_component_is_zero(self.ex)
|| !native_math_component_is_zero(self.ch)
|| !native_math_component_is_zero(self.cap)
|| !native_math_component_is_zero(self.lh)
|| !native_math_component_is_zero(self.rlh)
{
return None;
}
Some(CalcLength {
abs: Pt::from_f32(self.abs_pt),
percent: self.percent,
em: self.em,
rem: self.rem,
})
}
fn from_calc_length(value: CalcLength) -> Self {
Self {
abs_pt: value.abs.to_f32(),
percent: value.percent,
em: value.em,
rem: value.rem,
..Self::zero()
}
}
fn to_font_calc(self) -> FontCalcLength {
FontCalcLength {
abs: Pt::from_f32(self.abs_pt),
em: I32F32::from_num(self.em + self.percent),
rem: I32F32::from_num(self.rem),
vw: I32F32::from_num(self.vw),
vh: I32F32::from_num(self.vh),
vmin: I32F32::from_num(self.vmin),
vmax: I32F32::from_num(self.vmax),
}
}
}
fn native_math_component_is_zero(value: f32) -> bool {
value.abs() <= CALC_LENGTH_EPSILON
}
#[derive(Debug, Clone, Copy)]
enum NativeMathValue {
Number(f32),
Length(NativeLengthMath),
Clamped(NativeClampedMath),
Typed(NativeTypedMath),
}
impl NativeMathValue {
fn scale(self, factor: f32) -> Self {
match self {
Self::Number(value) => Self::Number(value * factor),
Self::Length(value) => Self::Length(value.scale(factor)),
Self::Clamped(value) => Self::Clamped(value.scale(factor)),
Self::Typed(value) => Self::Typed(value.scale(factor)),
}
}
fn normalized(self) -> Self {
match self {
Self::Typed(value) => value.normalized(),
value => value,
}
}
}
#[derive(Debug, Clone, Copy)]
struct NativeClampedMath {
value: NativeLengthMath,
min_abs_pt: Option<f32>,
max_abs_pt: Option<f32>,
}
impl NativeClampedMath {
fn scale(mut self, factor: f32) -> Self {
self.value = self.value.scale(factor);
self.min_abs_pt = self.min_abs_pt.map(|value| value * factor);
self.max_abs_pt = self.max_abs_pt.map(|value| value * factor);
if factor < 0.0 {
std::mem::swap(&mut self.min_abs_pt, &mut self.max_abs_pt);
}
self
}
fn shift_absolute(mut self, amount_pt: f32) -> Self {
self.value.abs_pt += amount_pt;
self.min_abs_pt = self.min_abs_pt.map(|value| value + amount_pt);
self.max_abs_pt = self.max_abs_pt.map(|value| value + amount_pt);
self
}
fn to_length_spec(self) -> Option<LengthSpec> {
let value = self.value.to_calc_length()?;
let min = self.min_abs_pt.map(Pt::from_f32);
let max = self.max_abs_pt.map(Pt::from_f32);
if native_math_component_is_zero(value.percent)
&& native_math_component_is_zero(value.em)
&& native_math_component_is_zero(value.rem)
{
let mut resolved = value.abs;
if let Some(maximum) = max {
resolved = resolved.min(maximum);
}
if let Some(minimum) = min {
resolved = resolved.max(minimum);
}
return Some(LengthSpec::Absolute(resolved));
}
Some(LengthSpec::Clamped(ClampedLength { value, min, max }))
}
}
const NATIVE_DIM_ABSOLUTE_LENGTH: usize = 0;
const NATIVE_DIM_PERCENT: usize = 1;
const NATIVE_DIM_EM: usize = 2;
const NATIVE_DIM_REM: usize = 3;
const NATIVE_DIM_VW: usize = 4;
const NATIVE_DIM_VH: usize = 5;
const NATIVE_DIM_VMIN: usize = 6;
const NATIVE_DIM_VMAX: usize = 7;
const NATIVE_DIM_ANGLE: usize = 8;
const NATIVE_DIM_TIME: usize = 9;
const NATIVE_DIM_FREQUENCY: usize = 10;
const NATIVE_DIM_RESOLUTION: usize = 11;
const NATIVE_DIM_FLEX: usize = 12;
const NATIVE_DIM_EX: usize = 13;
const NATIVE_DIM_CH: usize = 14;
const NATIVE_DIM_CAP: usize = 15;
const NATIVE_DIM_LH: usize = 16;
const NATIVE_DIM_RLH: usize = 17;
const NATIVE_DIMENSION_COUNT: usize = 18;
#[derive(Debug, Clone, Copy)]
struct NativeTypedMath {
factor: f32,
powers: [i8; NATIVE_DIMENSION_COUNT],
}
impl NativeTypedMath {
fn dimension(factor: f32, dimension: usize) -> Self {
let mut powers = [0; NATIVE_DIMENSION_COUNT];
powers[dimension] = 1;
Self { factor, powers }
}
fn scale(mut self, factor: f32) -> Self {
self.factor *= factor;
self
}
fn multiply(mut self, other: Self) -> Option<Self> {
self.factor *= other.factor;
if !self.factor.is_finite() {
return None;
}
for (power, other_power) in self.powers.iter_mut().zip(other.powers) {
*power = power.checked_add(other_power)?;
}
Some(self)
}
fn divide(mut self, other: Self) -> Option<Self> {
if other.factor.abs() <= f32::EPSILON {
return None;
}
self.factor /= other.factor;
if !self.factor.is_finite() {
return None;
}
for (power, other_power) in self.powers.iter_mut().zip(other.powers) {
*power = power.checked_sub(other_power)?;
}
Some(self)
}
fn normalized(self) -> NativeMathValue {
if self.powers.iter().all(|power| *power == 0) {
return NativeMathValue::Number(self.factor);
}
let populated: Vec<(usize, i8)> = self
.powers
.iter()
.copied()
.enumerate()
.filter(|(_, power)| *power != 0)
.collect();
if let [(dimension, 1)] = populated.as_slice() {
let mut length = NativeLengthMath::zero();
match *dimension {
NATIVE_DIM_ABSOLUTE_LENGTH => length.abs_pt = self.factor,
NATIVE_DIM_PERCENT => length.percent = self.factor,
NATIVE_DIM_EM => length.em = self.factor,
NATIVE_DIM_REM => length.rem = self.factor,
NATIVE_DIM_VW => length.vw = self.factor,
NATIVE_DIM_VH => length.vh = self.factor,
NATIVE_DIM_VMIN => length.vmin = self.factor,
NATIVE_DIM_VMAX => length.vmax = self.factor,
NATIVE_DIM_EX => length.ex = self.factor,
NATIVE_DIM_CH => length.ch = self.factor,
NATIVE_DIM_CAP => length.cap = self.factor,
NATIVE_DIM_LH => length.lh = self.factor,
NATIVE_DIM_RLH => length.rlh = self.factor,
_ => return NativeMathValue::Typed(self),
}
return NativeMathValue::Length(length);
}
NativeMathValue::Typed(self)
}
}
struct NativeLengthMathParser<'a> {
input: &'a [u8],
pos: usize,
allow_extrema: bool,
}
impl<'a> NativeLengthMathParser<'a> {
fn new(raw: &'a str, allow_extrema: bool) -> Self {
Self {
input: raw.as_bytes(),
pos: 0,
allow_extrema,
}
}
fn parse(mut self) -> Option<NativeMathValue> {
let value = self.parse_sum()?;
self.skip_whitespace();
(self.pos == self.input.len()).then_some(value.normalized())
}
fn parse_sum(&mut self) -> Option<NativeMathValue> {
let mut value = self.parse_product()?;
loop {
self.skip_whitespace();
let factor = match self.peek_byte() {
Some(b'+') => 1.0,
Some(b'-') => -1.0,
_ => break,
};
self.pos += 1;
let rhs = self.parse_product()?.scale(factor);
value = native_math_add(value, rhs)?;
}
Some(value)
}
fn parse_product(&mut self) -> Option<NativeMathValue> {
let mut value = self.parse_unary()?;
loop {
self.skip_whitespace();
let operator = match self.peek_byte() {
Some(b'*') => b'*',
Some(b'/') => b'/',
_ => break,
};
self.pos += 1;
let rhs = self.parse_unary()?;
value = match operator {
b'*' => native_math_multiply(value, rhs)?,
b'/' => native_math_divide(value, rhs)?,
_ => unreachable!(),
};
}
Some(value)
}
fn parse_unary(&mut self) -> Option<NativeMathValue> {
self.skip_whitespace();
match self.peek_byte() {
Some(b'+') => {
self.pos += 1;
self.parse_unary()
}
Some(b'-') => {
self.pos += 1;
self.parse_unary().map(|value| value.scale(-1.0))
}
_ => self.parse_primary(),
}
}
fn parse_primary(&mut self) -> Option<NativeMathValue> {
self.skip_whitespace();
if self.consume_byte(b'(') {
let value = self.parse_sum()?;
self.skip_whitespace();
return self.consume_byte(b')').then_some(value);
}
if self
.peek_byte()
.is_some_and(|byte| byte.is_ascii_digit() || byte == b'.')
{
return self.parse_numeric_value();
}
let name = self.parse_identifier()?.to_ascii_lowercase();
self.skip_whitespace();
if !self.consume_byte(b'(') {
return match name.as_str() {
"pi" => Some(NativeMathValue::Number(std::f32::consts::PI)),
"e" => Some(NativeMathValue::Number(std::f32::consts::E)),
"infinity" => Some(NativeMathValue::Number(f32::MAX)),
_ => None,
};
}
self.parse_function(&name)
}
fn parse_numeric_value(&mut self) -> Option<NativeMathValue> {
let value = self.parse_number()?;
let unit_start = self.pos;
while self
.peek_byte()
.is_some_and(|byte| byte.is_ascii_alphabetic())
{
self.pos += 1;
}
if self.consume_byte(b'%') {
return NativeLengthMath::from_dimension(value, "%").map(NativeMathValue::Length);
}
if self.pos == unit_start {
return Some(NativeMathValue::Number(value));
}
let unit = std::str::from_utf8(&self.input[unit_start..self.pos])
.ok()?
.to_ascii_lowercase();
NativeLengthMath::from_dimension(value, &unit)
.map(NativeMathValue::Length)
.or_else(|| native_typed_dimension(value, &unit).map(NativeMathValue::Typed))
}
fn parse_function(&mut self, name: &str) -> Option<NativeMathValue> {
match name {
"calc" => {
let value = self.parse_sum()?;
self.skip_whitespace();
self.consume_byte(b')').then_some(value)
}
"min" | "max" if self.allow_extrema => {
let values = self.parse_argument_list()?;
native_math_extreme(&values, name == "max")
}
"clamp" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 3 {
return None;
}
native_math_clamp(values[0], values[1], values[2])
}
"abs" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 1 {
return None;
}
native_math_abs(values[0])
}
"sign" if self.allow_extrema => {
let values = self.parse_argument_list()?;
(values.len() == 1)
.then(|| native_math_sign(values[0]))
.flatten()
}
"round" if self.allow_extrema => self.parse_round_function(),
"mod" | "rem" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 2 {
return None;
}
native_math_mod_rem(values[0], values[1], name == "mod")
}
"hypot" if self.allow_extrema => {
let values = self.parse_argument_list()?;
native_math_hypot(&values)
}
"sin" | "cos" | "tan" | "asin" | "acos" | "atan" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 1 {
return None;
}
native_math_trig(name, values[0])
}
"sqrt" | "exp" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 1 {
return None;
}
let value = native_math_number(values[0])?;
let result = if name == "sqrt" {
(value >= 0.0).then(|| value.sqrt())?
} else {
value.exp()
};
result
.is_finite()
.then_some(NativeMathValue::Number(result))
}
"pow" | "log" if self.allow_extrema => {
let values = self.parse_argument_list()?;
if values.len() != 2 {
return None;
}
let left = native_math_number(values[0])?;
let right = native_math_number(values[1])?;
let result = if name == "pow" {
left.powf(right)
} else if left > 0.0 && right > 0.0 && (right - 1.0).abs() > f32::EPSILON {
left.log(right)
} else {
return None;
};
result
.is_finite()
.then_some(NativeMathValue::Number(result))
}
_ => None,
}
}
fn parse_round_function(&mut self) -> Option<NativeMathValue> {
self.skip_whitespace();
let strategy_start = self.pos;
let strategy = self.parse_identifier()?.to_ascii_lowercase();
if !matches!(strategy.as_str(), "nearest" | "up" | "down" | "to-zero") {
self.pos = strategy_start;
return None;
}
self.skip_whitespace();
if !self.consume_byte(b',') {
return None;
}
let value = self.parse_sum()?;
self.skip_whitespace();
let step = if self.consume_byte(b',') {
self.parse_sum()?
} else {
NativeMathValue::Number(1.0)
};
self.skip_whitespace();
if !self.consume_byte(b')') {
return None;
}
native_math_round(&strategy, value, step)
}
fn parse_argument_list(&mut self) -> Option<Vec<NativeMathValue>> {
let mut values = Vec::new();
loop {
values.push(self.parse_sum()?);
self.skip_whitespace();
if self.consume_byte(b')') {
break;
}
if !self.consume_byte(b',') {
return None;
}
}
(!values.is_empty()).then_some(values)
}
fn parse_number(&mut self) -> Option<f32> {
let start = self.pos;
let mut saw_digit = false;
while self.peek_byte().is_some_and(|byte| byte.is_ascii_digit()) {
saw_digit = true;
self.pos += 1;
}
if self.consume_byte(b'.') {
while self.peek_byte().is_some_and(|byte| byte.is_ascii_digit()) {
saw_digit = true;
self.pos += 1;
}
}
if !saw_digit {
self.pos = start;
return None;
}
if matches!(self.peek_byte(), Some(b'e' | b'E')) {
let exponent_start = self.pos;
self.pos += 1;
if matches!(self.peek_byte(), Some(b'+' | b'-')) {
self.pos += 1;
}
let digits_start = self.pos;
while self.peek_byte().is_some_and(|byte| byte.is_ascii_digit()) {
self.pos += 1;
}
if self.pos == digits_start {
self.pos = exponent_start;
}
}
std::str::from_utf8(&self.input[start..self.pos])
.ok()?
.parse::<f32>()
.ok()
.filter(|value| value.is_finite())
}
fn parse_identifier(&mut self) -> Option<&'a str> {
let start = self.pos;
while self
.peek_byte()
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'-')
{
self.pos += 1;
}
(self.pos > start)
.then(|| std::str::from_utf8(&self.input[start..self.pos]).ok())
.flatten()
}
fn skip_whitespace(&mut self) {
while self
.peek_byte()
.is_some_and(|byte| byte.is_ascii_whitespace())
{
self.pos += 1;
}
}
fn peek_byte(&self) -> Option<u8> {
self.input.get(self.pos).copied()
}
fn consume_byte(&mut self, expected: u8) -> bool {
if self.peek_byte() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
}
fn native_typed_dimension(value: f32, unit: &str) -> Option<NativeTypedMath> {
let (factor, dimension) = match unit {
"deg" => (value.to_radians(), NATIVE_DIM_ANGLE),
"grad" => (value * std::f32::consts::PI / 200.0, NATIVE_DIM_ANGLE),
"rad" => (value, NATIVE_DIM_ANGLE),
"turn" => (value * std::f32::consts::TAU, NATIVE_DIM_ANGLE),
"s" => (value, NATIVE_DIM_TIME),
"ms" => (value / 1_000.0, NATIVE_DIM_TIME),
"hz" => (value, NATIVE_DIM_FREQUENCY),
"khz" => (value * 1_000.0, NATIVE_DIM_FREQUENCY),
"dppx" | "x" => (value, NATIVE_DIM_RESOLUTION),
"dpi" => (value / 96.0, NATIVE_DIM_RESOLUTION),
"dpcm" => (value * 2.54 / 96.0, NATIVE_DIM_RESOLUTION),
"fr" => (value, NATIVE_DIM_FLEX),
_ => return None,
};
factor
.is_finite()
.then_some(NativeTypedMath::dimension(factor, dimension))
}
fn native_length_to_typed(value: NativeLengthMath) -> Option<NativeTypedMath> {
let dimensions = [
(value.abs_pt, NATIVE_DIM_ABSOLUTE_LENGTH),
(value.percent, NATIVE_DIM_PERCENT),
(value.em, NATIVE_DIM_EM),
(value.rem, NATIVE_DIM_REM),
(value.vw, NATIVE_DIM_VW),
(value.vh, NATIVE_DIM_VH),
(value.vmin, NATIVE_DIM_VMIN),
(value.vmax, NATIVE_DIM_VMAX),
(value.ex, NATIVE_DIM_EX),
(value.ch, NATIVE_DIM_CH),
(value.cap, NATIVE_DIM_CAP),
(value.lh, NATIVE_DIM_LH),
(value.rlh, NATIVE_DIM_RLH),
];
let populated: Vec<(f32, usize)> = dimensions
.into_iter()
.filter(|(component, _)| !native_math_component_is_zero(*component))
.collect();
if let [(factor, dimension)] = populated.as_slice() {
return Some(NativeTypedMath::dimension(*factor, *dimension));
}
if populated.is_empty() {
let dimension = match value.zero_hint {
Some(NativeLengthZeroHint::Percent) => NATIVE_DIM_PERCENT,
Some(NativeLengthZeroHint::Em) => NATIVE_DIM_EM,
Some(NativeLengthZeroHint::Rem) => NATIVE_DIM_REM,
Some(NativeLengthZeroHint::Viewport) => NATIVE_DIM_VW,
Some(NativeLengthZeroHint::FontRelative) => NATIVE_DIM_EX,
_ => NATIVE_DIM_ABSOLUTE_LENGTH,
};
return Some(NativeTypedMath::dimension(0.0, dimension));
}
None
}
fn native_math_to_typed(value: NativeMathValue) -> Option<NativeTypedMath> {
match value.normalized() {
NativeMathValue::Number(value) => Some(NativeTypedMath {
factor: value,
powers: [0; NATIVE_DIMENSION_COUNT],
}),
NativeMathValue::Length(value) => native_length_to_typed(value),
NativeMathValue::Clamped(_) => None,
NativeMathValue::Typed(value) => Some(value),
}
}
fn native_math_multiply(left: NativeMathValue, right: NativeMathValue) -> Option<NativeMathValue> {
match (left.normalized(), right.normalized()) {
(NativeMathValue::Number(left), NativeMathValue::Number(right)) => {
let value = left * right;
value.is_finite().then_some(NativeMathValue::Number(value))
}
(NativeMathValue::Number(scale), NativeMathValue::Length(length))
| (NativeMathValue::Length(length), NativeMathValue::Number(scale)) => {
Some(NativeMathValue::Length(length.scale(scale)))
}
(NativeMathValue::Number(scale), NativeMathValue::Clamped(length))
| (NativeMathValue::Clamped(length), NativeMathValue::Number(scale)) => {
Some(NativeMathValue::Clamped(length.scale(scale)))
}
(left, right) => native_math_to_typed(left)?
.multiply(native_math_to_typed(right)?)
.map(NativeTypedMath::normalized),
}
}
fn native_math_divide(left: NativeMathValue, right: NativeMathValue) -> Option<NativeMathValue> {
match (left.normalized(), right.normalized()) {
(NativeMathValue::Number(left), NativeMathValue::Number(right))
if right.abs() > f32::EPSILON =>
{
let value = left / right;
value.is_finite().then_some(NativeMathValue::Number(value))
}
(NativeMathValue::Length(length), NativeMathValue::Number(divisor))
if divisor.abs() > f32::EPSILON =>
{
Some(NativeMathValue::Length(length.scale(1.0 / divisor)))
}
(NativeMathValue::Clamped(length), NativeMathValue::Number(divisor))
if divisor.abs() > f32::EPSILON =>
{
Some(NativeMathValue::Clamped(length.scale(1.0 / divisor)))
}
(left, right) => native_math_to_typed(left)?
.divide(native_math_to_typed(right)?)
.map(NativeTypedMath::normalized),
}
}
fn native_math_number(value: NativeMathValue) -> Option<f32> {
match value.normalized() {
NativeMathValue::Number(value) if value.is_finite() => Some(value),
_ => None,
}
}
fn native_math_sign(value: NativeMathValue) -> Option<NativeMathValue> {
let factor = native_math_to_typed(value)?.factor;
Some(NativeMathValue::Number(if factor > 0.0 {
1.0
} else if factor < 0.0 {
-1.0
} else {
0.0
}))
}
fn native_math_round(
strategy: &str,
value: NativeMathValue,
step: NativeMathValue,
) -> Option<NativeMathValue> {
let value = native_math_to_typed(value)?;
let step = native_math_to_typed(step)?;
if value.powers != step.powers || step.factor.abs() <= f32::EPSILON {
return None;
}
let quotient = value.factor / step.factor;
let rounded = match strategy {
"nearest" => quotient.round(),
"up" => quotient.ceil(),
"down" => quotient.floor(),
"to-zero" => quotient.trunc(),
_ => return None,
};
let result = NativeTypedMath {
factor: rounded * step.factor,
powers: value.powers,
};
result.factor.is_finite().then(|| result.normalized())
}
fn native_math_mod_rem(
left: NativeMathValue,
right: NativeMathValue,
modulo: bool,
) -> Option<NativeMathValue> {
let left = native_math_to_typed(left)?;
let right = native_math_to_typed(right)?;
if left.powers != right.powers || right.factor.abs() <= f32::EPSILON {
return None;
}
let remainder = left.factor % right.factor;
let factor = if modulo && remainder != 0.0 && remainder.signum() != right.factor.signum() {
remainder + right.factor
} else {
remainder
};
Some(
NativeTypedMath {
factor,
powers: left.powers,
}
.normalized(),
)
}
fn native_math_hypot(values: &[NativeMathValue]) -> Option<NativeMathValue> {
let first = native_math_to_typed(*values.first()?)?;
let mut sum = 0.0f32;
for value in values {
let value = native_math_to_typed(*value)?;
if value.powers != first.powers {
return None;
}
sum += value.factor * value.factor;
}
let factor = sum.sqrt();
factor.is_finite().then(|| {
NativeTypedMath {
factor,
powers: first.powers,
}
.normalized()
})
}
fn native_math_trig(name: &str, value: NativeMathValue) -> Option<NativeMathValue> {
match name {
"sin" | "cos" | "tan" => {
let value = match value.normalized() {
NativeMathValue::Number(value) => value,
value => {
let typed = native_math_to_typed(value)?;
let mut angle = [0; NATIVE_DIMENSION_COUNT];
angle[NATIVE_DIM_ANGLE] = 1;
if typed.powers != angle {
return None;
}
typed.factor
}
};
let result = match name {
"sin" => value.sin(),
"cos" => value.cos(),
"tan" => value.tan(),
_ => unreachable!(),
};
result
.is_finite()
.then_some(NativeMathValue::Number(result))
}
"asin" | "acos" | "atan" => {
let value = native_math_number(value)?;
let result = match name {
"asin" => value.asin(),
"acos" => value.acos(),
"atan" => value.atan(),
_ => unreachable!(),
};
result
.is_finite()
.then_some(NativeMathValue::Typed(NativeTypedMath::dimension(
result,
NATIVE_DIM_ANGLE,
)))
}
_ => None,
}
}
fn native_math_add(left: NativeMathValue, right: NativeMathValue) -> Option<NativeMathValue> {
let left = left.normalized();
let right = right.normalized();
match (left, right) {
(NativeMathValue::Number(left), NativeMathValue::Number(right)) => {
Some(NativeMathValue::Number(left + right))
}
(NativeMathValue::Length(left), NativeMathValue::Length(right)) => {
Some(NativeMathValue::Length(left.add(right)))
}
(NativeMathValue::Clamped(value), NativeMathValue::Length(offset))
| (NativeMathValue::Length(offset), NativeMathValue::Clamped(value)) => {
native_length_absolute_pt(offset)
.map(|offset| NativeMathValue::Clamped(value.shift_absolute(offset)))
}
(NativeMathValue::Number(number), NativeMathValue::Length(length))
| (NativeMathValue::Length(length), NativeMathValue::Number(number))
if native_math_component_is_zero(number) =>
{
Some(NativeMathValue::Length(length))
}
(NativeMathValue::Number(number), NativeMathValue::Clamped(length))
| (NativeMathValue::Clamped(length), NativeMathValue::Number(number))
if native_math_component_is_zero(number) =>
{
Some(NativeMathValue::Clamped(length))
}
(NativeMathValue::Typed(mut left), NativeMathValue::Typed(right))
if left.powers == right.powers =>
{
left.factor += right.factor;
left.factor.is_finite().then(|| left.normalized())
}
_ => None,
}
}
fn native_length_absolute_pt(value: NativeLengthMath) -> Option<f32> {
(native_math_component_is_zero(value.percent)
&& native_math_component_is_zero(value.em)
&& native_math_component_is_zero(value.rem)
&& native_math_component_is_zero(value.vw)
&& native_math_component_is_zero(value.vh)
&& native_math_component_is_zero(value.vmin)
&& native_math_component_is_zero(value.vmax))
.then_some(value.abs_pt)
}
fn native_math_absolute_value(value: NativeMathValue) -> Option<f32> {
match value.normalized() {
NativeMathValue::Length(value) => native_length_absolute_pt(value),
NativeMathValue::Number(value) if native_math_component_is_zero(value) => Some(0.0),
_ => None,
}
}
fn native_math_with_absolute_clamp(
value: NativeMathValue,
minimum: f32,
maximum: f32,
) -> Option<NativeMathValue> {
let mut value = match value.normalized() {
NativeMathValue::Length(value) => NativeClampedMath {
value,
min_abs_pt: None,
max_abs_pt: None,
},
NativeMathValue::Clamped(value) => value,
_ => return None,
};
value.min_abs_pt = Some(value.min_abs_pt.map_or(minimum, |bound| bound.max(minimum)));
value.max_abs_pt = Some(value.max_abs_pt.map_or(maximum, |bound| bound.min(maximum)));
Some(NativeMathValue::Clamped(value))
}
fn native_math_extreme_with_absolute_bounds(
values: &[NativeMathValue],
pick_max: bool,
) -> Option<NativeMathValue> {
let mut dynamic = None;
let mut bounds = Vec::new();
for value in values.iter().copied().map(NativeMathValue::normalized) {
if let Some(bound) = native_math_absolute_value(value) {
bounds.push(bound);
continue;
}
if dynamic.replace(value).is_some() {
return None;
}
}
let mut value = match dynamic? {
NativeMathValue::Length(value) => NativeClampedMath {
value,
min_abs_pt: None,
max_abs_pt: None,
},
NativeMathValue::Clamped(value) => value,
_ => return None,
};
for bound in bounds {
if pick_max {
value.min_abs_pt = Some(value.min_abs_pt.map_or(bound, |current| current.max(bound)));
} else {
value.max_abs_pt = Some(value.max_abs_pt.map_or(bound, |current| current.min(bound)));
}
}
Some(NativeMathValue::Clamped(value))
}
fn native_math_lengths(values: &[NativeMathValue]) -> Option<Vec<CalcLength>> {
let contains_length = values
.iter()
.any(|value| matches!(value, NativeMathValue::Length(_)));
if !contains_length {
return None;
}
values
.iter()
.map(|value| match value {
NativeMathValue::Length(length) => length.to_calc_length(),
NativeMathValue::Number(number) if native_math_component_is_zero(*number) => {
Some(CalcLength::zero())
}
NativeMathValue::Clamped(_) | NativeMathValue::Typed(_) => None,
_ => None,
})
.collect()
}
fn native_math_extreme(values: &[NativeMathValue], pick_max: bool) -> Option<NativeMathValue> {
if values
.iter()
.all(|value| matches!(value, NativeMathValue::Number(_)))
{
let mut best = match values.first()? {
NativeMathValue::Number(value) => *value,
_ => return None,
};
for value in values.iter().skip(1) {
let NativeMathValue::Number(value) = value else {
return None;
};
if (pick_max && *value > best) || (!pick_max && *value < best) {
best = *value;
}
}
return Some(NativeMathValue::Number(best));
}
if let Some(value) = native_math_extreme_with_absolute_bounds(values, pick_max) {
return Some(value);
}
let lengths = native_math_lengths(values)?;
calc_length_from_min_max(&lengths, pick_max)
.map(NativeLengthMath::from_calc_length)
.map(NativeMathValue::Length)
}
fn native_math_clamp(
min: NativeMathValue,
preferred: NativeMathValue,
max: NativeMathValue,
) -> Option<NativeMathValue> {
if let (
NativeMathValue::Number(min),
NativeMathValue::Number(preferred),
NativeMathValue::Number(max),
) = (min, preferred, max)
{
let upper = if min > max { min } else { max };
return Some(NativeMathValue::Number(preferred.max(min).min(upper)));
}
if let (Some(minimum), Some(maximum)) = (
native_math_absolute_value(min),
native_math_absolute_value(max),
) {
if let Some(value) = native_math_with_absolute_clamp(preferred, minimum, maximum) {
return Some(value);
}
}
let lengths = native_math_lengths(&[min, preferred, max])?;
calc_length_from_same_slope_clamp(lengths[0], lengths[1], lengths[2])
.or_else(|| calc_length_from_domain_clamp(lengths[0], lengths[1], lengths[2]))
.map(NativeLengthMath::from_calc_length)
.map(NativeMathValue::Length)
}
fn native_math_abs(value: NativeMathValue) -> Option<NativeMathValue> {
match value.normalized() {
NativeMathValue::Number(value) => Some(NativeMathValue::Number(value.abs())),
NativeMathValue::Length(length) => {
let value = length.to_calc_length()?;
let domain = calc_length_domain(value)?;
Some(NativeMathValue::Length(NativeLengthMath::from_calc_length(
calc_length_from_domain_value(
domain,
calc_length_domain_value(value, domain).abs(),
),
)))
}
NativeMathValue::Clamped(_) => None,
NativeMathValue::Typed(mut value) => {
value.factor = value.factor.abs();
Some(value.normalized())
}
}
}
fn parse_native_length_math(raw: &str, allow_extrema: bool) -> Option<NativeMathValue> {
NativeLengthMathParser::new(raw.trim(), allow_extrema).parse()
}
const CALC_LENGTH_EPSILON: f32 = 1.0e-6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CalcLengthDomain {
Absolute,
Percent,
Em,
Rem,
}
fn calc_length_component_is_zero(value: f32) -> bool {
value.abs() <= CALC_LENGTH_EPSILON
}
fn calc_length_domain(value: CalcLength) -> Option<CalcLengthDomain> {
let abs = value.abs.to_f32();
let percent = value.percent;
let em = value.em;
let rem = value.rem;
if !calc_length_component_is_zero(abs)
&& calc_length_component_is_zero(percent)
&& calc_length_component_is_zero(em)
&& calc_length_component_is_zero(rem)
{
return Some(CalcLengthDomain::Absolute);
}
if calc_length_component_is_zero(abs)
&& !calc_length_component_is_zero(percent)
&& calc_length_component_is_zero(em)
&& calc_length_component_is_zero(rem)
{
return Some(CalcLengthDomain::Percent);
}
if calc_length_component_is_zero(abs)
&& calc_length_component_is_zero(percent)
&& !calc_length_component_is_zero(em)
&& calc_length_component_is_zero(rem)
{
return Some(CalcLengthDomain::Em);
}
if calc_length_component_is_zero(abs)
&& calc_length_component_is_zero(percent)
&& calc_length_component_is_zero(em)
&& !calc_length_component_is_zero(rem)
{
return Some(CalcLengthDomain::Rem);
}
None
}
fn calc_length_domain_value(value: CalcLength, domain: CalcLengthDomain) -> f32 {
match domain {
CalcLengthDomain::Absolute => value.abs.to_f32(),
CalcLengthDomain::Percent => value.percent,
CalcLengthDomain::Em => value.em,
CalcLengthDomain::Rem => value.rem,
}
}
fn calc_length_from_domain_value(domain: CalcLengthDomain, value: f32) -> CalcLength {
match domain {
CalcLengthDomain::Absolute => CalcLength {
abs: Pt::from_f32(value),
percent: 0.0,
em: 0.0,
rem: 0.0,
},
CalcLengthDomain::Percent => CalcLength {
abs: Pt::ZERO,
percent: value,
em: 0.0,
rem: 0.0,
},
CalcLengthDomain::Em => CalcLength {
abs: Pt::ZERO,
percent: 0.0,
em: value,
rem: 0.0,
},
CalcLengthDomain::Rem => CalcLength {
abs: Pt::ZERO,
percent: 0.0,
em: 0.0,
rem: value,
},
}
}
fn calc_length_same_non_abs_components(a: CalcLength, b: CalcLength) -> bool {
calc_length_component_is_zero(a.percent - b.percent)
&& calc_length_component_is_zero(a.em - b.em)
&& calc_length_component_is_zero(a.rem - b.rem)
}
fn calc_length_from_same_slope_clamp(
min: CalcLength,
preferred: CalcLength,
max: CalcLength,
) -> Option<CalcLength> {
if !(calc_length_same_non_abs_components(min, preferred)
&& calc_length_same_non_abs_components(min, max))
{
return None;
}
let min_abs = min.abs.to_f32();
let preferred_abs = preferred.abs.to_f32();
let max_abs = max.abs.to_f32();
// Spec-compatible behavior for inverted bounds: minimum wins.
let upper = if min_abs > max_abs { min_abs } else { max_abs };
Some(CalcLength {
abs: Pt::from_f32(preferred_abs.max(min_abs).min(upper)),
percent: min.percent,
em: min.em,
rem: min.rem,
})
}
fn calc_length_from_domain_clamp(
min: CalcLength,
preferred: CalcLength,
max: CalcLength,
) -> Option<CalcLength> {
let domain = calc_length_domain(min)?;
if calc_length_domain(preferred) != Some(domain) || calc_length_domain(max) != Some(domain) {
return None;
}
let min_value = calc_length_domain_value(min, domain);
let preferred_value = calc_length_domain_value(preferred, domain);
let max_value = calc_length_domain_value(max, domain);
let upper = if min_value > max_value {
min_value
} else {
max_value
};
Some(calc_length_from_domain_value(
domain,
preferred_value.max(min_value).min(upper),
))
}
fn calc_length_from_min_max(values: &[CalcLength], pick_max: bool) -> Option<CalcLength> {
let first = *values.first()?;
if values
.iter()
.all(|value| calc_length_same_non_abs_components(first, *value))
{
let mut best = first;
for value in values.iter().skip(1) {
if (pick_max && value.abs > best.abs) || (!pick_max && value.abs < best.abs) {
best = *value;
}
}
return Some(best);
}
let domain = calc_length_domain(first)?;
if values
.iter()
.all(|value| calc_length_domain(*value) == Some(domain))
{
let mut best = calc_length_domain_value(first, domain);
for value in values.iter().skip(1) {
let current = calc_length_domain_value(*value, domain);
if (pick_max && current > best) || (!pick_max && current < best) {
best = current;
}
}
return Some(calc_length_from_domain_value(domain, best));
}
None
}
fn length_spec_from_calc_length(calc: CalcLength) -> LengthSpec {
if calc_length_component_is_zero(calc.percent)
&& calc_length_component_is_zero(calc.em)
&& calc_length_component_is_zero(calc.rem)
{
return LengthSpec::Absolute(calc.abs);
}
if calc_length_component_is_zero(calc.abs.to_f32())
&& calc_length_component_is_zero(calc.em)
&& calc_length_component_is_zero(calc.rem)
{
return LengthSpec::Percent(calc.percent);
}
if calc_length_component_is_zero(calc.abs.to_f32())
&& calc_length_component_is_zero(calc.percent)
&& calc_length_component_is_zero(calc.rem)
{
return LengthSpec::Em(calc.em);
}
if calc_length_component_is_zero(calc.abs.to_f32())
&& calc_length_component_is_zero(calc.percent)
&& calc_length_component_is_zero(calc.em)
{
return LengthSpec::Rem(calc.rem);
}
LengthSpec::Calc(calc)
}
fn apply_edge_delta(target: &mut EdgeSizes, delta: &EdgeDelta, parent: &EdgeSizes) {
if let Some(spec) = delta.top {
target.top = normalize_length_spec(spec, parent.top);
}
if let Some(spec) = delta.right {
target.right = normalize_length_spec(spec, parent.right);
}
if let Some(spec) = delta.bottom {
target.bottom = normalize_length_spec(spec, parent.bottom);
}
if let Some(spec) = delta.left {
target.left = normalize_length_spec(spec, parent.left);
}
}
fn apply_logical_size_delta(style: &mut ComputedStyle, delta: &StyleDelta, parent: &ComputedStyle) {
let vertical = is_vertical_writing_mode(style.writing_mode);
if let Some(spec) = delta.inline_size {
if vertical {
style.height = normalize_size_spec(spec, parent.height);
style.pending_height_var = None;
} else {
style.width = normalize_size_spec(spec, parent.width);
style.pending_width_var = None;
}
}
if let Some(spec) = delta.block_size {
if vertical {
style.width = normalize_size_spec(spec, parent.width);
style.pending_width_var = None;
} else {
style.height = normalize_size_spec(spec, parent.height);
style.pending_height_var = None;
}
}
if let Some(spec) = delta.min_inline_size {
if vertical {
style.min_height = normalize_size_spec(spec, parent.min_height);
style.pending_min_height_var = None;
} else {
style.min_width = normalize_size_spec(spec, parent.min_width);
style.pending_min_width_var = None;
}
}
if let Some(spec) = delta.max_inline_size {
if vertical {
style.max_height = normalize_size_spec(spec, parent.max_height);
style.pending_max_height_var = None;
} else {
style.max_width = normalize_size_spec(spec, parent.max_width);
style.pending_max_width_var = None;
}
}
if let Some(spec) = delta.min_block_size {
if vertical {
style.min_width = normalize_size_spec(spec, parent.min_width);
style.pending_min_width_var = None;
} else {
style.min_height = normalize_size_spec(spec, parent.min_height);
style.pending_min_height_var = None;
}
}
if let Some(spec) = delta.max_block_size {
if vertical {
style.max_width = normalize_size_spec(spec, parent.max_width);
style.pending_max_width_var = None;
} else {
style.max_height = normalize_size_spec(spec, parent.max_height);
style.pending_max_height_var = None;
}
}
}
fn apply_logical_size_var_delta(style: &mut ComputedStyle, delta: &StyleDelta) {
let vertical = is_vertical_writing_mode(style.writing_mode);
if let Some(var) = &delta.inline_size_var {
if vertical {
style.pending_height_var = Some(var.clone());
} else {
style.pending_width_var = Some(var.clone());
}
}
if let Some(var) = &delta.block_size_var {
if vertical {
style.pending_width_var = Some(var.clone());
} else {
style.pending_height_var = Some(var.clone());
}
}
if let Some(var) = &delta.min_inline_size_var {
if vertical {
style.pending_min_height_var = Some(var.clone());
} else {
style.pending_min_width_var = Some(var.clone());
}
}
if let Some(var) = &delta.max_inline_size_var {
if vertical {
style.pending_max_height_var = Some(var.clone());
} else {
style.pending_max_width_var = Some(var.clone());
}
}
if let Some(var) = &delta.min_block_size_var {
if vertical {
style.pending_min_width_var = Some(var.clone());
} else {
style.pending_min_height_var = Some(var.clone());
}
}
if let Some(var) = &delta.max_block_size_var {
if vertical {
style.pending_max_width_var = Some(var.clone());
} else {
style.pending_max_height_var = Some(var.clone());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PhysicalSide {
Left,
Top,
Right,
Bottom,
}
fn inline_start_side(writing_mode: WritingModeMode, direction: DirectionMode) -> PhysicalSide {
match writing_mode {
WritingModeMode::HorizontalTb => match direction {
DirectionMode::Ltr => PhysicalSide::Left,
DirectionMode::Rtl => PhysicalSide::Right,
},
// Unlike the other vertical writing modes, sideways-lr runs its LTR
// inline axis bottom-to-top.
WritingModeMode::SidewaysLr => match direction {
DirectionMode::Ltr => PhysicalSide::Bottom,
DirectionMode::Rtl => PhysicalSide::Top,
},
WritingModeMode::VerticalRl | WritingModeMode::VerticalLr | WritingModeMode::SidewaysRl => {
match direction {
DirectionMode::Ltr => PhysicalSide::Top,
DirectionMode::Rtl => PhysicalSide::Bottom,
}
}
}
}
fn inline_end_side(writing_mode: WritingModeMode, direction: DirectionMode) -> PhysicalSide {
match inline_start_side(writing_mode, direction) {
PhysicalSide::Left => PhysicalSide::Right,
PhysicalSide::Right => PhysicalSide::Left,
PhysicalSide::Top => PhysicalSide::Bottom,
PhysicalSide::Bottom => PhysicalSide::Top,
}
}
fn block_start_side(writing_mode: WritingModeMode) -> PhysicalSide {
match writing_mode {
WritingModeMode::HorizontalTb => PhysicalSide::Top,
WritingModeMode::VerticalRl | WritingModeMode::SidewaysRl => PhysicalSide::Right,
WritingModeMode::VerticalLr | WritingModeMode::SidewaysLr => PhysicalSide::Left,
}
}
fn block_end_side(writing_mode: WritingModeMode) -> PhysicalSide {
match block_start_side(writing_mode) {
PhysicalSide::Left => PhysicalSide::Right,
PhysicalSide::Right => PhysicalSide::Left,
PhysicalSide::Top => PhysicalSide::Bottom,
PhysicalSide::Bottom => PhysicalSide::Top,
}
}
fn physical_corner_from_sides(first: PhysicalSide, second: PhysicalSide) -> PhysicalCorner {
match (first, second) {
(PhysicalSide::Top, PhysicalSide::Left) | (PhysicalSide::Left, PhysicalSide::Top) => {
PhysicalCorner::TopLeft
}
(PhysicalSide::Top, PhysicalSide::Right) | (PhysicalSide::Right, PhysicalSide::Top) => {
PhysicalCorner::TopRight
}
(PhysicalSide::Bottom, PhysicalSide::Right)
| (PhysicalSide::Right, PhysicalSide::Bottom) => PhysicalCorner::BottomRight,
(PhysicalSide::Bottom, PhysicalSide::Left) | (PhysicalSide::Left, PhysicalSide::Bottom) => {
PhysicalCorner::BottomLeft
}
_ => PhysicalCorner::TopLeft,
}
}
fn resolve_border_radius_corner_target(
target: BorderRadiusCornerTarget,
writing_mode: WritingModeMode,
direction: DirectionMode,
) -> PhysicalCorner {
match target {
BorderRadiusCornerTarget::Physical(corner) => corner,
BorderRadiusCornerTarget::StartStart => physical_corner_from_sides(
block_start_side(writing_mode),
inline_start_side(writing_mode, direction),
),
BorderRadiusCornerTarget::StartEnd => physical_corner_from_sides(
block_start_side(writing_mode),
inline_end_side(writing_mode, direction),
),
BorderRadiusCornerTarget::EndStart => physical_corner_from_sides(
block_end_side(writing_mode),
inline_start_side(writing_mode, direction),
),
BorderRadiusCornerTarget::EndEnd => physical_corner_from_sides(
block_end_side(writing_mode),
inline_end_side(writing_mode, direction),
),
}
}
fn set_border_radius_corner(
radii: &mut BorderRadiiSpec,
corner: PhysicalCorner,
value: BorderRadiusCornerValue,
) {
match corner {
PhysicalCorner::TopLeft => {
radii.horizontal.top_left = value.horizontal;
radii.vertical.top_left = value.vertical;
}
PhysicalCorner::TopRight => {
radii.horizontal.top_right = value.horizontal;
radii.vertical.top_right = value.vertical;
}
PhysicalCorner::BottomRight => {
radii.horizontal.bottom_right = value.horizontal;
radii.vertical.bottom_right = value.vertical;
}
PhysicalCorner::BottomLeft => {
radii.horizontal.bottom_left = value.horizontal;
radii.vertical.bottom_left = value.vertical;
}
}
}
fn apply_border_radius_ops(style: &mut ComputedStyle, ops: &[BorderRadiusOp]) {
for op in ops {
match *op {
BorderRadiusOp::Shorthand(radii) => style.border_radius = radii,
BorderRadiusOp::Corner(target, value) => {
let corner = resolve_border_radius_corner_target(
target,
style.writing_mode,
style.direction,
);
set_border_radius_corner(&mut style.border_radius, corner, value);
}
}
}
}
fn border_target_sides(
target: BorderColorTarget,
writing_mode: WritingModeMode,
direction: DirectionMode,
) -> Vec<PhysicalSide> {
match target {
BorderColorTarget::All => vec![
PhysicalSide::Top,
PhysicalSide::Right,
PhysicalSide::Bottom,
PhysicalSide::Left,
],
BorderColorTarget::Top => vec![PhysicalSide::Top],
BorderColorTarget::Right => vec![PhysicalSide::Right],
BorderColorTarget::Bottom => vec![PhysicalSide::Bottom],
BorderColorTarget::Left => vec![PhysicalSide::Left],
BorderColorTarget::InlineStart => vec![inline_start_side(writing_mode, direction)],
BorderColorTarget::InlineEnd => vec![inline_end_side(writing_mode, direction)],
BorderColorTarget::BlockStart => vec![block_start_side(writing_mode)],
BorderColorTarget::BlockEnd => vec![block_end_side(writing_mode)],
}
}
fn clear_pending_border_color_var_for_side(style: &mut ComputedStyle, side: PhysicalSide) {
match side {
PhysicalSide::Left => style.pending_border_left_color_var = None,
PhysicalSide::Top => style.pending_border_top_color_var = None,
PhysicalSide::Right => style.pending_border_right_color_var = None,
PhysicalSide::Bottom => style.pending_border_bottom_color_var = None,
}
}
fn apply_border_cascade_ops(
style: &mut ComputedStyle,
ops: &[BorderCascadeOp],
parent: &ComputedStyle,
) {
for op in ops {
match op {
BorderCascadeOp::Width(target, component) => {
for side in border_target_sides(*target, style.writing_mode, style.direction) {
match component {
NativeLengthComponent::Spec(spec) => apply_edge_to_side(
&mut style.border_width,
side,
*spec,
&parent.border_width,
),
NativeLengthComponent::Var(name) => apply_edge_var_to_side(
&mut style.border_width,
side,
&LengthVarExpr {
name: name.clone(),
scale: 1.0,
},
&parent.border_width,
&style.custom_lengths,
&style.custom_color_refs,
),
}
}
}
BorderCascadeOp::Style(target, border_style) => {
for side in border_target_sides(*target, style.writing_mode, style.direction) {
set_border_style_for_side(&mut style.border_style, side, *border_style);
}
}
BorderCascadeOp::Color(target, component) => {
for side in border_target_sides(*target, style.writing_mode, style.direction) {
match component {
NativeBorderColorComponent::Spec(color) => {
apply_border_color_to_side(style, side, color, parent);
clear_pending_border_color_var_for_side(style, side);
}
NativeBorderColorComponent::Var(var) => {
set_pending_border_color_var_for_side(style, side, var);
}
}
}
}
}
}
}
fn apply_edge_to_side(
target: &mut EdgeSizes,
side: PhysicalSide,
spec: LengthSpec,
parent: &EdgeSizes,
) {
match side {
PhysicalSide::Left => {
target.left = normalize_length_spec(spec, parent.left);
}
PhysicalSide::Top => {
target.top = normalize_length_spec(spec, parent.top);
}
PhysicalSide::Right => {
target.right = normalize_length_spec(spec, parent.right);
}
PhysicalSide::Bottom => {
target.bottom = normalize_length_spec(spec, parent.bottom);
}
}
}
fn apply_logical_box_edge_delta(
target: &mut EdgeSizes,
parent: &EdgeSizes,
inline_start: Option<LengthSpec>,
inline_end: Option<LengthSpec>,
block_start: Option<LengthSpec>,
block_end: Option<LengthSpec>,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if let Some(spec) = inline_start {
apply_edge_to_side(
target,
inline_start_side(writing_mode, direction),
spec,
parent,
);
}
if let Some(spec) = inline_end {
apply_edge_to_side(
target,
inline_end_side(writing_mode, direction),
spec,
parent,
);
}
if let Some(spec) = block_start {
apply_edge_to_side(target, block_start_side(writing_mode), spec, parent);
}
if let Some(spec) = block_end {
apply_edge_to_side(target, block_end_side(writing_mode), spec, parent);
}
}
fn apply_edge_var_to_side(
target: &mut EdgeSizes,
side: PhysicalSide,
expr: &LengthVarExpr,
parent: &EdgeSizes,
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
) {
if let Some(spec) = resolve_custom_length_from_maps(custom_lengths, custom_refs, &expr.name) {
apply_edge_to_side(target, side, scale_length_spec(spec, expr.scale), parent);
}
}
fn apply_logical_box_edge_var_delta(
target: &mut EdgeSizes,
parent: &EdgeSizes,
inline_start: &Option<LengthVarExpr>,
inline_end: &Option<LengthVarExpr>,
block_start: &Option<LengthVarExpr>,
block_end: &Option<LengthVarExpr>,
writing_mode: WritingModeMode,
direction: DirectionMode,
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
) {
if let Some(expr) = inline_start {
apply_edge_var_to_side(
target,
inline_start_side(writing_mode, direction),
expr,
parent,
custom_lengths,
custom_refs,
);
}
if let Some(expr) = inline_end {
apply_edge_var_to_side(
target,
inline_end_side(writing_mode, direction),
expr,
parent,
custom_lengths,
custom_refs,
);
}
if let Some(expr) = block_start {
apply_edge_var_to_side(
target,
block_start_side(writing_mode),
expr,
parent,
custom_lengths,
custom_refs,
);
}
if let Some(expr) = block_end {
apply_edge_var_to_side(
target,
block_end_side(writing_mode),
expr,
parent,
custom_lengths,
custom_refs,
);
}
}
fn apply_inset_to_side(
style: &mut ComputedStyle,
side: PhysicalSide,
spec: LengthSpec,
parent: &ComputedStyle,
) {
match side {
PhysicalSide::Left => {
style.inset_left = normalize_size_spec(spec, parent.inset_left);
style.pending_inset_left_var = None;
}
PhysicalSide::Top => {
style.inset_top = normalize_size_spec(spec, parent.inset_top);
style.pending_inset_top_var = None;
}
PhysicalSide::Right => {
style.inset_right = normalize_size_spec(spec, parent.inset_right);
style.pending_inset_right_var = None;
}
PhysicalSide::Bottom => {
style.inset_bottom = normalize_size_spec(spec, parent.inset_bottom);
style.pending_inset_bottom_var = None;
}
}
}
fn apply_logical_inset_delta(
style: &mut ComputedStyle,
delta: &StyleDelta,
parent: &ComputedStyle,
) {
if let Some(spec) = delta.inset_inline_start {
apply_inset_to_side(
style,
inline_start_side(style.writing_mode, style.direction),
spec,
parent,
);
}
if let Some(spec) = delta.inset_inline_end {
apply_inset_to_side(
style,
inline_end_side(style.writing_mode, style.direction),
spec,
parent,
);
}
if let Some(spec) = delta.inset_block_start {
apply_inset_to_side(style, block_start_side(style.writing_mode), spec, parent);
}
if let Some(spec) = delta.inset_block_end {
apply_inset_to_side(style, block_end_side(style.writing_mode), spec, parent);
}
if let Some(value) = &delta.inset_inline_start_var {
set_pending_inset_var_for_side(
style,
inline_start_side(style.writing_mode, style.direction),
value.clone(),
);
}
if let Some(value) = &delta.inset_inline_end_var {
set_pending_inset_var_for_side(
style,
inline_end_side(style.writing_mode, style.direction),
value.clone(),
);
}
if let Some(value) = &delta.inset_block_start_var {
set_pending_inset_var_for_side(style, block_start_side(style.writing_mode), value.clone());
}
if let Some(value) = &delta.inset_block_end_var {
set_pending_inset_var_for_side(style, block_end_side(style.writing_mode), value.clone());
}
}
fn set_pending_inset_var_for_side(style: &mut ComputedStyle, side: PhysicalSide, value: String) {
match side {
PhysicalSide::Left => style.pending_inset_left_var = Some(value),
PhysicalSide::Top => style.pending_inset_top_var = Some(value),
PhysicalSide::Right => style.pending_inset_right_var = Some(value),
PhysicalSide::Bottom => style.pending_inset_bottom_var = Some(value),
}
}
fn set_border_style_for_side(
target: &mut BorderStyleState,
side: PhysicalSide,
style: BorderLineStyle,
) {
match side {
PhysicalSide::Left => target.left = style,
PhysicalSide::Top => target.top = style,
PhysicalSide::Right => target.right = style,
PhysicalSide::Bottom => target.bottom = style,
}
}
fn apply_logical_border_style_delta(
target: &mut BorderStyleState,
inline_start: Option<BorderLineStyle>,
inline_end: Option<BorderLineStyle>,
block_start: Option<BorderLineStyle>,
block_end: Option<BorderLineStyle>,
writing_mode: WritingModeMode,
direction: DirectionMode,
) {
if let Some(style) = inline_start {
set_border_style_for_side(target, inline_start_side(writing_mode, direction), style);
}
if let Some(style) = inline_end {
set_border_style_for_side(target, inline_end_side(writing_mode, direction), style);
}
if let Some(style) = block_start {
set_border_style_for_side(target, block_start_side(writing_mode), style);
}
if let Some(style) = block_end {
set_border_style_for_side(target, block_end_side(writing_mode), style);
}
}
fn apply_edge_var_delta(
target: &mut EdgeSizes,
delta: &EdgeDelta,
parent: &EdgeSizes,
custom_lengths: &HashMap<String, LengthSpec>,
custom_refs: &HashMap<String, String>,
) {
if let Some(expr) = &delta.top_var {
if let Some(spec) = resolve_custom_length_from_maps(custom_lengths, custom_refs, &expr.name)
{
target.top = normalize_length_spec(scale_length_spec(spec, expr.scale), parent.top);
}
}
if let Some(expr) = &delta.right_var {
if let Some(spec) = resolve_custom_length_from_maps(custom_lengths, custom_refs, &expr.name)
{
target.right = normalize_length_spec(scale_length_spec(spec, expr.scale), parent.right);
}
}
if let Some(expr) = &delta.bottom_var {
if let Some(spec) = resolve_custom_length_from_maps(custom_lengths, custom_refs, &expr.name)
{
target.bottom =
normalize_length_spec(scale_length_spec(spec, expr.scale), parent.bottom);
}
}
if let Some(expr) = &delta.left_var {
if let Some(spec) = resolve_custom_length_from_maps(custom_lengths, custom_refs, &expr.name)
{
target.left = normalize_length_spec(scale_length_spec(spec, expr.scale), parent.left);
}
}
}
fn resolve_border_side_color_spec(
spec: &ColorSpec,
parent: &ComputedStyle,
computed_color: Color,
target: BorderColorTarget,
) -> (Color, f32) {
match spec {
ColorSpec::Value(value) => (*value, 1.0),
ColorSpec::AlphaValue(value, alpha) => (*value, alpha.clamp(0.0, 1.0)),
ColorSpec::Inherit => match target {
BorderColorTarget::Right => (
parent
.border_right_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_right_alpha,
),
BorderColorTarget::Left => (
parent
.border_left_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_left_alpha,
),
BorderColorTarget::Top => (
parent
.border_top_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_top_alpha,
),
BorderColorTarget::Bottom => (
parent
.border_bottom_color
.or(parent.border_color)
.unwrap_or(parent.color),
parent.border_bottom_alpha,
),
BorderColorTarget::All
| BorderColorTarget::InlineStart
| BorderColorTarget::InlineEnd
| BorderColorTarget::BlockStart
| BorderColorTarget::BlockEnd => (
parent.border_color.unwrap_or(parent.color),
parent.border_top_alpha,
),
},
ColorSpec::Initial => (Color::BLACK, 1.0),
ColorSpec::CurrentColor => (computed_color, 1.0),
}
}
fn border_color_target_from_side(side: PhysicalSide) -> BorderColorTarget {
match side {
PhysicalSide::Left => BorderColorTarget::Left,
PhysicalSide::Top => BorderColorTarget::Top,
PhysicalSide::Right => BorderColorTarget::Right,
PhysicalSide::Bottom => BorderColorTarget::Bottom,
}
}
fn set_border_color_for_side(style: &mut ComputedStyle, side: PhysicalSide, color: Color) {
match side {
PhysicalSide::Left => style.border_left_color = Some(color),
PhysicalSide::Top => style.border_top_color = Some(color),
PhysicalSide::Right => style.border_right_color = Some(color),
PhysicalSide::Bottom => style.border_bottom_color = Some(color),
}
}
fn set_border_alpha_for_side(style: &mut ComputedStyle, side: PhysicalSide, alpha: f32) {
let alpha = alpha.clamp(0.0, 1.0);
match side {
PhysicalSide::Left => style.border_left_alpha = alpha,
PhysicalSide::Top => style.border_top_alpha = alpha,
PhysicalSide::Right => style.border_right_alpha = alpha,
PhysicalSide::Bottom => style.border_bottom_alpha = alpha,
}
}
fn set_pending_border_color_var_for_side(style: &mut ComputedStyle, side: PhysicalSide, var: &str) {
match side {
PhysicalSide::Left => style.pending_border_left_color_var = Some(var.to_string()),
PhysicalSide::Top => style.pending_border_top_color_var = Some(var.to_string()),
PhysicalSide::Right => style.pending_border_right_color_var = Some(var.to_string()),
PhysicalSide::Bottom => style.pending_border_bottom_color_var = Some(var.to_string()),
}
}
fn apply_border_color_to_side(
style: &mut ComputedStyle,
side: PhysicalSide,
color: &ColorSpec,
parent: &ComputedStyle,
) {
let (resolved, alpha) = resolve_border_side_color_spec(
color,
parent,
style.color,
border_color_target_from_side(side),
);
set_border_color_for_side(style, side, resolved);
set_border_alpha_for_side(style, side, alpha);
}
fn apply_logical_border_color_delta(
style: &mut ComputedStyle,
inline_start: Option<&ColorSpec>,
inline_end: Option<&ColorSpec>,
block_start: Option<&ColorSpec>,
block_end: Option<&ColorSpec>,
parent: &ComputedStyle,
) {
if let Some(color) = inline_start {
apply_border_color_to_side(
style,
inline_start_side(style.writing_mode, style.direction),
color,
parent,
);
}
if let Some(color) = inline_end {
apply_border_color_to_side(
style,
inline_end_side(style.writing_mode, style.direction),
color,
parent,
);
}
if let Some(color) = block_start {
apply_border_color_to_side(style, block_start_side(style.writing_mode), color, parent);
}
if let Some(color) = block_end {
apply_border_color_to_side(style, block_end_side(style.writing_mode), color, parent);
}
}
fn apply_logical_border_color_var_delta(
style: &mut ComputedStyle,
inline_start: Option<&String>,
inline_end: Option<&String>,
block_start: Option<&String>,
block_end: Option<&String>,
) {
if let Some(var) = inline_start {
set_pending_border_color_var_for_side(
style,
inline_start_side(style.writing_mode, style.direction),
var,
);
}
if let Some(var) = inline_end {
set_pending_border_color_var_for_side(
style,
inline_end_side(style.writing_mode, style.direction),
var,
);
}
if let Some(var) = block_start {
set_pending_border_color_var_for_side(style, block_start_side(style.writing_mode), var);
}
if let Some(var) = block_end {
set_pending_border_color_var_for_side(style, block_end_side(style.writing_mode), var);
}
}
fn normalize_length_spec(spec: LengthSpec, inherited: LengthSpec) -> LengthSpec {
let spec = match spec {
LengthSpec::Inherit => inherited,
LengthSpec::Initial => LengthSpec::Absolute(Pt::ZERO),
_ => spec,
};
quantize_length_spec(spec)
}
fn normalize_tab_size_spec(spec: TabSizeSpec, inherited: TabSizeSpec) -> TabSizeSpec {
match spec {
TabSizeSpec::Inherit => inherited,
TabSizeSpec::Initial => TabSizeSpec::initial(),
TabSizeSpec::Spaces(spaces) if spaces.is_finite() => TabSizeSpec::Spaces(spaces.max(0.0)),
TabSizeSpec::Spaces(_) => TabSizeSpec::initial(),
TabSizeSpec::Length(length) => TabSizeSpec::Length(quantize_length_spec(length)),
}
}
fn normalize_size_spec(spec: LengthSpec, inherited: LengthSpec) -> LengthSpec {
let spec = match spec {
LengthSpec::Inherit => inherited,
LengthSpec::Initial => LengthSpec::Auto,
_ => spec,
};
quantize_length_spec(spec)
}
fn quantize_length_spec(spec: LengthSpec) -> LengthSpec {
match spec {
LengthSpec::Absolute(value) => LengthSpec::Absolute(value),
_ => spec,
}
}
fn set_delta_logical_inline_edge_spec(
delta: &mut StyleDelta,
is_margin: bool,
start: Option<LengthSpec>,
end: Option<LengthSpec>,
) {
if is_margin {
if let Some(value) = start {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::InlineStart);
delta.margin_inline_start = Some(value);
delta.margin_inline_start_var = None;
}
if let Some(value) = end {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::InlineEnd);
delta.margin_inline_end = Some(value);
delta.margin_inline_end_var = None;
}
} else {
if let Some(value) = start {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::InlineStart);
delta.padding_inline_start = Some(value);
delta.padding_inline_start_var = None;
}
if let Some(value) = end {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::InlineEnd);
delta.padding_inline_end = Some(value);
delta.padding_inline_end_var = None;
}
}
}
fn set_delta_logical_inline_edge_var(
delta: &mut StyleDelta,
is_margin: bool,
start: Option<LengthVarExpr>,
end: Option<LengthVarExpr>,
) {
if is_margin {
if let Some(value) = start {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::InlineStart);
delta.margin_inline_start_var = Some(value);
delta.margin_inline_start = None;
}
if let Some(value) = end {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::InlineEnd);
delta.margin_inline_end_var = Some(value);
delta.margin_inline_end = None;
}
} else {
if let Some(value) = start {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::InlineStart);
delta.padding_inline_start_var = Some(value);
delta.padding_inline_start = None;
}
if let Some(value) = end {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::InlineEnd);
delta.padding_inline_end_var = Some(value);
delta.padding_inline_end = None;
}
}
}
fn set_delta_logical_block_edge_spec(
delta: &mut StyleDelta,
is_margin: bool,
start: Option<LengthSpec>,
end: Option<LengthSpec>,
) {
if is_margin {
if let Some(value) = start {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::BlockStart);
delta.margin_block_start = Some(value);
delta.margin_block_start_var = None;
}
if let Some(value) = end {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::BlockEnd);
delta.margin_block_end = Some(value);
delta.margin_block_end_var = None;
}
} else {
if let Some(value) = start {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::BlockStart);
delta.padding_block_start = Some(value);
delta.padding_block_start_var = None;
}
if let Some(value) = end {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::BlockEnd);
delta.padding_block_end = Some(value);
delta.padding_block_end_var = None;
}
}
}
fn set_delta_logical_block_edge_var(
delta: &mut StyleDelta,
is_margin: bool,
start: Option<LengthVarExpr>,
end: Option<LengthVarExpr>,
) {
if is_margin {
if let Some(value) = start {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::BlockStart);
delta.margin_block_start_var = Some(value);
delta.margin_block_start = None;
}
if let Some(value) = end {
delta
.revert_layer
.margin_logical
.clear(LogicalBoxEdgeTarget::BlockEnd);
delta.margin_block_end_var = Some(value);
delta.margin_block_end = None;
}
} else {
if let Some(value) = start {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::BlockStart);
delta.padding_block_start_var = Some(value);
delta.padding_block_start = None;
}
if let Some(value) = end {
delta
.revert_layer
.padding_logical
.clear(LogicalBoxEdgeTarget::BlockEnd);
delta.padding_block_end_var = Some(value);
delta.padding_block_end = None;
}
}
}
#[derive(Debug, Clone)]
struct FontShorthandToken {
text: String,
end: usize,
}
const FONT_SHORTHAND_VAR_PREFIX: &str = "__fullbleed_font_shorthand__:";
fn font_shorthand_var_expr(raw: &str) -> String {
format!("{FONT_SHORTHAND_VAR_PREFIX}{}", raw.trim())
}
fn font_shorthand_var_raw(expr: &str) -> Option<&str> {
expr.strip_prefix(FONT_SHORTHAND_VAR_PREFIX)
}
fn split_font_shorthand_tokens(raw: &str) -> Vec<FontShorthandToken> {
let mut out = Vec::new();
let mut start: Option<usize> = None;
let mut quote: Option<char> = None;
let mut escaped = false;
let mut depth = 0usize;
for (idx, ch) in raw.char_indices() {
if start.is_none() {
if ch.is_whitespace() {
continue;
}
start = Some(idx);
}
if let Some(quote_char) = quote {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == quote_char {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => quote = Some(ch),
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
c if c.is_whitespace() && depth == 0 => {
if let Some(token_start) = start.take() {
if token_start < idx {
out.push(FontShorthandToken {
text: raw[token_start..idx].to_string(),
end: idx,
});
}
}
}
_ => {}
}
}
if let Some(token_start) = start {
if token_start < raw.len() {
out.push(FontShorthandToken {
text: raw[token_start..].to_string(),
end: raw.len(),
});
}
}
out
}
fn is_system_font_keyword(raw: &str) -> bool {
matches!(
raw,
"caption" | "icon" | "menu" | "message-box" | "small-caption" | "status-bar"
)
}
fn is_font_stretch_keyword(raw: &str) -> bool {
matches!(
raw,
"ultra-condensed"
| "extra-condensed"
| "condensed"
| "semi-condensed"
| "semi-expanded"
| "expanded"
| "extra-expanded"
| "ultra-expanded"
)
}
fn apply_font_shorthand_inherit(delta: &mut StyleDelta) {
delta.font_size = Some(FontSizeSpec::Inherit);
delta.font_size_var = None;
delta.font_size_adjust = Some(FontSizeAdjustSpec::Inherit);
delta.font_kerning = Some(FontFeatureToggleSpec::Inherit);
delta.common_ligatures = Some(FontFeatureToggleSpec::Inherit);
delta.small_caps = Some(FontFeatureToggleSpec::Inherit);
delta.font_stretch = Some(FontStretchSpec::Inherit);
delta.line_height = Some(LineHeightSpec::Inherit);
delta.line_height_var = None;
delta.font_weight = Some(FontWeightSpec::Inherit);
delta.font_weight_var = None;
delta.font_style = None;
delta.font_style_var = None;
delta.font_name = Some(FontSpec::Inherit);
delta.font_name_var = None;
}
fn apply_font_shorthand_initial(delta: &mut StyleDelta) {
delta.font_size = Some(FontSizeSpec::Initial);
delta.font_size_var = None;
delta.font_size_adjust = Some(FontSizeAdjustSpec::Initial);
delta.font_kerning = Some(FontFeatureToggleSpec::Initial);
delta.common_ligatures = Some(FontFeatureToggleSpec::Initial);
delta.small_caps = Some(FontFeatureToggleSpec::Initial);
delta.font_stretch = Some(FontStretchSpec::Initial);
delta.line_height = Some(LineHeightSpec::Initial);
delta.line_height_var = None;
delta.font_weight = Some(FontWeightSpec::Initial);
delta.font_weight_var = None;
delta.font_style = Some(FontStyleMode::Normal);
delta.font_style_var = None;
delta.font_name = Some(FontSpec::Initial);
delta.font_name_var = None;
}
fn parse_font_shorthand_prefix(
tokens: &[FontShorthandToken],
) -> Option<(FontStyleMode, FontWeightSpec)> {
let mut style = FontStyleMode::Normal;
let mut weight = FontWeightSpec::Initial;
let mut i = 0usize;
while i < tokens.len() {
let raw = tokens[i].text.trim();
let lowered = raw.to_ascii_lowercase();
if lowered == "normal" || lowered == "small-caps" || is_font_stretch_keyword(&lowered) {
i += 1;
continue;
}
if lowered == "oblique" {
if let Some(next) = tokens.get(i + 1) {
let combined = format!("oblique {}", next.text.trim());
if let Some(mode) = parse_font_style_str(&combined) {
style = mode;
i += 2;
continue;
}
}
}
if let Some(mode) = parse_font_style_str(&lowered) {
style = mode;
i += 1;
continue;
}
if let Some(spec) = parse_font_weight_spec_str(&lowered) {
weight = spec;
i += 1;
continue;
}
return None;
}
Some((style, weight))
}
fn parse_font_shorthand_size_spec(raw: &str) -> Option<FontSizeSpec> {
let trimmed = raw.trim();
if trimmed.parse::<f32>().is_ok() {
return None;
}
parse_font_size_spec_str(&trimmed.to_ascii_lowercase())
}
fn parse_font_shorthand_raw(
raw: &str,
) -> Option<(
FontStyleMode,
FontWeightSpec,
FontSizeSpec,
LineHeightSpec,
FontSpec,
)> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let lowered = trimmed.to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => {
return Some((
FontStyleMode::Normal,
FontWeightSpec::Inherit,
FontSizeSpec::Inherit,
LineHeightSpec::Inherit,
FontSpec::Inherit,
));
}
"initial" => {
return Some((
FontStyleMode::Normal,
FontWeightSpec::Initial,
FontSizeSpec::Initial,
LineHeightSpec::Initial,
FontSpec::Initial,
));
}
value if is_system_font_keyword(value) => {
return Some((
FontStyleMode::Normal,
FontWeightSpec::Initial,
FontSizeSpec::Initial,
LineHeightSpec::Initial,
FontSpec::Initial,
));
}
_ => {}
}
let tokens = split_font_shorthand_tokens(trimmed);
for (idx, token) in tokens.iter().enumerate() {
let parsed = if let Some((size_raw, line_raw)) = split_top_level_slash_once(&token.text) {
match (
parse_font_shorthand_size_spec(size_raw),
parse_line_height_spec_str(&line_raw.to_ascii_lowercase()),
) {
(Some(size), Some(line_height)) => Some((size, line_height, token.end)),
_ => None,
}
} else if let Some(size_raw) = token.text.strip_suffix('/') {
if let (Some(size), Some(line_token)) = (
parse_font_shorthand_size_spec(size_raw),
tokens.get(idx + 1),
) {
parse_line_height_spec_str(&line_token.text.trim().to_ascii_lowercase())
.map(|line_height| (size, line_height, line_token.end))
} else {
None
}
} else {
if let Some(size) = parse_font_shorthand_size_spec(&token.text) {
let mut line_height = LineHeightSpec::Normal;
let mut family_start = token.end;
if let Some(next) = tokens.get(idx + 1) {
if next.text == "/" {
let line_token = tokens.get(idx + 2)?;
let parsed_line_height = parse_line_height_spec_str(
&line_token.text.trim().to_ascii_lowercase(),
)?;
line_height = parsed_line_height;
family_start = line_token.end;
} else if let Some(line_raw) = next.text.strip_prefix('/') {
let parsed_line_height =
parse_line_height_spec_str(&line_raw.trim().to_ascii_lowercase())?;
line_height = parsed_line_height;
family_start = next.end;
}
}
Some((size, line_height, family_start))
} else {
None
}
};
let Some((font_size, line_height, family_start)) = parsed else {
continue;
};
let (style, weight) = parse_font_shorthand_prefix(&tokens[..idx])?;
let family_raw = trimmed[family_start..].trim();
let family = font_spec_from_raw_string(family_raw)?;
return Some((style, weight, font_size, line_height, family));
}
None
}
fn apply_font_shorthand_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let Some((style, weight, size, line_height, family)) = parse_font_shorthand_raw(trimmed) else {
if trimmed.to_ascii_lowercase().contains("var(") {
let expr = font_shorthand_var_expr(trimmed);
delta.font_style = None;
delta.font_style_var = Some(expr.clone());
delta.font_weight = None;
delta.font_weight_var = Some(expr.clone());
delta.font_size = None;
delta.font_size_var = Some(expr.clone());
delta.line_height = None;
delta.line_height_var = Some(expr.clone());
delta.font_name = None;
delta.font_name_var = Some(expr);
return true;
}
return false;
};
match (&size, &line_height, &weight, &family) {
(
FontSizeSpec::Inherit,
LineHeightSpec::Inherit,
FontWeightSpec::Inherit,
FontSpec::Inherit,
) => {
apply_font_shorthand_inherit(delta);
return true;
}
(
FontSizeSpec::Initial,
LineHeightSpec::Initial,
FontWeightSpec::Initial,
FontSpec::Initial,
) => {
apply_font_shorthand_initial(delta);
return true;
}
_ => {}
}
delta.font_style = Some(style);
delta.font_style_var = None;
delta.font_weight = Some(weight);
delta.font_weight_var = None;
delta.font_size = Some(size);
delta.font_size_var = None;
delta.font_size_adjust = Some(FontSizeAdjustSpec::Initial);
delta.font_kerning = Some(FontFeatureToggleSpec::Initial);
delta.common_ligatures = Some(FontFeatureToggleSpec::Initial);
delta.small_caps = Some(FontFeatureToggleSpec::Initial);
delta.font_stretch = Some(FontStretchSpec::Initial);
delta.line_height = Some(line_height);
delta.line_height_var = None;
delta.font_name = Some(family);
delta.font_name_var = None;
true
}
fn apply_font_family_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
if lowered.contains("var(") {
delta.font_name = None;
delta.font_name_var = None;
delta.font_name_var = Some(trimmed.to_string());
return true;
}
if let Some(spec) = font_spec_from_raw_string(trimmed) {
delta.font_name = None;
delta.font_name_var = None;
delta.font_name = Some(spec);
return true;
}
false
}
fn apply_font_size_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
if let Some(spec) = parse_font_size_spec_str(&lowered) {
delta.font_size = Some(spec);
delta.font_size_var = None;
return true;
}
if lowered.contains("var(") {
delta.font_size = None;
delta.font_size_var = Some(lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
delta.font_size = None;
delta.font_size_var = Some(var);
return true;
}
false
}
fn apply_font_size_adjust_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"none" => FontSizeAdjustSpec::None,
"inherit" | "unset" | "revert" => FontSizeAdjustSpec::Inherit,
"initial" => FontSizeAdjustSpec::Initial,
// Full revert-layer bookkeeping is unnecessary for a declaration that
// contributes no value in the current layer.
"revert-layer" => return true,
_ => {
let Ok(value) = lowered.parse::<f32>() else {
return false;
};
if !value.is_finite() || value < 0.0 {
return false;
}
FontSizeAdjustSpec::Number(I32F32::from_num(value))
}
};
delta.font_size_adjust = Some(spec);
true
}
fn font_feature_cascade_spec(raw: &str) -> Option<FontFeatureToggleSpec> {
match raw.trim().to_ascii_lowercase().as_str() {
"inherit" | "unset" | "revert" => Some(FontFeatureToggleSpec::Inherit),
"initial" => Some(FontFeatureToggleSpec::Initial),
"revert-layer" => None,
_ => None,
}
}
fn apply_font_kerning_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"auto" | "normal" => FontFeatureToggleSpec::Value(true),
"none" => FontFeatureToggleSpec::Value(false),
"revert-layer" => return true,
_ => match font_feature_cascade_spec(&lowered) {
Some(spec) => spec,
None => return false,
},
};
delta.font_kerning = Some(spec);
true
}
fn apply_font_stretch_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"inherit" | "unset" | "revert" => FontStretchSpec::Inherit,
"initial" => FontStretchSpec::Initial,
"revert-layer" => return true,
_ => match native_font_stretch_value(&lowered) {
Some(value) => FontStretchSpec::Value(value),
None => return false,
},
};
delta.font_stretch = Some(spec);
true
}
fn apply_font_variant_ligatures_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = if matches!(lowered.as_str(), "none" | "no-common-ligatures") {
FontFeatureToggleSpec::Value(false)
} else if lowered == "normal"
|| lowered
.split_whitespace()
.any(|part| part == "common-ligatures")
{
FontFeatureToggleSpec::Value(true)
} else if lowered == "revert-layer" {
return true;
} else {
match font_feature_cascade_spec(&lowered) {
Some(spec) => spec,
None => return false,
}
};
delta.common_ligatures = Some(spec);
true
}
fn apply_font_variant_caps_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
let spec = match lowered.as_str() {
"small-caps" => FontFeatureToggleSpec::Value(true),
"normal" => FontFeatureToggleSpec::Value(false),
"revert-layer" => return true,
_ => match font_feature_cascade_spec(&lowered) {
Some(spec) => spec,
None => return false,
},
};
delta.small_caps = Some(spec);
true
}
fn apply_font_variant_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"normal" => {
delta.small_caps = Some(FontFeatureToggleSpec::Value(false));
delta.common_ligatures = Some(FontFeatureToggleSpec::Value(true));
true
}
"small-caps" => {
delta.small_caps = Some(FontFeatureToggleSpec::Value(true));
true
}
"inherit" | "unset" | "revert" => {
delta.small_caps = Some(FontFeatureToggleSpec::Inherit);
delta.common_ligatures = Some(FontFeatureToggleSpec::Inherit);
true
}
"initial" => {
delta.small_caps = Some(FontFeatureToggleSpec::Initial);
delta.common_ligatures = Some(FontFeatureToggleSpec::Initial);
true
}
"revert-layer" => true,
_ => false,
}
}
fn apply_font_feature_settings_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
if lowered == "normal" {
delta.common_ligatures = Some(FontFeatureToggleSpec::Value(true));
return true;
}
if lowered == "revert-layer" {
return true;
}
if let Some(spec) = font_feature_cascade_spec(&lowered) {
delta.common_ligatures = Some(spec);
return true;
}
let normal = lowered.replace('\'', "\"");
if normal.contains("\"liga\"") {
let enabled = !(normal.contains("\"liga\" 0") || normal.contains("\"liga\" off"));
delta.common_ligatures = Some(FontFeatureToggleSpec::Value(enabled));
return true;
}
false
}
fn apply_line_height_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
if let Some(spec) = parse_line_height_spec_str(&lowered) {
delta.line_height = Some(spec);
delta.line_height_var = None;
return true;
}
if lowered.contains("var(") {
delta.line_height = None;
delta.line_height_var = Some(lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
delta.line_height = None;
delta.line_height_var = Some(var);
return true;
}
false
}
fn apply_font_weight_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
if let Some(spec) = parse_font_weight_spec_str(&lowered) {
delta.font_weight = Some(spec);
delta.font_weight_var = None;
return true;
}
if lowered.contains("var(") {
delta.font_weight = None;
delta.font_weight_var = Some(lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
delta.font_weight = None;
delta.font_weight_var = Some(var);
return true;
}
false
}
fn apply_font_synthesis_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"none" => {
delta.font_synthesis_weight = Some(false);
true
}
"auto" | "weight" | "style" | "small-caps" | "position" | "initial" => {
delta.font_synthesis_weight = Some(true);
true
}
"inherit" | "unset" | "revert" | "revert-layer" => true,
_ => {
let tokens: Vec<&str> = lowered.split_ascii_whitespace().collect();
if tokens
.iter()
.all(|token| matches!(*token, "weight" | "style" | "small-caps" | "position"))
{
delta.font_synthesis_weight = Some(tokens.contains(&"weight"));
true
} else {
false
}
}
}
}
fn apply_font_style_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" | "revert-layer" => {
delta.font_style = None;
delta.font_style_var = None;
return true;
}
"initial" => {
delta.font_style = Some(FontStyleMode::Normal);
delta.font_style_var = None;
return true;
}
_ => {}
}
if let Some(mode) = parse_font_style_str(&lowered) {
delta.font_style = Some(mode);
delta.font_style_var = None;
return true;
}
if lowered.contains("var(") {
delta.font_style = None;
delta.font_style_var = Some(lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
delta.font_style = None;
delta.font_style_var = Some(var);
return true;
}
false
}
fn apply_vertical_align_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
if lowered == "revert-layer" {
return set_revert_layer_vertical_align(delta);
}
if lowered == "inherit" {
set_delta_vertical_align_var(delta, lowered);
return true;
}
if let Some(mode) = parse_vertical_align_mode_str(&lowered) {
set_delta_vertical_align(delta, mode);
return true;
}
if lowered.contains("var(") {
set_delta_vertical_align_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_vertical_align_var(delta, var);
return true;
}
false
}
fn apply_white_space_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_white_space(delta, WhiteSpaceSpec::Inherit);
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_white_space_shorthand(delta),
"initial" => {
set_delta_white_space(delta, WhiteSpaceSpec::Initial);
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Initial);
return true;
}
_ => {}
}
if let Some((white_space, text_wrap_mode)) = parse_white_space_shorthand_str(&lowered) {
set_delta_white_space(delta, WhiteSpaceSpec::Value(white_space));
set_delta_text_wrap_mode(delta, TextWrapModeSpec::Value(text_wrap_mode));
return true;
}
if lowered.contains("var(") {
let expr = white_space_shorthand_var_expr(&lowered);
set_delta_white_space_var(delta, expr.clone());
set_delta_text_wrap_mode_var(delta, expr);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
let expr = white_space_shorthand_var_expr(&var);
set_delta_white_space_var(delta, expr.clone());
set_delta_text_wrap_mode_var(delta, expr);
return true;
}
false
}
fn apply_white_space_collapse_from_raw(delta: &mut StyleDelta, raw: &str) -> bool {
let trimmed = raw.trim();
let lowered = trimmed.to_ascii_lowercase();
match lowered.as_str() {
"inherit" | "unset" | "revert" => {
set_delta_white_space(delta, WhiteSpaceSpec::Inherit);
return true;
}
"revert-layer" => return set_revert_layer_white_space(delta),
"initial" => {
set_delta_white_space(delta, WhiteSpaceSpec::Initial);
return true;
}
_ => {}
}
if let Some(value) = parse_white_space_collapse_mode_str(&lowered) {
set_delta_white_space(delta, WhiteSpaceSpec::Value(value));
return true;
}
if lowered.contains("var(") {
set_delta_white_space_var(delta, lowered);
return true;
}
if let Some(var) = var_name_from_string(&lowered) {
set_delta_white_space_var(delta, var);
return true;
}
false
}
impl StyleDelta {
fn is_empty(&self) -> bool {
self.all.is_none()
&& self.background_reset.is_none()
&& self.revert_layer.is_empty()
&& self.font_size.is_none()
&& self.font_size_var.is_none()
&& self.font_size_adjust.is_none()
&& self.line_height.is_none()
&& self.line_height_var.is_none()
&& self.color.is_none()
&& self.background_color.is_none()
&& self.color_var.is_none()
&& self.background_color_var.is_none()
&& self.border_color_var.is_none()
&& self.border_top_color_var.is_none()
&& self.border_right_color_var.is_none()
&& self.border_bottom_color_var.is_none()
&& self.border_left_color_var.is_none()
&& self.width.is_none()
&& self.height.is_none()
&& self.width_var.is_none()
&& self.height_var.is_none()
&& self.max_width.is_none()
&& self.min_width.is_none()
&& self.min_height.is_none()
&& self.max_height.is_none()
&& self.aspect_ratio.is_none()
&& self.min_width_var.is_none()
&& self.min_height_var.is_none()
&& self.max_height_var.is_none()
&& self.max_width_var.is_none()
&& self.text_align.is_none()
&& self.text_align_last.is_none()
&& self.text_justify.is_none()
&& self.text_justify_var.is_none()
&& self.hyphens.is_none()
&& self.hyphens_var.is_none()
&& self.hyphenate_character.is_none()
&& self.hyphenate_character_var.is_none()
&& self.text_wrap_mode.is_none()
&& self.text_wrap_mode_var.is_none()
&& self.text_wrap_style.is_none()
&& self.text_wrap_style_var.is_none()
&& self.direction.is_none()
&& self.writing_mode.is_none()
&& self.text_orientation.is_none()
&& self.vertical_align.is_none()
&& self.vertical_align_var.is_none()
&& self.font_weight.is_none()
&& self.font_weight_var.is_none()
&& self.font_kerning.is_none()
&& self.common_ligatures.is_none()
&& self.small_caps.is_none()
&& self.font_stretch.is_none()
&& self.font_synthesis_weight.is_none()
&& self.font_style.is_none()
&& self.font_style_var.is_none()
&& self.text_transform.is_none()
&& self.transform.is_none()
&& self.transform_var.is_none()
&& self.translate.is_none()
&& self.translate_var.is_none()
&& self.rotate.is_none()
&& self.rotate_var.is_none()
&& self.scale.is_none()
&& self.scale_var.is_none()
&& self.transform_origin.is_none()
&& self.transform_box.is_none()
&& self.transform_style.is_none()
&& self.perspective_origin.is_none()
&& self.text_decoration.is_none()
&& self.text_decoration_color.is_none()
&& self.text_decoration_color_var.is_none()
&& self.text_decoration_thickness.is_none()
&& self.text_decoration_thickness_var.is_none()
&& self.text_decoration_style.is_none()
&& self.text_decoration_skip_ink.is_none()
&& self.text_emphasis_style.is_none()
&& self.text_emphasis_color.is_none()
&& self.text_emphasis_position.is_none()
&& self.initial_letter.is_none()
&& self.text_underline_offset.is_none()
&& self.text_underline_offset_var.is_none()
&& self.text_underline_position.is_none()
&& self.text_underline_position_var.is_none()
&& self.text_shadow.is_none()
&& self.text_shadow_var.is_none()
&& self.text_overflow.is_none()
&& self.text_indent.is_none()
&& self.content.is_none()
&& self.quotes.is_none()
&& self.counter_reset.is_none()
&& self.counter_increment.is_none()
&& self.counter_set.is_none()
&& self.word_break.is_none()
&& self.overflow_wrap.is_none()
&& self.line_break.is_none()
&& self.line_break_var.is_none()
&& self.list_style_type.is_none()
&& self.list_style_image.is_none()
&& self.list_style_position.is_none()
&& self.letter_spacing.is_none()
&& self.word_spacing.is_none()
&& self.tab_size.is_none()
&& self.border_width.top.is_none()
&& self.border_width.right.is_none()
&& self.border_width.bottom.is_none()
&& self.border_width.left.is_none()
&& self.border_width.top_var.is_none()
&& self.border_width.right_var.is_none()
&& self.border_width.bottom_var.is_none()
&& self.border_width.left_var.is_none()
&& self.border_inline_start_width.is_none()
&& self.border_inline_end_width.is_none()
&& self.border_block_start_width.is_none()
&& self.border_block_end_width.is_none()
&& self.border_inline_start_width_var.is_none()
&& self.border_inline_end_width_var.is_none()
&& self.border_block_start_width_var.is_none()
&& self.border_block_end_width_var.is_none()
&& self.border_color.is_none()
&& self.border_top_color.is_none()
&& self.border_right_color.is_none()
&& self.border_bottom_color.is_none()
&& self.border_left_color.is_none()
&& self.border_inline_start_color.is_none()
&& self.border_inline_end_color.is_none()
&& self.border_block_start_color.is_none()
&& self.border_block_end_color.is_none()
&& self.border_inline_start_color_var.is_none()
&& self.border_inline_end_color_var.is_none()
&& self.border_block_start_color_var.is_none()
&& self.border_block_end_color_var.is_none()
&& self.border_style.top.is_none()
&& self.border_style.right.is_none()
&& self.border_style.bottom.is_none()
&& self.border_style.left.is_none()
&& self.border_inline_start_style.is_none()
&& self.border_inline_end_style.is_none()
&& self.border_block_start_style.is_none()
&& self.border_block_end_style.is_none()
&& self.border_cascade_ops.is_empty()
&& self.outline_width.is_none()
&& self.outline_offset.is_none()
&& self.outline_offset_var.is_none()
&& self.outline_style.is_none()
&& self.outline_color.is_none()
&& self.outline_visible.is_none()
&& self.border_collapse.is_none()
&& self.caption_side.is_none()
&& self.border_spacing.is_none()
&& self.table_layout.is_none()
&& self.empty_cells_hide.is_none()
&& self.visibility.is_none()
&& self.border_radius.is_none()
&& self.border_radius_var.is_none()
&& self.border_radius_ops.is_empty()
&& self.box_decoration_break.is_none()
&& self.border_image.is_empty()
&& self.font_name.is_none()
&& self.font_name_var.is_none()
&& self.box_shadow.is_none()
&& self.box_shadows.is_none()
&& self.paint_filter.is_none()
&& self.backdrop_filter.is_none()
&& self.will_change_backdrop_root.is_none()
&& self.mask_backdrop_root.is_none()
&& self.mix_blend_mode.is_none()
&& self.isolation.is_none()
&& self.opacity.is_none()
&& self.opacity_var.is_none()
&& self.background_paint.is_none()
&& self.background_paints.is_none()
&& self.background_sizes.is_none()
&& self.background_positions.is_none()
&& self.background_position_x.is_none()
&& self.background_position_y.is_none()
&& self.background_repeats.is_none()
&& self.background_attachments.is_none()
&& self.background_blend_modes.is_none()
&& self.background_origins.is_none()
&& self.background_clips.is_none()
&& self.mask_paints.is_none()
&& self.mask_sizes.is_none()
&& self.mask_positions.is_none()
&& self.mask_repeats.is_none()
&& self.mask_origins.is_none()
&& self.mask_clips.is_none()
&& self.mask_modes.is_none()
&& self.mask_composites.is_none()
&& self.mask_border.is_empty()
&& self.clip_path.is_none()
&& self.pagination.break_before.is_none()
&& self.pagination.break_after.is_none()
&& self.pagination.break_inside.is_none()
&& self.pagination.orphans.is_none()
&& self.pagination.widows.is_none()
&& self.page_name.is_none()
&& self.margin.top.is_none()
&& self.margin.right.is_none()
&& self.margin.bottom.is_none()
&& self.margin.left.is_none()
&& self.margin.top_var.is_none()
&& self.margin.right_var.is_none()
&& self.margin.bottom_var.is_none()
&& self.margin.left_var.is_none()
&& self.margin_inline_start.is_none()
&& self.margin_inline_end.is_none()
&& self.margin_block_start.is_none()
&& self.margin_block_end.is_none()
&& self.margin_inline_start_var.is_none()
&& self.margin_inline_end_var.is_none()
&& self.margin_block_start_var.is_none()
&& self.margin_block_end_var.is_none()
&& self.padding.top.is_none()
&& self.padding.right.is_none()
&& self.padding.bottom.is_none()
&& self.padding.left.is_none()
&& self.padding.top_var.is_none()
&& self.padding.right_var.is_none()
&& self.padding.bottom_var.is_none()
&& self.padding.left_var.is_none()
&& self.padding_inline_start.is_none()
&& self.padding_inline_end.is_none()
&& self.padding_block_start.is_none()
&& self.padding_block_end.is_none()
&& self.padding_inline_start_var.is_none()
&& self.padding_inline_end_var.is_none()
&& self.padding_block_start_var.is_none()
&& self.padding_block_end_var.is_none()
&& self.inline_size.is_none()
&& self.block_size.is_none()
&& self.min_inline_size.is_none()
&& self.max_inline_size.is_none()
&& self.min_block_size.is_none()
&& self.max_block_size.is_none()
&& self.inline_size_var.is_none()
&& self.block_size_var.is_none()
&& self.min_inline_size_var.is_none()
&& self.max_inline_size_var.is_none()
&& self.min_block_size_var.is_none()
&& self.max_block_size_var.is_none()
&& self.white_space.is_none()
&& self.white_space_var.is_none()
&& self.display.is_none()
&& self.position.is_none()
&& self.running_name.is_none()
&& self.string_set.is_none()
&& self.float_mode.is_none()
&& self.clear_mode.is_none()
&& self.z_index.is_none()
&& self.perspective.is_none()
&& self.box_sizing.is_none()
&& self.inset_left.is_none()
&& self.inset_top.is_none()
&& self.inset_right.is_none()
&& self.inset_bottom.is_none()
&& self.inset_inline_start.is_none()
&& self.inset_inline_end.is_none()
&& self.inset_block_start.is_none()
&& self.inset_block_end.is_none()
&& self.inset_inline_start_var.is_none()
&& self.inset_inline_end_var.is_none()
&& self.inset_block_start_var.is_none()
&& self.inset_block_end_var.is_none()
&& self.inset_left_var.is_none()
&& self.inset_top_var.is_none()
&& self.inset_right_var.is_none()
&& self.inset_bottom_var.is_none()
&& self.flex_direction.is_none()
&& self.flex_wrap.is_none()
&& self.flex_basis.is_none()
&& self.flex_basis_var.is_none()
&& self.order.is_none()
&& self.justify_content.is_none()
&& self.align_items.is_none()
&& self.align_self.is_none()
&& self.justify_items.is_none()
&& self.justify_self.is_none()
&& self.align_content.is_none()
&& self.column_count.is_none()
&& self.column_count_auto.is_none()
&& self.column_width.is_none()
&& self.column_fill.is_none()
&& self.column_span.is_none()
&& self.column_rule_width.is_none()
&& self.column_rule_style.is_none()
&& self.column_rule_color.is_none()
&& self.grid_columns.is_none()
&& self.grid_rows.is_none()
&& self.grid_column_tracks.is_none()
&& self.grid_row_tracks.is_none()
&& self.grid_subgrid_columns.is_none()
&& self.grid_subgrid_rows.is_none()
&& self.grid_column_tracks_var.is_none()
&& self.grid_row_tracks_var.is_none()
&& self.grid_auto_column_tracks.is_none()
&& self.grid_auto_row_tracks.is_none()
&& self.grid_auto_flow.is_none()
&& self.grid_column_auto_repeat.is_none()
&& self.grid_row_auto_repeat.is_none()
&& self.grid_template_areas.is_none()
&& self.grid_column_line_names.is_none()
&& self.grid_row_line_names.is_none()
&& self.grid_column_line_start.is_none()
&& self.grid_column_line_end.is_none()
&& self.grid_row_line_start.is_none()
&& self.grid_row_line_end.is_none()
&& self.grid_area_name.is_none()
&& self.grid_column_start.is_none()
&& self.grid_row_start.is_none()
&& self.row_gap.is_none()
&& self.row_gap_var.is_none()
&& self.gap.is_none()
&& self.gap_var.is_none()
&& self.column_gap_normal.is_none()
&& self.flex_grow.is_none()
&& self.flex_shrink.is_none()
&& self.overflow_x.is_none()
&& self.overflow_y.is_none()
&& self.overflow_inline.is_none()
&& self.overflow_block.is_none()
&& self.overflow_clip_margin.is_none()
&& self.scrollbar_gutter.is_none()
&& self.line_clamp.is_none()
&& self.object_fit.is_none()
&& self.image_rendering.is_none()
&& self.object_position.is_none()
&& self.custom_lengths.is_empty()
&& self.custom_colors.is_empty()
&& self.custom_color_alpha.is_empty()
&& self.custom_color_refs.is_empty()
&& self.custom_raw_values.is_empty()
&& self.custom_font_stacks.is_empty()
}
}
fn default_ua_css() -> &'static str {
r#"
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.17em; }
h4 { font-size: 1em; }
h5 { font-size: 0.83em; }
h6 { font-size: 0.67em; }
h1, h2, h3, h4, h5, h6 { font-weight: bold; }
pre { white-space: pre; }
code, kbd, samp, tt { white-space: pre; }
span, a, em, strong, i, b, u, small, label { display: inline; }
/* Treat <svg> like a replaced inline element so it participates in inline layout. */
svg { display: inline-block; }
/* Treat <img> like a replaced inline element so it is rendered as an atomic box. */
img { display: inline-block; }
html, body, div, p, section, article, header, footer, aside, nav, main, blockquote,
figure, figcaption,
h1, h2, h3, h4, h5, h6,
ul, ol, dl, dt, dd, li, table, thead, tbody, tfoot, tr, td, th, pre, hr { display: block; }
caption { display: table-caption; text-align: center; }
td, th { display: table-cell; padding: 1px; }
th { font-weight: bold; text-align: center; }
ul, ol { padding-inline-start: 40px; }
table { break-inside: avoid; }
thead { break-inside: avoid; }
"#
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
fn element(tag: &str, id: Option<&str>, classes: &[&str]) -> ElementInfo {
ElementInfo {
tag: tag.to_string(),
id: id.map(|v| v.to_string()),
classes: classes.iter().map(|c| c.to_string()).collect(),
attrs: HashMap::new(),
container_names: Vec::new(),
container_type: ContainerQueryType::Normal,
container_width: None,
container_height: None,
container_inline_size: None,
container_block_size: None,
is_root: false,
is_empty: true,
is_defined: true,
language: None,
direction: None,
child_index: 1,
child_count: 1,
type_index: 1,
type_count: 1,
prev_siblings: Vec::new(),
next_siblings: Vec::new(),
children: Vec::new(),
}
}
fn rgb8(r: u8, g: u8, b: u8) -> Color {
Color::rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
}
#[test]
fn descendant_selector_overrides_simple() {
let css = "p { color: blue; } div p { color: red; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_info = element("div", None, &[]);
let div_style = resolver.compute_style(&div_info, &root, None, &[]);
let p_info = element("p", None, &[]);
let p_style = resolver.compute_style(&p_info, &div_style, None, &[div_info]);
assert_eq!(p_style.color, Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn of_type_pseudos_use_type_relative_indices() {
let mut info = element("span", None, &["token"]);
info.child_index = 3;
info.child_count = 5;
info.type_index = 1;
info.type_count = 2;
assert!(
parse_pseudo_class("first-of-type")
.expect("first-of-type")
.matches_with_pseudo(&info, PseudoTarget::None)
);
assert!(
!parse_pseudo_class("last-of-type")
.expect("last-of-type")
.matches_with_pseudo(&info, PseudoTarget::None)
);
info.type_index = 2;
assert!(
parse_pseudo_class("last-of-type")
.expect("last-of-type")
.matches_with_pseudo(&info, PseudoTarget::None)
);
assert!(
parse_pseudo_class("nth-of-type(2)")
.expect("nth-of-type")
.matches_with_pseudo(&info, PseudoTarget::None)
);
}
#[test]
fn level_four_structural_pseudos_match_element_context() {
let mut info = element("p", None, &["box", "flagged"]);
info.child_index = 3;
info.child_count = 4;
info.type_index = 2;
info.type_count = 3;
info.is_empty = true;
info.is_defined = true;
info.language = Some("en-US".to_string());
info.direction = Some("rtl".to_string());
info.prev_siblings = vec![
element("p", None, &["flagged"]),
element("div", None, &["item"]),
];
for selector in [
"empty",
"defined",
"lang(en)",
"dir(rtl)",
"nth-last-child(2)",
"nth-last-of-type(2)",
"nth-child(2 of .flagged)",
] {
assert!(
parse_pseudo_class(selector)
.unwrap_or_else(|| panic!("parse {selector}"))
.matches_with_pseudo(&info, PseudoTarget::None),
"{selector} should match"
);
}
info.child_count = 1;
info.type_count = 1;
assert!(
parse_pseudo_class("only-child")
.expect("only-child")
.matches_with_pseudo(&info, PseudoTarget::None)
);
assert!(
parse_pseudo_class("only-of-type")
.expect("only-of-type")
.matches_with_pseudo(&info, PseudoTarget::None)
);
}
#[test]
fn has_relative_selectors_match_descendants_children_and_following_siblings() {
let mut card = element("div", None, &["card"]);
let mut wrapper = element("div", None, &["wrapper"]);
wrapper.children.push(element("span", None, &["flag"]));
card.children.push(wrapper);
card.next_siblings.push(element("div", None, &["flag"]));
for selector in ["has(.flag)", "has(+ .flag)"] {
assert!(
parse_pseudo_class(selector)
.unwrap_or_else(|| panic!("parse {selector}"))
.matches_with_pseudo(&card, PseudoTarget::None),
"{selector} should match"
);
}
let mut direct = element("div", None, &["card"]);
direct.children.push(element("span", None, &["flag"]));
assert!(
parse_pseudo_class("has(> .flag)")
.expect("child :has")
.matches_with_pseudo(&direct, PseudoTarget::None)
);
}
#[test]
fn attribute_selector_ascii_case_modifier_is_honored() {
let selector = parse_simple_selector(".box[data-id=\"yes\" i]").expect("selector");
let mut info = element("div", None, &["box"]);
info.attrs.insert("data-id".to_string(), "YES".to_string());
assert!(selector.matches_with_pseudo(&info, PseudoTarget::None));
}
#[test]
fn where_selector_list_matches_without_adding_specificity() {
let css = ":where(div, p, sub) { color: red; } p { color: blue; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_style = resolver.compute_style(&element("div", None, &[]), &root, None, &[]);
let p_style = resolver.compute_style(&element("p", None, &[]), &root, None, &[]);
let sub_style = resolver.compute_style(&element("sub", None, &[]), &root, None, &[]);
assert_eq!(div_style.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(p_style.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(sub_style.color, Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn is_selector_uses_most_specific_list_entry() {
let css = ":is(p, #target) { color: red; } p { color: blue; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("p", None, &[]), &root, None, &[]);
assert_eq!(style.color, Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn pinned_ua_where_rule_applies_subscript_metrics() {
let css = ":where(sub) { font-size: 83%; vertical-align: sub; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("sub", None, &[]), &root, None, &[]);
assert!((style.font_size.to_f32() - root.font_size.to_f32() * 0.83).abs() < 0.01);
assert_eq!(style.vertical_align, VerticalAlignMode::Sub);
}
#[test]
fn important_overrides_specificity() {
let css = "p.note { color: blue; } p { color: red !important; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let p_info = element("p", None, &["note"]);
let p_style = resolver.compute_style(&p_info, &root, None, &[]);
assert_eq!(p_style.color, Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn background_shorthand_honors_cascade_wide_keywords() {
let css = ".parent { background: rgb(10, 127, 46); } \
.box { background: rgb(189, 189, 189); } \
.box.inherit { background: inherit; } \
.box.initial { background: initial; } \
.box.unset { background: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(parent.background_color, Some(rgb8(10, 127, 46)));
let inherited_info = element("div", None, &["box", "inherit"]);
assert!(
parse_selector_pattern(".box.inherit")
.expect("compound class selector")
.matches(&inherited_info, std::slice::from_ref(&parent_info))
);
let parsed = crate::css_native::parse_declaration_block("background: inherit")
.expect("background declaration");
let (parsed_delta, _) = style_from_native_declarations(
&parsed,
Size {
width: Pt::ZERO,
height: Pt::ZERO,
},
);
assert_eq!(
parsed_delta.background_reset,
Some(CascadeWideKeyword::Inherit)
);
let inherited = resolver.compute_style(
&inherited_info,
&parent,
None,
std::slice::from_ref(&parent_info),
);
assert_eq!(inherited.background_color, Some(rgb8(10, 127, 46)));
for keyword in ["initial", "unset"] {
let style = resolver.compute_style(
&element("div", None, &["box", keyword]),
&parent,
None,
std::slice::from_ref(&parent_info),
);
assert_eq!(style.background_color, None, "background: {keyword}");
}
}
#[test]
fn all_initial_respects_declaration_order_and_preserves_exclusions() {
let css = ".before { --w: 20px; direction: rtl; background: green; padding: 12px; \
all: initial; display: block; width: var(--w); } \
.after { all: initial; display: block; width: 20px; height: 10px; \
background: green; padding: 4px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let before = resolver.compute_style(&element("div", None, &["before"]), &root, None, &[]);
assert_eq!(before.background_color, None);
assert_eq!(before.padding, EdgeSizes::zero());
assert_eq!(before.direction, DirectionMode::Rtl);
assert_eq!(before.width, LengthSpec::Absolute(Pt::from_f32(15.0)));
let after = resolver.compute_style(&element("div", None, &["after"]), &root, None, &[]);
assert_eq!(after.background_color, Some(rgb8(0, 128, 0)));
assert_eq!(after.padding.top, LengthSpec::Absolute(Pt::from_f32(3.0)));
}
#[test]
fn invalid_computed_var_does_not_reveal_lower_cascade_value() {
let css = ".target { width: 150px; } .stage .target { width: var(--missing-width); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let stage_info = element("div", None, &["stage"]);
let stage = resolver.compute_style(&stage_info, &root, None, &[]);
let target = resolver.compute_style(
&element("div", None, &["target"]),
&stage,
None,
std::slice::from_ref(&stage_info),
);
assert_eq!(target.width, LengthSpec::Auto);
}
#[test]
fn cascade_layers_rank_before_specificity_for_normal_rules() {
let css = "@layer base, theme; \
@layer base { #target.item { color: rgb(0, 0, 255); } } \
@layer theme { .item { color: rgb(0, 128, 0); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("p", Some("target"), &["item"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
}
#[test]
fn unlayered_normal_rules_outrank_layered_rules() {
let css = "@layer theme { #target.item { color: rgb(0, 0, 255); } } \
.item { color: rgb(128, 0, 128); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("p", Some("target"), &["item"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.color, Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0));
}
#[test]
fn important_layer_order_is_reversed_and_above_unlayered_important() {
let css = "@layer base, theme; \
.item { color: rgb(128, 0, 128) !important; } \
@layer theme { #target.item { color: rgb(0, 128, 0) !important; } } \
@layer base { .item { color: rgb(0, 0, 255) !important; } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("p", Some("target"), &["item"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn revert_layer_resets_supported_properties_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { color: rgb(0, 0, 255); background-color: rgb(255, 255, 0); width: 96px; height: 24px; } \
.feature { color: rgb(0, 128, 0); background-color: rgb(0, 255, 255); width: 120px; height: 30px; } \
} \
@layer special { \
.item { color: rgb(255, 0, 0); background-color: rgb(0, 0, 0); width: 48px; height: 12px; } \
.feature { color: revert-layer; background-color: revert-layer; width: revert-layer; height: revert-layer; } \
.override { color: revert-layer; background-color: revert-layer; width: revert-layer; height: revert-layer; color: rgb(10, 20, 30); background-color: rgb(40, 50, 60); width: 72px; height: 18px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(reverted.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(reverted.background_color, Some(Color::rgb(0.0, 1.0, 1.0)));
assert_eq!(reverted.width, LengthSpec::Absolute(px_to_pt(120.0)));
assert_eq!(reverted.height, LengthSpec::Absolute(px_to_pt(30.0)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(current_layer.background_color, Some(Color::BLACK));
assert_eq!(current_layer.width, LengthSpec::Absolute(px_to_pt(48.0)));
assert_eq!(current_layer.height, LengthSpec::Absolute(px_to_pt(12.0)));
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.color,
Color::rgb(10.0 / 255.0, 20.0 / 255.0, 30.0 / 255.0)
);
assert_eq!(
later_declaration.background_color,
Some(Color::rgb(40.0 / 255.0, 50.0 / 255.0, 60.0 / 255.0))
);
assert_eq!(
later_declaration.width,
LengthSpec::Absolute(px_to_pt(72.0))
);
assert_eq!(
later_declaration.height,
LengthSpec::Absolute(px_to_pt(18.0))
);
}
#[test]
fn revert_layer_resets_physical_margin_and_padding_edges_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { margin: 4px 8px 12px 16px; padding: 6px 10px 14px 18px; } \
.feature { margin: 20px 24px 28px 32px; padding: 22px 26px 30px 34px; } \
.override { margin: 10px 11px 12px 13px; padding: 14px 15px 16px 17px; } \
} \
@layer special { \
.item { margin: 1px 2px 3px 4px; padding: 5px 6px 7px 8px; } \
.feature { margin: revert-layer; padding: revert-layer; } \
.override { margin: revert-layer; padding: revert-layer; margin-left: 44px; padding-top: 46px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(reverted.margin.top, LengthSpec::Absolute(px_to_pt(20.0)));
assert_eq!(reverted.margin.right, LengthSpec::Absolute(px_to_pt(24.0)));
assert_eq!(reverted.margin.bottom, LengthSpec::Absolute(px_to_pt(28.0)));
assert_eq!(reverted.margin.left, LengthSpec::Absolute(px_to_pt(32.0)));
assert_eq!(reverted.padding.top, LengthSpec::Absolute(px_to_pt(22.0)));
assert_eq!(reverted.padding.right, LengthSpec::Absolute(px_to_pt(26.0)));
assert_eq!(
reverted.padding.bottom,
LengthSpec::Absolute(px_to_pt(30.0))
);
assert_eq!(reverted.padding.left, LengthSpec::Absolute(px_to_pt(34.0)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.margin.top,
LengthSpec::Absolute(px_to_pt(1.0))
);
assert_eq!(
current_layer.margin.right,
LengthSpec::Absolute(px_to_pt(2.0))
);
assert_eq!(
current_layer.margin.bottom,
LengthSpec::Absolute(px_to_pt(3.0))
);
assert_eq!(
current_layer.margin.left,
LengthSpec::Absolute(px_to_pt(4.0))
);
assert_eq!(
current_layer.padding.top,
LengthSpec::Absolute(px_to_pt(5.0))
);
assert_eq!(
current_layer.padding.right,
LengthSpec::Absolute(px_to_pt(6.0))
);
assert_eq!(
current_layer.padding.bottom,
LengthSpec::Absolute(px_to_pt(7.0))
);
assert_eq!(
current_layer.padding.left,
LengthSpec::Absolute(px_to_pt(8.0))
);
let later_edge =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_edge.margin.top, LengthSpec::Absolute(px_to_pt(10.0)));
assert_eq!(
later_edge.margin.right,
LengthSpec::Absolute(px_to_pt(11.0))
);
assert_eq!(
later_edge.margin.bottom,
LengthSpec::Absolute(px_to_pt(12.0))
);
assert_eq!(later_edge.margin.left, LengthSpec::Absolute(px_to_pt(44.0)));
assert_eq!(later_edge.padding.top, LengthSpec::Absolute(px_to_pt(46.0)));
assert_eq!(
later_edge.padding.right,
LengthSpec::Absolute(px_to_pt(15.0))
);
assert_eq!(
later_edge.padding.bottom,
LengthSpec::Absolute(px_to_pt(16.0))
);
assert_eq!(
later_edge.padding.left,
LengthSpec::Absolute(px_to_pt(17.0))
);
}
#[test]
fn revert_layer_resets_logical_margin_and_padding_edges_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { writing-mode: vertical-rl; direction: ltr; margin-inline: 4px 8px; margin-block: 12px 16px; padding-inline: 6px 10px; padding-block: 14px 18px; } \
.feature { margin-inline: 20px 24px; margin-block: 28px 32px; padding-inline: 22px 26px; padding-block: 30px 34px; } \
.override { margin-inline: 10px 11px; margin-block: 12px 13px; padding-inline: 14px 15px; padding-block: 16px 17px; } \
} \
@layer special { \
.item { margin-inline: 1px 2px; margin-block: 3px 4px; padding-inline: 5px 6px; padding-block: 7px 8px; } \
.feature { margin-inline: revert-layer; margin-block: revert-layer; padding-inline: revert-layer; padding-block: revert-layer; } \
.override { margin-inline: revert-layer; padding-block: revert-layer; margin-inline-start: 44px; padding-block-end: 46px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(reverted.margin.top, LengthSpec::Absolute(px_to_pt(20.0)));
assert_eq!(reverted.margin.right, LengthSpec::Absolute(px_to_pt(28.0)));
assert_eq!(reverted.margin.bottom, LengthSpec::Absolute(px_to_pt(24.0)));
assert_eq!(reverted.margin.left, LengthSpec::Absolute(px_to_pt(32.0)));
assert_eq!(reverted.padding.top, LengthSpec::Absolute(px_to_pt(22.0)));
assert_eq!(reverted.padding.right, LengthSpec::Absolute(px_to_pt(30.0)));
assert_eq!(
reverted.padding.bottom,
LengthSpec::Absolute(px_to_pt(26.0))
);
assert_eq!(reverted.padding.left, LengthSpec::Absolute(px_to_pt(34.0)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.margin.top,
LengthSpec::Absolute(px_to_pt(1.0))
);
assert_eq!(
current_layer.margin.right,
LengthSpec::Absolute(px_to_pt(3.0))
);
assert_eq!(
current_layer.margin.bottom,
LengthSpec::Absolute(px_to_pt(2.0))
);
assert_eq!(
current_layer.margin.left,
LengthSpec::Absolute(px_to_pt(4.0))
);
assert_eq!(
current_layer.padding.top,
LengthSpec::Absolute(px_to_pt(5.0))
);
assert_eq!(
current_layer.padding.right,
LengthSpec::Absolute(px_to_pt(7.0))
);
assert_eq!(
current_layer.padding.bottom,
LengthSpec::Absolute(px_to_pt(6.0))
);
assert_eq!(
current_layer.padding.left,
LengthSpec::Absolute(px_to_pt(8.0))
);
let later_logical_edge =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_logical_edge.margin.top,
LengthSpec::Absolute(px_to_pt(44.0))
);
assert_eq!(
later_logical_edge.margin.right,
LengthSpec::Absolute(px_to_pt(3.0))
);
assert_eq!(
later_logical_edge.margin.bottom,
LengthSpec::Absolute(px_to_pt(11.0))
);
assert_eq!(
later_logical_edge.margin.left,
LengthSpec::Absolute(px_to_pt(4.0))
);
assert_eq!(
later_logical_edge.padding.top,
LengthSpec::Absolute(px_to_pt(5.0))
);
assert_eq!(
later_logical_edge.padding.right,
LengthSpec::Absolute(px_to_pt(16.0))
);
assert_eq!(
later_logical_edge.padding.bottom,
LengthSpec::Absolute(px_to_pt(6.0))
);
assert_eq!(
later_logical_edge.padding.left,
LengthSpec::Absolute(px_to_pt(46.0))
);
}
#[test]
fn revert_layer_resets_border_width_edges_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { border-style: solid; border-width: 4px 8px 12px 16px; } \
.feature { border-width: 20px 24px 28px 32px; } \
.logical { writing-mode: vertical-rl; direction: ltr; border-inline-start-width: 20px; border-inline-end-width: 24px; border-block-start-width: 28px; border-block-end-width: 32px; } \
.override { border-width: 10px 11px 12px 13px; } \
} \
@layer special { \
.item { border-width: 1px 2px 3px 4px; } \
.feature { border-width: revert-layer; } \
.logical { border-inline-start-width: revert-layer; border-inline-end-width: revert-layer; border-block-start-width: revert-layer; border-block-end-width: revert-layer; } \
.override { border-width: revert-layer; border-left-width: 44px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let physical =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(
physical.border_width.top,
LengthSpec::Absolute(px_to_pt(20.0))
);
assert_eq!(
physical.border_width.right,
LengthSpec::Absolute(px_to_pt(24.0))
);
assert_eq!(
physical.border_width.bottom,
LengthSpec::Absolute(px_to_pt(28.0))
);
assert_eq!(
physical.border_width.left,
LengthSpec::Absolute(px_to_pt(32.0))
);
let logical =
resolver.compute_style(&element("p", None, &["item", "logical"]), &root, None, &[]);
assert_eq!(
logical.border_width.top,
LengthSpec::Absolute(px_to_pt(20.0))
);
assert_eq!(
logical.border_width.right,
LengthSpec::Absolute(px_to_pt(28.0))
);
assert_eq!(
logical.border_width.bottom,
LengthSpec::Absolute(px_to_pt(24.0))
);
assert_eq!(
logical.border_width.left,
LengthSpec::Absolute(px_to_pt(32.0))
);
let later_edge =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_edge.border_width.top,
LengthSpec::Absolute(px_to_pt(10.0))
);
assert_eq!(
later_edge.border_width.right,
LengthSpec::Absolute(px_to_pt(11.0))
);
assert_eq!(
later_edge.border_width.bottom,
LengthSpec::Absolute(px_to_pt(12.0))
);
assert_eq!(
later_edge.border_width.left,
LengthSpec::Absolute(px_to_pt(44.0))
);
}
#[test]
fn revert_layer_resets_border_style_edges_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { border-width: 4px; border-style: solid; } \
.feature { border-style: dotted dashed double groove; } \
.logical { writing-mode: vertical-rl; direction: ltr; border-inline-start-style: dotted; border-inline-end-style: dashed; border-block-start-style: double; border-block-end-style: groove; } \
.override { border-style: inset outset ridge groove; } \
} \
@layer special { \
.item { border-style: dashed dotted solid double; } \
.feature { border-style: revert-layer; } \
.logical { border-inline-start-style: revert-layer; border-inline-end-style: revert-layer; border-block-start-style: revert-layer; border-block-end-style: revert-layer; } \
.override { border-style: revert-layer; border-left-style: solid; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let physical =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(
physical.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Dotted)
);
assert_eq!(
physical.border_style.right,
BorderLineStyle::Style(OutlineLineStyle::Dashed)
);
assert_eq!(
physical.border_style.bottom,
BorderLineStyle::Style(OutlineLineStyle::Double)
);
assert_eq!(
physical.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Groove)
);
let logical =
resolver.compute_style(&element("p", None, &["item", "logical"]), &root, None, &[]);
assert_eq!(
logical.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Dotted)
);
assert_eq!(
logical.border_style.right,
BorderLineStyle::Style(OutlineLineStyle::Double)
);
assert_eq!(
logical.border_style.bottom,
BorderLineStyle::Style(OutlineLineStyle::Dashed)
);
assert_eq!(
logical.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Groove)
);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Dashed)
);
assert_eq!(
current_layer.border_style.right,
BorderLineStyle::Style(OutlineLineStyle::Dotted)
);
assert_eq!(
current_layer.border_style.bottom,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
assert_eq!(
current_layer.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Double)
);
let later_edge =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_edge.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Inset)
);
assert_eq!(
later_edge.border_style.right,
BorderLineStyle::Style(OutlineLineStyle::Outset)
);
assert_eq!(
later_edge.border_style.bottom,
BorderLineStyle::Style(OutlineLineStyle::Ridge)
);
assert_eq!(
later_edge.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
}
#[test]
fn revert_layer_resets_border_color_edges_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { color: rgb(9, 19, 29); border-style: solid; border-width: 4px; border-color: rgb(1, 2, 3); } \
.feature { border-color: rgb(20, 40, 60) rgb(70, 80, 90) rgb(100, 110, 120) rgb(130, 140, 150); } \
.logical { writing-mode: vertical-rl; direction: ltr; border-inline-start-color: rgb(20, 40, 60); border-inline-end-color: rgb(70, 80, 90); border-block-start-color: rgb(100, 110, 120); border-block-end-color: rgb(130, 140, 150); } \
.override { border-color: rgb(10, 20, 30) rgb(40, 50, 60) rgb(70, 80, 90) rgb(100, 110, 120); } \
} \
@layer special { \
.item { border-color: rgb(200, 0, 0) rgb(0, 200, 0) rgb(0, 0, 200) rgb(200, 200, 0); } \
.feature { border-color: revert-layer; } \
.logical { border-inline-start-color: revert-layer; border-inline-end-color: revert-layer; border-block-start-color: revert-layer; border-block-end-color: revert-layer; } \
.override { border-color: revert-layer; border-left-color: rgb(9, 8, 7); } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let physical =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(physical.border_top_color, Some(rgb8(20, 40, 60)));
assert_eq!(physical.border_right_color, Some(rgb8(70, 80, 90)));
assert_eq!(physical.border_bottom_color, Some(rgb8(100, 110, 120)));
assert_eq!(physical.border_left_color, Some(rgb8(130, 140, 150)));
let logical =
resolver.compute_style(&element("p", None, &["item", "logical"]), &root, None, &[]);
assert_eq!(logical.border_top_color, Some(rgb8(20, 40, 60)));
assert_eq!(logical.border_right_color, Some(rgb8(100, 110, 120)));
assert_eq!(logical.border_bottom_color, Some(rgb8(70, 80, 90)));
assert_eq!(logical.border_left_color, Some(rgb8(130, 140, 150)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.border_top_color, Some(rgb8(200, 0, 0)));
assert_eq!(current_layer.border_right_color, Some(rgb8(0, 200, 0)));
assert_eq!(current_layer.border_bottom_color, Some(rgb8(0, 0, 200)));
assert_eq!(current_layer.border_left_color, Some(rgb8(200, 200, 0)));
let later_edge =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_edge.border_top_color, Some(rgb8(10, 20, 30)));
assert_eq!(later_edge.border_right_color, Some(rgb8(40, 50, 60)));
assert_eq!(later_edge.border_bottom_color, Some(rgb8(70, 80, 90)));
assert_eq!(later_edge.border_left_color, Some(rgb8(9, 8, 7)));
}
#[test]
fn revert_layer_resets_outline_shorthand_and_longhands_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { outline: 4px solid rgb(10, 20, 30); } \
.feature { outline: 6px dashed rgb(40, 50, 60); } \
.parts { outline-width: 8px; outline-style: dotted; outline-color: rgb(70, 80, 90); } \
.override { outline: 10px groove rgb(100, 110, 120); } \
} \
@layer special { \
.item { outline: 1px double rgb(200, 0, 0); } \
.feature { outline: revert-layer; } \
.parts { outline-width: revert-layer; outline-style: revert-layer; outline-color: revert-layer; } \
.override { outline-width: revert-layer; outline-style: revert-layer; outline-color: revert-layer; outline-color: rgb(9, 8, 7); } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let shorthand =
resolver.compute_style(&element("p", None, &["item", "feature"]), &root, None, &[]);
assert_eq!(shorthand.outline_width, LengthSpec::Absolute(px_to_pt(6.0)));
assert_eq!(shorthand.outline_style, OutlineLineStyle::Dashed);
assert_eq!(shorthand.resolved_outline_color(), rgb8(40, 50, 60));
assert!(shorthand.outline_visible);
let longhands =
resolver.compute_style(&element("p", None, &["item", "parts"]), &root, None, &[]);
assert_eq!(longhands.outline_width, LengthSpec::Absolute(px_to_pt(8.0)));
assert_eq!(longhands.outline_style, OutlineLineStyle::Dotted);
assert_eq!(longhands.resolved_outline_color(), rgb8(70, 80, 90));
assert!(longhands.outline_visible);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.outline_width,
LengthSpec::Absolute(px_to_pt(1.0))
);
assert_eq!(current_layer.outline_style, OutlineLineStyle::Double);
assert_eq!(current_layer.resolved_outline_color(), rgb8(200, 0, 0));
assert!(current_layer.outline_visible);
let later_color =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_color.outline_width,
LengthSpec::Absolute(px_to_pt(10.0))
);
assert_eq!(later_color.outline_style, OutlineLineStyle::Groove);
assert_eq!(later_color.resolved_outline_color(), rgb8(9, 8, 7));
assert!(later_color.outline_visible);
}
#[test]
fn revert_layer_resets_outline_offset_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { outline: 4px solid rgb(10, 20, 30); outline-offset: 2px; } \
.offset { outline-offset: 9px; } \
.override { outline-offset: 12px; } \
} \
@layer special { \
.item { outline-offset: 1px; } \
.offset { outline-offset: revert-layer; } \
.override { outline-offset: revert-layer; outline-offset: -3px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "offset"]), &root, None, &[]);
assert_eq!(reverted.outline_offset, LengthSpec::Absolute(px_to_pt(9.0)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.outline_offset,
LengthSpec::Absolute(px_to_pt(1.0))
);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.outline_offset,
LengthSpec::Absolute(px_to_pt(-3.0))
);
}
#[test]
fn revert_layer_resets_box_sizing_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { box-sizing: content-box; } \
.revert { box-sizing: border-box; } \
.override { box-sizing: border-box; } \
} \
@layer special { \
.item { box-sizing: content-box; } \
.revert { box-sizing: revert-layer; } \
.override { box-sizing: revert-layer; box-sizing: content-box; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.box_sizing, BoxSizingMode::BorderBox);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.box_sizing, BoxSizingMode::ContentBox);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.box_sizing, BoxSizingMode::ContentBox);
}
#[test]
fn revert_layer_resets_display_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { display: block; } \
.revert { display: none; } \
.override { display: grid; } \
} \
@layer special { \
.item { display: inline; } \
.revert { display: revert-layer; } \
.override { display: revert-layer; display: block; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted = resolver.compute_style(
&element("x-display", None, &["item", "revert"]),
&root,
None,
&[],
);
assert_eq!(reverted.display, DisplayMode::None);
let current_layer =
resolver.compute_style(&element("x-display", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.display, DisplayMode::Inline);
let later_declaration = resolver.compute_style(
&element("x-display", None, &["item", "override"]),
&root,
None,
&[],
);
assert_eq!(later_declaration.display, DisplayMode::Block);
}
#[test]
fn revert_layer_resets_position_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { position: static; } \
.revert { position: absolute; } \
.override { position: fixed; } \
} \
@layer special { \
.item { position: relative; } \
.revert { position: revert-layer; } \
.override { position: revert-layer; position: static; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted = resolver.compute_style(
&element("x-position", None, &["item", "revert"]),
&root,
None,
&[],
);
assert_eq!(reverted.position, PositionMode::Absolute);
let current_layer =
resolver.compute_style(&element("x-position", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.position, PositionMode::Relative);
let later_declaration = resolver.compute_style(
&element("x-position", None, &["item", "override"]),
&root,
None,
&[],
);
assert_eq!(later_declaration.position, PositionMode::Static);
}
#[test]
fn revert_layer_resets_insets_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { position: absolute; inset: auto; } \
.revert { top: 3px; right: 4px; bottom: 5px; left: 6px; } \
.logical { writing-mode: vertical-rl; inset-inline-start: 7px; inset-block-start: 8px; } \
.override { inset: 1px 2px 3px 4px; } \
} \
@layer special { \
.item { top: 11px; right: 12px; bottom: 13px; left: 14px; } \
.revert { inset: revert-layer; } \
.logical { inset-inline-start: revert-layer; inset-block-start: revert-layer; } \
.override { inset: revert-layer; inset: 9px 10px 11px 12px; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.inset_top, LengthSpec::Absolute(px_to_pt(3.0)));
assert_eq!(reverted.inset_right, LengthSpec::Absolute(px_to_pt(4.0)));
assert_eq!(reverted.inset_bottom, LengthSpec::Absolute(px_to_pt(5.0)));
assert_eq!(reverted.inset_left, LengthSpec::Absolute(px_to_pt(6.0)));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.inset_top,
LengthSpec::Absolute(px_to_pt(11.0))
);
assert_eq!(
current_layer.inset_right,
LengthSpec::Absolute(px_to_pt(12.0))
);
assert_eq!(
current_layer.inset_bottom,
LengthSpec::Absolute(px_to_pt(13.0))
);
assert_eq!(
current_layer.inset_left,
LengthSpec::Absolute(px_to_pt(14.0))
);
let logical =
resolver.compute_style(&element("p", None, &["item", "logical"]), &root, None, &[]);
assert_eq!(logical.inset_top, LengthSpec::Absolute(px_to_pt(7.0)));
assert_eq!(logical.inset_right, LengthSpec::Absolute(px_to_pt(8.0)));
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.inset_top,
LengthSpec::Absolute(px_to_pt(9.0))
);
assert_eq!(
later_declaration.inset_right,
LengthSpec::Absolute(px_to_pt(10.0))
);
assert_eq!(
later_declaration.inset_bottom,
LengthSpec::Absolute(px_to_pt(11.0))
);
assert_eq!(
later_declaration.inset_left,
LengthSpec::Absolute(px_to_pt(12.0))
);
}
#[test]
fn revert_layer_resets_z_index_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { position: absolute; z-index: 0; } \
.revert { z-index: 3; } \
.override { z-index: 1; } \
} \
@layer special { \
.item { z-index: 2; } \
.revert { z-index: revert-layer; } \
.override { z-index: revert-layer; z-index: 4; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.z_index, 3);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.z_index, 2);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.z_index, 4);
}
#[test]
fn stacking_context_inputs_preserve_authored_identity_and_auto_state() {
let css = ".auto { position: absolute; z-index: auto; }\
.zero { position: absolute; z-index: 0; }\
.sticky { position: sticky; }\
.perspective { perspective: 400px; }\
.filter { filter: brightness(1); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let auto = resolver.compute_style(&element("div", None, &["auto"]), &root, None, &[]);
assert_eq!(auto.z_index, 0);
assert!(auto.z_index_auto);
let zero = resolver.compute_style(&element("div", None, &["zero"]), &root, None, &[]);
assert_eq!(zero.z_index, 0);
assert!(!zero.z_index_auto);
let sticky = resolver.compute_style(&element("div", None, &["sticky"]), &root, None, &[]);
assert_eq!(sticky.position, PositionMode::Sticky);
let perspective =
resolver.compute_style(&element("div", None, &["perspective"]), &root, None, &[]);
assert_eq!(
perspective.perspective,
Some(LengthSpec::Absolute(px_to_pt(400.0)))
);
let filter = resolver.compute_style(&element("div", None, &["filter"]), &root, None, &[]);
assert!(filter.paint_filter_stacking_context);
}
#[test]
fn revert_layer_resets_opacity_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { opacity: 1; } \
.revert { opacity: 0.25; } \
.override { opacity: 0.4; } \
} \
@layer special { \
.item { opacity: 0.75; } \
.revert { opacity: revert-layer; } \
.override { opacity: revert-layer; opacity: 0.6; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert!((reverted.opacity - 0.25).abs() < 0.0001);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert!((current_layer.opacity - 0.75).abs() < 0.0001);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!((later_declaration.opacity - 0.6).abs() < 0.0001);
}
#[test]
fn revert_layer_resets_filter_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { filter: none; } \
.revert { filter: sepia(100%); } \
.override { filter: invert(100%); } \
} \
@layer special { \
.item { filter: invert(100%); } \
.revert { filter: revert-layer; } \
.override { filter: revert-layer; filter: none; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
let reverted_filter = reverted.paint_filter.expect("expected reverted filter");
assert!((reverted_filter.sepia - 1.0).abs() < 0.0001);
assert!((reverted_filter.invert - 0.0).abs() < 0.0001);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
let current_filter = current_layer.paint_filter.expect("expected current filter");
assert!((current_filter.invert - 1.0).abs() < 0.0001);
assert!((current_filter.sepia - 0.0).abs() < 0.0001);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(later_declaration.paint_filter.is_none());
}
#[test]
fn revert_layer_resets_backdrop_filter_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { backdrop-filter: none; } \
.revert { backdrop-filter: sepia(100%); } \
.override { backdrop-filter: invert(100%); } \
} \
@layer special { \
.item { backdrop-filter: invert(100%); } \
.revert { backdrop-filter: revert-layer; } \
.override { backdrop-filter: revert-layer; backdrop-filter: none; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
let reverted_filter = reverted
.backdrop_filter
.expect("expected reverted backdrop filter");
assert!((reverted_filter.sepia - 1.0).abs() < 0.0001);
assert!((reverted_filter.invert - 0.0).abs() < 0.0001);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
let current_filter = current_layer
.backdrop_filter
.expect("expected current backdrop filter");
assert!((current_filter.invert - 1.0).abs() < 0.0001);
assert!((current_filter.sepia - 0.0).abs() < 0.0001);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(later_declaration.backdrop_filter.is_none());
}
#[test]
fn revert_layer_resets_clip_path_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { clip-path: none; } \
.revert { clip-path: inset(25%); } \
.override { clip-path: circle(35% at 50% 50%); } \
} \
@layer special { \
.item { clip-path: circle(40% at 50% 50%); } \
.revert { clip-path: revert-layer; } \
.override { clip-path: revert-layer; clip-path: none; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(
clip_path_debug(&reverted).as_deref(),
Some("inset(25.000% 25.000% 25.000% 25.000%)")
);
assert!(reverted.clip_path_inset.is_some());
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
clip_path_debug(¤t_layer).as_deref(),
Some("circle(40.000% at 50.000% 50.000%)")
);
assert_eq!(current_layer.clip_path_inset, None);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(later_declaration.clip_path.is_none());
assert_eq!(
later_declaration.clip_path_reference_box,
ClipPathReferenceBox::Border
);
assert_eq!(later_declaration.clip_path_inset, None);
}
#[test]
fn revert_layer_resets_box_shadow_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { box-shadow: none; } \
.revert { box-shadow: 12px 0 0 0 rgb(0, 0, 255); } \
.override { box-shadow: 12px 0 0 0 rgb(255, 0, 0); } \
} \
@layer special { \
.item { box-shadow: 12px 0 0 0 rgb(255, 0, 0); } \
.revert { box-shadow: revert-layer; } \
.override { box-shadow: revert-layer; box-shadow: none; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.box_shadows.len(), 1);
assert!(reverted.box_shadow.is_some());
assert!(reverted.box_shadows[0].color.b > 0.99);
assert!(reverted.box_shadows[0].color.r < 0.01);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.box_shadows.len(), 1);
assert!(current_layer.box_shadows[0].color.r > 0.99);
assert!(current_layer.box_shadows[0].color.b < 0.01);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(later_declaration.box_shadow.is_none());
assert!(later_declaration.box_shadows.is_empty());
}
#[test]
fn revert_layer_resets_text_shadow_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { text-shadow: none; } \
.revert { text-shadow: 6px 0 0 rgb(0, 0, 255); } \
.override { text-shadow: 6px 0 0 rgb(255, 0, 0); } \
} \
@layer special { \
.item { text-shadow: 6px 0 0 rgb(255, 0, 0); } \
.revert { text-shadow: revert-layer; } \
.override { text-shadow: revert-layer; text-shadow: none; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.text_shadows.len(), 1);
assert!(reverted.text_shadows[0].color.b > 0.99);
assert!(reverted.text_shadows[0].color.r < 0.01);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.text_shadows.len(), 1);
assert!(current_layer.text_shadows[0].color.r > 0.99);
assert!(current_layer.text_shadows[0].color.b < 0.01);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(later_declaration.text_shadows.is_empty());
}
#[test]
fn revert_layer_resets_text_decoration_color_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { color: rgb(0, 128, 0); text-decoration-line: underline; text-decoration-color: currentColor; } \
.revert { text-decoration-color: rgb(0, 0, 255); } \
.override { text-decoration-color: rgb(0, 0, 255); } \
} \
@layer special { \
.item { color: rgb(255, 0, 0); text-decoration-color: rgb(255, 0, 0); } \
.revert { text-decoration-color: revert-layer; } \
.override { text-decoration-color: revert-layer; text-decoration-color: currentColor; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert!(reverted.text_decoration.underline);
assert_eq!(reverted.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(reverted.text_decoration_color, Color::rgb(0.0, 0.0, 1.0));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.text_decoration_color,
Color::rgb(1.0, 0.0, 0.0)
);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.text_decoration_color,
Color::rgb(1.0, 0.0, 0.0)
);
}
#[test]
fn revert_layer_resets_text_decoration_metrics_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { text-decoration-line: underline; text-decoration-thickness: 2pt; text-decoration-style: solid; text-underline-offset: 2pt; text-underline-position: auto; } \
.revert { text-decoration-line: underline overline; text-decoration-thickness: 7pt; text-decoration-style: dashed; text-underline-offset: 8pt; text-underline-position: under right; } \
.override { text-decoration-line: underline overline; text-decoration-thickness: 7pt; text-decoration-style: dashed; text-underline-offset: 8pt; text-underline-position: under right; } \
} \
@layer special { \
.item { text-decoration-line: line-through; text-decoration-thickness: 4pt; text-decoration-style: double; text-underline-offset: -0.25em; text-underline-position: auto; } \
.revert { text-decoration-line: revert-layer; text-decoration-thickness: revert-layer; text-decoration-style: revert-layer; text-underline-offset: revert-layer; text-underline-position: revert-layer; } \
.override { text-decoration-line: revert-layer; text-decoration-thickness: revert-layer; text-decoration-style: revert-layer; text-underline-offset: revert-layer; text-underline-position: revert-layer; text-decoration-line: underline; text-decoration-thickness: 5pt; text-decoration-style: wavy; text-underline-offset: 10%; text-underline-position: under; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(
reverted.text_decoration,
TextDecorationMode {
underline: true,
overline: true,
line_through: false,
}
);
assert_eq!(
reverted.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(7.0)))
);
assert_eq!(
reverted.text_decoration_style,
TextDecorationStyleMode::Dashed
);
assert_eq!(
reverted.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Absolute(Pt::from_f32(8.0)))
);
assert_eq!(
reverted.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: Some(TextUnderlinePositionSide::Right),
}
);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.text_decoration,
TextDecorationMode {
underline: false,
overline: false,
line_through: true,
}
);
assert_eq!(
current_layer.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(4.0)))
);
assert_eq!(
current_layer.text_decoration_style,
TextDecorationStyleMode::Double
);
assert_eq!(
current_layer.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Em(-0.25))
);
assert_eq!(
current_layer.text_underline_position,
TextUnderlinePositionMode::auto()
);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.text_decoration,
TextDecorationMode {
underline: true,
overline: false,
line_through: false,
}
);
assert_eq!(
later_declaration.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(5.0)))
);
assert_eq!(
later_declaration.text_decoration_style,
TextDecorationStyleMode::Wavy
);
assert_eq!(
later_declaration.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Percent(0.1))
);
assert_eq!(
later_declaration.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: None,
}
);
}
#[test]
fn revert_layer_resets_text_overflow_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { text-overflow: clip; } \
.revert { text-overflow: clip; } \
.override { text-overflow: clip; } \
} \
@layer special { \
.item { text-overflow: ellipsis; } \
.revert { text-overflow: revert-layer; } \
.override { text-overflow: revert-layer; text-overflow: ellipsis; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.text_overflow, TextOverflowMode::Clip);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.text_overflow, TextOverflowMode::Ellipsis);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.text_overflow, TextOverflowMode::Ellipsis);
}
#[test]
fn revert_layer_resets_text_indent_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { text-indent: 6pt; } \
.revert { text-indent: 18pt; } \
.override { text-indent: 18pt; } \
} \
@layer special { \
.item { text-indent: 12pt hanging; } \
.revert { text-indent: revert-layer; } \
.override { text-indent: revert-layer; text-indent: 24pt each-line; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(
reverted.text_indent,
LengthSpec::Absolute(Pt::from_f32(18.0))
);
assert!(!reverted.text_indent_hanging);
assert!(!reverted.text_indent_each_line);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.text_indent,
LengthSpec::Absolute(Pt::from_f32(12.0))
);
assert!(current_layer.text_indent_hanging);
assert!(!current_layer.text_indent_each_line);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.text_indent,
LengthSpec::Absolute(Pt::from_f32(24.0))
);
assert!(!later_declaration.text_indent_hanging);
assert!(later_declaration.text_indent_each_line);
}
#[test]
fn revert_layer_resets_text_spacing_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { letter-spacing: 1pt; word-spacing: 2pt; } \
.revert { letter-spacing: 3pt; word-spacing: 4pt; } \
.override { letter-spacing: 3pt; word-spacing: 4pt; } \
} \
@layer special { \
.item { letter-spacing: 5pt; word-spacing: 6pt; } \
.revert { letter-spacing: revert-layer; word-spacing: revert-layer; } \
.override { letter-spacing: revert-layer; word-spacing: revert-layer; letter-spacing: 7pt; word-spacing: 8pt; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.letter_spacing, Pt::from_f32(3.0));
assert_eq!(reverted.word_spacing, Pt::from_f32(4.0));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.letter_spacing, Pt::from_f32(5.0));
assert_eq!(current_layer.word_spacing, Pt::from_f32(6.0));
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.letter_spacing, Pt::from_f32(7.0));
assert_eq!(later_declaration.word_spacing, Pt::from_f32(8.0));
}
#[test]
fn revert_layer_resets_tab_size_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { tab-size: 2; } \
.revert { tab-size: 4; } \
.override { tab-size: 4; } \
} \
@layer special { \
.item { tab-size: 6; } \
.revert { tab-size: revert-layer; } \
.override { tab-size: revert-layer; tab-size: 18pt; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.tab_size, TabSizeSpec::Spaces(4.0));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.tab_size, TabSizeSpec::Spaces(6.0));
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(
later_declaration.tab_size,
TabSizeSpec::Length(LengthSpec::Absolute(Pt::from_f32(18.0)))
);
}
#[test]
fn revert_layer_resets_text_alignment_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { text-align: center; text-align-last: center; text-justify: inter-word; } \
.revert { text-align: left; text-align-last: right; text-justify: none; } \
.override { text-align: left; text-align-last: right; text-justify: none; } \
} \
@layer special { \
.item { text-align: right; text-align-last: justify; text-justify: inter-character; } \
.revert { text-align: revert-layer; text-align-last: revert-layer; text-justify: revert-layer; } \
.override { text-align: revert-layer; text-align-last: revert-layer; text-justify: revert-layer; text-align: justify; text-align-last: center; text-justify: inter-word; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.text_align, TextAlignMode::Left);
assert_eq!(reverted.text_align_last, TextAlignLastMode::Right);
assert_eq!(reverted.text_justify, TextJustifyMode::None);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.text_align, TextAlignMode::Right);
assert_eq!(current_layer.text_align_last, TextAlignLastMode::Justify);
assert_eq!(current_layer.text_justify, TextJustifyMode::InterCharacter);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.text_align, TextAlignMode::Justify);
assert_eq!(later_declaration.text_align_last, TextAlignLastMode::Center);
assert_eq!(later_declaration.text_justify, TextJustifyMode::InterWord);
}
#[test]
fn revert_layer_resets_text_transform_line_break_and_hyphenation_to_previous_layer() {
let css = r#"@layer base, special;
@layer base {
.item { text-transform: uppercase; line-break: loose; hyphens: auto; hyphenate-character: "~"; }
.revert { text-transform: lowercase; line-break: strict; hyphens: none; hyphenate-character: "="; }
.override { text-transform: lowercase; line-break: strict; hyphens: none; hyphenate-character: "="; }
}
@layer special {
.item { text-transform: capitalize; line-break: anywhere; hyphens: manual; hyphenate-character: "_"; }
.revert { text-transform: revert-layer; line-break: revert-layer; hyphens: revert-layer; hyphenate-character: revert-layer; }
.override { text-transform: revert-layer; line-break: revert-layer; hyphens: revert-layer; hyphenate-character: revert-layer; text-transform: none; line-break: normal; hyphens: auto; hyphenate-character: auto; }
}"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.text_transform, TextTransformMode::Lowercase);
assert_eq!(reverted.line_break, LineBreakMode::Strict);
assert_eq!(reverted.hyphens, HyphensMode::None);
assert_eq!(reverted.hyphenate_character.as_deref(), Some("="));
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.text_transform, TextTransformMode::Capitalize);
assert_eq!(current_layer.line_break, LineBreakMode::Anywhere);
assert_eq!(current_layer.hyphens, HyphensMode::Manual);
assert_eq!(current_layer.hyphenate_character.as_deref(), Some("_"));
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.text_transform, TextTransformMode::None);
assert_eq!(later_declaration.line_break, LineBreakMode::Normal);
assert_eq!(later_declaration.hyphens, HyphensMode::Auto);
assert_eq!(later_declaration.hyphenate_character.as_deref(), None);
}
#[test]
fn revert_layer_resets_word_break_and_overflow_wrap_to_previous_layer() {
let css = r#"@layer base, special;
@layer base {
.item { overflow-wrap: normal; word-break: normal; }
.word { word-break: break-all; }
.overflow { overflow-wrap: break-word; }
.alias { word-wrap: break-word; }
.override { word-break: keep-all; }
}
@layer special {
.item { overflow-wrap: anywhere; }
.word { word-break: revert-layer; }
.overflow { overflow-wrap: revert-layer; }
.alias { word-wrap: revert-layer; }
.override { word-break: revert-layer; word-break: normal; }
}"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.word_break, WordBreakMode::Normal);
assert_eq!(current_layer.overflow_wrap, OverflowWrapMode::Anywhere);
let word_break_reverted =
resolver.compute_style(&element("p", None, &["item", "word"]), &root, None, &[]);
assert_eq!(word_break_reverted.word_break, WordBreakMode::BreakAll);
assert_eq!(
word_break_reverted.overflow_wrap,
OverflowWrapMode::Anywhere
);
let overflow_wrap_reverted =
resolver.compute_style(&element("p", None, &["item", "overflow"]), &root, None, &[]);
assert_eq!(overflow_wrap_reverted.word_break, WordBreakMode::Normal);
assert_eq!(
overflow_wrap_reverted.overflow_wrap,
OverflowWrapMode::BreakWord
);
let word_wrap_alias_reverted =
resolver.compute_style(&element("p", None, &["item", "alias"]), &root, None, &[]);
assert_eq!(word_wrap_alias_reverted.word_break, WordBreakMode::Normal);
assert_eq!(
word_wrap_alias_reverted.overflow_wrap,
OverflowWrapMode::BreakWord
);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.word_break, WordBreakMode::Normal);
assert_eq!(later_declaration.overflow_wrap, OverflowWrapMode::Anywhere);
}
#[test]
fn word_break_and_overflow_wrap_compile_independently() {
let resolver = StyleResolver::new(
".item { word-break: keep-all; overflow-wrap: normal; }\
.breakable { overflow-wrap: anywhere; }",
);
let root = resolver.default_style();
let keep_all = resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(keep_all.word_break, WordBreakMode::KeepAll);
assert_eq!(keep_all.overflow_wrap, OverflowWrapMode::Normal);
let breakable = resolver.compute_style(
&element("p", None, &["item", "breakable"]),
&root,
None,
&[],
);
assert_eq!(breakable.word_break, WordBreakMode::KeepAll);
assert_eq!(breakable.overflow_wrap, OverflowWrapMode::Anywhere);
}
#[test]
fn revert_layer_resets_white_space_and_text_wrap_to_previous_layer() {
let css = r#"@layer base, special;
@layer base {
.item { white-space: normal; text-wrap: wrap; }
.white { white-space: preserve nowrap; }
.collapse { white-space-collapse: preserve-spaces; }
.wrap { text-wrap: nowrap pretty; }
.mode { text-wrap-mode: nowrap; }
.style { text-wrap-style: stable; }
.override { white-space: pre-wrap; text-wrap: nowrap pretty; }
}
@layer special {
.item { white-space: collapse; text-wrap: balance; }
.white { white-space: revert-layer; }
.collapse { white-space-collapse: revert-layer; }
.wrap { text-wrap: revert-layer; }
.mode { text-wrap-mode: revert-layer; }
.style { text-wrap-style: revert-layer; }
.override { white-space: revert-layer; text-wrap: revert-layer; white-space: pre-line; text-wrap: nowrap stable; }
}"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.white_space, WhiteSpaceMode::Normal);
assert_eq!(current_layer.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(current_layer.text_wrap_style, TextWrapStyleMode::Balance);
let white_space_reverted =
resolver.compute_style(&element("p", None, &["item", "white"]), &root, None, &[]);
assert_eq!(white_space_reverted.white_space, WhiteSpaceMode::Pre);
assert_eq!(white_space_reverted.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(
white_space_reverted.text_wrap_style,
TextWrapStyleMode::Balance
);
let collapse_reverted =
resolver.compute_style(&element("p", None, &["item", "collapse"]), &root, None, &[]);
assert_eq!(
collapse_reverted.white_space,
WhiteSpaceMode::PreserveSpaces
);
assert_eq!(collapse_reverted.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(
collapse_reverted.text_wrap_style,
TextWrapStyleMode::Balance
);
let text_wrap_reverted =
resolver.compute_style(&element("p", None, &["item", "wrap"]), &root, None, &[]);
assert_eq!(text_wrap_reverted.white_space, WhiteSpaceMode::Normal);
assert_eq!(text_wrap_reverted.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(
text_wrap_reverted.text_wrap_style,
TextWrapStyleMode::Pretty
);
let mode_reverted =
resolver.compute_style(&element("p", None, &["item", "mode"]), &root, None, &[]);
assert_eq!(mode_reverted.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(mode_reverted.text_wrap_style, TextWrapStyleMode::Balance);
let style_reverted =
resolver.compute_style(&element("p", None, &["item", "style"]), &root, None, &[]);
assert_eq!(style_reverted.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(style_reverted.text_wrap_style, TextWrapStyleMode::Stable);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.white_space, WhiteSpaceMode::PreLine);
assert_eq!(later_declaration.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(later_declaration.text_wrap_style, TextWrapStyleMode::Stable);
}
#[test]
fn revert_layer_resets_vertical_align_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { vertical-align: baseline; } \
.revert { vertical-align: middle; } \
.var { --va: bottom; vertical-align: var(--va); } \
.override { vertical-align: top; } \
} \
@layer special { \
.item { vertical-align: text-top; } \
.revert { vertical-align: revert-layer; } \
.var { vertical-align: revert-layer; } \
.override { vertical-align: revert-layer; vertical-align: text-bottom; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("span", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.vertical_align, VerticalAlignMode::TextTop);
let reverted = resolver.compute_style(
&element("span", None, &["item", "revert"]),
&root,
None,
&[],
);
assert_eq!(reverted.vertical_align, VerticalAlignMode::Middle);
let var_reverted =
resolver.compute_style(&element("span", None, &["item", "var"]), &root, None, &[]);
assert_eq!(var_reverted.vertical_align, VerticalAlignMode::Bottom);
let later_declaration = resolver.compute_style(
&element("span", None, &["item", "override"]),
&root,
None,
&[],
);
assert_eq!(
later_declaration.vertical_align,
VerticalAlignMode::TextBottom
);
}
#[test]
fn revert_layer_resets_list_style_position_to_previous_layer() {
let css = "ul { list-style-position: inside; } \
@layer base, special; \
@layer base { \
.item { list-style-position: outside; } \
.override { list-style-position: inside; } \
} \
@layer special { \
.item { list-style-position: inside; } \
.revert { list-style-position: revert-layer; } \
.override { list-style-position: revert-layer; list-style-position: outside; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let ul_info = element("ul", None, &[]);
let ul_style = resolver.compute_style(&ul_info, &root, None, &[]);
assert_eq!(ul_style.list_style_position, ListStylePositionMode::Inside);
let current_layer = resolver.compute_style(
&element("li", None, &["item"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(
current_layer.list_style_position,
ListStylePositionMode::Inside
);
let reverted = resolver.compute_style(
&element("li", None, &["item", "revert"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(reverted.list_style_position, ListStylePositionMode::Outside);
let later_declaration = resolver.compute_style(
&element("li", None, &["item", "override"]),
&ul_style,
None,
&[ul_info],
);
assert_eq!(
later_declaration.list_style_position,
ListStylePositionMode::Outside
);
}
#[test]
fn revert_layer_resets_list_style_type_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { list-style-type: square; } \
.custom { list-style-type: \"* \"; } \
.override { list-style-type: circle; } \
} \
@layer special { \
.item { list-style-type: decimal; } \
.revert { list-style-type: revert-layer; } \
.custom { list-style-type: revert-layer; } \
.override { list-style-type: revert-layer; list-style-type: disclosure-open; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("li", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.list_style_type, ListStyleTypeMode::Decimal);
let reverted =
resolver.compute_style(&element("li", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.list_style_type, ListStyleTypeMode::Square);
let custom_reverted =
resolver.compute_style(&element("li", None, &["item", "custom"]), &root, None, &[]);
assert_eq!(
custom_reverted.list_style_type,
ListStyleTypeMode::CustomString
);
assert_eq!(custom_reverted.list_style_marker.as_deref(), Some("* "));
let later_declaration = resolver.compute_style(
&element("li", None, &["item", "override"]),
&root,
None,
&[],
);
assert_eq!(
later_declaration.list_style_type,
ListStyleTypeMode::DisclosureOpen
);
}
#[test]
fn list_style_image_parses_inherits_and_resets_from_shorthand() {
let css = "ul { list-style-image: url(\"data:image/png;base64,AAAA\"); } \
.none { list-style-image: none; } \
.inherit { list-style-image: inherit; } \
.shorthand { list-style: url(\"marker.png\") square inside; } \
.reset { list-style: lower-alpha outside; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let ul_info = element("ul", None, &[]);
let ul_style = resolver.compute_style(&ul_info, &root, None, &[]);
assert_eq!(
ul_style.list_style_image.as_deref(),
Some("data:image/png;base64,AAAA")
);
let inherited = resolver.compute_style(
&element("li", None, &[]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(
inherited.list_style_image.as_deref(),
Some("data:image/png;base64,AAAA")
);
let none = resolver.compute_style(
&element("li", None, &["none"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert!(none.list_style_image.is_none());
assert!(none.list_style_image_paint.is_none());
let explicit_inherit = resolver.compute_style(
&element("li", None, &["inherit"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(
explicit_inherit.list_style_image.as_deref(),
Some("data:image/png;base64,AAAA")
);
let shorthand = resolver.compute_style(
&element("li", None, &["shorthand"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(shorthand.list_style_image.as_deref(), Some("marker.png"));
assert!(shorthand.list_style_image_paint.is_none());
assert_eq!(shorthand.list_style_type, ListStyleTypeMode::Square);
assert_eq!(shorthand.list_style_position, ListStylePositionMode::Inside);
let reset = resolver.compute_style(
&element("li", None, &["reset"]),
&ul_style,
None,
&[ul_info],
);
assert!(reset.list_style_image.is_none());
assert!(reset.list_style_image_paint.is_none());
assert_eq!(reset.list_style_type, ListStyleTypeMode::LowerAlpha);
assert_eq!(reset.list_style_position, ListStylePositionMode::Outside);
}
#[test]
fn list_style_image_gradient_computes_marker_paint() {
let css = ".gradient { list-style-image: linear-gradient(to right, red 0%, blue 100%); } \
.current { color: rgb(0, 128, 0); list-style-image: linear-gradient(to right, currentColor, blue); } \
.radial { list-style-image: radial-gradient(circle at center, red 0%, blue 100%); } \
.conic { list-style-image: conic-gradient(from 0deg, red 0deg, blue 360deg); } \
.shorthand { list-style: linear-gradient(to right, red, blue) square outside; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let gradient =
resolver.compute_style(&element("li", None, &["gradient"]), &root, None, &[]);
assert!(
gradient
.list_style_image
.as_deref()
.unwrap_or_default()
.contains("linear-gradient")
);
match gradient.list_style_image_paint {
Some(BackgroundPaint::LinearGradient { stops, .. }) => {
assert_eq!(stops.len(), 2);
}
other => panic!("expected list-style-image linear-gradient paint, got {other:?}"),
}
let current = resolver.compute_style(&element("li", None, &["current"]), &root, None, &[]);
assert!(matches!(
current.list_style_image_paint,
Some(BackgroundPaint::LinearGradient { .. })
));
let radial = resolver.compute_style(&element("li", None, &["radial"]), &root, None, &[]);
assert!(matches!(
radial.list_style_image_paint,
Some(BackgroundPaint::RadialGradient { .. })
));
let conic = resolver.compute_style(&element("li", None, &["conic"]), &root, None, &[]);
assert!(matches!(
conic.list_style_image_paint,
Some(BackgroundPaint::ConicGradient { .. })
));
let shorthand =
resolver.compute_style(&element("li", None, &["shorthand"]), &root, None, &[]);
assert!(matches!(
shorthand.list_style_image_paint,
Some(BackgroundPaint::LinearGradient { .. })
));
assert_eq!(shorthand.list_style_type, ListStyleTypeMode::Square);
}
#[test]
fn revert_layer_resets_list_style_image_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { list-style-image: url(\"base.png\"); } \
.override { list-style-image: url(\"base.png\"); } \
} \
@layer special { \
.item { list-style-image: url(\"special.png\"); } \
.revert { list-style-image: revert-layer; } \
.override { list-style-image: revert-layer; list-style-image: url(\"later.png\"); } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("li", None, &["item"]), &root, None, &[]);
assert_eq!(
current_layer.list_style_image.as_deref(),
Some("special.png")
);
let reverted =
resolver.compute_style(&element("li", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.list_style_image.as_deref(), Some("base.png"));
let later_declaration = resolver.compute_style(
&element("li", None, &["item", "override"]),
&root,
None,
&[],
);
assert_eq!(
later_declaration.list_style_image.as_deref(),
Some("later.png")
);
}
#[test]
fn revert_layer_resets_direction_and_writing_mode_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { position: absolute; width: 10px; height: 10px; direction: ltr; writing-mode: horizontal-tb; } \
.dir { direction: rtl; } \
.mode { writing-mode: vertical-rl; } \
.both { direction: rtl; writing-mode: vertical-rl; inset-inline-start: 8px; inset-block-start: 12px; } \
.override { direction: rtl; writing-mode: vertical-rl; } \
} \
@layer special { \
.item { direction: ltr; writing-mode: horizontal-tb; } \
.dir { direction: revert-layer; } \
.mode { writing-mode: revert-layer; } \
.both { direction: revert-layer; writing-mode: revert-layer; } \
.override { direction: revert-layer; writing-mode: revert-layer; direction: ltr; writing-mode: vertical-lr; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.direction, DirectionMode::Ltr);
assert_eq!(current_layer.writing_mode, WritingModeMode::HorizontalTb);
let direction_reverted =
resolver.compute_style(&element("p", None, &["item", "dir"]), &root, None, &[]);
assert_eq!(direction_reverted.direction, DirectionMode::Rtl);
assert_eq!(
direction_reverted.writing_mode,
WritingModeMode::HorizontalTb
);
let writing_mode_reverted =
resolver.compute_style(&element("p", None, &["item", "mode"]), &root, None, &[]);
assert_eq!(writing_mode_reverted.direction, DirectionMode::Ltr);
assert_eq!(
writing_mode_reverted.writing_mode,
WritingModeMode::VerticalRl
);
let both_reverted =
resolver.compute_style(&element("p", None, &["item", "both"]), &root, None, &[]);
assert_eq!(both_reverted.direction, DirectionMode::Rtl);
assert_eq!(both_reverted.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(
both_reverted.inset_bottom,
LengthSpec::Absolute(px_to_pt(8.0))
);
assert_eq!(
both_reverted.inset_right,
LengthSpec::Absolute(px_to_pt(12.0))
);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.direction, DirectionMode::Ltr);
assert_eq!(later_declaration.writing_mode, WritingModeMode::VerticalLr);
}
#[test]
fn text_orientation_parses_and_inherits_in_vertical_flow() {
let css = "body { writing-mode: vertical-rl; text-orientation: upright; } \
.sideways { text-orientation: sideways; } \
.initial { text-orientation: initial; } \
.inherit { text-orientation: inherit; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body.text_orientation, TextOrientationMode::Upright);
let sideways = resolver.compute_style(
&element("span", None, &["sideways"]),
&body,
None,
&[body_info.clone()],
);
assert_eq!(sideways.text_orientation, TextOrientationMode::Sideways);
let initial = resolver.compute_style(
&element("span", None, &["initial"]),
&body,
None,
&[body_info.clone()],
);
assert_eq!(initial.text_orientation, TextOrientationMode::Mixed);
let inherited = resolver.compute_style(
&element("span", None, &["inherit"]),
&body,
None,
&[body_info],
);
assert_eq!(inherited.text_orientation, TextOrientationMode::Upright);
}
#[test]
fn revert_layer_resets_overflow_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { overflow: visible; } \
.revert { overflow: hidden visible; } \
.inline { writing-mode: vertical-rl; overflow-inline: hidden; overflow-block: visible; } \
.override { overflow: hidden; } \
} \
@layer special { \
.item { overflow: hidden; } \
.revert { overflow: revert-layer; } \
.inline { overflow-inline: revert-layer; overflow-block: revert-layer; } \
.override { overflow: revert-layer; overflow: visible; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.overflow_x, OverflowMode::Hidden);
assert_eq!(reverted.overflow_y, OverflowMode::Visible);
assert_eq!(reverted.overflow, OverflowMode::Hidden);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.overflow_x, OverflowMode::Hidden);
assert_eq!(current_layer.overflow_y, OverflowMode::Hidden);
assert_eq!(current_layer.overflow, OverflowMode::Hidden);
let logical =
resolver.compute_style(&element("p", None, &["item", "inline"]), &root, None, &[]);
assert_eq!(logical.overflow_x, OverflowMode::Visible);
assert_eq!(logical.overflow_y, OverflowMode::Hidden);
assert_eq!(logical.overflow, OverflowMode::Hidden);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.overflow_x, OverflowMode::Visible);
assert_eq!(later_declaration.overflow_y, OverflowMode::Visible);
assert_eq!(later_declaration.overflow, OverflowMode::Visible);
}
#[test]
fn revert_layer_resets_mix_blend_mode_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { mix-blend-mode: normal; } \
.revert { mix-blend-mode: difference; } \
.override { mix-blend-mode: multiply; } \
} \
@layer special { \
.item { mix-blend-mode: multiply; } \
.revert { mix-blend-mode: revert-layer; } \
.override { mix-blend-mode: revert-layer; mix-blend-mode: normal; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert_eq!(reverted.mix_blend_mode, MixBlendMode::Difference);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(current_layer.mix_blend_mode, MixBlendMode::Multiply);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert_eq!(later_declaration.mix_blend_mode, MixBlendMode::Normal);
}
#[test]
fn revert_layer_resets_isolation_to_previous_layer() {
let css = "@layer base, special; \
@layer base { \
.item { isolation: auto; } \
.revert { isolation: auto; } \
.override { isolation: isolate; } \
} \
@layer special { \
.item { isolation: isolate; } \
.revert { isolation: revert-layer; } \
.override { isolation: revert-layer; isolation: auto; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let reverted =
resolver.compute_style(&element("p", None, &["item", "revert"]), &root, None, &[]);
assert!(!reverted.isolation);
let current_layer =
resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert!(current_layer.isolation);
let later_declaration =
resolver.compute_style(&element("p", None, &["item", "override"]), &root, None, &[]);
assert!(!later_declaration.isolation);
}
#[test]
fn supports_rule_conditions_gate_nested_rules() {
let css = ".item { color: rgb(0, 0, 0); } \
@supports (display: flex) { .supported { color: rgb(255, 0, 0); } } \
@supports not (display: no-such-value) { .not-supported { color: rgb(0, 128, 0); } } \
@supports (display: flex) and (color: rgb(0, 0, 0)) { .and-supported { color: rgb(0, 0, 255); } } \
@supports (display: no-such-value) or (display: block) { .or-supported { color: rgb(128, 0, 128); } } \
@supports selector(.selector-supported) { .selector-supported { color: rgb(255, 128, 0); } } \
@supports (display: no-such-value) { .unsupported { color: rgb(255, 0, 0); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let supported = resolver.compute_style(
&element("p", None, &["item", "supported"]),
&root,
None,
&[],
);
let not_supported = resolver.compute_style(
&element("p", None, &["item", "not-supported"]),
&root,
None,
&[],
);
let and_supported = resolver.compute_style(
&element("p", None, &["item", "and-supported"]),
&root,
None,
&[],
);
let or_supported = resolver.compute_style(
&element("p", None, &["item", "or-supported"]),
&root,
None,
&[],
);
let selector_supported = resolver.compute_style(
&element("p", None, &["item", "selector-supported"]),
&root,
None,
&[],
);
let unsupported = resolver.compute_style(
&element("p", None, &["item", "unsupported"]),
&root,
None,
&[],
);
assert_eq!(supported.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(not_supported.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(and_supported.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(
or_supported.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(
selector_supported.color,
Color::rgb(1.0, 128.0 / 255.0, 0.0)
);
assert_eq!(unsupported.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn starting_style_rules_do_not_apply_to_static_output() {
let css = ".item { color: rgb(0, 0, 0); } \
@starting-style { .entry { color: rgb(255, 0, 0) !important; } .only-starting { color: rgb(255, 0, 0); } } \
.entry { color: rgb(0, 128, 0); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let entry =
resolver.compute_style(&element("p", None, &["item", "entry"]), &root, None, &[]);
let only_starting = resolver.compute_style(
&element("p", None, &["item", "only-starting"]),
&root,
None,
&[],
);
assert_eq!(entry.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(only_starting.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn keyframes_rules_do_not_apply_to_static_output() {
let css = ".item { color: rgb(0, 0, 0); } \
@keyframes alert { from { color: rgb(255, 0, 0) !important; } to { color: rgb(0, 0, 255); } } \
.animated { animation: alert 1s linear infinite; color: rgb(0, 128, 0); } \
.only-keyframed { animation-name: alert; animation-duration: 1s; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let animated =
resolver.compute_style(&element("p", None, &["item", "animated"]), &root, None, &[]);
let only_keyframed = resolver.compute_style(
&element("p", None, &["item", "only-keyframed"]),
&root,
None,
&[],
);
assert_eq!(animated.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(only_keyframed.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn view_transition_rules_do_not_apply_to_static_output() {
let css = ".item { color: rgb(0, 0, 0); } \
@view-transition { navigation: auto; types: slide detail; --accent: rgb(255, 0, 0); } \
.transitioned { view-transition-name: hero; color: rgb(0, 128, 0); } \
.descriptor-var { color: var(--accent, rgb(0, 0, 255)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let transitioned = resolver.compute_style(
&element("p", None, &["item", "transitioned"]),
&root,
None,
&[],
);
let descriptor_var = resolver.compute_style(
&element("p", None, &["item", "descriptor-var"]),
&root,
None,
&[],
);
assert_eq!(transitioned.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(descriptor_var.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn property_rules_seed_initial_values_and_control_inheritance() {
let css = "@property --tone { syntax: \"<color>\"; inherits: false; initial-value: rgb(0, 0, 255); } \
@property --box-width { syntax: \"<length>\"; inherits: true; initial-value: 24px; } \
.parent { --tone: rgb(255, 0, 0); --box-width: 48px; } \
.child { color: var(--tone); } \
.own { --tone: rgb(0, 128, 0); color: var(--tone); } \
.invalid-color { --tone: 10px; color: var(--tone); } \
.invalid-length { --box-width: red; width: var(--box-width); } \
.box { width: var(--box-width); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("section", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child = resolver.compute_style(
&element("p", None, &["child"]),
&parent_style,
None,
&[parent_info.clone()],
);
let own = resolver.compute_style(
&element("p", None, &["own"]),
&parent_style,
None,
&[parent_info.clone()],
);
let initial_width = resolver.compute_style(&element("p", None, &["box"]), &root, None, &[]);
let inherited_width = resolver.compute_style(
&element("p", None, &["box"]),
&parent_style,
None,
&[parent_info.clone()],
);
let invalid_color =
resolver.compute_style(&element("p", None, &["invalid-color"]), &root, None, &[]);
let invalid_length =
resolver.compute_style(&element("p", None, &["invalid-length"]), &root, None, &[]);
assert_eq!(child.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(own.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(initial_width.width, LengthSpec::Absolute(px_to_pt(24.0)));
assert_eq!(inherited_width.width, LengthSpec::Absolute(px_to_pt(48.0)));
assert_eq!(invalid_color.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(invalid_length.width, LengthSpec::Absolute(px_to_pt(24.0)));
}
#[test]
fn container_rule_size_conditions_gate_descendant_rules() {
let css = ".box { container-type: inline-size; width: 400px; } \
.item { color: rgb(0, 0, 0); } \
@container (width > 300px) { .wide { color: rgb(255, 0, 0); } } \
@container (width < 300px) { .narrow { color: rgb(0, 0, 255); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut box_info = element("section", None, &["box"]);
let box_style = resolver.compute_style(&box_info, &root, None, &[]);
box_info.apply_computed_container_style(&box_style);
assert_eq!(box_style.container_type, ContainerQueryType::InlineSize);
assert_eq!(box_info.container_width, Some(Pt::from_f32(300.0)));
let wide = resolver.compute_style(
&element("p", None, &["item", "wide"]),
&box_style,
None,
&[box_info.clone()],
);
let narrow = resolver.compute_style(
&element("p", None, &["item", "narrow"]),
&box_style,
None,
&[box_info],
);
assert_eq!(wide.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(narrow.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn named_container_rule_uses_matching_ancestor_container() {
let css = ".box { container: card / size; width: 500px; height: 250px; } \
.item { color: rgb(0, 0, 0); } \
@container card (aspect-ratio = 2/1) { .named { color: rgb(0, 128, 0); } } \
@container other (width > 300px) { .other { color: rgb(255, 0, 0); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut box_info = element("section", None, &["box"]);
let box_style = resolver.compute_style(&box_info, &root, None, &[]);
box_info.apply_computed_container_style(&box_style);
assert_eq!(box_style.container_names, vec!["card".to_string()]);
assert_eq!(box_style.container_type, ContainerQueryType::Size);
let named = resolver.compute_style(
&element("p", None, &["item", "named"]),
&box_style,
None,
&[box_info.clone()],
);
let other = resolver.compute_style(
&element("p", None, &["item", "other"]),
&box_style,
None,
&[box_info],
);
assert_eq!(named.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(other.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn scope_rule_limits_nested_selectors_to_matching_subtrees() {
let css = ".item { color: rgb(0, 0, 0); } \
@scope (.card) { .item { color: rgb(255, 0, 0); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let card_info = element("section", None, &["card"]);
let card_style = resolver.compute_style(&card_info, &root, None, &[]);
let scoped = resolver.compute_style(
&element("p", None, &["item"]),
&card_style,
None,
&[card_info],
);
let outside = resolver.compute_style(&element("p", None, &["item"]), &root, None, &[]);
assert_eq!(scoped.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(outside.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn scope_rule_limit_selector_excludes_lower_subtree() {
let css = ".item { color: rgb(0, 0, 0); } \
@scope (.card) to (.stop) { .item { color: rgb(0, 128, 0); } }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let card_info = element("section", None, &["card"]);
let card_style = resolver.compute_style(&card_info, &root, None, &[]);
let stop_info = element("div", None, &["stop"]);
let stop_style =
resolver.compute_style(&stop_info, &card_style, None, &[card_info.clone()]);
let before_limit = resolver.compute_style(
&element("p", None, &["item"]),
&card_style,
None,
&[card_info.clone()],
);
let inside_limit = resolver.compute_style(
&element("p", None, &["item"]),
&stop_style,
None,
&[card_info, stop_info],
);
assert_eq!(before_limit.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(inside_limit.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_orientation_feature_matches_viewport_shape() {
let css = ".landscape, .portrait { color: rgb(0, 0, 0); } \
@media (orientation: landscape) { .landscape { color: rgb(255, 0, 0); } } \
@media (orientation: portrait) { .portrait { color: rgb(0, 0, 255); } }";
let landscape_viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let landscape_resolver =
StyleResolver::new_with_debug_and_viewport(css, None, Some(landscape_viewport));
let root = landscape_resolver.default_style();
let landscape =
landscape_resolver.compute_style(&element("p", None, &["landscape"]), &root, None, &[]);
let portrait_skipped =
landscape_resolver.compute_style(&element("p", None, &["portrait"]), &root, None, &[]);
let portrait_viewport = Size {
width: Pt::from_f32(400.0),
height: Pt::from_f32(600.0),
};
let portrait_resolver =
StyleResolver::new_with_debug_and_viewport(css, None, Some(portrait_viewport));
let root = portrait_resolver.default_style();
let landscape_skipped =
portrait_resolver.compute_style(&element("p", None, &["landscape"]), &root, None, &[]);
let portrait =
portrait_resolver.compute_style(&element("p", None, &["portrait"]), &root, None, &[]);
assert_eq!(landscape.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(portrait_skipped.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(landscape_skipped.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(portrait.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_shape_feature_matches_rect_static_surface() {
let css = ".shape-rect, .shape-round, .shape-bool { color: rgb(0, 0, 0); } \
@media (shape: rect) { .shape-rect { color: rgb(0, 128, 0); } } \
@media (shape: round) { .shape-round { color: rgb(255, 0, 0); } } \
@media (shape) { .shape-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let shape_rect =
resolver.compute_style(&element("p", None, &["shape-rect"]), &root, None, &[]);
let shape_round =
resolver.compute_style(&element("p", None, &["shape-round"]), &root, None, &[]);
let shape_bool =
resolver.compute_style(&element("p", None, &["shape-bool"]), &root, None, &[]);
assert_eq!(shape_rect.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(shape_round.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(shape_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_device_posture_feature_matches_continuous_static_surface() {
let css = ".posture-continuous, .posture-folded, .posture-bool { color: rgb(0, 0, 0); } \
@media (device-posture: continuous) { .posture-continuous { color: rgb(0, 128, 0); } } \
@media (device-posture: folded) { .posture-folded { color: rgb(255, 0, 0); } } \
@media (device-posture) { .posture-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let posture_continuous = resolver.compute_style(
&element("p", None, &["posture-continuous"]),
&root,
None,
&[],
);
let posture_folded =
resolver.compute_style(&element("p", None, &["posture-folded"]), &root, None, &[]);
let posture_bool =
resolver.compute_style(&element("p", None, &["posture-bool"]), &root, None, &[]);
assert_eq!(
posture_continuous.color,
Color::rgb(0.0, 128.0 / 255.0, 0.0)
);
assert_eq!(posture_folded.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(posture_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_webkit_extension_boolean_features_match_static_engine_support() {
let css = ".transform-2d, .transform-3d, .animation, .transition { color: rgb(0, 0, 0); } \
@media (-webkit-transform-2d) { .transform-2d { color: rgb(0, 128, 0); } } \
@media (-webkit-transform-3d) { .transform-3d { color: rgb(255, 0, 0); } } \
@media (-webkit-animation) { .animation { color: rgb(255, 0, 0); } } \
@media (-webkit-transition) { .transition { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let transform_2d =
resolver.compute_style(&element("p", None, &["transform-2d"]), &root, None, &[]);
let transform_3d =
resolver.compute_style(&element("p", None, &["transform-3d"]), &root, None, &[]);
let animation =
resolver.compute_style(&element("p", None, &["animation"]), &root, None, &[]);
let transition =
resolver.compute_style(&element("p", None, &["transition"]), &root, None, &[]);
assert_eq!(transform_2d.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(transform_3d.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(animation.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(transition.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_aspect_ratio_feature_matches_viewport_ratio() {
let css = ".exact, .minimum, .maximum { color: rgb(0, 0, 0); } \
@media (aspect-ratio: 3/2) { .exact { color: rgb(255, 0, 0); } } \
@media (min-aspect-ratio: 3/2) { .minimum { color: rgb(0, 128, 0); } } \
@media (max-aspect-ratio: 4/3) { .maximum { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let exact = resolver.compute_style(&element("p", None, &["exact"]), &root, None, &[]);
let minimum = resolver.compute_style(&element("p", None, &["minimum"]), &root, None, &[]);
let maximum = resolver.compute_style(&element("p", None, &["maximum"]), &root, None, &[]);
assert_eq!(exact.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(minimum.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(maximum.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_deprecated_device_features_match_paged_surface() {
let css = ".device-width, .device-width-high, .device-height, .device-height-low, \
.device-ratio, .device-ratio-high { color: rgb(0, 0, 0); } \
@media (device-width: 6in) { .device-width { color: rgb(0, 128, 0); } } \
@media (min-device-width: 7in) { .device-width-high { color: rgb(255, 0, 0); } } \
@media (device-height: 4in) { .device-height { color: rgb(0, 0, 255); } } \
@media (max-device-height: 3in) { .device-height-low { color: rgb(255, 0, 0); } } \
@media (device-aspect-ratio: 3/2) { .device-ratio { color: rgb(128, 0, 128); } } \
@media (min-device-aspect-ratio: 16/9) { .device-ratio-high { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(432.0),
height: Pt::from_f32(288.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let device_width =
resolver.compute_style(&element("p", None, &["device-width"]), &root, None, &[]);
let device_width_high = resolver.compute_style(
&element("p", None, &["device-width-high"]),
&root,
None,
&[],
);
let device_height =
resolver.compute_style(&element("p", None, &["device-height"]), &root, None, &[]);
let device_height_low = resolver.compute_style(
&element("p", None, &["device-height-low"]),
&root,
None,
&[],
);
let device_ratio =
resolver.compute_style(&element("p", None, &["device-ratio"]), &root, None, &[]);
let device_ratio_high = resolver.compute_style(
&element("p", None, &["device-ratio-high"]),
&root,
None,
&[],
);
assert_eq!(device_width.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(device_width_high.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(device_height.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(device_height_low.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
device_ratio.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(device_ratio_high.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_print_type_takes_precedence_for_paged_output() {
let css = ".print, .screen, .all, .not-print, .listed { color: rgb(0, 0, 0); } \
@media print { .print { color: rgb(0, 128, 0); } } \
@media screen { .screen { color: rgb(255, 0, 0); } } \
@media all { .all { color: rgb(0, 0, 255); } } \
@media not print { .not-print { color: rgb(255, 128, 0); } } \
@media screen, print { .listed { color: rgb(128, 0, 128); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let print = resolver.compute_style(&element("p", None, &["print"]), &root, None, &[]);
let screen = resolver.compute_style(&element("p", None, &["screen"]), &root, None, &[]);
let all = resolver.compute_style(&element("p", None, &["all"]), &root, None, &[]);
let not_print =
resolver.compute_style(&element("p", None, &["not-print"]), &root, None, &[]);
let listed = resolver.compute_style(&element("p", None, &["listed"]), &root, None, &[]);
assert_eq!(print.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(screen.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(all.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(not_print.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(listed.color, Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0));
}
#[test]
fn media_color_capability_features_match_pdf_output() {
let css = ".color, .min-color, .high-color, .mono, .grid { color: rgb(0, 0, 0); } \
@media (color) { .color { color: rgb(255, 0, 0); } } \
@media (min-color: 8) { .min-color { color: rgb(0, 128, 0); } } \
@media (min-color: 16) { .high-color { color: rgb(0, 0, 255); } } \
@media (monochrome) { .mono { color: rgb(255, 128, 0); } } \
@media (grid) { .grid { color: rgb(128, 0, 128); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let color = resolver.compute_style(&element("p", None, &["color"]), &root, None, &[]);
let min_color =
resolver.compute_style(&element("p", None, &["min-color"]), &root, None, &[]);
let high_color =
resolver.compute_style(&element("p", None, &["high-color"]), &root, None, &[]);
let mono = resolver.compute_style(&element("p", None, &["mono"]), &root, None, &[]);
let grid = resolver.compute_style(&element("p", None, &["grid"]), &root, None, &[]);
assert_eq!(color.color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(min_color.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(high_color.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(mono.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(grid.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_color_range_features_match_pdf_output() {
let css = ".index-zero, .index-min-zero, .index-min-one, .index-bool, \
.gamut-srgb, .gamut-p3, .gamut-bool, \
.dynamic-standard, .dynamic-high, .dynamic-bool { color: rgb(0, 0, 0); } \
@media (color-index: 0) { .index-zero { color: rgb(0, 128, 0); } } \
@media (min-color-index: 0) { .index-min-zero { color: rgb(0, 0, 255); } } \
@media (min-color-index: 1) { .index-min-one { color: rgb(255, 0, 0); } } \
@media (color-index) { .index-bool { color: rgb(255, 0, 0); } } \
@media (color-gamut: srgb) { .gamut-srgb { color: rgb(128, 0, 128); } } \
@media (color-gamut: p3) { .gamut-p3 { color: rgb(255, 0, 0); } } \
@media (color-gamut) { .gamut-bool { color: rgb(0, 128, 0); } } \
@media (dynamic-range: standard) { .dynamic-standard { color: rgb(0, 0, 255); } } \
@media (dynamic-range: high) { .dynamic-high { color: rgb(255, 0, 0); } } \
@media (dynamic-range) { .dynamic-bool { color: rgb(128, 0, 128); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let index_zero =
resolver.compute_style(&element("p", None, &["index-zero"]), &root, None, &[]);
let index_min_zero =
resolver.compute_style(&element("p", None, &["index-min-zero"]), &root, None, &[]);
let index_min_one =
resolver.compute_style(&element("p", None, &["index-min-one"]), &root, None, &[]);
let index_bool =
resolver.compute_style(&element("p", None, &["index-bool"]), &root, None, &[]);
let gamut_srgb =
resolver.compute_style(&element("p", None, &["gamut-srgb"]), &root, None, &[]);
let gamut_p3 = resolver.compute_style(&element("p", None, &["gamut-p3"]), &root, None, &[]);
let gamut_bool =
resolver.compute_style(&element("p", None, &["gamut-bool"]), &root, None, &[]);
let dynamic_standard =
resolver.compute_style(&element("p", None, &["dynamic-standard"]), &root, None, &[]);
let dynamic_high =
resolver.compute_style(&element("p", None, &["dynamic-high"]), &root, None, &[]);
let dynamic_bool =
resolver.compute_style(&element("p", None, &["dynamic-bool"]), &root, None, &[]);
assert_eq!(index_zero.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(index_min_zero.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(index_min_one.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(index_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
gamut_srgb.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(gamut_p3.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(gamut_bool.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(dynamic_standard.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(dynamic_high.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
dynamic_bool.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
}
#[test]
fn media_video_dynamic_range_matches_standard_static_output() {
let css = ".video-standard, .video-high, .video-bool { color: rgb(0, 0, 0); } \
@media (video-dynamic-range: standard) { .video-standard { color: rgb(0, 128, 0); } } \
@media (video-dynamic-range: high) { .video-high { color: rgb(255, 0, 0); } } \
@media (video-dynamic-range) { .video-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let video_standard =
resolver.compute_style(&element("p", None, &["video-standard"]), &root, None, &[]);
let video_high =
resolver.compute_style(&element("p", None, &["video-high"]), &root, None, &[]);
let video_bool =
resolver.compute_style(&element("p", None, &["video-bool"]), &root, None, &[]);
assert_eq!(video_standard.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(video_high.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(video_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_level5_output_features_match_static_pdf_defaults() {
let css = ".environment-opaque, .environment-additive, .environment-bool, \
.nav-none, .nav-back, .nav-bool, \
.video-gamut-srgb, .video-gamut-p3, .video-gamut-bool { color: rgb(0, 0, 0); } \
@media (environment-blending: opaque) { .environment-opaque { color: rgb(0, 128, 0); } } \
@media (environment-blending: additive) { .environment-additive { color: rgb(255, 0, 0); } } \
@media (environment-blending) { .environment-bool { color: rgb(0, 0, 255); } } \
@media (nav-controls: none) { .nav-none { color: rgb(128, 0, 128); } } \
@media (nav-controls: back) { .nav-back { color: rgb(255, 0, 0); } } \
@media (nav-controls) { .nav-bool { color: rgb(255, 0, 0); } } \
@media (video-color-gamut: srgb) { .video-gamut-srgb { color: rgb(0, 128, 0); } } \
@media (video-color-gamut: p3) { .video-gamut-p3 { color: rgb(255, 0, 0); } } \
@media (video-color-gamut) { .video-gamut-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let environment_opaque = resolver.compute_style(
&element("p", None, &["environment-opaque"]),
&root,
None,
&[],
);
let environment_additive = resolver.compute_style(
&element("p", None, &["environment-additive"]),
&root,
None,
&[],
);
let environment_bool =
resolver.compute_style(&element("p", None, &["environment-bool"]), &root, None, &[]);
let nav_none = resolver.compute_style(&element("p", None, &["nav-none"]), &root, None, &[]);
let nav_back = resolver.compute_style(&element("p", None, &["nav-back"]), &root, None, &[]);
let nav_bool = resolver.compute_style(&element("p", None, &["nav-bool"]), &root, None, &[]);
let video_gamut_srgb =
resolver.compute_style(&element("p", None, &["video-gamut-srgb"]), &root, None, &[]);
let video_gamut_p3 =
resolver.compute_style(&element("p", None, &["video-gamut-p3"]), &root, None, &[]);
let video_gamut_bool =
resolver.compute_style(&element("p", None, &["video-gamut-bool"]), &root, None, &[]);
assert_eq!(
environment_opaque.color,
Color::rgb(0.0, 128.0 / 255.0, 0.0)
);
assert_eq!(environment_additive.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(environment_bool.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(
nav_none.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(nav_back.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(nav_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(video_gamut_srgb.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(video_gamut_p3.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(video_gamut_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_prefers_color_scheme_defaults_to_light() {
let css = ".light, .dark { color: rgb(0, 0, 0); } \
@media (prefers-color-scheme: light) { .light { color: rgb(0, 128, 0); } } \
@media (prefers-color-scheme: dark) { .dark { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let light = resolver.compute_style(&element("p", None, &["light"]), &root, None, &[]);
let dark = resolver.compute_style(&element("p", None, &["dark"]), &root, None, &[]);
assert_eq!(light.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(dark.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_user_preference_features_default_to_no_preference() {
let css = ".motion-default, .motion-reduce, .motion-bool, \
.contrast-default, .contrast-more, .contrast-bool, \
.forced-none, .forced-active, .forced-bool { color: rgb(0, 0, 0); } \
@media (prefers-reduced-motion: no-preference) { .motion-default { color: rgb(0, 128, 0); } } \
@media (prefers-reduced-motion: reduce) { .motion-reduce { color: rgb(255, 0, 0); } } \
@media (prefers-reduced-motion) { .motion-bool { color: rgb(255, 0, 0); } } \
@media (prefers-contrast: no-preference) { .contrast-default { color: rgb(0, 0, 255); } } \
@media (prefers-contrast: more) { .contrast-more { color: rgb(255, 0, 0); } } \
@media (prefers-contrast) { .contrast-bool { color: rgb(255, 0, 0); } } \
@media (forced-colors: none) { .forced-none { color: rgb(128, 0, 128); } } \
@media (forced-colors: active) { .forced-active { color: rgb(255, 0, 0); } } \
@media (forced-colors) { .forced-bool { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let motion_default =
resolver.compute_style(&element("p", None, &["motion-default"]), &root, None, &[]);
let motion_reduce =
resolver.compute_style(&element("p", None, &["motion-reduce"]), &root, None, &[]);
let motion_bool =
resolver.compute_style(&element("p", None, &["motion-bool"]), &root, None, &[]);
let contrast_default =
resolver.compute_style(&element("p", None, &["contrast-default"]), &root, None, &[]);
let contrast_more =
resolver.compute_style(&element("p", None, &["contrast-more"]), &root, None, &[]);
let contrast_bool =
resolver.compute_style(&element("p", None, &["contrast-bool"]), &root, None, &[]);
let forced_none =
resolver.compute_style(&element("p", None, &["forced-none"]), &root, None, &[]);
let forced_active =
resolver.compute_style(&element("p", None, &["forced-active"]), &root, None, &[]);
let forced_bool =
resolver.compute_style(&element("p", None, &["forced-bool"]), &root, None, &[]);
assert_eq!(motion_default.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(motion_reduce.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(motion_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(contrast_default.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(contrast_more.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(contrast_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
forced_none.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(forced_active.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(forced_bool.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_additional_user_preference_features_default_to_no_preference() {
let css = ".transparency-default, .transparency-reduce, .transparency-bool, \
.data-default, .data-reduce, .data-bool, \
.inverted-none, .inverted-active, .inverted-bool { color: rgb(0, 0, 0); } \
@media (prefers-reduced-transparency: no-preference) { .transparency-default { color: rgb(0, 128, 0); } } \
@media (prefers-reduced-transparency: reduce) { .transparency-reduce { color: rgb(255, 0, 0); } } \
@media (prefers-reduced-transparency) { .transparency-bool { color: rgb(255, 0, 0); } } \
@media (prefers-reduced-data: no-preference) { .data-default { color: rgb(0, 0, 255); } } \
@media (prefers-reduced-data: reduce) { .data-reduce { color: rgb(255, 0, 0); } } \
@media (prefers-reduced-data) { .data-bool { color: rgb(255, 0, 0); } } \
@media (inverted-colors: none) { .inverted-none { color: rgb(128, 0, 128); } } \
@media (inverted-colors: inverted) { .inverted-active { color: rgb(255, 0, 0); } } \
@media (inverted-colors) { .inverted-bool { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let transparency_default = resolver.compute_style(
&element("p", None, &["transparency-default"]),
&root,
None,
&[],
);
let transparency_reduce = resolver.compute_style(
&element("p", None, &["transparency-reduce"]),
&root,
None,
&[],
);
let transparency_bool = resolver.compute_style(
&element("p", None, &["transparency-bool"]),
&root,
None,
&[],
);
let data_default =
resolver.compute_style(&element("p", None, &["data-default"]), &root, None, &[]);
let data_reduce =
resolver.compute_style(&element("p", None, &["data-reduce"]), &root, None, &[]);
let data_bool =
resolver.compute_style(&element("p", None, &["data-bool"]), &root, None, &[]);
let inverted_none =
resolver.compute_style(&element("p", None, &["inverted-none"]), &root, None, &[]);
let inverted_active =
resolver.compute_style(&element("p", None, &["inverted-active"]), &root, None, &[]);
let inverted_bool =
resolver.compute_style(&element("p", None, &["inverted-bool"]), &root, None, &[]);
assert_eq!(
transparency_default.color,
Color::rgb(0.0, 128.0 / 255.0, 0.0)
);
assert_eq!(transparency_reduce.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(transparency_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(data_default.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(data_reduce.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(data_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
inverted_none.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(inverted_active.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(inverted_bool.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_static_output_environment_features_match_pdf_defaults() {
let css = ".overflow-block-default, .overflow-block-scroll, .overflow-block-bool, \
.overflow-inline-default, .overflow-inline-scroll, .overflow-inline-bool, \
.update-default, .update-fast, .update-bool, \
.scripting-default, .scripting-enabled, .scripting-bool { color: rgb(0, 0, 0); } \
@media (overflow-block: paged) { .overflow-block-default { color: rgb(0, 128, 0); } } \
@media (overflow-block: scroll) { .overflow-block-scroll { color: rgb(255, 0, 0); } } \
@media (overflow-block) { .overflow-block-bool { color: rgb(0, 0, 255); } } \
@media (overflow-inline: none) { .overflow-inline-default { color: rgb(128, 0, 128); } } \
@media (overflow-inline: scroll) { .overflow-inline-scroll { color: rgb(255, 0, 0); } } \
@media (overflow-inline) { .overflow-inline-bool { color: rgb(255, 0, 0); } } \
@media (update: none) { .update-default { color: rgb(0, 128, 0); } } \
@media (update: fast) { .update-fast { color: rgb(255, 0, 0); } } \
@media (update) { .update-bool { color: rgb(255, 0, 0); } } \
@media (scripting: none) { .scripting-default { color: rgb(0, 0, 255); } } \
@media (scripting: enabled) { .scripting-enabled { color: rgb(255, 0, 0); } } \
@media (scripting) { .scripting-bool { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let overflow_block_default = resolver.compute_style(
&element("p", None, &["overflow-block-default"]),
&root,
None,
&[],
);
let overflow_block_scroll = resolver.compute_style(
&element("p", None, &["overflow-block-scroll"]),
&root,
None,
&[],
);
let overflow_block_bool = resolver.compute_style(
&element("p", None, &["overflow-block-bool"]),
&root,
None,
&[],
);
let overflow_inline_default = resolver.compute_style(
&element("p", None, &["overflow-inline-default"]),
&root,
None,
&[],
);
let overflow_inline_scroll = resolver.compute_style(
&element("p", None, &["overflow-inline-scroll"]),
&root,
None,
&[],
);
let overflow_inline_bool = resolver.compute_style(
&element("p", None, &["overflow-inline-bool"]),
&root,
None,
&[],
);
let update_default =
resolver.compute_style(&element("p", None, &["update-default"]), &root, None, &[]);
let update_fast =
resolver.compute_style(&element("p", None, &["update-fast"]), &root, None, &[]);
let update_bool =
resolver.compute_style(&element("p", None, &["update-bool"]), &root, None, &[]);
let scripting_default = resolver.compute_style(
&element("p", None, &["scripting-default"]),
&root,
None,
&[],
);
let scripting_enabled = resolver.compute_style(
&element("p", None, &["scripting-enabled"]),
&root,
None,
&[],
);
let scripting_bool =
resolver.compute_style(&element("p", None, &["scripting-bool"]), &root, None, &[]);
assert_eq!(
overflow_block_default.color,
Color::rgb(0.0, 128.0 / 255.0, 0.0)
);
assert_eq!(overflow_block_scroll.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(overflow_block_bool.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(
overflow_inline_default.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(overflow_inline_scroll.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(overflow_inline_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(update_default.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(update_fast.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(update_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(scripting_default.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(scripting_enabled.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(scripting_bool.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_display_mode_feature_matches_browser_default() {
let css = ".browser, .fullscreen, .standalone, .pip, .mode-bool { color: rgb(0, 0, 0); } \
@media (display-mode: browser) { .browser { color: rgb(0, 128, 0); } } \
@media (display-mode: fullscreen) { .fullscreen { color: rgb(255, 0, 0); } } \
@media (display-mode: standalone) { .standalone { color: rgb(255, 0, 0); } } \
@media (display-mode: picture-in-picture) { .pip { color: rgb(255, 0, 0); } } \
@media (display-mode) { .mode-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let browser = resolver.compute_style(&element("p", None, &["browser"]), &root, None, &[]);
let fullscreen =
resolver.compute_style(&element("p", None, &["fullscreen"]), &root, None, &[]);
let standalone =
resolver.compute_style(&element("p", None, &["standalone"]), &root, None, &[]);
let pip = resolver.compute_style(&element("p", None, &["pip"]), &root, None, &[]);
let mode_bool =
resolver.compute_style(&element("p", None, &["mode-bool"]), &root, None, &[]);
assert_eq!(browser.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(fullscreen.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(standalone.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(pip.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(mode_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_resolution_feature_matches_reference_pixel_density() {
let css = ".dpi, .dppx, .min-x, .high-dppx, .max-dpcm, .low-dpcm, .resolution-bool { color: rgb(0, 0, 0); } \
@media (resolution: 96dpi) { .dpi { color: rgb(0, 128, 0); } } \
@media (resolution: 1dppx) { .dppx { color: rgb(0, 0, 255); } } \
@media (min-resolution: 1x) { .min-x { color: rgb(128, 0, 128); } } \
@media (min-resolution: 2dppx) { .high-dppx { color: rgb(255, 0, 0); } } \
@media (max-resolution: 40dpcm) { .max-dpcm { color: rgb(0, 128, 0); } } \
@media (max-resolution: 30dpcm) { .low-dpcm { color: rgb(255, 0, 0); } } \
@media (resolution) { .resolution-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let dpi = resolver.compute_style(&element("p", None, &["dpi"]), &root, None, &[]);
let dppx = resolver.compute_style(&element("p", None, &["dppx"]), &root, None, &[]);
let min_x = resolver.compute_style(&element("p", None, &["min-x"]), &root, None, &[]);
let high_dppx =
resolver.compute_style(&element("p", None, &["high-dppx"]), &root, None, &[]);
let max_dpcm = resolver.compute_style(&element("p", None, &["max-dpcm"]), &root, None, &[]);
let low_dpcm = resolver.compute_style(&element("p", None, &["low-dpcm"]), &root, None, &[]);
let resolution_bool =
resolver.compute_style(&element("p", None, &["resolution-bool"]), &root, None, &[]);
assert_eq!(dpi.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(dppx.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(min_x.color, Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0));
assert_eq!(high_dppx.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(max_dpcm.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(low_dpcm.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(resolution_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_vendor_device_pixel_ratio_aliases_reference_pixel_density() {
let css = ".webkit-one, .webkit-high, .webkit-bool, \
.moz-one, .moz-high, .moz-bool { color: rgb(0, 0, 0); } \
@media (-webkit-device-pixel-ratio: 1) { .webkit-one { color: rgb(0, 128, 0); } } \
@media (-webkit-device-pixel-ratio: 2) { .webkit-high { color: rgb(255, 0, 0); } } \
@media (-webkit-device-pixel-ratio) { .webkit-bool { color: rgb(0, 0, 255); } } \
@media (-moz-device-pixel-ratio: 1) { .moz-one { color: rgb(128, 0, 128); } } \
@media (-moz-device-pixel-ratio: 2) { .moz-high { color: rgb(255, 0, 0); } } \
@media (-moz-device-pixel-ratio) { .moz-bool { color: rgb(0, 128, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let webkit_one =
resolver.compute_style(&element("p", None, &["webkit-one"]), &root, None, &[]);
let webkit_high =
resolver.compute_style(&element("p", None, &["webkit-high"]), &root, None, &[]);
let webkit_bool =
resolver.compute_style(&element("p", None, &["webkit-bool"]), &root, None, &[]);
let moz_one = resolver.compute_style(&element("p", None, &["moz-one"]), &root, None, &[]);
let moz_high = resolver.compute_style(&element("p", None, &["moz-high"]), &root, None, &[]);
let moz_bool = resolver.compute_style(&element("p", None, &["moz-bool"]), &root, None, &[]);
assert_eq!(webkit_one.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(webkit_high.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(webkit_bool.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(moz_one.color, Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0));
assert_eq!(moz_high.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(moz_bool.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
}
#[test]
fn media_scan_feature_matches_progressive_static_output() {
let css = ".scan-progressive, .scan-interlace, .scan-bool { color: rgb(0, 0, 0); } \
@media (scan: progressive) { .scan-progressive { color: rgb(0, 128, 0); } } \
@media (scan: interlace) { .scan-interlace { color: rgb(255, 0, 0); } } \
@media (scan) { .scan-bool { color: rgb(0, 0, 255); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let scan_progressive =
resolver.compute_style(&element("p", None, &["scan-progressive"]), &root, None, &[]);
let scan_interlace =
resolver.compute_style(&element("p", None, &["scan-interlace"]), &root, None, &[]);
let scan_bool =
resolver.compute_style(&element("p", None, &["scan-bool"]), &root, None, &[]);
assert_eq!(scan_progressive.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(scan_interlace.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(scan_bool.color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn media_input_device_features_match_no_pointer_defaults() {
let css = ".hover-none, .hover-hover, .hover-bool, \
.any-hover-none, .any-hover-hover, .any-hover-bool, \
.pointer-none, .pointer-fine, .pointer-bool, \
.any-pointer-none, .any-pointer-coarse, .any-pointer-bool { color: rgb(0, 0, 0); } \
@media (hover: none) { .hover-none { color: rgb(0, 128, 0); } } \
@media (hover: hover) { .hover-hover { color: rgb(255, 0, 0); } } \
@media (hover) { .hover-bool { color: rgb(255, 0, 0); } } \
@media (any-hover: none) { .any-hover-none { color: rgb(0, 0, 255); } } \
@media (any-hover: hover) { .any-hover-hover { color: rgb(255, 0, 0); } } \
@media (any-hover) { .any-hover-bool { color: rgb(255, 0, 0); } } \
@media (pointer: none) { .pointer-none { color: rgb(128, 0, 128); } } \
@media (pointer: fine) { .pointer-fine { color: rgb(255, 0, 0); } } \
@media (pointer) { .pointer-bool { color: rgb(255, 0, 0); } } \
@media (any-pointer: none) { .any-pointer-none { color: rgb(0, 128, 0); } } \
@media (any-pointer: coarse) { .any-pointer-coarse { color: rgb(255, 0, 0); } } \
@media (any-pointer) { .any-pointer-bool { color: rgb(255, 0, 0); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let hover_none =
resolver.compute_style(&element("p", None, &["hover-none"]), &root, None, &[]);
let hover_hover =
resolver.compute_style(&element("p", None, &["hover-hover"]), &root, None, &[]);
let hover_bool =
resolver.compute_style(&element("p", None, &["hover-bool"]), &root, None, &[]);
let any_hover_none =
resolver.compute_style(&element("p", None, &["any-hover-none"]), &root, None, &[]);
let any_hover_hover =
resolver.compute_style(&element("p", None, &["any-hover-hover"]), &root, None, &[]);
let any_hover_bool =
resolver.compute_style(&element("p", None, &["any-hover-bool"]), &root, None, &[]);
let pointer_none =
resolver.compute_style(&element("p", None, &["pointer-none"]), &root, None, &[]);
let pointer_fine =
resolver.compute_style(&element("p", None, &["pointer-fine"]), &root, None, &[]);
let pointer_bool =
resolver.compute_style(&element("p", None, &["pointer-bool"]), &root, None, &[]);
let any_pointer_none =
resolver.compute_style(&element("p", None, &["any-pointer-none"]), &root, None, &[]);
let any_pointer_coarse = resolver.compute_style(
&element("p", None, &["any-pointer-coarse"]),
&root,
None,
&[],
);
let any_pointer_bool =
resolver.compute_style(&element("p", None, &["any-pointer-bool"]), &root, None, &[]);
assert_eq!(hover_none.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(hover_hover.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(hover_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(any_hover_none.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(any_hover_hover.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(any_hover_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
pointer_none.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(pointer_fine.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(pointer_bool.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(any_pointer_none.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(any_pointer_coarse.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(any_pointer_bool.color, Color::rgb(0.0, 0.0, 0.0));
}
#[test]
fn media_viewport_segment_features_match_single_page_viewport() {
let css = ".horizontal-one, .horizontal-min, .horizontal-two, .horizontal-bool, \
.vertical-one, .vertical-min, .vertical-two, .vertical-bool { color: rgb(0, 0, 0); } \
@media (horizontal-viewport-segments: 1) { .horizontal-one { color: rgb(0, 128, 0); } } \
@media (min-horizontal-viewport-segments: 1) { .horizontal-min { color: rgb(0, 0, 255); } } \
@media (horizontal-viewport-segments: 2) { .horizontal-two { color: rgb(255, 0, 0); } } \
@media (horizontal-viewport-segments) { .horizontal-bool { color: rgb(128, 0, 128); } } \
@media (vertical-viewport-segments: 1) { .vertical-one { color: rgb(0, 128, 0); } } \
@media (min-vertical-viewport-segments: 1) { .vertical-min { color: rgb(0, 0, 255); } } \
@media (vertical-viewport-segments: 2) { .vertical-two { color: rgb(255, 0, 0); } } \
@media (vertical-viewport-segments) { .vertical-bool { color: rgb(128, 0, 128); } }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(400.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let horizontal_one =
resolver.compute_style(&element("p", None, &["horizontal-one"]), &root, None, &[]);
let horizontal_min =
resolver.compute_style(&element("p", None, &["horizontal-min"]), &root, None, &[]);
let horizontal_two =
resolver.compute_style(&element("p", None, &["horizontal-two"]), &root, None, &[]);
let horizontal_bool =
resolver.compute_style(&element("p", None, &["horizontal-bool"]), &root, None, &[]);
let vertical_one =
resolver.compute_style(&element("p", None, &["vertical-one"]), &root, None, &[]);
let vertical_min =
resolver.compute_style(&element("p", None, &["vertical-min"]), &root, None, &[]);
let vertical_two =
resolver.compute_style(&element("p", None, &["vertical-two"]), &root, None, &[]);
let vertical_bool =
resolver.compute_style(&element("p", None, &["vertical-bool"]), &root, None, &[]);
assert_eq!(horizontal_one.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(horizontal_min.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(horizontal_two.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
horizontal_bool.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
assert_eq!(vertical_one.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(vertical_min.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(vertical_two.color, Color::rgb(0.0, 0.0, 0.0));
assert_eq!(
vertical_bool.color,
Color::rgb(128.0 / 255.0, 0.0, 128.0 / 255.0)
);
}
#[test]
fn pseudo_content_attr_resolves_originating_element_attributes() {
let css = ".sku::before { content: \"SKU \" attr(data-code) \": \"; } \
.sku::after { content: \" (\" attr(title) \")\"; } \
.missing::before { content: \"missing=\" attr(data-missing); } \
.none::before { content: attr(data-code); content: none; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut sku = element("p", None, &["sku"]);
sku.attrs
.insert("data-code".to_string(), "A-42".to_string());
sku.attrs.insert("title".to_string(), "ready".to_string());
let before = resolver
.compute_pseudo_style(&sku, &root, &[], PseudoTarget::Before)
.expect("before generated content");
assert_eq!(before.content.as_deref(), Some("SKU A-42: "));
let after = resolver
.compute_pseudo_style(&sku, &root, &[], PseudoTarget::After)
.expect("after generated content");
assert_eq!(after.content.as_deref(), Some(" (ready)"));
let missing = element("p", None, &["missing"]);
let missing_before = resolver
.compute_pseudo_style(&missing, &root, &[], PseudoTarget::Before)
.expect("missing attr still has literal content");
assert_eq!(missing_before.content.as_deref(), Some("missing="));
let mut none = element("p", None, &["none"]);
none.attrs
.insert("data-code".to_string(), "A-42".to_string());
assert!(
resolver
.compute_pseudo_style(&none, &root, &[], PseudoTarget::Before)
.is_none()
);
}
#[test]
fn pseudo_content_open_close_quote_uses_inherited_quotes_pair() {
let css = ".q { quotes: \"[\" \"]\"; } \
.q::before { content: open-quote; } \
.q::after { content: close-quote; } \
.none { quotes: none; } \
.none::before { content: open-quote \"empty\" close-quote; } \
.silent::before { content: no-open-quote \"plain\" no-close-quote; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("p", None, &["q"]);
let parent = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(parent.quotes, vec![("[".to_string(), "]".to_string())]);
let before = resolver
.compute_pseudo_style(&info, &parent, &[], PseudoTarget::Before)
.expect("open quote content");
assert_eq!(before.content.as_deref(), Some("["));
let after = resolver
.compute_pseudo_style(&info, &parent, &[], PseudoTarget::After)
.expect("close quote content");
assert_eq!(after.content.as_deref(), Some("]"));
let none_info = element("p", None, &["none"]);
let none_parent = resolver.compute_style(&none_info, &root, None, &[]);
let none_before = resolver
.compute_pseudo_style(&none_info, &none_parent, &[], PseudoTarget::Before)
.expect("content remains because literal text is present");
assert_eq!(none_before.content.as_deref(), Some("empty"));
let silent_info = element("p", None, &["silent"]);
let silent_parent = resolver.compute_style(&silent_info, &root, None, &[]);
let silent_before = resolver
.compute_pseudo_style(&silent_info, &silent_parent, &[], PseudoTarget::Before)
.expect("no-open/no-close omit marks but keep literal text");
assert_eq!(silent_before.content.as_deref(), Some("plain"));
}
#[test]
fn pseudo_content_counter_parts_and_counter_mutations_parse() {
let css = "body { counter-reset: chapter 3; } \
h2::before { counter-increment: chapter 2; content: \"#\" counter(chapter, lower-alpha) \" \" counters(section, \".\", upper-roman); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(
body_style.counter_reset,
vec![CounterMutation {
name: "chapter".to_string(),
value: 3,
reversed: false,
auto_reversed_initial: false,
}]
);
let h2_info = element("h2", None, &[]);
let h2_style = resolver.compute_style(&h2_info, &body_style, None, &[body_info.clone()]);
let before = resolver
.compute_pseudo_style(&h2_info, &h2_style, &[body_info], PseudoTarget::Before)
.expect("counter generated content");
assert_eq!(
before.counter_increment,
vec![CounterMutation {
name: "chapter".to_string(),
value: 2,
reversed: false,
auto_reversed_initial: false,
}]
);
let parts = before
.generated_content
.as_ref()
.expect("structured generated content");
assert!(matches!(parts.first(), Some(GeneratedContentPart::Text(text)) if text == "#"));
assert!(matches!(
parts.get(1),
Some(GeneratedContentPart::Counter(counter))
if counter.name == "chapter"
&& counter.style.list_style_type == ListStyleTypeMode::LowerAlpha
));
assert!(matches!(
parts.get(3),
Some(GeneratedContentPart::Counters(counter))
if counter.name == "section"
&& counter.separator == "."
&& counter.style.list_style_type == ListStyleTypeMode::UpperRoman
));
}
#[test]
fn pseudo_content_target_text_resolves_the_originating_href() {
let resolver = StyleResolver::new("a::after { content: 'REF ' target-text(attr(href)); }");
let root = resolver.default_style();
let mut link_info = element("a", None, &[]);
link_info
.attrs
.insert("href".to_string(), "#section".to_string());
let link_style = resolver.compute_style(&link_info, &root, None, &[]);
let after = resolver
.compute_pseudo_style(&link_info, &link_style, &[], PseudoTarget::After)
.expect("target-text generated content");
assert!(matches!(
after.generated_content.as_deref(),
Some([
GeneratedContentPart::Text(prefix),
GeneratedContentPart::TargetText(target),
]) if prefix == "REF " && target == "#section"
));
}
#[test]
fn pseudo_content_target_counter_resolves_the_originating_href() {
let resolver = StyleResolver::new(
"a::after { content: ' p.' target-counter(attr(href), page, lower-roman); }",
);
assert!(resolver.has_target_counter_content());
let root = resolver.default_style();
let mut link_info = element("a", None, &[]);
link_info
.attrs
.insert("href".to_string(), "#section".to_string());
let link_style = resolver.compute_style(&link_info, &root, None, &[]);
let after = resolver
.compute_pseudo_style(&link_info, &link_style, &[], PseudoTarget::After)
.expect("target-counter generated content");
assert!(matches!(
after.generated_content.as_deref(),
Some([
GeneratedContentPart::Text(prefix),
GeneratedContentPart::TargetCounter(counter),
]) if prefix == " p."
&& counter.target == "#section"
&& counter.name == "page"
&& counter.style.list_style_type == ListStyleTypeMode::LowerRoman
));
}
#[test]
fn pseudo_content_url_preserves_a_generated_image_part() {
let resolver =
StyleResolver::new(".icon::before { content: url('data:image/png;base64,AAAA'); }");
let root = resolver.default_style();
let icon_info = element("span", None, &["icon"]);
let icon_style = resolver.compute_style(&icon_info, &root, None, &[]);
let before = resolver
.compute_pseudo_style(&icon_info, &icon_style, &[], PseudoTarget::Before)
.expect("URL generated content");
assert!(matches!(
before.generated_content.as_deref(),
Some([GeneratedContentPart::Image(source)])
if source == "data:image/png;base64,AAAA"
));
}
#[test]
fn pseudo_content_leader_preserves_the_fill_and_suffix_parts() {
let resolver = StyleResolver::new("a::after { content: leader('.') ' 7'; }");
let root = resolver.default_style();
let link_info = element("a", None, &[]);
let link_style = resolver.compute_style(&link_info, &root, None, &[]);
let after = resolver
.compute_pseudo_style(&link_info, &link_style, &[], PseudoTarget::After)
.expect("leader generated content");
assert!(matches!(
after.generated_content.as_deref(),
Some([
GeneratedContentPart::Leader(pattern),
GeneratedContentPart::Text(suffix),
]) if pattern == "." && suffix == " 7"
));
}
#[test]
fn marker_pseudo_content_counter_parts_parse() {
let css = "li::marker { content: \"[\" counter(step, upper-alpha) \"] \"; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let li_info = element("li", None, &[]);
let li_style = resolver.compute_style(&li_info, &root, None, &[]);
let marker = resolver
.compute_pseudo_style(&li_info, &li_style, &[], PseudoTarget::Marker)
.expect("marker generated content");
let parts = marker
.generated_content
.as_ref()
.expect("structured marker content");
assert!(matches!(parts.first(), Some(GeneratedContentPart::Text(text)) if text == "["));
assert!(matches!(
parts.get(1),
Some(GeneratedContentPart::Counter(counter))
if counter.name == "step"
&& counter.style.list_style_type == ListStyleTypeMode::UpperAlpha
));
}
#[test]
fn marker_pseudo_content_none_suppresses_but_normal_falls_back() {
let css = ".none::marker { content: none; } .normal::marker { content: normal; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let none_info = element("li", None, &["none"]);
let none_style = resolver.compute_style(&none_info, &root, None, &[]);
let none_marker = resolver
.compute_pseudo_style(&none_info, &none_style, &[], PseudoTarget::Marker)
.expect("content:none marker rule remains distinguishable");
assert!(none_marker.content.is_none());
assert!(none_marker.generated_content.is_none());
assert!(none_marker.marker_content_overridden);
let normal_info = element("li", None, &["normal"]);
let normal_style = resolver.compute_style(&normal_info, &root, None, &[]);
assert!(
resolver
.compute_pseudo_style(&normal_info, &normal_style, &[], PseudoTarget::Marker)
.is_none()
);
}
#[test]
fn marker_presentation_style_keeps_font_and_color_without_replacing_content() {
let css = ".paint::marker { color: red; font-size: 32px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("li", None, &["paint"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(
resolver
.compute_pseudo_style(&info, &style, &[], PseudoTarget::Marker)
.is_none()
);
let marker = resolver
.compute_marker_style(&info, &style, &[])
.expect("marker presentation style");
assert_eq!(marker.font_size, Pt::from_f32(24.0));
assert_ne!(marker.color, style.color);
assert!(!marker.marker_content_overridden);
}
#[test]
fn marker_pseudo_only_generates_for_list_items() {
let css =
".plain::marker, .item::marker { content: \"! \"; } .item { display: list-item; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let plain_info = element("p", None, &["plain"]);
let plain_style = resolver.compute_style(&plain_info, &root, None, &[]);
assert!(
resolver
.compute_pseudo_style(&plain_info, &plain_style, &[], PseudoTarget::Marker)
.is_none()
);
let item_info = element("p", None, &["item"]);
let item_style = resolver.compute_style(&item_info, &root, None, &[]);
assert_eq!(item_style.display, DisplayMode::ListItem);
let marker = resolver
.compute_pseudo_style(&item_info, &item_style, &[], PseudoTarget::Marker)
.expect("display:list-item can generate ::marker");
assert_eq!(marker.content.as_deref(), Some("! "));
}
#[test]
fn duplicate_counter_reset_keeps_last_entry_for_name() {
let css = "body { counter-reset: section 1 section 9 page 2; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(
body_style.counter_reset,
vec![
CounterMutation {
name: "section".to_string(),
value: 9,
reversed: false,
auto_reversed_initial: false,
},
CounterMutation {
name: "page".to_string(),
value: 2,
reversed: false,
auto_reversed_initial: false,
},
]
);
}
#[test]
fn overflow_clip_keeps_its_non_bfc_clipping_mode() {
let cases = [
("visible", OverflowMode::Visible),
("hidden", OverflowMode::Hidden),
("clip", OverflowMode::Clip),
("scroll", OverflowMode::Hidden),
("auto", OverflowMode::Hidden),
];
for (keyword, expected) in cases {
let css = format!(".x {{ overflow: {keyword}; }}");
let resolver = StyleResolver::new(&css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.overflow, expected, "keyword={keyword}");
}
let resolver = StyleResolver::new(".x { overflow-x: hidden; overflow-y: visible; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.overflow, OverflowMode::Hidden);
let resolver = StyleResolver::new(
".x { overflow-x: hidden; overflow-y: visible; overflow-x: visible; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.overflow, OverflowMode::Visible);
let resolver =
StyleResolver::new(".x { overflow-inline: hidden; overflow-block: visible; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.overflow, OverflowMode::Hidden);
let resolver = StyleResolver::new(
".x { writing-mode: vertical-rl; overflow-inline: visible; overflow-block: clip; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.overflow, OverflowMode::Clip);
}
#[test]
fn overflow_level_four_controls_compile_to_typed_style() {
let resolver = StyleResolver::new(
".x { overflow: clip; overflow-clip-margin: 18px; \
scrollbar-gutter: stable both-edges; -webkit-line-clamp: 2; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.overflow_clip_margin,
LengthSpec::Absolute(Pt::from_f32(13.5))
);
assert_eq!(style.scrollbar_gutter, ScrollbarGutterMode::StableBothEdges);
assert_eq!(style.line_clamp, Some(2));
}
#[test]
fn visibility_parses_inherits_and_reverts_layer() {
let css = "@layer base, special; \
@layer base { \
.parent { visibility: hidden; } \
.child { visibility: visible; } \
.collapse { visibility: collapse; } \
.revert { visibility: hidden; } \
} \
@layer special { \
.revert { visibility: visible; } \
.revert.layer { visibility: revert-layer; } \
.override { visibility: hidden; visibility: visible; } \
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent = resolver.compute_style(&element("div", None, &["parent"]), &root, None, &[]);
assert_eq!(parent.visibility, VisibilityMode::Hidden);
let inherited =
resolver.compute_style(&element("div", None, &["plain"]), &parent, None, &[]);
assert_eq!(inherited.visibility, VisibilityMode::Hidden);
let child = resolver.compute_style(&element("div", None, &["child"]), &parent, None, &[]);
assert_eq!(child.visibility, VisibilityMode::Visible);
let collapse =
resolver.compute_style(&element("div", None, &["collapse"]), &root, None, &[]);
assert_eq!(collapse.visibility, VisibilityMode::Collapse);
let reverted = resolver.compute_style(
&element("div", None, &["revert", "layer"]),
&root,
None,
&[],
);
assert_eq!(reverted.visibility, VisibilityMode::Hidden);
let override_style =
resolver.compute_style(&element("div", None, &["override"]), &root, None, &[]);
assert_eq!(override_style.visibility, VisibilityMode::Visible);
}
#[test]
fn background_named_color_matches_single_letter_class_selector() {
let css = ".a { background: red; } .b { background: rgb(0, 255, 0); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let a_style = resolver.compute_style(&element("div", None, &["a"]), &root, None, &[]);
let b_style = resolver.compute_style(&element("div", None, &["b"]), &root, None, &[]);
assert_eq!(a_style.background_color, Some(Color::rgb(1.0, 0.0, 0.0)));
assert_eq!(b_style.background_color, Some(Color::rgb(0.0, 1.0, 0.0)));
}
#[test]
fn background_shorthand_extracts_color_token() {
let css = ".x { background: red no-repeat center / cover; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_color, Some(Color::rgb(1.0, 0.0, 0.0)));
}
#[test]
fn attribute_includes_selector_matches_space_separated_values() {
let css = "[data-tags~='beta'] { color: red; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut info = element("div", None, &[]);
info.attrs
.insert("data-tags".to_string(), "alpha beta".to_string());
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.color, Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn width_unset_resets_to_auto() {
let css = ".x { width: 100px; width: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.width, LengthSpec::Auto);
}
#[test]
fn background_color_unset_resets_to_initial_none() {
let css = ".x { background-color: rgb(0,0,255); background-color: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_color, None);
}
#[test]
fn background_color_currentcolor_uses_element_color() {
let css = ".parent { color: rgb(255, 0, 0); } .x { color: rgb(0, 0, 255); background-color: currentColor; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let style =
resolver.compute_style(&element("div", None, &["x"]), &parent, None, &[parent_info]);
assert_eq!(style.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(style.background_color, Some(Color::rgb(0.0, 0.0, 1.0)));
}
#[test]
fn background_shorthand_currentcolor_uses_element_color() {
let css = ".parent { color: rgb(255, 0, 0); } .x { color: rgb(0, 128, 0); background: currentColor; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let style =
resolver.compute_style(&element("div", None, &["x"]), &parent, None, &[parent_info]);
assert_eq!(style.color, Color::rgb(0.0, 128.0 / 255.0, 0.0));
assert_eq!(
style.background_color,
Some(Color::rgb(0.0, 128.0 / 255.0, 0.0))
);
}
#[test]
fn display_unset_resets_to_initial_inline() {
let css = ".x { display: flex; display: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.display, DisplayMode::Inline);
}
#[test]
fn inherit_and_initial_color() {
let css = "div { color: red; } p { color: inherit; } span { color: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_info = element("div", None, &[]);
let div_style = resolver.compute_style(&div_info, &root, None, &[]);
let p_info = element("p", None, &[]);
let p_style = resolver.compute_style(&p_info, &div_style, None, &[div_info.clone()]);
assert_eq!(p_style.color, Color::rgb(1.0, 0.0, 0.0));
let span_info = element("span", None, &[]);
let span_style = resolver.compute_style(&span_info, &p_style, None, &[div_info, p_info]);
assert_eq!(span_style.color, Color::BLACK);
}
#[test]
fn inline_style_wins_over_rules() {
let css = "p { color: blue; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let p_info = element("p", None, &[]);
let p_style = resolver.compute_style(&p_info, &root, Some("color: rgb(0,255,0);"), &[]);
assert_eq!(p_style.color, Color::rgb(0.0, 1.0, 0.0));
}
#[test]
fn ua_css_sets_img_display_inline_block() {
let resolver = StyleResolver::new("");
let root = resolver.default_style();
let img_info = element("img", None, &[]);
let img_style = resolver.compute_style(&img_info, &root, None, &[]);
assert_eq!(img_style.display, DisplayMode::InlineBlock);
}
#[test]
fn ua_css_sets_document_elements_display_block() {
let resolver = StyleResolver::new("");
let root = resolver.default_style();
for tag in ["html", "body"] {
let style = resolver.compute_style(&element(tag, None, &[]), &root, None, &[]);
assert_eq!(style.display, DisplayMode::Block, "{tag}");
}
}
#[test]
fn ua_css_sets_figure_elements_display_block() {
let resolver = StyleResolver::new("");
let root = resolver.default_style();
for tag in ["figure", "figcaption"] {
let style = resolver.compute_style(&element(tag, None, &[]), &root, None, &[]);
assert_eq!(style.display, DisplayMode::Block, "{tag}");
}
}
#[test]
fn ua_css_sets_heading_font_weight_bold() {
let resolver = StyleResolver::new("body { font: 16px/1.25 ParitySans; }");
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body = resolver.compute_style(&body_info, &root, None, &[]);
for tag in ["h1", "h2", "h3", "h4", "h5", "h6"] {
let style = resolver.compute_style(
&element(tag, None, &[]),
&body,
None,
std::slice::from_ref(&body_info),
);
assert_eq!(style.font_weight, 700, "{tag}");
assert!(!style.font_weight_authored, "{tag}");
}
let authored_resolver = StyleResolver::new("h2 { font-weight: 700; }");
let authored_root = authored_resolver.default_style();
let authored =
authored_resolver.compute_style(&element("h2", None, &[]), &authored_root, None, &[]);
assert_eq!(authored.font_weight, 700);
assert!(authored.font_weight_authored);
}
#[test]
fn extract_css_page_setup_parses_size_and_margin() {
let css = "@page { size: 8.5in 11in; margin: 0.5in 1in; }";
let setup = extract_css_page_setup(css, None, None);
let size = setup.size.expect("expected @page size");
assert!((size.width.to_f32() - 612.0).abs() < 0.01);
assert!((size.height.to_f32() - 792.0).abs() < 0.01);
assert_eq!(setup.margin_top, Some(Pt::from_f32(36.0)));
assert_eq!(setup.margin_right, Some(Pt::from_f32(72.0)));
assert_eq!(setup.margin_bottom, Some(Pt::from_f32(36.0)));
assert_eq!(setup.margin_left, Some(Pt::from_f32(72.0)));
}
#[test]
fn extract_css_page_setup_parses_sheet_presentation() {
let setup = extract_css_page_setup(
"@page { bleed: 12px; marks: crop cross; page-orientation: rotate-left; }",
None,
None,
);
assert_eq!(setup.bleed, Some(Pt::from_f32(9.0)));
assert_eq!(
setup.marks,
Some(PageMarks {
crop: true,
cross: true,
})
);
assert_eq!(setup.page_orientation, Some(PageOrientation::RotateLeft));
assert_eq!(setup.page_presentation().media_extent(), Pt::from_f32(9.0));
}
#[test]
fn page_marks_none_and_upright_reset_cascaded_presentation() {
let styles = extract_css_page_styles(
"@page { bleed: 12px; marks: crop; page-orientation: rotate-left; }\
@page :first { bleed: auto; marks: none; page-orientation: upright; }",
None,
None,
);
let first = styles.base.cascaded_with(&styles.first).page_presentation();
assert_eq!(first.bleed, Pt::ZERO);
assert_eq!(first.marks, PageMarks::default());
assert_eq!(first.orientation, PageOrientation::Upright);
}
#[test]
fn page_margin_percentages_resolve_against_each_page_axis() {
let setup = extract_css_page_setup("@page { size: 200px 160px; margin: 10%; }", None, None);
let size = setup.size.expect("page size");
let margins = setup
.resolve_margins(Margins::all(0.0), size)
.expect("percentage margins");
assert_eq!(margins.top, Pt::from_f32(12.0));
assert_eq!(margins.right, Pt::from_f32(15.0));
assert_eq!(margins.bottom, Pt::from_f32(12.0));
assert_eq!(margins.left, Pt::from_f32(15.0));
}
#[test]
fn page_context_line_height_distinguishes_numbers_from_lengths() {
let styles = extract_css_page_styles(
"@page { line-height: 1.5; \
@top-center { content: 'H'; line-height: 18px; } }",
None,
None,
);
assert_eq!(
styles.base.line_height,
Some(CssPageLineHeight::Number(1.5))
);
let top_center = styles
.base
.margin_boxes
.iter()
.find(|margin_box| margin_box.kind == Some(CssPageMarginBoxKind::TopCenter))
.expect("top-center margin box");
assert_eq!(
top_center.line_height,
Some(CssPageLineHeight::Absolute(Pt::from_f32(13.5)))
);
}
#[test]
fn page_footnote_area_parses_separator_padding_and_max_height() {
let setup = extract_css_page_setup(
"@page { @footnote { border-top: 8px solid #111111; padding-top: 8px; max-height: 30px; } }",
None,
None,
);
assert_eq!(
setup.footnote_area.border_top_width,
Some(Pt::from_f32(6.0))
);
assert_eq!(
setup.footnote_area.border_top_color,
Some(Color::rgb(17.0 / 255.0, 17.0 / 255.0, 17.0 / 255.0))
);
assert_eq!(setup.footnote_area.border_top_visible, Some(true));
assert_eq!(setup.footnote_area.padding_top, Some(Pt::from_f32(6.0)));
assert_eq!(setup.footnote_area.max_height, Some(Pt::from_f32(22.5)));
}
#[test]
fn page_margin_content_parses_running_elements_and_named_strings() {
assert_eq!(
parse_page_margin_box_content("element(Head, first-except)"),
Some(vec![CssPageMarginContentPart::RunningElement {
name: "Head".to_string(),
position: CssPageContentPosition::FirstExcept,
}])
);
assert_eq!(
parse_page_margin_box_content("'S' string(section, last)"),
Some(vec![
CssPageMarginContentPart::Text("S".to_string()),
CssPageMarginContentPart::NamedString {
name: "section".to_string(),
position: CssPageContentPosition::Last,
},
])
);
}
#[test]
fn page_property_survives_nested_page_margin_box_rules() {
let css = "@page { size: 160px 120px; margin: 24px 0 0; \
@top-center { content: ' '; background: #1d4ed8; width: 160px; } } \
@page chapter { size: 160px 120px; margin: 24px 0 0; \
@top-center { content: ' '; background: #16a34a; width: 160px; } } \
.page { height: 96px; } .chapter { page: chapter; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(
&element("div", None, &["page", "chapter"]),
&root,
None,
&[],
);
assert_eq!(
style.pagination.page_name,
Some(css_page_name_id("chapter"))
);
assert_ne!(css_page_name_id("Chapter"), css_page_name_id("chapter"));
}
#[test]
fn extract_css_page_setup_handles_named_size_orientation() {
let css = "@page { size: letter landscape; }";
let setup = extract_css_page_setup(css, None, None);
let size = setup.size.expect("expected @page size");
assert!((size.width.to_f32() - 792.0).abs() < 0.01);
assert!((size.height.to_f32() - 612.0).abs() < 0.01);
}
#[test]
fn extract_css_page_setup_handles_jis_b4_landscape() {
let setup = extract_css_page_setup("@page { size: jis-b4 landscape; }", None, None);
let size = setup.size.expect("expected @page size");
assert_eq!(size.width, Pt::from_f32(1032.0));
assert_eq!(size.height, Pt::from_f32(729.0));
}
#[test]
fn extract_css_page_styles_parses_pseudo_margins_and_backgrounds() {
let styles = extract_css_page_styles(
r#"
@page { size: 192px 200px; margin: 16px; background: #ffd6d6; }
@page :left { margin: 8px 32px 24px 8px; }
@page :right { margin: 16px 8px 8px 32px; }
@page :first { margin: 32px 8px 16px 24px; }
@page :blank { background: #ffd166; }
@page :blank {
@top-center { content: " "; background: #118ab2; width: 160px; }
}
"#,
None,
None,
);
assert_eq!(styles.base.margin_top, Some(Pt::from_f32(12.0)));
assert_eq!(styles.left.margin_right, Some(Pt::from_f32(24.0)));
assert_eq!(styles.right.margin_left, Some(Pt::from_f32(24.0)));
assert_eq!(styles.first.margin_top, Some(Pt::from_f32(24.0)));
assert_eq!(
styles.blank.background,
Some((Color::rgb(1.0, 209.0 / 255.0, 102.0 / 255.0), 1.0))
);
let top_center = styles
.blank
.margin_boxes
.iter()
.find(|margin_box| margin_box.kind == Some(CssPageMarginBoxKind::TopCenter))
.expect("blank top-center margin box");
assert_eq!(
top_center.content,
Some(vec![CssPageMarginContentPart::Text(" ".to_string())])
);
assert_eq!(
top_center.background,
Some((Color::rgb(17.0 / 255.0, 138.0 / 255.0, 178.0 / 255.0), 1.0))
);
assert_eq!(top_center.width, Some(Pt::from_f32(120.0)));
assert_eq!(
styles.base.background,
Some((Color::rgb(1.0, 214.0 / 255.0, 214.0 / 255.0), 1.0))
);
assert!(styles.has_pseudo_rules());
}
#[test]
fn debug_logs_declaration_parsed_no_effect_for_unknown_property() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
"div { vendor-unknown-prop: 12px; color: red; }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(log.contains("\"DECLARATION_PARSED_NO_EFFECT\""));
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_no_effect_for_background_blend_mode() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_background_blend_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { background-image: linear-gradient(red, red), linear-gradient(blue, blue), linear-gradient(green, green); background-blend-mode: screen, multiply; }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"DECLARATION_PARSED_NO_EFFECT\""),
"log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_no_effect_for_supported_border_radius_var() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_border_radius_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
r#"[data-fb-role="document-root"] .ui-card {
--fb-radius-card: 0.5rem;
border-radius: var(--fb-radius-card);
}"#,
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"DECLARATION_PARSED_NO_EFFECT\"")
&& !log.contains("\"property\":\"border-radius\""),
"supported border-radius should not emit no-effect diagnostics, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_fallback_for_native_multicol_properties() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_multicol_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { column-count: 2; column-rule: 1px solid black; column-width: 120px; column-span: all; background: red; }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(!log.contains("\"MULTICOL_SINGLE_COLUMN_FALLBACK\""));
assert!(!log.contains("\"property\":\"column-span\""));
assert!(!log.contains("\"property\":\"column-width\""));
assert!(!log.contains("\"property\":\"column-count\""));
assert!(!log.contains("\"property\":\"column-rule\""));
assert!(!log.contains("\"DECLARATION_PARSED_NO_EFFECT\""));
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_logs_filters_effects_fallback_for_effect_props() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { opacity: 0.5; filter: url(\"filters.svg#x\"); backdrop-filter: blur(1px); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(log.contains("\"FILTERS_EFFECTS_FALLBACK\""));
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_inset() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: inset(8px 12px 16px 20px round 10px); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path inset should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_circle() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_circle_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: circle(40% at 50% 50%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path circle should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_ellipse() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_ellipse_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: ellipse(40% 50% at 50% 50%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path ellipse should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_polygon() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_polygon_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: polygon(evenodd, 50% 0%, 100% 100%, 0% 100%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path polygon should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_path() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_path_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: path(evenodd, \"M 48 0 L 96 96 L 0 96 Z\"); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path path should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_shape_function() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_shape_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: shape(evenodd from 50% 0%, line to 100% 100%, line to 0% 100%, close); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path shape() should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_xywh() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_xywh_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: xywh(10% 20% 30% 40%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path xywh should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_clip_path_rect() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_rect_supported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: rect(10% auto 75% 25%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"supported clip-path rect should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_uniform_rounded_clip_path_xywh() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_xywh_round_unsupported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: xywh(10% 20% 30% 40% round 8px); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"uniform rounded clip-path xywh should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_uniform_rounded_clip_path_rect() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_rect_round_unsupported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: rect(10% 90% 75% 25% round 8px); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"uniform rounded clip-path rect should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_elliptical_clip_path_rounding() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_clip_path_multi_round_unsupported_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { clip-path: xywh(10% 20% 30% 40% round 8px / 4px); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"clip-path\""),
"elliptical clip-path rounding should not emit filters/effects fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn filter_saturate_property_resolves_on_computed_style() {
let css = ".x { filter: saturate(125%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.saturate - 1.25).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_grayscale_property_resolves_as_desaturation() {
let css = ".x { filter: grayscale(80%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.saturate - 0.2).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_brightness_property_resolves_on_computed_style() {
let css = ".x { filter: brightness(150%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.brightness - 1.5).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_contrast_property_resolves_on_computed_style() {
let css = ".x { filter: contrast(200%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.contrast - 2.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_invert_property_resolves_on_computed_style() {
let css = ".x { filter: invert(60%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.invert - 0.6).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_sepia_property_resolves_on_computed_style() {
let css = ".x { filter: sepia(70%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.sepia - 0.7).abs() < 0.0001);
assert!((filter.invert - 0.0).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_hue_rotate_property_resolves_on_computed_style() {
let css = ".x { filter: hue-rotate(180deg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.hue_rotate - std::f32::consts::PI).abs() < 0.0001);
assert!((filter.sepia - 0.0).abs() < 0.0001);
assert!((filter.invert - 0.0).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_opacity_property_resolves_on_computed_style() {
let css = ".x { filter: opacity(50%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.opacity - 0.5).abs() < 0.0001);
assert!((filter.hue_rotate - 0.0).abs() < 0.0001);
assert!((filter.sepia - 0.0).abs() < 0.0001);
assert!((filter.invert - 0.0).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert_eq!(filter.blur_radius, Pt::ZERO);
}
#[test]
fn filter_blur_property_resolves_on_computed_style() {
let css = ".x { filter: blur(4px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert!((filter.opacity - 1.0).abs() < 0.0001);
assert!(filter.blur_radius > Pt::ZERO);
}
#[test]
fn filter_program_preserves_authored_operation_order() {
let resolver = StyleResolver::new(
".a { filter: contrast(5) blur(8px); } .b { filter: blur(8px) contrast(5); }",
);
let root = resolver.default_style();
let first = resolver
.compute_style(&element("div", None, &["a"]), &root, None, &[])
.paint_filter
.expect("first filter program");
let second = resolver
.compute_style(&element("div", None, &["b"]), &root, None, &[])
.paint_filter
.expect("second filter program");
assert_eq!(
first.operations,
vec![
PaintFilterOperation::Contrast(5.0),
PaintFilterOperation::Blur(px_to_pt(8.0)),
]
);
assert_eq!(
second.operations,
vec![
PaintFilterOperation::Blur(px_to_pt(8.0)),
PaintFilterOperation::Contrast(5.0),
]
);
assert_ne!(first.operations, second.operations);
}
#[test]
fn filter_empty_function_arguments_resolve_to_css_defaults() {
let css = ".x { filter: blur() saturate() brightness() contrast() hue-rotate() opacity() invert(); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.blur_radius, Pt::ZERO);
assert!((filter.saturate - 1.0).abs() < 0.0001);
assert!((filter.brightness - 1.0).abs() < 0.0001);
assert!((filter.contrast - 1.0).abs() < 0.0001);
assert!(filter.hue_rotate.abs() < 0.0001);
assert!((filter.opacity - 1.0).abs() < 0.0001);
assert!((filter.invert - 1.0).abs() < 0.0001);
}
#[test]
fn filter_drop_shadow_property_resolves_on_computed_style() {
let css = ".x { filter: drop-shadow(8px 4px 6px rgba(0, 0, 255, 0.5)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
let shadow = filter.drop_shadows[0];
assert_eq!(shadow.offset_x, px_to_pt(8.0));
assert_eq!(shadow.offset_y, px_to_pt(4.0));
assert_eq!(shadow.blur_radius, px_to_pt(6.0));
assert!(shadow.color.b > 0.99);
assert!((shadow.opacity - 0.5).abs() < 0.003);
}
#[test]
fn filter_drop_shadow_without_color_resolves_current_color() {
let css = ".x { color: rgb(0, 0, 255); filter: drop-shadow(8px 4px 6px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
let shadow = filter.drop_shadows[0];
assert_eq!(shadow.offset_x, px_to_pt(8.0));
assert_eq!(shadow.offset_y, px_to_pt(4.0));
assert_eq!(shadow.blur_radius, px_to_pt(6.0));
assert!(shadow.color.b > 0.99);
assert!(shadow.color.r < 0.01);
assert!((shadow.opacity - 1.0).abs() < 0.0001);
assert!(!shadow.color_is_current_color);
}
#[test]
fn filter_multiple_drop_shadows_resolve_on_computed_style() {
let css = ".x { filter: drop-shadow(8px 0 0 red) drop-shadow(0 8px 0 blue); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.drop_shadows.len(), 2);
assert_eq!(filter.drop_shadows[0].offset_x, px_to_pt(8.0));
assert_eq!(filter.drop_shadows[0].offset_y, Pt::ZERO);
assert!(filter.drop_shadows[0].color.r > 0.99);
assert_eq!(filter.drop_shadows[1].offset_x, Pt::ZERO);
assert_eq!(filter.drop_shadows[1].offset_y, px_to_pt(8.0));
assert!(filter.drop_shadows[1].color.b > 0.99);
}
#[test]
fn filter_drop_shadow_relative_lengths_and_color_first_resolve() {
let css =
".x { font-size: 24px; filter: blur(0.5em) drop-shadow(blue 1em -0.5em 0.25em); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(style.font_size, px_to_pt(24.0));
assert_eq!(filter.blur_radius, Pt::from_f32(9.0));
assert_eq!(filter.drop_shadows.len(), 1);
let shadow = filter.drop_shadows[0];
assert_eq!(shadow.offset_x, Pt::from_f32(18.0));
assert_eq!(shadow.offset_y, Pt::from_f32(-9.0));
assert_eq!(shadow.blur_radius, Pt::from_f32(4.5));
assert!(shadow.color.b > 0.99);
assert!(shadow.color.r < 0.01);
}
#[test]
fn filter_drop_shadow_interleaved_color_order_resolves() {
let css = ".x { filter: drop-shadow(8px blue 4px 2px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
let shadow = filter.drop_shadows[0];
assert_eq!(shadow.offset_x, px_to_pt(8.0));
assert_eq!(shadow.offset_y, px_to_pt(4.0));
assert_eq!(shadow.blur_radius, px_to_pt(2.0));
assert!(shadow.color.b > 0.99);
assert!(shadow.color.r < 0.01);
}
#[test]
fn filter_drop_shadow_modern_rgb_and_mm_lengths_resolve() {
let css = ".x { filter: drop-shadow(6mm 0 0 rgb(160 0 210)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style.paint_filter.expect("expected filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
let shadow = filter.drop_shadows[0];
assert_eq!(shadow.offset_x, Pt::from_f32(6.0 * 72.0 / 25.4));
assert_eq!(shadow.offset_y, Pt::ZERO);
assert_eq!(shadow.blur_radius, Pt::ZERO);
assert!((shadow.color.r - (160.0 / 255.0)).abs() < 0.001);
assert!(shadow.color.g < 0.001);
assert!((shadow.color.b - (210.0 / 255.0)).abs() < 0.001);
}
#[test]
fn filter_drop_shadow_rejects_duplicate_colors() {
let css = ".x { filter: drop-shadow(red 8px blue 4px 2px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(style.paint_filter.is_none());
}
#[test]
fn filter_drop_shadow_negative_blur_is_invalid() {
let css = ".x { filter: drop-shadow(8px 0 -1px red); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(style.paint_filter.is_none());
}
#[test]
fn backdrop_filter_blur_and_saturate_resolves_on_computed_style() {
let css = ".x { backdrop-filter: blur(4px) saturate(140%) brightness(120%) contrast(150%) invert(20%) sepia(30%) hue-rotate(0.5turn) opacity(80%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style
.backdrop_filter
.expect("expected backdrop filter spec");
assert!((filter.saturate - 1.4).abs() < 0.0001);
assert!((filter.brightness - 1.2).abs() < 0.0001);
assert!((filter.contrast - 1.5).abs() < 0.0001);
assert!((filter.invert - 0.2).abs() < 0.0001);
assert!((filter.sepia - 0.3).abs() < 0.0001);
assert!((filter.hue_rotate - std::f32::consts::PI).abs() < 0.0001);
assert!((filter.opacity - 0.8).abs() < 0.0001);
assert!(filter.blur_radius > Pt::ZERO);
}
#[test]
fn backdrop_filter_drop_shadow_resolves_on_computed_style() {
let css = ".x { backdrop-filter: drop-shadow(8px 0 0 red); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style
.backdrop_filter
.expect("expected backdrop filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
assert_eq!(filter.drop_shadows[0].offset_x, px_to_pt(8.0));
assert_eq!(filter.drop_shadows[0].offset_y, Pt::ZERO);
assert!(filter.drop_shadows[0].color.r > 0.99);
}
#[test]
fn backdrop_filter_drop_shadow_without_color_resolves_current_color() {
let css = ".x { color: rgb(0, 0, 255); backdrop-filter: drop-shadow(8px 0 0); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let filter = style
.backdrop_filter
.expect("expected backdrop filter spec");
assert_eq!(filter.drop_shadows.len(), 1);
assert_eq!(filter.drop_shadows[0].offset_x, px_to_pt(8.0));
assert_eq!(filter.drop_shadows[0].offset_y, Pt::ZERO);
assert!(filter.drop_shadows[0].color.b > 0.99);
assert!(filter.drop_shadows[0].color.r < 0.01);
assert!(!filter.drop_shadows[0].color_is_current_color);
}
#[test]
fn mix_blend_mode_property_resolves_on_computed_style() {
let css = ".x { mix-blend-mode: difference; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.mix_blend_mode, MixBlendMode::Difference);
}
#[test]
fn will_change_root_properties_set_backdrop_root_flag() {
let css = ".root { will-change: transform, opacity; } .plain { will-change: transform; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let root_style = resolver.compute_style(&element("div", None, &["root"]), &root, None, &[]);
let plain_style =
resolver.compute_style(&element("div", None, &["plain"]), &root, None, &[]);
assert!(root_style.will_change_backdrop_root);
assert!(!plain_style.will_change_backdrop_root);
}
#[test]
fn mask_image_non_none_sets_backdrop_root_flag() {
let css =
".root { mask-image: linear-gradient(black, black); } .plain { mask-image: none; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let root_style = resolver.compute_style(&element("div", None, &["root"]), &root, None, &[]);
let plain_style =
resolver.compute_style(&element("div", None, &["plain"]), &root, None, &[]);
assert!(root_style.mask_backdrop_root);
assert!(!plain_style.mask_backdrop_root);
}
#[test]
fn mask_longhands_compile_layer_lists_and_webkit_composites() {
let css = ".x {
mask-image: linear-gradient(black, transparent), radial-gradient(white, black);
mask-size: 40px 20px, contain;
mask-position: 10px 5px, center;
mask-repeat: no-repeat, repeat-x;
mask-origin: padding-box, content-box;
mask-clip: content-box, border-box;
mask-mode: luminance, alpha;
-webkit-mask-composite: source-out, xor;
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(style.mask_backdrop_root);
assert_eq!(style.mask.paints.len(), 2);
assert_eq!(style.mask.sizes.len(), 2);
assert_eq!(style.mask.positions.len(), 2);
assert_eq!(style.mask.repeats.len(), 2);
assert_eq!(
style.mask.origins,
vec![BackgroundBox::Padding, BackgroundBox::Content]
);
assert_eq!(
style.mask.clips,
vec![BackgroundClipBox::Content, BackgroundClipBox::Border]
);
assert_eq!(style.mask.modes, vec![MaskMode::Luminance, MaskMode::Alpha]);
assert_eq!(
style.mask.composites,
vec![MaskComposite::DestinationOut, MaskComposite::Exclude]
);
}
#[test]
fn mask_shorthand_uses_mask_geometry_and_compiles_program() {
let css = ".x { mask: linear-gradient(90deg, black, transparent) 10px 5px / 40px 20px no-repeat content-box padding-box alpha intersect; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(style.mask_backdrop_root);
assert_eq!(style.mask.paints.len(), 1);
assert_eq!(style.mask.origins, vec![BackgroundBox::Content]);
assert_eq!(style.mask.clips, vec![BackgroundClipBox::Padding]);
assert_eq!(style.mask.modes, vec![MaskMode::Alpha]);
assert_eq!(style.mask.composites, vec![MaskComposite::Intersect]);
assert_eq!(style.mask.repeats[0].x, BackgroundRepeatMode::NoRepeat);
assert_eq!(style.mask.repeats[0].y, BackgroundRepeatMode::NoRepeat);
assert_eq!(
style.mask.sizes[0].width,
LengthSpec::Absolute(px_to_pt(40.0))
);
assert_eq!(
style.mask.sizes[0].height,
LengthSpec::Absolute(px_to_pt(20.0))
);
}
#[test]
fn mask_border_longhands_compile_to_nine_slice_program() {
let css = ".x {
mask-border-source: linear-gradient(black, black);
mask-border-slice: 28;
mask-border-width: 28px;
mask-border-repeat: stretch;
}";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(style.mask_backdrop_root);
assert!(style.mask.border.source.is_some());
assert_eq!(
style.mask.border.slice,
[BorderImageSliceValue::Number(28.0); 4]
);
assert_eq!(
style.mask.border.width,
[BorderImageWidthValue::Length(LengthSpec::Absolute(px_to_pt(28.0))); 4]
);
assert_eq!(style.mask.border.repeat_x, BorderImageRepeatMode::Stretch);
assert_eq!(style.mask.border.repeat_y, BorderImageRepeatMode::Stretch);
}
#[test]
fn mix_blend_mode_standard_keywords_resolve() {
let cases = [
("normal", MixBlendMode::Normal),
("multiply", MixBlendMode::Multiply),
("screen", MixBlendMode::Screen),
("overlay", MixBlendMode::Overlay),
("darken", MixBlendMode::Darken),
("lighten", MixBlendMode::Lighten),
("color-dodge", MixBlendMode::ColorDodge),
("color-burn", MixBlendMode::ColorBurn),
("hard-light", MixBlendMode::HardLight),
("soft-light", MixBlendMode::SoftLight),
("difference", MixBlendMode::Difference),
("exclusion", MixBlendMode::Exclusion),
("hue", MixBlendMode::Hue),
("saturation", MixBlendMode::Saturation),
("color", MixBlendMode::Color),
("luminosity", MixBlendMode::Luminosity),
("plus-lighter", MixBlendMode::PlusLighter),
("plus-darker", MixBlendMode::PlusDarker),
];
for (keyword, expected) in cases {
let css = format!(".x {{ mix-blend-mode: {}; }}", keyword);
let resolver = StyleResolver::new(&css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.mix_blend_mode, expected, "keyword={keyword}");
}
}
#[test]
fn isolation_property_resolves_and_is_not_inherited_by_default() {
let css = ".parent { isolation: isolate; } .child { isolation: auto; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent = resolver.compute_style(&element("div", None, &["parent"]), &root, None, &[]);
let child = resolver.compute_style(
&element("div", None, &["child"]),
&parent,
None,
&[element("div", None, &["parent"])],
);
assert!(parent.isolation);
assert!(!child.isolation);
}
#[test]
fn opacity_property_resolves_percentage_and_is_not_inherited_by_default() {
let css = ".parent { opacity: 25%; } .child { opacity: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent = resolver.compute_style(&element("div", None, &["parent"]), &root, None, &[]);
let child = resolver.compute_style(
&element("div", None, &["child"]),
&parent,
None,
&[element("div", None, &["parent"])],
);
assert!((parent.opacity - 0.25).abs() < 0.0001);
assert!((child.opacity - 1.0).abs() < 0.0001);
}
#[test]
fn opacity_var_reparses_percentage_after_substitution() {
let css = ":root { --half-opacity: 50%; } .target { opacity: var(--half-opacity); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let target = resolver.compute_style(
&element("div", None, &["target"]),
&root_style,
None,
&[root_info],
);
assert!((target.opacity - 0.5).abs() < 0.0001);
}
#[test]
fn translucent_background_retains_source_color_and_alpha_for_paint() {
let resolver = StyleResolver::new(".x { background: rgba(255, 87, 34, 0.5); }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let source = style
.background_source_color
.expect("translucent source color");
assert!((source.r - 1.0).abs() < 0.001);
assert!((source.g - (87.0 / 255.0)).abs() < 0.001);
assert!((source.b - (34.0 / 255.0)).abs() < 0.001);
assert!((style.background_alpha - 0.5).abs() < 0.001);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_filter_and_mix_blend_mode() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_supported_filter_mix_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { font-size: 24px; filter: blur(0.5em) drop-shadow(blue 1em 0 0.25em) drop-shadow(-2px 1px 0 red) saturate(1.2) grayscale(20%) brightness(120%) contrast(90%) invert(10%) sepia(15%) hue-rotate(90deg) opacity(85%); mix-blend-mode: plus-lighter; }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"filter\""),
"supported filter should not emit fallback, log={log}"
);
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"mix-blend-mode\""),
"supported mix-blend-mode should not emit fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_filters_effects_fallback_for_supported_backdrop_filter() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_effects_supported_backdrop_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { backdrop-filter: blur(3px) drop-shadow(4px 0 0 red) saturate(120%) sepia(25%) hue-rotate(0.25turn) opacity(90%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"FILTERS_EFFECTS_FALLBACK\",\"property\":\"backdrop-filter\""),
"supported backdrop-filter should not emit fallback, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn clip_path_inset_property_resolves_on_computed_style() {
let css = ".x { clip-path: inset(10px 20px 30px 40px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path_inset,
Some(ClipPathInsetSpec {
top: LengthSpec::Absolute(px_to_pt(10.0)),
right: LengthSpec::Absolute(px_to_pt(20.0)),
bottom: LengthSpec::Absolute(px_to_pt(30.0)),
left: LengthSpec::Absolute(px_to_pt(40.0)),
radius: None,
})
);
}
#[test]
fn clip_path_circle_property_resolves_on_computed_style() {
let css = ".x { clip-path: circle(40% at 50% 25%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Circle(ClipPathCircleSpec {
radius: ClipPathShapeRadius::Length(LengthSpec::Percent(0.4)),
center_x: LengthSpec::Percent(0.5),
center_y: LengthSpec::Percent(0.25),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn legacy_clip_rect_resolves_independently_from_clip_path() {
let resolver = StyleResolver::new(
".x { position: absolute; clip: rect(15px, 120px, 75px, 35px); clip-path: circle(); }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.legacy_clip,
Some(ClipPathRectSpec {
top: LengthSpec::Absolute(px_to_pt(15.0)),
right: LengthSpec::Absolute(px_to_pt(120.0)),
bottom: LengthSpec::Absolute(px_to_pt(75.0)),
left: LengthSpec::Absolute(px_to_pt(35.0)),
radius: None,
})
);
assert!(matches!(
style.clip_path,
Some(ClipPathShapeSpec::Circle(_))
));
}
#[test]
fn local_svg_clip_url_survives_style_compilation_for_dom_resolution() {
let resolver = StyleResolver::new(".x { clip-path: url('#ring'); }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Url("#ring".to_string()))
);
}
#[test]
fn clip_path_empty_circle_and_ellipse_default_to_closest_side() {
let resolver = StyleResolver::new(
".circle { clip-path: circle(); } .ellipse { clip-path: ellipse(); }",
);
let root = resolver.default_style();
let circle = resolver.compute_style(&element("div", None, &["circle"]), &root, None, &[]);
let ellipse = resolver.compute_style(&element("div", None, &["ellipse"]), &root, None, &[]);
assert!(matches!(
circle.clip_path,
Some(ClipPathShapeSpec::Circle(ClipPathCircleSpec {
radius: ClipPathShapeRadius::ClosestSide,
..
}))
));
assert!(matches!(
ellipse.clip_path,
Some(ClipPathShapeSpec::Ellipse(ClipPathEllipseSpec {
radius_x: ClipPathShapeRadius::ClosestSide,
radius_y: ClipPathShapeRadius::ClosestSide,
..
}))
));
}
#[test]
fn clip_path_ellipse_property_resolves_on_computed_style() {
let css = ".x { clip-path: ellipse(40% 50% at 25% 75%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Ellipse(ClipPathEllipseSpec {
radius_x: ClipPathShapeRadius::Length(LengthSpec::Percent(0.4)),
radius_y: ClipPathShapeRadius::Length(LengthSpec::Percent(0.5)),
center_x: LengthSpec::Percent(0.25),
center_y: LengthSpec::Percent(0.75),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_polygon_property_resolves_on_computed_style() {
let css = ".x { clip-path: polygon(evenodd, 50% 0%, 100% 100%, 0% 100%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Polygon(ClipPathPolygonSpec {
evenodd: true,
fill_rule_explicit: true,
points: vec![
(LengthSpec::Percent(0.5), LengthSpec::Percent(0.0)),
(LengthSpec::Percent(1.0), LengthSpec::Percent(1.0)),
(LengthSpec::Percent(0.0), LengthSpec::Percent(1.0)),
],
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_path_property_resolves_on_computed_style() {
let css = ".x { clip-path: path(evenodd, \"M 48 0 L 96 96 L 0 96 Z\"); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Path(ClipPathPathSpec {
evenodd: true,
fill_rule_explicit: true,
data: "M 48 0 L 96 96 L 0 96 Z".to_string(),
commands: vec![
ClipPathPathCommand::MoveTo {
x: px_to_pt(48.0),
y: px_to_pt(0.0),
},
ClipPathPathCommand::LineTo {
x: px_to_pt(96.0),
y: px_to_pt(96.0),
},
ClipPathPathCommand::LineTo {
x: px_to_pt(0.0),
y: px_to_pt(96.0),
},
ClipPathPathCommand::Close,
],
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_path_explicit_nonzero_fill_rule_is_preserved() {
let css = ".x { clip-path: path(nonzero, \"M 0 0 L 96 0 L 96 96 L 0 96 Z\"); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let spec = match style.clip_path.as_ref() {
Some(ClipPathShapeSpec::Path(spec)) => spec,
other => panic!("expected path() clip-path, got {other:?}"),
};
assert!(!spec.evenodd);
assert!(spec.fill_rule_explicit);
assert_eq!(
clip_path_debug(&style).as_deref(),
Some("path(nonzero, \"M 0 0 L 96 0 L 96 96 L 0 96 Z\")")
);
}
#[test]
fn clip_path_shape_function_property_resolves_on_computed_style() {
let css =
".x { clip-path: shape(evenodd from 50% 0%, line to 100% 100%, hline to 0%, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: true,
fill_rule_explicit: true,
start_x: LengthSpec::Percent(0.5),
start_y: LengthSpec::Percent(0.0),
commands: vec![
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(1.0),
relative: false,
},
ClipPathShapeFunctionCommand::HLine {
x: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_position_keywords_resolve_on_computed_style() {
let css = ".x { clip-path: shape(from center left, line to top center, hline to right, vline to bottom, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: false,
fill_rule_explicit: false,
start_x: LengthSpec::Percent(0.0),
start_y: LengthSpec::Percent(0.5),
commands: vec![
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.5),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::HLine {
x: LengthSpec::Percent(1.0),
relative: false,
},
ClipPathShapeFunctionCommand::VLine {
y: LengthSpec::Percent(1.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_position_offsets_resolve_on_computed_style() {
let css = ".x { clip-path: shape(from left 0% top 50%, line to left 50% top 0%, line to right 0% top 50%, line to left 50% bottom 0%, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: false,
fill_rule_explicit: false,
start_x: LengthSpec::Percent(0.0),
start_y: LengthSpec::Percent(0.5),
commands: vec![
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.5),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(0.5),
relative: false,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.5),
y: LengthSpec::Percent(1.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_curve_property_resolves_on_computed_style() {
let css = ".x { clip-path: shape(from 0% 100%, curve to 100% 100% with 35% -100% / 65% -100%, line to 100% 0%, line to 0% 0%, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: false,
fill_rule_explicit: false,
start_x: LengthSpec::Percent(0.0),
start_y: LengthSpec::Percent(1.0),
commands: vec![
ClipPathShapeFunctionCommand::CurveTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(1.0),
relative: false,
control1_x: LengthSpec::Percent(0.35),
control1_y: LengthSpec::Percent(-1.0),
control1_anchor: ClipPathShapeControlAnchor::Start,
control2_x: Some(LengthSpec::Percent(0.65)),
control2_y: Some(LengthSpec::Percent(-1.0)),
control2_anchor: Some(ClipPathShapeControlAnchor::Start),
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_smooth_property_resolves_on_computed_style() {
let css = ".x { clip-path: shape(from 0% 100%, curve to 50% 100% with 25% 100%, smooth to 100% 100%, line to 100% 0%, line to 0% 0%, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: false,
fill_rule_explicit: false,
start_x: LengthSpec::Percent(0.0),
start_y: LengthSpec::Percent(1.0),
commands: vec![
ClipPathShapeFunctionCommand::CurveTo {
x: LengthSpec::Percent(0.5),
y: LengthSpec::Percent(1.0),
relative: false,
control1_x: LengthSpec::Percent(0.25),
control1_y: LengthSpec::Percent(1.0),
control1_anchor: ClipPathShapeControlAnchor::Start,
control2_x: None,
control2_y: None,
control2_anchor: None,
},
ClipPathShapeFunctionCommand::SmoothTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(1.0),
relative: false,
control_x: None,
control_y: None,
control_anchor: None,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_relative_control_points_resolve_on_computed_style() {
let css = ".x { clip-path: shape(from 0% 100%, curve to 100% 100% with 35% 0% from origin / -35% -100% from end, smooth to 0% 100% with 35% 0% from origin, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let spec = match style.clip_path {
Some(ClipPathShapeSpec::ShapeFunction(spec)) => spec,
other => panic!("expected shape() clip-path, got {other:?}"),
};
assert_eq!(spec.commands.len(), 3);
match spec.commands[0] {
ClipPathShapeFunctionCommand::CurveTo {
control1_x,
control1_y,
control1_anchor,
control2_x,
control2_y,
control2_anchor,
..
} => {
assert_eq!(control1_x, LengthSpec::Percent(0.35));
assert_eq!(control1_y, LengthSpec::Percent(0.0));
assert_eq!(control1_anchor, ClipPathShapeControlAnchor::Origin);
assert_eq!(control2_x, Some(LengthSpec::Percent(-0.35)));
assert_eq!(control2_y, Some(LengthSpec::Percent(-1.0)));
assert_eq!(control2_anchor, Some(ClipPathShapeControlAnchor::End));
}
other => panic!("expected curve command, got {other:?}"),
}
match spec.commands[1] {
ClipPathShapeFunctionCommand::SmoothTo {
control_x,
control_y,
control_anchor,
..
} => {
assert_eq!(control_x, Some(LengthSpec::Percent(0.35)));
assert_eq!(control_y, Some(LengthSpec::Percent(0.0)));
assert_eq!(control_anchor, Some(ClipPathShapeControlAnchor::Origin));
}
other => panic!("expected smooth command, got {other:?}"),
}
assert_eq!(spec.commands[2], ClipPathShapeFunctionCommand::Close);
}
#[test]
fn clip_path_shape_function_arc_property_resolves_on_computed_style() {
let css = ".x { clip-path: shape(from 0% 100%, arc to 100% 100% of 50% 50% large cw, line to 100% 0%, line to 0% 0%, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::ShapeFunction(
ClipPathShapeFunctionSpec {
evenodd: false,
fill_rule_explicit: false,
start_x: LengthSpec::Percent(0.0),
start_y: LengthSpec::Percent(1.0),
commands: vec![
ClipPathShapeFunctionCommand::ArcTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(1.0),
relative: false,
radius_x: LengthSpec::Percent(0.5),
radius_y: LengthSpec::Percent(0.5),
large_arc: true,
sweep: true,
rotation_deg: 0.0,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(1.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::LineTo {
x: LengthSpec::Percent(0.0),
y: LengthSpec::Percent(0.0),
relative: false,
},
ClipPathShapeFunctionCommand::Close,
],
}
))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_shape_function_arc_options_resolve_on_computed_style() {
let css = ".x { clip-path: shape(from 0% 0%, arc by 50% 25% of 10% 20% rotate 90deg ccw small, close); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let spec = match style.clip_path {
Some(ClipPathShapeSpec::ShapeFunction(spec)) => spec,
other => panic!("expected shape() clip-path, got {other:?}"),
};
assert_eq!(spec.commands.len(), 2);
match spec.commands[0] {
ClipPathShapeFunctionCommand::ArcTo {
x,
y,
relative,
radius_x,
radius_y,
large_arc,
sweep,
rotation_deg,
} => {
assert_eq!(x, LengthSpec::Percent(0.5));
assert_eq!(y, LengthSpec::Percent(0.25));
assert!(relative);
assert_eq!(radius_x, LengthSpec::Percent(0.1));
assert_eq!(radius_y, LengthSpec::Percent(0.2));
assert!(!large_arc);
assert!(!sweep);
assert!((rotation_deg - 90.0).abs() < 0.001);
}
other => panic!("expected arc command, got {other:?}"),
}
assert_eq!(spec.commands[1], ClipPathShapeFunctionCommand::Close);
}
#[test]
fn clip_path_xywh_property_resolves_on_computed_style() {
let css = ".x { clip-path: xywh(10% 20% 30% 40%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Xywh(ClipPathXywhSpec {
x: LengthSpec::Percent(0.1),
y: LengthSpec::Percent(0.2),
width: LengthSpec::Percent(0.3),
height: LengthSpec::Percent(0.4),
radius: None,
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_rect_property_resolves_on_computed_style() {
let css = ".x { clip-path: rect(10%, auto, 75%, 25%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Rect(ClipPathRectSpec {
top: LengthSpec::Percent(0.1),
right: LengthSpec::Auto,
bottom: LengthSpec::Percent(0.75),
left: LengthSpec::Percent(0.25),
radius: None,
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_xywh_round_property_resolves_on_computed_style() {
let css = ".x { clip-path: xywh(10% 20% 30% 40% round 8px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Xywh(ClipPathXywhSpec {
x: LengthSpec::Percent(0.1),
y: LengthSpec::Percent(0.2),
width: LengthSpec::Percent(0.3),
height: LengthSpec::Percent(0.4),
radius: Some(ClipPathRadiusSpec {
horizontal: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(8.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(8.0)),
},
vertical: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(8.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(8.0)),
},
}),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_rect_round_property_resolves_on_computed_style() {
let css = ".x { clip-path: rect(10% auto 75% 25% round 8px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Rect(ClipPathRectSpec {
top: LengthSpec::Percent(0.1),
right: LengthSpec::Auto,
bottom: LengthSpec::Percent(0.75),
left: LengthSpec::Percent(0.25),
radius: Some(ClipPathRadiusSpec {
horizontal: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(8.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(8.0)),
},
vertical: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(8.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(8.0)),
},
}),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_xywh_multi_radius_property_resolves_on_computed_style() {
let css = ".x { clip-path: xywh(10% 20% 30% 40% round 4px 8px 12px 16px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Xywh(ClipPathXywhSpec {
x: LengthSpec::Percent(0.1),
y: LengthSpec::Percent(0.2),
width: LengthSpec::Percent(0.3),
height: LengthSpec::Percent(0.4),
radius: Some(ClipPathRadiusSpec {
horizontal: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(4.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(12.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(16.0)),
},
vertical: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(4.0)),
top_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(12.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(16.0)),
},
}),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn clip_path_xywh_elliptical_radius_property_resolves_on_computed_style() {
let css = ".x { clip-path: xywh(10% 20% 30% 40% round 8px 4px / 6px 2px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.clip_path,
Some(ClipPathShapeSpec::Xywh(ClipPathXywhSpec {
x: LengthSpec::Percent(0.1),
y: LengthSpec::Percent(0.2),
width: LengthSpec::Percent(0.3),
height: LengthSpec::Percent(0.4),
radius: Some(ClipPathRadiusSpec {
horizontal: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(8.0)),
top_right: LengthSpec::Absolute(px_to_pt(4.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(8.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(4.0)),
},
vertical: BorderRadiusSpec {
top_left: LengthSpec::Absolute(px_to_pt(6.0)),
top_right: LengthSpec::Absolute(px_to_pt(2.0)),
bottom_right: LengthSpec::Absolute(px_to_pt(6.0)),
bottom_left: LengthSpec::Absolute(px_to_pt(2.0)),
},
}),
}))
);
assert_eq!(style.clip_path_inset, None);
}
#[test]
fn debug_logs_conic_gradient_fallback_for_unsupported_background() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_conic_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { background: conic-gradient(red); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(log.contains("\"CONIC_GRADIENT_FALLBACK\""));
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_conic_gradient_fallback_for_dynamic_background() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_conic_known_loss_dynamic_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { --pct: 60; color: rgb(0, 0, 255); background: conic-gradient(from -90deg, currentColor calc(var(--pct) * 1%), rgba(128, 142, 185, 0.16) 0%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"CONIC_GRADIENT_FALLBACK\""),
"dynamic conic gradients should resolve without fallback diagnostics, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn debug_does_not_log_conic_gradient_fallback_for_supported_background() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_conic_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".x { background: conic-gradient(from 90deg at 25% 75%, red 0%, blue 100%); }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"CONIC_GRADIENT_FALLBACK\""),
"supported conic gradients should not emit fallback diagnostics, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn conic_gradient_background_sets_background_paint() {
let resolver = StyleResolver::new(
".x { background: conic-gradient(from 90deg at 25% 75%, red 0%, blue 100%); }",
);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.background_paint {
Some(BackgroundPaint::ConicGradient {
start_angle_deg,
center_x: GradientPosition::Percent(center_x_pct),
center_y: GradientPosition::Percent(center_y_pct),
stops,
..
}) => {
assert!((start_angle_deg - 90.0).abs() < 0.01);
assert!((center_x_pct - 0.25).abs() < 0.01);
assert!((center_y_pct - 0.75).abs() < 0.01);
assert_eq!(stops.len(), 2);
assert_eq!(stops[0].offset, 0.0);
assert_eq!(stops[1].offset, 1.0);
}
other => panic!("expected conic background paint, got {:?}", other),
}
}
#[test]
fn conic_gradient_angle_offsets_resolve_to_turn_fractions() {
let resolver = StyleResolver::new(
".x { background: conic-gradient(red 0deg, green 180deg, blue 360deg); }",
);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.background_paint {
Some(BackgroundPaint::ConicGradient { stops, .. }) => {
assert_eq!(stops.len(), 3);
assert!((stops[0].offset - 0.0).abs() < 0.001);
assert!((stops[1].offset - 0.5).abs() < 0.001);
assert!((stops[2].offset - 1.0).abs() < 0.001);
}
other => panic!("expected conic background paint, got {:?}", other),
}
}
#[test]
fn conic_gradient_background_with_currentcolor_and_var_stop_resolves() {
let css = ":root { --pct: 60; } .x { color: rgb(0, 0, 255); background: conic-gradient(from -90deg, currentColor calc(var(--pct) * 1%), #ebedf4 0%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
match style.background_paint {
Some(BackgroundPaint::ConicGradient { stops, .. }) => {
assert!(
!stops.is_empty(),
"expected conic-gradient stops for dynamic currentColor + var expression"
);
assert!(
stops.iter().any(|stop| (stop.offset - 0.6).abs() < 0.01),
"expected at least one stop offset near 60%, stops={stops:?}"
);
assert!(
stops.iter().any(|stop| {
(stop.color.r - 0.0).abs() < 0.01
&& (stop.color.g - 0.0).abs() < 0.01
&& (stop.color.b - 1.0).abs() < 0.01
}),
"expected a stop resolved from currentColor, stops={stops:?}"
);
}
other => panic!("expected conic background paint, got {other:?}"),
}
}
#[test]
fn background_image_longhand_gradient_sets_background_paint() {
let resolver = StyleResolver::new(
".x { background-color: rgb(0, 255, 0); background-image: linear-gradient(to right, red 0%, blue 100%); }",
);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.background_color, Some(Color::rgb(0.0, 1.0, 0.0)));
match style.background_paint {
Some(BackgroundPaint::LinearGradient {
angle_deg, stops, ..
}) => {
assert!((angle_deg - 90.0).abs() < 0.01);
assert_eq!(stops.len(), 2);
assert!((stops[0].color.r - 1.0).abs() < 0.01);
assert!((stops[1].color.b - 1.0).abs() < 0.01);
}
other => panic!("expected background-image gradient paint, got {other:?}"),
}
}
#[test]
fn translucent_gradient_stops_retain_alpha_for_paint_compositing() {
let resolver = StyleResolver::new(
".x { background-color: #dceeff; background-image: linear-gradient(135deg, rgba(255, 209, 102, .82), rgba(6, 214, 160, .42)); }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
match style.background_paint {
Some(BackgroundPaint::LinearGradient { stops, .. }) => {
assert_eq!(stops.len(), 2);
assert!((stops[0].color.r - 1.0).abs() < 0.002);
assert!((stops[0].color.g - 209.0 / 255.0).abs() < 0.002);
assert!((stops[0].color.b - 102.0 / 255.0).abs() < 0.002);
assert!((stops[0].alpha - 0.82).abs() < 0.002);
assert!((stops[1].color.r - 6.0 / 255.0).abs() < 0.002);
assert!((stops[1].color.g - 214.0 / 255.0).abs() < 0.002);
assert!((stops[1].color.b - 160.0 / 255.0).abs() < 0.002);
assert!((stops[1].alpha - 0.42).abs() < 0.002);
}
other => panic!("expected translucent linear gradient, got {other:?}"),
}
}
#[test]
fn linear_gradient_two_position_length_stops_keep_authored_colors_and_geometry() {
match parse_linear_gradient_str(
"linear-gradient(to bottom, #c62828 0 18px, #ffd54f 18px 24px)",
) {
Some(BackgroundPaint::LinearGradient {
stops,
stop_positions: Some(positions),
..
}) => {
assert_eq!(stops.len(), 4);
assert_eq!(positions.len(), 4);
assert_eq!(
stops[0].color,
Color::rgb(198.0 / 255.0, 40.0 / 255.0, 40.0 / 255.0)
);
assert_eq!(stops[1].color, stops[0].color);
assert_eq!(stops[2].color, Color::rgb(1.0, 213.0 / 255.0, 79.0 / 255.0));
assert_eq!(stops[3].color, stops[2].color);
assert_eq!(positions[1].expect("18px stop").length, Pt::from_f32(13.5));
assert_eq!(positions[3].expect("24px stop").length, Pt::from_f32(18.0));
}
other => panic!("expected geometry-aware linear gradient, got {other:?}"),
}
}
#[test]
fn background_image_multiple_layers_and_longhands_resolve() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, red), linear-gradient(blue, blue); background-size: 50% 100%, 100% 100%; background-position: left top, left top; background-repeat: no-repeat, no-repeat; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_paints.len(), 2);
assert_eq!(style.background_sizes.len(), 2);
assert_eq!(style.background_positions.len(), 2);
assert_eq!(style.background_repeats.len(), 2);
assert_eq!(style.background_sizes[0].width, LengthSpec::Percent(0.5));
assert_eq!(style.background_sizes[0].height, LengthSpec::Percent(1.0));
assert_eq!(
style.background_repeats[0].x,
BackgroundRepeatMode::NoRepeat
);
assert_eq!(
style.background_repeats[0].y,
BackgroundRepeatMode::NoRepeat
);
}
#[test]
fn background_attachment_compiles_longhand_and_shorthand_layer_bindings() {
let resolver = StyleResolver::new(
".long { background-image: linear-gradient(red, red), linear-gradient(blue, blue); background-attachment: fixed, local; } .short { background: linear-gradient(red, blue) fixed left top / 20px 20px no-repeat; }",
);
let root = resolver.default_style();
let long = resolver.compute_style(&element("section", None, &["long"]), &root, None, &[]);
let short = resolver.compute_style(&element("section", None, &["short"]), &root, None, &[]);
assert_eq!(
long.background_attachments,
vec![BackgroundAttachment::Fixed, BackgroundAttachment::Local]
);
assert_eq!(
short.background_attachments,
vec![BackgroundAttachment::Fixed]
);
}
#[test]
fn background_image_none_preserves_layer_alignment() {
let resolver = StyleResolver::new(
".x { background-image: none, url(\"data:image/png;base64,AAAA\"); background-size: 10px 10px, 30px 30px; background-repeat: repeat, no-repeat; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_paints.len(), 2);
assert!(matches!(style.background_paints[0], BackgroundPaint::None));
assert!(matches!(
style.background_paints[1],
BackgroundPaint::Image { .. }
));
assert_eq!(style.background_sizes.len(), 2);
assert_eq!(style.background_repeats.len(), 2);
}
#[test]
fn background_position_xy_longhands_merge_axes() {
let resolver =
StyleResolver::new(".x { background-position-x: 80px; background-position-y: 30px; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_positions.len(), 1);
assert_eq!(
style.background_positions[0].x,
BackgroundPositionComponent::Start(LengthSpec::Absolute(Pt::from_f32(60.0)))
);
assert_eq!(
style.background_positions[0].y,
BackgroundPositionComponent::Start(LengthSpec::Absolute(Pt::from_f32(22.5)))
);
}
#[test]
fn background_blend_mode_list_resolves() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, red), linear-gradient(blue, blue); background-blend-mode: multiply, screen; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(
style.background_blend_modes,
vec![MixBlendMode::Multiply, MixBlendMode::Screen]
);
}
#[test]
fn background_shorthand_image_position_size_repeat_resolve() {
let resolver = StyleResolver::new(
".x { background: white url(\"data:image/png;base64,AAAA\") 25% 75% / 0.25in 0.25in no-repeat; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_paints.len(), 1);
assert_eq!(style.background_sizes.len(), 1);
assert_eq!(style.background_positions.len(), 1);
assert_eq!(style.background_repeats.len(), 1);
assert_eq!(
style.background_positions[0].x,
BackgroundPositionComponent::Start(LengthSpec::Percent(0.25))
);
assert_eq!(
style.background_positions[0].y,
BackgroundPositionComponent::Start(LengthSpec::Percent(0.75))
);
assert_eq!(
style.background_repeats[0].x,
BackgroundRepeatMode::NoRepeat
);
assert_eq!(
style.background_repeats[0].y,
BackgroundRepeatMode::NoRepeat
);
}
#[test]
fn background_origin_and_clip_boxes_resolve() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, red); background-origin: content-box; background-clip: content-box; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_origins, vec![BackgroundBox::Content]);
assert_eq!(style.background_clips, vec![BackgroundClipBox::Content]);
}
#[test]
fn background_shorthand_origin_and_clip_boxes_resolve() {
let resolver = StyleResolver::new(
".x { background: white url(\"data:image/png;base64,AAAA\") 0 0 / 0.25in 0.25in no-repeat content-box content-box; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_origins, vec![BackgroundBox::Content]);
assert_eq!(style.background_clips, vec![BackgroundClipBox::Content]);
}
#[test]
fn background_repeat_space_round_modes_resolve() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, blue); background-repeat: space round; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_repeats.len(), 1);
assert_eq!(style.background_repeats[0].x, BackgroundRepeatMode::Space);
assert_eq!(style.background_repeats[0].y, BackgroundRepeatMode::Round);
}
#[test]
fn background_repeat_logical_aliases_resolve_for_horizontal_tb() {
let resolver = StyleResolver::new(
".inline { background-repeat: repeat-inline; } .block { background-repeat: repeat-block; }",
);
let root = resolver.default_style();
let inline_style =
resolver.compute_style(&element("section", None, &["inline"]), &root, None, &[]);
let block_style =
resolver.compute_style(&element("section", None, &["block"]), &root, None, &[]);
assert_eq!(inline_style.background_repeats.len(), 1);
assert_eq!(
inline_style.background_repeats[0].x,
BackgroundRepeatMode::Repeat
);
assert_eq!(
inline_style.background_repeats[0].y,
BackgroundRepeatMode::NoRepeat
);
assert_eq!(block_style.background_repeats.len(), 1);
assert_eq!(
block_style.background_repeats[0].x,
BackgroundRepeatMode::NoRepeat
);
assert_eq!(
block_style.background_repeats[0].y,
BackgroundRepeatMode::Repeat
);
}
#[test]
fn background_image_url_sets_background_paint() {
let resolver = StyleResolver::new(
".x { background-image: url(\"data:image/png;base64,AAAA\"); background-size: 1in 1in; background-repeat: no-repeat; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_paints.len(), 1);
match &style.background_paints[0] {
BackgroundPaint::Image { source } => {
assert_eq!(source, "data:image/png;base64,AAAA");
}
other => panic!("expected background image paint, got {other:?}"),
}
}
#[test]
fn background_size_contain_cover_keywords_resolve() {
let resolver = StyleResolver::new(
".x { background-image: url(\"data:image/png;base64,AAAA\"), url(\"data:image/png;base64,BBBB\"); background-size: contain, cover; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_sizes.len(), 2);
assert_eq!(style.background_sizes[0].mode, BackgroundSizeMode::Contain);
assert_eq!(style.background_sizes[1].mode, BackgroundSizeMode::Cover);
}
#[test]
fn background_size_rejects_negative_length_percentage_components() {
assert!(parse_background_size_layer("-1px").is_none());
assert!(parse_background_size_layer("1in -25%").is_none());
assert!(parse_background_size_layer("-2em auto").is_none());
assert!(parse_background_size_layer("auto auto").is_some());
assert!(parse_background_size_layer("0 0").is_some());
}
#[test]
fn background_position_single_center_sets_both_axes() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, blue); background-position: center; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_positions.len(), 1);
assert_eq!(
style.background_positions[0].x,
BackgroundPositionComponent::Center
);
assert_eq!(
style.background_positions[0].y,
BackgroundPositionComponent::Center
);
}
#[test]
fn background_position_single_edge_centers_other_axis() {
let position = parse_background_position_layer("right").expect("expected position");
assert_eq!(
position.x,
BackgroundPositionComponent::End(LengthSpec::Absolute(Pt::ZERO))
);
assert_eq!(position.y, BackgroundPositionComponent::Center);
}
#[test]
fn background_position_two_value_keyword_length_maps_axes() {
let resolver = StyleResolver::new(
".x { background-image: linear-gradient(red, blue); background-position: right 20%; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("section", None, &["x"]), &root, None, &[]);
assert_eq!(style.background_positions.len(), 1);
assert_eq!(
style.background_positions[0].x,
BackgroundPositionComponent::End(LengthSpec::Absolute(Pt::ZERO))
);
assert_eq!(
style.background_positions[0].y,
BackgroundPositionComponent::Start(LengthSpec::Percent(0.2))
);
}
#[test]
fn background_position_logical_axis_keywords_resolve_for_horizontal_tb() {
let logical =
parse_background_position_layer("inline-end block-end").expect("expected position");
assert_eq!(
logical.x,
BackgroundPositionComponent::End(LengthSpec::Absolute(Pt::ZERO))
);
assert_eq!(
logical.y,
BackgroundPositionComponent::End(LengthSpec::Absolute(Pt::ZERO))
);
let offset =
parse_background_position_layer("x-end 0.1in y-end 0.2in").expect("expected position");
assert_eq!(
offset.x,
BackgroundPositionComponent::End(length_spec_from_string("0.1in").unwrap())
);
assert_eq!(
offset.y,
BackgroundPositionComponent::End(length_spec_from_string("0.2in").unwrap())
);
}
#[test]
fn object_position_inherits_and_resolves_position_grammar() {
let resolver = StyleResolver::new(
"body { object-position: right top; } .pct { object-position: 25% 75%; } .edge { object-position: right 0.1in bottom 0.2in; }",
);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
let inherited = resolver.compute_style(
&element("img", None, &[]),
&body_style,
None,
&[body_info.clone()],
);
assert_eq!(
inherited.object_position.x,
BackgroundPositionComponent::End(LengthSpec::Absolute(Pt::ZERO))
);
assert_eq!(
inherited.object_position.y,
BackgroundPositionComponent::Start(LengthSpec::Absolute(Pt::ZERO))
);
let pct = resolver.compute_style(&element("img", None, &["pct"]), &body_style, None, &[]);
assert_eq!(
pct.object_position.x,
BackgroundPositionComponent::Start(LengthSpec::Percent(0.25))
);
assert_eq!(
pct.object_position.y,
BackgroundPositionComponent::Start(LengthSpec::Percent(0.75))
);
let edge = resolver.compute_style(&element("img", None, &["edge"]), &body_style, None, &[]);
assert_eq!(
edge.object_position.x,
BackgroundPositionComponent::End(length_spec_from_string("0.1in").unwrap())
);
assert_eq!(
edge.object_position.y,
BackgroundPositionComponent::End(length_spec_from_string("0.2in").unwrap())
);
}
#[test]
fn radial_gradient_background_sets_background_paint() {
let direct = parse_background_paint_str(
"radial-gradient(circle at 10% 0%, #1d2f6d 0%, #0c1430 48%, #070b19 100%)",
);
match direct {
Some(BackgroundPaint::RadialGradient {
center_x: GradientPosition::Percent(center_x_pct),
center_y: GradientPosition::Percent(center_y_pct),
..
}) => {
assert!(
(center_x_pct - 0.10).abs() < 0.01,
"direct parse center_x_pct={center_x_pct}"
);
assert!(
(center_y_pct - 0.00).abs() < 0.01,
"direct parse center_y_pct={center_y_pct}"
);
}
other => panic!("expected direct radial parse, got {other:?}"),
}
let resolver = StyleResolver::new(
".x { background: radial-gradient(circle at 10% 0%, #1d2f6d 0%, #0c1430 48%, #070b19 100%); }",
);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.background_paint {
Some(BackgroundPaint::RadialGradient {
center_x: GradientPosition::Percent(center_x_pct),
center_y: GradientPosition::Percent(center_y_pct),
stops,
..
}) => {
assert!(
(center_x_pct - 0.10).abs() < 0.01,
"unexpected center_x_pct={center_x_pct}"
);
assert!(
(center_y_pct - 0.00).abs() < 0.01,
"unexpected center_y_pct={center_y_pct}"
);
assert_eq!(stops.len(), 3);
assert!((stops[0].offset - 0.0).abs() < 0.001);
assert!((stops[1].offset - 0.48).abs() < 0.01);
assert!((stops[2].offset - 1.0).abs() < 0.001);
}
other => panic!("expected radial background paint, got {other:?}"),
}
}
#[test]
fn radial_gradient_background_with_var_stop_resolves() {
let css = ":root { --bg0: #0c1430; } .x { background: radial-gradient(circle at 10% 0%, #1d2f6d 0%, var(--bg0) 48%, #070b19 100%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
match style.background_paint {
Some(BackgroundPaint::RadialGradient {
center_x: GradientPosition::Percent(center_x_pct),
center_y: GradientPosition::Percent(center_y_pct),
stops,
..
}) => {
assert!(
(center_x_pct - 0.10).abs() < 0.01,
"unexpected center_x_pct={center_x_pct}"
);
assert!(
(center_y_pct - 0.00).abs() < 0.01,
"unexpected center_y_pct={center_y_pct}"
);
assert_eq!(stops.len(), 3);
assert!(
(stops[1].color.r - (12.0 / 255.0)).abs() < 0.01
&& (stops[1].color.g - (20.0 / 255.0)).abs() < 0.01
&& (stops[1].color.b - (48.0 / 255.0)).abs() < 0.01,
"expected var-resolved middle stop, got {:?}",
stops[1].color
);
}
other => panic!("expected radial background paint, got {other:?}"),
}
}
#[test]
fn display_table_is_not_normalized() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_layout_known_loss_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver =
StyleResolver::new_with_debug(".menu { display: table; }", Some(logger.clone()));
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert!(
!log.contains("\"LAYOUT_MODE_NORMALIZED\""),
"display:table should be preserved, log={log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn unsupported_ruby_and_legacy_box_layouts_are_normalized_and_logged() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"fullbleed_style_layout_normalized_{}_{}.jsonl",
std::process::id(),
nanos
));
let logger = Arc::new(DebugLogger::new(&path).expect("debug logger"));
let resolver = StyleResolver::new_with_debug(
".ruby { display: inline ruby; } .legacy { display: -webkit-box; }",
Some(logger.clone()),
);
drop(resolver);
drop(logger);
let log = std::fs::read_to_string(&path).expect("read debug log");
assert_eq!(
log.matches("\"LAYOUT_MODE_NORMALIZED\"").count(),
2,
"{log}"
);
assert!(
log.contains("\"requested\":\"inline ruby\",\"applied\":\"inline\"")
&& log.contains("\"reason\":\"display_ruby_not_supported\""),
"missing ruby normalization: {log}"
);
assert!(
log.contains("\"requested\":\"-webkit-box\",\"applied\":\"block\"")
&& log.contains("\"reason\":\"display_legacy_box_not_supported\""),
"missing legacy-box normalization: {log}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn grid_template_columns_sets_column_count() {
let resolver =
StyleResolver::new(".menu { display: grid; grid-template-columns: 1fr 1fr 1fr; }");
let root = resolver.default_style();
let info = element("div", None, &["menu"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.display, DisplayMode::Grid);
assert_eq!(style.grid_columns, Some(3));
assert_eq!(
style.grid_column_tracks,
vec![
GridTrackSize::fraction(1.0),
GridTrackSize::fraction(1.0),
GridTrackSize::fraction(1.0),
]
);
}
#[test]
fn grid_auto_tracks_and_flow_are_retained() {
let resolver = StyleResolver::new(
".menu { grid-auto-columns: 40px 60px; grid-auto-rows: 25px; \
grid-auto-flow: column dense; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["menu"]), &root, None, &[]);
assert_eq!(style.grid_auto_column_tracks.len(), 2);
assert_eq!(style.grid_auto_row_tracks.len(), 1);
assert_eq!(style.grid_auto_flow, GridAutoFlowMode::ColumnDense);
}
#[test]
fn two_keyword_display_syntax_preserves_inner_layout_mode() {
let resolver = StyleResolver::new(
".bf { display: block flex; } .if { display: inline flex; } \
.bg { display: block grid; } .ig { display: inline grid; }",
);
let root = resolver.default_style();
let computed =
|class| resolver.compute_style(&element("div", None, &[class]), &root, None, &[]);
assert_eq!(computed("bf").display, DisplayMode::Flex);
assert_eq!(computed("if").display, DisplayMode::InlineFlex);
assert_eq!(computed("bg").display, DisplayMode::Grid);
assert_eq!(computed("ig").display, DisplayMode::InlineGrid);
}
#[test]
fn grid_template_rows_sets_row_count() {
let resolver = StyleResolver::new(".menu { display: grid; grid-template-rows: 1fr 2fr; }");
let root = resolver.default_style();
let info = element("div", None, &["menu"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.display, DisplayMode::Grid);
assert_eq!(style.grid_rows, Some(2));
assert_eq!(
style.grid_row_tracks,
vec![GridTrackSize::fraction(1.0), GridTrackSize::fraction(2.0)]
);
}
#[test]
fn grid_track_count_handles_repeat_segments() {
let resolver = StyleResolver::new(
".menu { display: grid; grid-template-columns: repeat(2, 1fr 2fr) 10px; }",
);
let root = resolver.default_style();
let info = element("div", None, &["menu"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.grid_columns, Some(5));
assert_eq!(style.grid_column_tracks[0], GridTrackSize::fraction(1.0));
assert_eq!(style.grid_column_tracks[1], GridTrackSize::fraction(2.0));
assert_eq!(style.grid_column_tracks[2], GridTrackSize::fraction(1.0));
assert_eq!(style.grid_column_tracks[3], GridTrackSize::fraction(2.0));
assert_eq!(
style.grid_column_tracks[4],
GridTrackSize::fixed(LengthSpec::Absolute(Pt::from_f32(7.5)))
);
}
#[test]
fn grid_auto_repeat_retains_its_mode_and_track_pattern() {
let resolver = StyleResolver::new(concat!(
".fill{grid-template-columns:repeat(auto-fill,80px)}",
".fit{grid-template-columns:repeat(auto-fit,minmax(80px,1fr))}"
));
let root = resolver.default_style();
let fill = resolver.compute_style(&element("div", None, &["fill"]), &root, None, &[]);
let fit = resolver.compute_style(&element("div", None, &["fit"]), &root, None, &[]);
assert_eq!(
fill.grid_column_auto_repeat
.as_ref()
.map(|repeat| repeat.mode),
Some(GridAutoRepeatMode::Fill)
);
assert_eq!(fill.grid_column_auto_repeat.unwrap().tracks.len(), 1);
assert_eq!(
fit.grid_column_auto_repeat
.as_ref()
.map(|repeat| repeat.mode),
Some(GridAutoRepeatMode::Fit)
);
assert_eq!(fit.grid_column_auto_repeat.unwrap().tracks.len(), 1);
}
#[test]
fn grid_track_functions_resolve_nested_custom_lengths() {
let resolver = StyleResolver::new(concat!(
".probe{grid-template-columns:fit-content(var(--limit)) minmax(0,1fr)}",
".percent{--limit:30%}.calc{--limit:calc(20% + 36px)}"
));
let root = resolver.default_style();
let percent = resolver.compute_style(
&element("div", None, &["probe", "percent"]),
&root,
None,
&[],
);
let calc =
resolver.compute_style(&element("div", None, &["probe", "calc"]), &root, None, &[]);
assert_eq!(percent.grid_column_tracks.len(), 2);
assert!(matches!(
percent.grid_column_tracks[0].max,
GridTrackBreadth::FitContent(LengthSpec::Percent(value)) if (value - 0.3).abs() < 1.0e-6
));
assert_eq!(calc.grid_column_tracks.len(), 2);
assert!(matches!(
calc.grid_column_tracks[0].max,
GridTrackBreadth::FitContent(LengthSpec::Calc(value))
if (value.percent - 0.2).abs() < 1.0e-6
&& (value.abs.to_f32() - 27.0).abs() < 0.01
));
}
#[test]
fn grid_fixed_tracks_retain_authored_lengths() {
let resolver = StyleResolver::new(
".menu { display: grid; grid-template-columns: 100px 100px; grid-template-rows: repeat(2, 100px); }",
);
let root = resolver.default_style();
let info = element("div", None, &["menu"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let fixed = GridTrackSize::fixed(LengthSpec::Absolute(Pt::from_f32(75.0)));
assert_eq!(style.grid_column_tracks, vec![fixed, fixed]);
assert_eq!(style.grid_row_tracks, vec![fixed, fixed]);
}
#[test]
fn grid_start_lines_parse_from_longhands_and_shorthands() {
let resolver = StyleResolver::new(
".a{grid-column-start:2;grid-row-start:3}.b{grid-column:4/6;grid-row:5/8}.c{grid-area:7/8/span 2/span 1}.d{grid-column:-3/-1;grid-row:span 2/4}.e{grid-column:2 content/span 3 content}",
);
let root = resolver.default_style();
let a = resolver.compute_style(&element("div", None, &["a"]), &root, None, &[]);
let b = resolver.compute_style(&element("div", None, &["b"]), &root, None, &[]);
let c = resolver.compute_style(&element("div", None, &["c"]), &root, None, &[]);
let d = resolver.compute_style(&element("div", None, &["d"]), &root, None, &[]);
let e = resolver.compute_style(&element("div", None, &["e"]), &root, None, &[]);
assert_eq!(a.grid_column_start, Some(2));
assert_eq!(a.grid_row_start, Some(3));
assert_eq!(b.grid_column_start, Some(4));
assert_eq!(b.grid_row_start, Some(5));
assert_eq!(c.grid_row_start, Some(7));
assert_eq!(c.grid_column_start, Some(8));
assert_eq!(b.grid_column_line_start, GridLineSpec::Line(4));
assert_eq!(b.grid_column_line_end, GridLineSpec::Line(6));
assert_eq!(b.grid_row_line_start, GridLineSpec::Line(5));
assert_eq!(b.grid_row_line_end, GridLineSpec::Line(8));
assert_eq!(c.grid_row_line_end, GridLineSpec::Span(2));
assert_eq!(c.grid_column_line_end, GridLineSpec::Span(1));
assert_eq!(d.grid_column_line_start, GridLineSpec::Line(-3));
assert_eq!(d.grid_column_line_end, GridLineSpec::Line(-1));
assert_eq!(d.grid_row_line_start, GridLineSpec::Span(2));
assert_eq!(d.grid_row_line_end, GridLineSpec::Line(4));
assert_eq!(
e.grid_column_line_start,
GridLineSpec::NamedOccurrence {
occurrence: 2,
name: "content".to_string(),
}
);
assert_eq!(
e.grid_column_line_end,
GridLineSpec::SpanNamed {
count: 3,
name: "content".to_string(),
}
);
}
#[test]
fn grid_track_line_names_expand_repeat_segments() {
let resolver = StyleResolver::new(
".menu{display:grid;grid-template-columns:[outer] 20px repeat(2,[content] 1fr [gutter] 10px) [outer-end]}",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["menu"]), &root, None, &[]);
assert_eq!(style.grid_column_tracks.len(), 5);
assert_eq!(style.grid_column_line_names.len(), 6);
assert_eq!(style.grid_column_line_names[0], vec!["outer"]);
assert_eq!(style.grid_column_line_names[1], vec!["content"]);
assert_eq!(style.grid_column_line_names[2], vec!["gutter"]);
assert_eq!(style.grid_column_line_names[3], vec!["content"]);
assert_eq!(style.grid_column_line_names[4], vec!["gutter"]);
assert_eq!(style.grid_column_line_names[5], vec!["outer-end"]);
}
#[test]
fn grid_template_areas_require_equal_rectangular_rows() {
let resolver = StyleResolver::new(concat!(
".valid{grid-template-areas:'header header' 'main side'}",
".ragged{grid-template-areas:'a a' 'b'}",
".nonrect{grid-template-areas:'a a' 'a b' 'a a'}"
));
let root = resolver.default_style();
let valid = resolver.compute_style(&element("div", None, &["valid"]), &root, None, &[]);
let ragged = resolver.compute_style(&element("div", None, &["ragged"]), &root, None, &[]);
let nonrect = resolver.compute_style(&element("div", None, &["nonrect"]), &root, None, &[]);
assert_eq!(valid.grid_template_areas.len(), 2);
assert_eq!(valid.grid_template_areas[0].len(), 2);
assert_eq!(valid.grid_template_areas[0][0].as_deref(), Some("header"));
assert_eq!(valid.grid_template_areas[1][1].as_deref(), Some("side"));
assert!(ragged.grid_template_areas.is_empty());
assert!(nonrect.grid_template_areas.is_empty());
}
#[test]
fn grid_template_shorthand_sets_areas_and_both_track_axes() {
let resolver = StyleResolver::new(
".grid{display:grid;grid-template:'head head' 50px 'side main' 70px / 80px 120px}",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["grid"]), &root, None, &[]);
assert_eq!(style.grid_column_tracks.len(), 2);
assert_eq!(style.grid_row_tracks.len(), 2);
assert_eq!(
style.grid_row_tracks,
vec![
GridTrackSize::fixed(LengthSpec::Absolute(Pt::from_f32(37.5))),
GridTrackSize::fixed(LengthSpec::Absolute(Pt::from_f32(52.5))),
]
);
assert_eq!(style.grid_template_areas.len(), 2);
assert_eq!(style.grid_template_areas[0][0].as_deref(), Some("head"));
assert_eq!(style.grid_template_areas[1][1].as_deref(), Some("main"));
}
#[test]
fn grid_shorthand_supports_auto_flow_rows_and_resets_implicit_properties() {
let resolver = StyleResolver::new(concat!(
".auto{grid:auto-flow 60px/70px 70px}",
".reset{grid-auto-flow:column;grid-auto-rows:99px;",
"grid-auto-columns:88px;grid:60px 60px/70px 70px}"
));
let root = resolver.default_style();
let auto = resolver.compute_style(&element("div", None, &["auto"]), &root, None, &[]);
let reset = resolver.compute_style(&element("div", None, &["reset"]), &root, None, &[]);
assert_eq!(auto.grid_auto_flow, GridAutoFlowMode::Row);
assert!(auto.grid_row_tracks.is_empty());
assert_eq!(auto.grid_column_tracks.len(), 2);
assert_eq!(auto.grid_auto_row_tracks.len(), 1);
assert_eq!(
auto.grid_auto_row_tracks[0],
GridTrackSize::fixed(LengthSpec::Absolute(Pt::from_f32(45.0)))
);
assert_eq!(reset.grid_auto_flow, GridAutoFlowMode::Row);
assert_eq!(reset.grid_row_tracks.len(), 2);
assert_eq!(reset.grid_column_tracks.len(), 2);
assert!(reset.grid_auto_row_tracks.is_empty());
assert!(reset.grid_auto_column_tracks.is_empty());
}
#[test]
fn display_table_keywords_map_to_table_modes() {
let resolver = StyleResolver::new(
".t{display:table}.it{display:inline-table}.tr{display:table-row}.tc{display:table-cell}.tcg{display:table-column-group}.tcol{display:table-column}.th{display:table-header-group}",
);
let root = resolver.default_style();
let t = resolver.compute_style(&element("div", None, &["t"]), &root, None, &[]);
let it = resolver.compute_style(&element("div", None, &["it"]), &root, None, &[]);
let tr = resolver.compute_style(&element("div", None, &["tr"]), &root, None, &[]);
let tc = resolver.compute_style(&element("div", None, &["tc"]), &root, None, &[]);
let tcg = resolver.compute_style(&element("div", None, &["tcg"]), &root, None, &[]);
let tcol = resolver.compute_style(&element("div", None, &["tcol"]), &root, None, &[]);
let th = resolver.compute_style(&element("div", None, &["th"]), &root, None, &[]);
assert_eq!(t.display, DisplayMode::Table);
assert_eq!(it.display, DisplayMode::InlineTable);
assert_eq!(tr.display, DisplayMode::TableRow);
assert_eq!(tc.display, DisplayMode::TableCell);
assert_eq!(tcg.display, DisplayMode::TableColumnGroup);
assert_eq!(tcol.display, DisplayMode::TableColumn);
assert_eq!(th.display, DisplayMode::TableHeaderGroup);
}
#[test]
fn display_list_item_keywords_map_to_list_item_modes() {
let resolver = StyleResolver::new(
".li{display:list-item}.ili{display:inline list-item}.bli{display:block list-item}",
);
let root = resolver.default_style();
let li = resolver.compute_style(&element("div", None, &["li"]), &root, None, &[]);
let ili = resolver.compute_style(&element("div", None, &["ili"]), &root, None, &[]);
let bli = resolver.compute_style(&element("div", None, &["bli"]), &root, None, &[]);
assert_eq!(li.display, DisplayMode::ListItem);
assert_eq!(ili.display, DisplayMode::InlineListItem);
assert_eq!(bli.display, DisplayMode::ListItem);
}
#[test]
fn relative_font_size_uses_parent() {
let css = "div { font-size: 20px; } p { font-size: 50%; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_info = element("div", None, &[]);
let div_style = resolver.compute_style(&div_info, &root, None, &[]);
let p_info = element("p", None, &[]);
let p_style = resolver.compute_style(&p_info, &div_style, None, &[div_info]);
assert!((p_style.font_size.to_f32() - 7.5).abs() < 0.01);
}
#[test]
fn font_size_resolves_keywords_vars_and_invalid_negative_values() {
let css = concat!(
"body { --fs: larger; --bad: -5px; font-size: 20px; }",
".small { font-size: small; }",
".larger { font-size: larger; }",
".smaller { font-size: smaller; }",
".percent { font-size: 80%; }",
".em { font-size: 1.5em; }",
".rem { font-size: 1.5rem; }",
".var { font-size: var(--fs); }",
".fallback { font-size: var(--missing, 150%); }",
".initial { font-size: initial; }",
".invalid { font-size: 30px; font-size: var(--bad); }"
);
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
let ancestors = [body_info];
let font_size_for = |class_name: &str| {
let info = element("p", None, &[class_name]);
resolver
.compute_style(&info, &body_style, None, &ancestors)
.font_size
.to_f32()
};
assert!((font_size_for("small") - 9.75).abs() < 0.01);
assert!((font_size_for("larger") - 18.0).abs() < 0.01);
assert!((font_size_for("smaller") - 12.0).abs() < 0.01);
assert!((font_size_for("percent") - 12.0).abs() < 0.01);
assert!((font_size_for("em") - 22.5).abs() < 0.01);
assert!((font_size_for("rem") - 18.0).abs() < 0.01);
assert!((font_size_for("var") - 18.0).abs() < 0.01);
assert!((font_size_for("fallback") - 22.5).abs() < 0.01);
assert!((font_size_for("initial") - 12.0).abs() < 0.01);
assert!((font_size_for("invalid") - 15.0).abs() < 0.01);
}
#[test]
fn line_height_distinguishes_unitless_from_computed_lengths() {
let css = concat!(
":root { font-size: 16px; }",
"body { --lh: 150%; --bad: -1; font-size: 20px; line-height: normal; }",
".number { line-height: 1.5; }",
".percent { line-height: 150%; }",
".em { line-height: 1.5em; }",
".rem { line-height: 1.5rem; }",
".var { line-height: var(--lh); }",
".fallback { line-height: var(--missing, 2); }",
".initial { line-height: initial; }",
".invalid { line-height: 30pt; line-height: var(--bad); }",
".parent-length { line-height: 1.5em; }",
".parent-number { line-height: 1.5; }",
".child { font-size: 30pt; line-height: inherit; }"
);
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let body_info = element("body", None, &[]);
let body_style =
resolver.compute_style(&body_info, &root_style, None, &[root_info.clone()]);
let ancestors = [root_info, body_info.clone()];
let line_height_for = |class_name: &str| {
let info = element("p", None, &[class_name]);
let style = resolver.compute_style(&info, &body_style, None, &ancestors);
style.line_height.to_line_height(style.font_size).to_f32()
};
assert!((line_height_for("number") - 22.5).abs() < 0.01);
assert!((line_height_for("percent") - 22.5).abs() < 0.01);
assert!((line_height_for("em") - 22.5).abs() < 0.01);
assert!((line_height_for("rem") - 18.0).abs() < 0.01);
assert!((line_height_for("var") - 22.5).abs() < 0.01);
assert!((line_height_for("fallback") - 30.0).abs() < 0.01);
assert!((line_height_for("initial") - 18.0).abs() < 0.01);
assert!((line_height_for("invalid") - 18.0).abs() < 0.01);
let parent_length_info = element("section", None, &["parent-length"]);
let parent_length =
resolver.compute_style(&parent_length_info, &body_style, None, &ancestors);
let child_info = element("p", None, &["child"]);
let child_length = resolver.compute_style(
&child_info,
&parent_length,
None,
&[body_info.clone(), parent_length_info],
);
assert!((child_length.font_size.to_f32() - 30.0).abs() < 0.01);
assert!(
(child_length
.line_height
.to_line_height(child_length.font_size)
.to_f32()
- 22.5)
.abs()
< 0.01
);
let parent_number_info = element("section", None, &["parent-number"]);
let parent_number =
resolver.compute_style(&parent_number_info, &body_style, None, &ancestors);
let child_number = resolver.compute_style(
&child_info,
&parent_number,
None,
&[body_info, parent_number_info],
);
assert!(
(child_number
.line_height
.to_line_height(child_number.font_size)
.to_f32()
- 45.0)
.abs()
< 0.01
);
}
#[test]
fn font_size_calc_resolves_viewport_units() {
let css = ":root { font-size: 20px; } .display { font-size: calc(1rem + 1vw); }";
let viewport = Size {
width: Pt::from_f32(600.0),
height: Pt::from_f32(800.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let child_info = element("div", None, &["display"]);
let child_style = resolver.compute_style(&child_info, &root_style, None, &[root_info]);
assert!((child_style.font_size.to_f32() - 21.0).abs() < 0.01);
}
#[test]
fn layout_lengths_resolve_viewport_and_dynamic_viewport_units() {
let css = ".vw { width: 30vw; height: 20vh; } \
.minmax { width: 40vmin; height: 8vmax; } \
.small { width: 50svw; height: 25svh; }";
let viewport = Size {
width: Pt::from_f32(228.0),
height: Pt::from_f32(150.0),
};
let resolver = StyleResolver::new_with_debug_and_viewport(css, None, Some(viewport));
let root = resolver.default_style();
let expected = [
("vw", 68.4, 30.0),
("minmax", 60.0, 18.24),
("small", 114.0, 37.5),
];
for (class_name, width, height) in expected {
let style =
resolver.compute_style(&element("div", None, &[class_name]), &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(width)));
assert_eq!(style.height, LengthSpec::Absolute(Pt::from_f32(height)));
}
}
#[test]
fn negative_css_math_width_is_valid_and_clamps_during_layout() {
let resolver = StyleResolver::new_with_debug_and_viewport(
".x { width: calc(50vw - 280px); }",
None,
Some(Size {
width: Pt::from_f32(360.0),
height: Pt::from_f32(468.0),
}),
);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style
.width
.resolve_width(Pt::from_f32(315.0), style.font_size, style.root_font_size)
.max(Pt::ZERO),
Pt::ZERO
);
}
#[test]
fn margin_and_padding_shorthand_apply() {
let css = "div { margin: 10px 20px; padding: 5px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_info = element("div", None, &[]);
let div_style = resolver.compute_style(&div_info, &root, None, &[]);
assert_eq!(
div_style.margin.top,
LengthSpec::Absolute(Pt::from_f32(7.5))
);
assert_eq!(
div_style.margin.right,
LengthSpec::Absolute(Pt::from_f32(15.0))
);
assert_eq!(
div_style.margin.bottom,
LengthSpec::Absolute(Pt::from_f32(7.5))
);
assert_eq!(
div_style.margin.left,
LengthSpec::Absolute(Pt::from_f32(15.0))
);
assert_eq!(
div_style.padding.top,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
assert_eq!(
div_style.padding.right,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
assert_eq!(
div_style.padding.bottom,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
assert_eq!(
div_style.padding.left,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
}
#[test]
fn transform_translate_functions_resolve() {
let css = ".x { transform: translate(16px, 8px) translateY(4px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![
CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::from_f32(6.0))
},
CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::ZERO),
y: LengthSpec::Absolute(Pt::from_f32(3.0))
}
]
);
}
#[test]
fn transform_rejects_nonzero_unitless_translation() {
let resolver = StyleResolver::new(
".bad { transform: translate(40, 0); } .zero { transform: translate(0, 0); }",
);
let root = resolver.default_style();
let bad = resolver.compute_style(&element("div", None, &["bad"]), &root, None, &[]);
let zero = resolver.compute_style(&element("div", None, &["zero"]), &root, None, &[]);
assert!(bad.transform.is_empty());
assert_eq!(
zero.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::ZERO),
y: LengthSpec::Absolute(Pt::ZERO),
}]
);
}
#[test]
fn invalid_negative_flex_grow_preserves_prior_declaration() {
let resolver = StyleResolver::new(".x { flex-grow: 1; flex-grow: -1; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.flex_grow, 1.0);
}
#[test]
fn translate_longhand_resolves() {
let css = ".x { translate: 16px 8px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::from_f32(6.0))
}]
);
}
#[test]
fn transform_scale_and_rotate_functions_resolve() {
let css = ".x { transform: scale(2, 0.5) rotate(90deg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 2);
match style.transform[0] {
CssTransformOp::Scale { x, y } => {
assert!((x - 2.0).abs() < 1.0e-6);
assert!((y - 0.5).abs() < 1.0e-6);
}
_ => panic!("expected scale op"),
}
match style.transform[1] {
CssTransformOp::Rotate { radians } => {
assert!((radians - std::f32::consts::FRAC_PI_2).abs() < 1.0e-6);
}
_ => panic!("expected rotate op"),
}
}
#[test]
fn rotate_longhand_resolves() {
let css = ".x { rotate: 90deg; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Rotate { radians } => {
assert!((radians - std::f32::consts::FRAC_PI_2).abs() < 1.0e-6);
}
_ => panic!("expected rotate op"),
}
}
#[test]
fn scale_longhand_resolves() {
let css = ".x { scale: 200% 50%; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Scale { x, y } => {
assert!((x - 2.0).abs() < 1.0e-6);
assert!((y - 0.5).abs() < 1.0e-6);
}
_ => panic!("expected scale op"),
}
}
#[test]
fn transform_skew_functions_resolve() {
let css = ".x { transform: skewX(45deg) skewY(30deg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 2);
match style.transform[0] {
CssTransformOp::Skew {
x_radians,
y_radians,
} => {
assert!((x_radians - std::f32::consts::FRAC_PI_4).abs() < 1.0e-6);
assert!(y_radians.abs() < 1.0e-6);
}
_ => panic!("expected skew op"),
}
match style.transform[1] {
CssTransformOp::Skew {
x_radians,
y_radians,
} => {
assert!(x_radians.abs() < 1.0e-6);
assert!((y_radians - std::f32::consts::PI / 6.0).abs() < 1.0e-6);
}
_ => panic!("expected skew op"),
}
}
#[test]
fn transform_matrix_function_resolves() {
let css = ".x { transform: matrix(1, 0.5, 0.25, 1, 96, 12); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Matrix { a, b, c, d, e, f } => {
assert!((a - 1.0).abs() < 1.0e-6);
assert!((b - 0.5).abs() < 1.0e-6);
assert!((c - 0.25).abs() < 1.0e-6);
assert!((d - 1.0).abs() < 1.0e-6);
assert!((e.to_f32() - 72.0).abs() < 0.001);
assert!((f.to_f32() - 9.0).abs() < 0.001);
}
_ => panic!("expected matrix op"),
}
}
#[test]
fn transform_matrix3d_2d_components_resolve() {
let css = ".x { transform: matrix3d(1, 0, 0, 0, 0.5, 1, 0, 0, 0, 0, 1, 0, 96, 12, 0, 1); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Matrix { a, b, c, d, e, f } => {
assert!((a - 1.0).abs() < 1.0e-6);
assert!(b.abs() < 1.0e-6);
assert!((c - 0.5).abs() < 1.0e-6);
assert!((d - 1.0).abs() < 1.0e-6);
assert!((e.to_f32() - 72.0).abs() < 0.001);
assert!((f.to_f32() - 9.0).abs() < 0.001);
}
_ => panic!("expected matrix op"),
}
}
#[test]
fn transform_matrix3d_with_3d_terms_is_retained_for_projective_lowering() {
let css = ".x { transform: matrix3d(1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(matches!(
style.transform.as_slice(),
[CssTransformOp::Matrix3d { values }] if (values[2] - 1.0).abs() < 1.0e-6
));
}
#[test]
fn true_3d_transform_functions_compile_to_projective_ops() {
let css = ".x { transform: translate3d(16px, 8px, 4px) rotateY(45deg) scale3d(1, 2, 3) perspective(200px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.transform.len(), 4);
assert!(matches!(
style.transform[0],
CssTransformOp::Translate3d {
x: LengthSpec::Absolute(x),
y: LengthSpec::Absolute(y),
z: LengthSpec::Absolute(z),
} if x == Pt::from_f32(12.0) && y == Pt::from_f32(6.0) && z == Pt::from_f32(3.0)
));
assert!(matches!(
style.transform[1],
CssTransformOp::Rotate3d { x, y, z, .. }
if x == 0.0 && y == 1.0 && z == 0.0
));
assert_eq!(
style.transform[2],
CssTransformOp::Scale3d {
x: 1.0,
y: 2.0,
z: 3.0,
}
);
assert_eq!(
style.transform[3],
CssTransformOp::Perspective {
distance: LengthSpec::Absolute(Pt::from_f32(150.0)),
}
);
}
#[test]
fn transform_origin_keywords_resolve() {
let css = ".x { transform-origin: left top; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform_origin,
CssTransformOrigin {
x: LengthSpec::Percent(0.0),
y: LengthSpec::Percent(0.0),
z: LengthSpec::Absolute(Pt::ZERO),
}
);
}
#[test]
fn transform_origin_retains_non_percentage_z_offset() {
let resolver = StyleResolver::new(".x { transform-origin: 50% 50% 55px; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.transform_origin.x, LengthSpec::Percent(0.5));
assert_eq!(style.transform_origin.y, LengthSpec::Percent(0.5));
assert_eq!(
style.transform_origin.z,
LengthSpec::Absolute(Pt::from_f32(41.25))
);
}
#[test]
fn projective_reference_properties_compile_and_inherit_explicitly() {
let resolver = StyleResolver::new(
".parent { transform-box: content-box; transform-style: preserve-3d; perspective-origin: left top; } .child { transform-box: inherit; transform-style: inherit; perspective-origin: inherit; }",
);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(parent.transform_box, TransformBoxMode::ContentBox);
assert_eq!(parent.transform_style, TransformStyleMode::Preserve3d);
assert_eq!(parent.perspective_origin.x, LengthSpec::Percent(0.0));
assert_eq!(parent.perspective_origin.y, LengthSpec::Percent(0.0));
let child = resolver.compute_style(
&element("div", None, &["child"]),
&parent,
None,
&[parent_info],
);
assert_eq!(child.transform_box, parent.transform_box);
assert_eq!(child.transform_style, parent.transform_style);
assert_eq!(child.perspective_origin, parent.perspective_origin);
}
#[test]
fn transform_origin_far_edge_offset_resolves() {
let css = ".x { transform-origin: right 10px bottom 4px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let w = Pt::from_f32(100.0);
let h = Pt::from_f32(80.0);
let ox = style
.transform_origin
.x
.resolve_width(w, style.font_size, style.root_font_size);
let oy = style
.transform_origin
.y
.resolve_height(h, style.font_size, style.root_font_size);
assert!((ox.to_f32() - 92.5).abs() < 0.001);
assert!((oy.to_f32() - 77.0).abs() < 0.001);
}
#[test]
fn transform_origin_unset_resets_to_center() {
let css = ".x { transform-origin: left top; transform-origin: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform_origin, CssTransformOrigin::center());
}
#[test]
fn transform_origin_inherit_uses_parent_value() {
let css = ".parent { transform-origin: left top; } .child { transform-origin: inherit; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("span", None, &["child"]);
let child_style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(child_style.transform_origin, parent_style.transform_origin);
}
#[test]
fn transform_unset_resets_to_none() {
let css = ".x { transform: translateX(16px); transform: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(style.transform.is_empty());
}
#[test]
fn transform_inherit_uses_parent_value() {
let css = ".parent { transform: translateX(16px); } .child { transform: inherit; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("span", None, &["child"]);
let child_style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(child_style.transform, parent_style.transform);
}
#[test]
fn individual_transform_properties_compose_with_transform() {
let css = ".x { translate: 16px 8px; rotate: 90deg; scale: 200% 50%; transform: translateX(4px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 4);
match style.transform[0] {
CssTransformOp::Translate { x, y } => {
assert_eq!(x, LengthSpec::Absolute(Pt::from_f32(12.0)));
assert_eq!(y, LengthSpec::Absolute(Pt::from_f32(6.0)));
}
_ => panic!("expected translate op"),
}
match style.transform[1] {
CssTransformOp::Rotate { radians } => {
assert!((radians - std::f32::consts::FRAC_PI_2).abs() < 1.0e-6);
}
_ => panic!("expected rotate op"),
}
match style.transform[2] {
CssTransformOp::Scale { x, y } => {
assert!((x - 2.0).abs() < 1.0e-6);
assert!((y - 0.5).abs() < 1.0e-6);
}
_ => panic!("expected scale op"),
}
match style.transform[3] {
CssTransformOp::Translate { x, y } => {
assert_eq!(x, LengthSpec::Absolute(Pt::from_f32(3.0)));
assert_eq!(y, LengthSpec::Absolute(Pt::ZERO));
}
_ => panic!("expected translate op"),
}
}
#[test]
fn translate_inherit_is_scoped_to_translate_property() {
let css = ".parent { translate: 16px 0; rotate: 90deg; transform: translateX(4px); } .child { translate: inherit; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("span", None, &["child"]);
let child_style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(
child_style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
}
#[test]
fn transform_inherit_does_not_pull_translate_rotate_or_scale() {
let css = ".parent { translate: 16px 0; rotate: 90deg; scale: 200% 50%; transform: translateX(4px); } .child { transform: inherit; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("span", None, &["child"]);
let child_style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(
child_style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(3.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
assert_eq!(parent_style.transform.len(), 4);
}
#[test]
fn transform_var_reference_resolves_custom_transform_list() {
let css = ".x { --t: translateX(16px); transform: var(--t); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
}
#[test]
fn transform_var_fallback_resolves_when_missing() {
let css = ".x { transform: var(--missing, translateX(16px)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
}
#[test]
fn transform_var_cycle_uses_fallback() {
let css = ".x { --a: var(--b); --b: var(--a); transform: var(--a, translateX(8px)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(6.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
}
#[test]
fn translate_var_fallback_resolves_when_missing() {
let css = ".x { translate: var(--missing, 16px 8px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::from_f32(6.0))
}]
);
}
#[test]
fn translate_var_chain_resolves_custom_value() {
let css = ".x { --a: var(--b); --b: 16px 8px; translate: var(--a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(12.0)),
y: LengthSpec::Absolute(Pt::from_f32(6.0))
}]
);
}
#[test]
fn rotate_var_reference_resolves_custom_angle() {
let css = ".x { --r: 90deg; rotate: var(--r); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Rotate { radians } => {
assert!((radians - std::f32::consts::FRAC_PI_2).abs() < 1.0e-6);
}
_ => panic!("expected rotate op"),
}
}
#[test]
fn scale_var_reference_resolves_custom_pair() {
let css = ".x { --s: 200% 50%; scale: var(--s); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.transform.len(), 1);
match style.transform[0] {
CssTransformOp::Scale { x, y } => {
assert!((x - 2.0).abs() < 1.0e-6);
assert!((y - 0.5).abs() < 1.0e-6);
}
_ => panic!("expected scale op"),
}
}
#[test]
fn concrete_transform_overrides_prior_pending_var() {
let css = ".x { transform: var(--missing, translateX(16px)); transform: translateX(4px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.transform,
vec![CssTransformOp::Translate {
x: LengthSpec::Absolute(Pt::from_f32(3.0)),
y: LengthSpec::Absolute(Pt::ZERO)
}]
);
}
#[test]
fn custom_property_var_height_resolves() {
let css = ".bar { height: var(--h); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let bar_info = element("div", None, &["bar"]);
let bar_style = resolver.compute_style(&bar_info, &root, Some("--h: 40%;"), &[]);
assert_eq!(
bar_style.custom_lengths.get("--h").copied(),
Some(LengthSpec::Percent(0.4))
);
assert_eq!(bar_style.height, LengthSpec::Percent(0.4));
}
#[test]
fn custom_property_length_var_chain_resolves() {
let css = ".x { --a: var(--b); --b: 40%; height: var(--a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.height, LengthSpec::Percent(0.4));
}
#[test]
fn custom_property_length_var_fallback_resolves() {
let css = ".x { --w: var(--missing, 25px); width: var(--w); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(18.75)));
}
#[test]
fn custom_property_length_min_max_functions_resolve() {
let css = ".x { --w-min: min(40px, 20px); --w-max: max(40px, 20px); width: var(--w-min); min-width: var(--w-max); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(15.0)));
assert_eq!(style.min_width, LengthSpec::Absolute(Pt::from_f32(30.0)));
}
#[test]
fn custom_property_length_clamp_function_resolves() {
let css = ".x { --w: clamp(10px, 30px, 20px); width: var(--w); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(15.0)));
}
#[test]
fn css_math_round_mod_trig_and_hypot_resolve_to_lengths() {
let css = ".round { width: round(down, 137px, 20px); } \
.mod { width: calc(100px + mod(50px, 30px)); } \
.sin { width: calc(100px + 40px * sin(30deg)); } \
.hypot { width: hypot(72px, 96px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
for class_name in ["round", "mod", "sin", "hypot"] {
let info = element("div", None, &[class_name]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.width,
LengthSpec::Absolute(Pt::from_f32(90.0)),
"{class_name}"
);
}
}
#[test]
fn css_typed_arithmetic_cancels_dimensions_before_used_value_resolution() {
let css = ".length { width: calc(40px * 3px / 1px); } \
.angle { width: calc(60px * 2deg / 1deg); } \
.time { width: calc(6px * 1s / 50ms); } \
.frequency { width: calc(30px * 2kHz / 500Hz); } \
.resolution { width: calc(60px * 192dpi / 1dppx); } \
.flex { width: calc(40px * 3fr / 1fr); } \
.font { width: calc(240px / 2em * 1em); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
for class_name in [
"length",
"angle",
"time",
"frequency",
"resolution",
"flex",
"font",
] {
let info = element("div", None, &[class_name]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.width,
LengthSpec::Absolute(Pt::from_f32(90.0)),
"{class_name}"
);
}
}
#[test]
fn font_relative_layout_units_bind_to_computed_line_and_font_metrics() {
let css = "html { font-size: 16px; line-height: 24px; } \
.ex { font-size: 20px; line-height: 30px; width: 2ex; } \
.ch { font-size: 20px; line-height: 30px; width: 2ch; } \
.cap { font-size: 20px; line-height: 30px; width: 2cap; } \
.lh { font-size: 20px; line-height: 30px; width: 2lh; } \
.rlh { font-size: 20px; line-height: 30px; width: 2rlh; }";
let resolver = StyleResolver::new(css);
let initial = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root = resolver.compute_style(&root_info, &initial, None, &[]);
let expected = [15.0, 18.0, 21.0, 45.0, 36.0];
for (class_name, expected) in ["ex", "ch", "cap", "lh", "rlh"].into_iter().zip(expected) {
let info = element("div", None, &[class_name]);
let style = resolver.compute_style(&info, &root, None, &[root_info.clone()]);
assert_eq!(
style.width,
LengthSpec::Absolute(Pt::from_f32(expected)),
"{class_name}"
);
}
}
#[test]
fn font_relative_font_size_uses_parent_face_metrics() {
let font_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("python/fullbleed_assets/fonts/Inter-Variable.ttf");
let mut registry = FontRegistry::new();
registry.register_file(&font_path);
let registered_name = registry
.resolve("Inter-Variable")
.expect("registered font")
.name
.clone();
let registry = Arc::new(registry);
let css = format!(
".parent {{ font-family: '{registered_name}'; font-size: 20px; }} \
.ch {{ font-size: 5ch; }} .ex {{ font-size: 4ex; }}"
);
let resolver = StyleResolver::new_with_debug_viewport_and_fonts(
&css,
None,
None,
Some(registry.clone()),
);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let (ex, ch, _) = registry
.css_font_relative_metrics(parent.font_name.as_ref(), parent.font_size)
.expect("registered CSS metrics");
let ch_style = resolver.compute_style(
&element("span", None, &["ch"]),
&parent,
None,
&[parent_info.clone()],
);
let ex_style = resolver.compute_style(
&element("span", None, &["ex"]),
&parent,
None,
&[parent_info],
);
assert_eq!(ch_style.font_size, ch * 5);
assert_eq!(ex_style.font_size, ex * 4);
}
#[test]
fn font_size_adjust_compiles_face_x_height_into_used_glyph_size() {
let font_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("python/fullbleed_assets/fonts/Inter-Variable.ttf");
let mut registry = FontRegistry::new();
registry.register_file(&font_path);
let registered_name = registry
.resolve("Inter-Variable")
.expect("registered font")
.name
.clone();
let registry = Arc::new(registry);
let css = format!(
".adjusted {{ font-family: '{registered_name}'; font-size: 30px; \
line-height: 48px; font-size-adjust: 0.8; }}"
);
let resolver = StyleResolver::new_with_debug_viewport_and_fonts(
&css,
None,
None,
Some(registry.clone()),
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["adjusted"]), &root, None, &[]);
let text_style = style.to_text_style();
let (used_ex, _, _) = registry
.css_font_relative_metrics(style.font_name.as_ref(), text_style.font_size)
.expect("adjusted CSS metrics");
assert!(
(used_ex - style.font_size.mul_fixed(I32F32::from_num(0.8))).abs()
<= Pt::from_milli_i64(1)
);
assert_eq!(text_style.line_height, Pt::from_f32(36.0));
assert!(text_style.font_size > style.font_size);
}
#[test]
fn shaping_properties_and_stretch_descriptors_compile_to_typed_choices() {
let css = r#"
@font-face {
font-family: StretchPick;
src: url("NormalFace.ttf");
font-stretch: normal;
}
@font-face {
font-family: StretchPick;
src: url("CondensedFace.ttf");
font-stretch: condensed;
}
.normal { font-family: StretchPick; }
.controlled {
font-family: StretchPick;
font-stretch: condensed;
font-kerning: none;
font-variant-ligatures: no-common-ligatures;
font-variant-caps: small-caps;
}
"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let normal = resolver.compute_style(&element("div", None, &["normal"]), &root, None, &[]);
let controlled =
resolver.compute_style(&element("div", None, &["controlled"]), &root, None, &[]);
assert_eq!(normal.font_name.as_ref(), "NormalFace");
assert_eq!(controlled.font_name.as_ref(), "CondensedFace");
assert!(normal.font_kerning && normal.common_ligatures && !normal.small_caps);
assert!(!controlled.font_kerning);
assert!(!controlled.common_ligatures);
assert!(controlled.small_caps);
}
#[test]
fn typed_font_relative_dimensions_cancel_in_css_math() {
let resolver = StyleResolver::new(".x { width: calc(120px / 2ch * 1ch); }");
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(45.0)));
}
#[test]
fn css_comparison_math_defers_mixed_percentage_bounds_until_layout() {
let css = ".a { width: min(180px, 50%); } \
.b { width: max(120px, 35%); } \
.c { width: clamp(100px, 50%, 160px); } \
.d { width: clamp(100px, 25%, 160px); } \
.e { width: min(calc(50% - 20px), 220px); } \
.f { width: max(min(50%, 180px), 120px); } \
.g { width: calc(max(30%, 100px) + 20px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let basis = Pt::from_f32(300.0);
let expected = [135.0, 105.0, 120.0, 75.0, 135.0, 135.0, 105.0];
for (class_name, expected) in ["a", "b", "c", "d", "e", "f", "g"]
.into_iter()
.zip(expected)
{
let info = element("div", None, &[class_name]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style
.width
.resolve_width(basis, style.font_size, style.root_font_size),
Pt::from_f32(expected),
"{class_name}: {:?}",
style.width
);
}
}
#[test]
fn custom_property_length_min_same_slope_calc_resolves() {
let css = ".x { --w: min(calc(50% + 10px), calc(50% + 20px)); width: var(--w); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Calc(calc) => {
assert!(
(calc.abs.to_f32() - 7.5).abs() < 0.001,
"unexpected calc={calc:?}"
);
assert!(
(calc.percent - 0.5).abs() < 0.0001,
"unexpected calc={calc:?}"
);
assert!(calc.em.abs() < 0.0001 && calc.rem.abs() < 0.0001);
}
other => panic!("expected calc width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_var_times_percent_resolves_width_percent() {
let css = ".x { --pct: 60; width: calc(var(--pct) * 1%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Percent(value) => assert!((value - 0.6).abs() < 1.0e-6),
other => panic!("expected percent width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_percent_times_var_resolves_width_percent() {
let css = ".x { --pct: 60; width: calc(1% * var(--pct)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Percent(value) => assert!((value - 0.6).abs() < 1.0e-6),
other => panic!("expected percent width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_var_percent_fallback_resolves() {
let css = ".x { width: calc(var(--missing, 25) * 1%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Percent(value) => assert!((value - 0.25).abs() < 1.0e-6),
other => panic!("expected percent width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_var_times_number_scales_length() {
let css = ".x { --w: 20px; width: calc(var(--w) * 2); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(30.0)));
}
#[test]
fn custom_property_calc_var_plus_absolute_resolves_mixed_calc() {
let css = ".x { --base: 50%; width: calc(var(--base) + 10px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Calc(calc) => {
assert!(
(calc.percent - 0.5).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
assert!(
(calc.abs.to_f32() - 7.5).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
}
other => panic!("expected calc width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_percent_minus_var_length_resolves_mixed_calc() {
let css = ".x { --pad: 24pt; width: calc(100% - var(--pad)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
match style.width {
LengthSpec::Calc(calc) => {
assert!(
(calc.percent - 1.0).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
assert!(
(calc.abs.to_f32() + 24.0).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
}
other => panic!("expected calc width, got {other:?}"),
}
}
#[test]
fn custom_property_calc_var_plus_var_mixed_units_resolves() {
let css = ".x { --a: 1in; --b: 20px; width: calc(var(--a) + var(--b)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(87.0)));
}
#[test]
fn custom_property_calc_percent_minus_var_scaled_term_resolves() {
let css = ".x { --gutter: 10px; width: calc(100% - var(--gutter) * 2); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.custom_lengths.get("--gutter").copied(),
Some(LengthSpec::Absolute(Pt::from_f32(7.5)))
);
match style.width {
LengthSpec::Calc(calc) => {
assert!(
(calc.percent - 1.0).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
assert!(
(calc.abs.to_f32() + 15.0).abs() < 1.0e-6,
"unexpected calc={calc:?}"
);
}
other => panic!("expected calc width, got {other:?}"),
}
}
#[test]
fn custom_property_length_var_cycle_bails_out_deterministically() {
let css = ".x { --a: var(--b); --b: var(--a); width: var(--a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Auto);
}
#[test]
fn custom_property_redefinition_clears_stale_length_cache() {
let css = ".x { --w: 20px; --w: nope; width: var(--w); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Auto);
}
#[test]
fn concrete_size_overrides_prior_pending_var() {
let css = ".x { width: var(--missing, 40px); width: 10px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(7.5)));
}
#[test]
fn edge_length_var_chain_resolves_through_custom_refs() {
let css = ".x { --pad-a: var(--pad-b); --pad-b: 8px; padding-left: var(--pad-a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.padding.left, LengthSpec::Absolute(Pt::from_f32(6.0)));
}
#[test]
fn min_and_max_height_vars_resolve() {
let css = ".x { --mn: 10px; --mx: var(--missing, 20px); min-height: var(--mn); max-height: var(--mx); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.min_height, LengthSpec::Absolute(Pt::from_f32(7.5)));
assert_eq!(style.max_height, LengthSpec::Absolute(Pt::from_f32(15.0)));
}
#[test]
fn min_width_var_cycle_does_not_override_initial() {
let css = ".x { --a: var(--b); --b: var(--a); min-width: var(--a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.min_width, LengthSpec::Auto);
}
#[test]
fn gap_var_resolves_from_custom_length() {
let css = ".x { --g: 12px; gap: var(--g); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(9.0)));
assert_eq!(style.gap, LengthSpec::Absolute(Pt::from_f32(9.0)));
}
#[test]
fn row_gap_and_column_gap_remain_independent() {
let css = ".x { row-gap: 10px; column-gap: 4px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(7.5)));
assert_eq!(style.gap, LengthSpec::Absolute(Pt::from_f32(3.0)));
}
#[test]
fn two_value_gap_maps_row_then_column() {
let css = ".x { gap: 10px 14px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(7.5)));
assert_eq!(style.gap, LengthSpec::Absolute(Pt::from_f32(10.5)));
}
#[test]
fn direction_rtl_remaps_logical_inline_insets() {
let css = ".parent { direction: rtl; } .child { position: absolute; inset-inline-start: 0.25in; inset-inline-end: 0.5in; text-align: end; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.direction, DirectionMode::Rtl);
assert_eq!(style.text_align, TextAlignMode::End);
assert_eq!(style.inset_right, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(style.inset_left, LengthSpec::Absolute(Pt::from_f32(36.0)));
}
#[test]
fn text_align_justify_keywords_compute() {
let css = ".justify { text-align: justify; } .all { text-align: justify-all; } .last { text-align-last: justify; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let justify_info = element("p", None, &["justify"]);
let justify_style = resolver.compute_style(&justify_info, &root, None, &[]);
assert_eq!(justify_style.text_align, TextAlignMode::Justify);
let all_info = element("p", None, &["all"]);
let all_style = resolver.compute_style(&all_info, &root, None, &[]);
assert_eq!(all_style.text_align, TextAlignMode::JustifyAll);
let last_info = element("p", None, &["last"]);
let last_style = resolver.compute_style(&last_info, &root, None, &[]);
assert_eq!(last_style.text_align_last, TextAlignLastMode::Justify);
}
#[test]
fn text_justify_parses_keywords_inheritance_and_vars() {
let css = "body { --tj: inter-character; text-justify: inter-word; } .none { text-justify: none; } .child { text-justify: inherit; } .var { text-justify: var(--tj); } .initial { text-justify: initial; } .dist { text-justify: distribute; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.text_justify, TextJustifyMode::InterWord);
let none_info = element("p", None, &["none"]);
let none_style =
resolver.compute_style(&none_info, &body_style, None, &[body_info.clone()]);
assert_eq!(none_style.text_justify, TextJustifyMode::None);
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(child_style.text_justify, TextJustifyMode::InterWord);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.text_justify, TextJustifyMode::InterCharacter);
let initial_info = element("p", None, &["initial"]);
let initial_style =
resolver.compute_style(&initial_info, &body_style, None, &[body_info.clone()]);
assert_eq!(initial_style.text_justify, TextJustifyMode::Auto);
let dist_info = element("p", None, &["dist"]);
let dist_style = resolver.compute_style(&dist_info, &body_style, None, &[body_info]);
assert_eq!(dist_style.text_justify, TextJustifyMode::InterCharacter);
}
#[test]
fn hyphens_parses_keywords_inheritance_and_vars() {
let css = "body { --hy: auto; hyphens: manual; } .none { hyphens: none; } .child { hyphens: inherit; } .var { hyphens: var(--hy); } .initial { hyphens: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.hyphens, HyphensMode::Manual);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.hyphens, HyphensMode::Manual);
let none_info = element("p", None, &["none"]);
let none_style =
resolver.compute_style(&none_info, &body_style, None, &[body_info.clone()]);
assert_eq!(none_style.hyphens, HyphensMode::None);
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(child_style.hyphens, HyphensMode::Manual);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.hyphens, HyphensMode::Auto);
let initial_info = element("p", None, &["initial"]);
let initial_style = resolver.compute_style(&initial_info, &body_style, None, &[body_info]);
assert_eq!(initial_style.hyphens, HyphensMode::Manual);
}
#[test]
fn hyphenate_character_parses_inherits_and_resolves_vars() {
let css = r#"body { --hc: "="; hyphenate-character: "~"; } .child { hyphenate-character: inherit; } .var { hyphenate-character: var(--hc); } .initial { hyphenate-character: initial; } .webkit { -webkit-hyphenate-character: "_"; }"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.hyphenate_character.as_deref(), None);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.hyphenate_character.as_deref(), Some("~"));
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(child_style.hyphenate_character.as_deref(), Some("~"));
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.hyphenate_character.as_deref(), Some("="));
let initial_info = element("p", None, &["initial"]);
let initial_style =
resolver.compute_style(&initial_info, &body_style, None, &[body_info.clone()]);
assert_eq!(initial_style.hyphenate_character.as_deref(), None);
let webkit_info = element("p", None, &["webkit"]);
let webkit_style = resolver.compute_style(&webkit_info, &body_style, None, &[body_info]);
assert_eq!(webkit_style.hyphenate_character.as_deref(), Some("_"));
}
#[test]
fn white_space_parses_inherits_and_resolves_vars() {
let css = "body { --ws: preserve nowrap; --wsc: preserve-spaces; white-space: pre-line; } .nowrap { white-space: nowrap; } .wrap { white-space: wrap; } .collapse { white-space: collapse; } .preserve { white-space: preserve nowrap; } .longhand { white-space-collapse: var(--wsc); } .override { white-space: nowrap; text-wrap-mode: wrap; } .child { white-space: inherit; } .var { white-space: var(--ws); } .fallback { white-space: var(--missing, break-spaces nowrap); } .initial { white-space: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.white_space, WhiteSpaceMode::Normal);
assert_eq!(root.text_wrap_mode, TextWrapMode::Wrap);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.white_space, WhiteSpaceMode::PreLine);
assert_eq!(body_style.text_wrap_mode, TextWrapMode::Wrap);
let nowrap_info = element("p", None, &["nowrap"]);
let nowrap_style =
resolver.compute_style(&nowrap_info, &body_style, None, &[body_info.clone()]);
assert_eq!(nowrap_style.white_space, WhiteSpaceMode::NoWrap);
assert_eq!(nowrap_style.text_wrap_mode, TextWrapMode::NoWrap);
let wrap_info = element("p", None, &["wrap"]);
let wrap_style =
resolver.compute_style(&wrap_info, &body_style, None, &[body_info.clone()]);
assert_eq!(wrap_style.white_space, WhiteSpaceMode::Normal);
assert_eq!(wrap_style.text_wrap_mode, TextWrapMode::Wrap);
let collapse_info = element("p", None, &["collapse"]);
let collapse_style =
resolver.compute_style(&collapse_info, &body_style, None, &[body_info.clone()]);
assert_eq!(collapse_style.white_space, WhiteSpaceMode::Normal);
assert_eq!(collapse_style.text_wrap_mode, TextWrapMode::Wrap);
let preserve_info = element("p", None, &["preserve"]);
let preserve_style =
resolver.compute_style(&preserve_info, &body_style, None, &[body_info.clone()]);
assert_eq!(preserve_style.white_space, WhiteSpaceMode::Pre);
assert_eq!(preserve_style.text_wrap_mode, TextWrapMode::NoWrap);
let longhand_info = element("p", None, &["longhand"]);
let longhand_style =
resolver.compute_style(&longhand_info, &body_style, None, &[body_info.clone()]);
assert_eq!(longhand_style.white_space, WhiteSpaceMode::PreserveSpaces);
assert_eq!(longhand_style.text_wrap_mode, TextWrapMode::Wrap);
let override_info = element("p", None, &["override"]);
let override_style =
resolver.compute_style(&override_info, &body_style, None, &[body_info.clone()]);
assert_eq!(override_style.white_space, WhiteSpaceMode::NoWrap);
assert_eq!(override_style.text_wrap_mode, TextWrapMode::Wrap);
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(child_style.white_space, WhiteSpaceMode::PreLine);
assert_eq!(child_style.text_wrap_mode, TextWrapMode::Wrap);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.white_space, WhiteSpaceMode::Pre);
assert_eq!(var_style.text_wrap_mode, TextWrapMode::NoWrap);
let fallback_info = element("p", None, &["fallback"]);
let fallback_style =
resolver.compute_style(&fallback_info, &body_style, None, &[body_info.clone()]);
assert_eq!(fallback_style.white_space, WhiteSpaceMode::BreakSpaces);
assert_eq!(fallback_style.text_wrap_mode, TextWrapMode::NoWrap);
let initial_info = element("p", None, &["initial"]);
let initial_style = resolver.compute_style(&initial_info, &body_style, None, &[body_info]);
assert_eq!(initial_style.white_space, WhiteSpaceMode::Normal);
assert_eq!(initial_style.text_wrap_mode, TextWrapMode::Wrap);
}
#[test]
fn text_wrap_parses_longhands_shorthand_inheritance_and_vars() {
let css = "body { --tw: nowrap; --tws: pretty; text-wrap: balance; } .nowrap { text-wrap: nowrap; } .longhand { text-wrap-mode: nowrap; text-wrap-style: stable; } .combo { text-wrap: nowrap pretty; } .avoid { text-wrap-style: avoid-orphans; } .child { text-wrap: inherit; } .var { text-wrap: var(--tw); } .stylevar { text-wrap-style: var(--tws); } .fallback { text-wrap: var(--missing, nowrap balance); } .initial { text-wrap: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(root.text_wrap_style, TextWrapStyleMode::Auto);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(body_style.text_wrap_style, TextWrapStyleMode::Balance);
let nowrap_info = element("p", None, &["nowrap"]);
let nowrap_style =
resolver.compute_style(&nowrap_info, &body_style, None, &[body_info.clone()]);
assert_eq!(nowrap_style.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(nowrap_style.text_wrap_style, TextWrapStyleMode::Auto);
let longhand_info = element("p", None, &["longhand"]);
let longhand_style =
resolver.compute_style(&longhand_info, &body_style, None, &[body_info.clone()]);
assert_eq!(longhand_style.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(longhand_style.text_wrap_style, TextWrapStyleMode::Stable);
let combo_info = element("p", None, &["combo"]);
let combo_style =
resolver.compute_style(&combo_info, &body_style, None, &[body_info.clone()]);
assert_eq!(combo_style.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(combo_style.text_wrap_style, TextWrapStyleMode::Pretty);
let avoid_info = element("p", None, &["avoid"]);
let avoid_style =
resolver.compute_style(&avoid_info, &body_style, None, &[body_info.clone()]);
assert_eq!(avoid_style.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(avoid_style.text_wrap_style, TextWrapStyleMode::AvoidOrphans);
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(child_style.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(child_style.text_wrap_style, TextWrapStyleMode::Balance);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(var_style.text_wrap_style, TextWrapStyleMode::Auto);
let stylevar_info = element("p", None, &["stylevar"]);
let stylevar_style =
resolver.compute_style(&stylevar_info, &body_style, None, &[body_info.clone()]);
assert_eq!(stylevar_style.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(stylevar_style.text_wrap_style, TextWrapStyleMode::Pretty);
let fallback_info = element("p", None, &["fallback"]);
let fallback_style =
resolver.compute_style(&fallback_info, &body_style, None, &[body_info.clone()]);
assert_eq!(fallback_style.text_wrap_mode, TextWrapMode::NoWrap);
assert_eq!(fallback_style.text_wrap_style, TextWrapStyleMode::Balance);
let initial_info = element("p", None, &["initial"]);
let initial_style = resolver.compute_style(&initial_info, &body_style, None, &[body_info]);
assert_eq!(initial_style.text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(initial_style.text_wrap_style, TextWrapStyleMode::Auto);
}
#[test]
fn font_style_preserves_oblique_angle_and_resolves_vars() {
let css = "body { --fs: oblique -20deg; font-style: oblique 40deg; } .italic { font-style: italic; } .child { font-style: inherit; } .var { font-style: var(--fs); } .fallback { font-style: var(--missing, oblique 15deg); } .initial { font-style: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.font_style, FontStyleMode::Normal);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(
oblique_font_style_degrees(body_style.font_style),
Some(40.0)
);
let italic_info = element("p", None, &["italic"]);
let italic_style =
resolver.compute_style(&italic_info, &body_style, None, &[body_info.clone()]);
assert_eq!(italic_style.font_style, FontStyleMode::Italic);
let child_info = element("p", None, &["child"]);
let child_style =
resolver.compute_style(&child_info, &body_style, None, &[body_info.clone()]);
assert_eq!(
oblique_font_style_degrees(child_style.font_style),
Some(40.0)
);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(
oblique_font_style_degrees(var_style.font_style),
Some(-20.0)
);
let fallback_info = element("p", None, &["fallback"]);
let fallback_style =
resolver.compute_style(&fallback_info, &body_style, None, &[body_info.clone()]);
assert_eq!(
oblique_font_style_degrees(fallback_style.font_style),
Some(15.0)
);
let initial_info = element("p", None, &["initial"]);
let initial_style = resolver.compute_style(&initial_info, &body_style, None, &[body_info]);
assert_eq!(initial_style.font_style, FontStyleMode::Normal);
}
#[test]
fn font_weight_relative_keywords_resolve_from_parent() {
let css = "body { --fw: bolder; font-weight: 500; } .bolder { font-weight: bolder; } .lighter { font-weight: lighter; } .heavy { font-weight: 800; } .heavy .bolder { font-weight: bolder; } .heavy .lighter { font-weight: lighter; } .var { font-weight: var(--fw); } .fallback { font-weight: var(--missing, lighter); } .initial { font-weight: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(root.font_weight, 400);
let body_info = element("body", None, &[]);
let body_style = resolver.compute_style(&body_info, &root, None, &[]);
assert_eq!(body_style.font_weight, 500);
let bolder_info = element("p", None, &["bolder"]);
let bolder_style =
resolver.compute_style(&bolder_info, &body_style, None, &[body_info.clone()]);
assert_eq!(bolder_style.font_weight, 700);
let lighter_info = element("p", None, &["lighter"]);
let lighter_style =
resolver.compute_style(&lighter_info, &body_style, None, &[body_info.clone()]);
assert_eq!(lighter_style.font_weight, 100);
let heavy_info = element("section", None, &["heavy"]);
let heavy_style =
resolver.compute_style(&heavy_info, &body_style, None, &[body_info.clone()]);
assert_eq!(heavy_style.font_weight, 800);
let heavy_bolder_info = element("p", None, &["bolder"]);
let heavy_bolder_style = resolver.compute_style(
&heavy_bolder_info,
&heavy_style,
None,
&[body_info.clone(), heavy_info.clone()],
);
assert_eq!(heavy_bolder_style.font_weight, 900);
let heavy_lighter_info = element("p", None, &["lighter"]);
let heavy_lighter_style = resolver.compute_style(
&heavy_lighter_info,
&heavy_style,
None,
&[body_info.clone(), heavy_info],
);
assert_eq!(heavy_lighter_style.font_weight, 700);
let var_info = element("p", None, &["var"]);
let var_style = resolver.compute_style(&var_info, &body_style, None, &[body_info.clone()]);
assert_eq!(var_style.font_weight, 700);
let fallback_info = element("p", None, &["fallback"]);
let fallback_style =
resolver.compute_style(&fallback_info, &body_style, None, &[body_info.clone()]);
assert_eq!(fallback_style.font_weight, 100);
let initial_info = element("p", None, &["initial"]);
let initial_style = resolver.compute_style(&initial_info, &body_style, None, &[body_info]);
assert_eq!(initial_style.font_weight, 400);
}
#[test]
fn direction_rtl_remaps_logical_inline_margin_and_padding() {
let css = ".parent { direction: rtl; } .child { margin-inline-start: 0.25in; margin-inline-end: 0.5in; padding-inline-start: 0.125in; padding-inline-end: 0.375in; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.direction, DirectionMode::Rtl);
assert_eq!(style.margin.right, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(style.margin.left, LengthSpec::Absolute(Pt::from_f32(36.0)));
assert_eq!(style.padding.right, LengthSpec::Absolute(Pt::from_f32(9.0)));
assert_eq!(style.padding.left, LengthSpec::Absolute(Pt::from_f32(27.0)));
}
#[test]
fn writing_mode_vertical_rl_remaps_logical_margin_and_padding_edges() {
let css = ".parent { writing-mode: vertical-rl; } .child { margin-inline-start: 0.25in; margin-inline-end: 0.125in; margin-block-start: 0.375in; margin-block-end: 0.5in; padding-inline-start: 0.25in; padding-inline-end: 0.125in; padding-block-start: 0.375in; padding-block-end: 0.5in; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(style.margin.top, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(style.margin.right, LengthSpec::Absolute(Pt::from_f32(27.0)));
assert_eq!(style.margin.bottom, LengthSpec::Absolute(Pt::from_f32(9.0)));
assert_eq!(style.margin.left, LengthSpec::Absolute(Pt::from_f32(36.0)));
assert_eq!(style.padding.top, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(
style.padding.right,
LengthSpec::Absolute(Pt::from_f32(27.0))
);
assert_eq!(
style.padding.bottom,
LengthSpec::Absolute(Pt::from_f32(9.0))
);
assert_eq!(style.padding.left, LengthSpec::Absolute(Pt::from_f32(36.0)));
}
#[test]
fn writing_mode_vertical_rl_remaps_logical_size_axes() {
let css = ".parent { writing-mode: vertical-rl; } .child { inline-size: 1in; block-size: 0.5in; min-inline-size: 0.75in; max-inline-size: 0.625in; min-block-size: 0.25in; max-block-size: 0.875in; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(style.width, LengthSpec::Absolute(Pt::from_f32(36.0)));
assert_eq!(style.height, LengthSpec::Absolute(Pt::from_f32(72.0)));
assert_eq!(style.min_height, LengthSpec::Absolute(Pt::from_f32(54.0)));
assert_eq!(style.max_height, LengthSpec::Absolute(Pt::from_f32(45.0)));
assert_eq!(style.min_width, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(style.max_width, LengthSpec::Absolute(Pt::from_f32(63.0)));
}
#[test]
fn writing_mode_vertical_rl_remaps_logical_insets() {
let css = ".parent { writing-mode: vertical-rl; } .child { position: absolute; inset-inline-start: 0.25in; inset-inline-end: 0.5in; inset-block-start: 0.75in; inset-block-end: 1in; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(style.direction, DirectionMode::Ltr);
assert_eq!(style.inset_top, LengthSpec::Absolute(Pt::from_f32(18.0)));
assert_eq!(style.inset_bottom, LengthSpec::Absolute(Pt::from_f32(36.0)));
assert_eq!(style.inset_right, LengthSpec::Absolute(Pt::from_f32(54.0)));
assert_eq!(style.inset_left, LengthSpec::Absolute(Pt::from_f32(72.0)));
}
#[test]
fn logical_inset_variables_follow_vertical_rtl_axes() {
let css = ".parent { writing-mode: vertical-rl; direction: rtl; --is: 1px; --ie: 2px; --bs: 3px; --be: 4px; } .child { position: absolute; inset-inline-start: var(--is); inset-inline-end: var(--ie); inset-block-start: var(--bs); inset-block-end: var(--be); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(style.direction, DirectionMode::Rtl);
assert_eq!(style.inset_bottom, LengthSpec::Absolute(px_to_pt(1.0)));
assert_eq!(style.inset_top, LengthSpec::Absolute(px_to_pt(2.0)));
assert_eq!(style.inset_right, LengthSpec::Absolute(px_to_pt(3.0)));
assert_eq!(style.inset_left, LengthSpec::Absolute(px_to_pt(4.0)));
}
#[test]
fn row_gap_var_resolves_from_custom_length() {
let css = ".x { --rg: 14px; row-gap: var(--rg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(10.5)));
}
#[test]
fn column_gap_normal_resets_to_zero() {
let css = ".x { column-gap: normal; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.gap, LengthSpec::Absolute(Pt::ZERO));
assert!(style.column_gap_normal);
}
#[test]
fn flex_shorthands_set_flow_and_item_components() {
let css = ".x { flex-flow: column wrap; } \
.triple { flex: 1 1 0; } \
.grow { flex: 2; } \
.basis { flex: 24px; } \
.auto { flex: auto; } \
.none { flex: none; } \
.invalid { flex: 2 3 4; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.flex_direction, FlexDirectionMode::Column);
assert_eq!(style.flex_wrap, FlexWrapMode::Wrap);
let triple = resolver.compute_style(&element("div", None, &["triple"]), &root, None, &[]);
assert_eq!(triple.flex_grow, 1.0);
assert_eq!(triple.flex_shrink, 1.0);
assert_eq!(triple.flex_basis, LengthSpec::Absolute(Pt::ZERO));
let grow = resolver.compute_style(&element("div", None, &["grow"]), &root, None, &[]);
assert_eq!(grow.flex_grow, 2.0);
assert_eq!(grow.flex_shrink, 1.0);
assert_eq!(grow.flex_basis, LengthSpec::Percent(0.0));
let basis = resolver.compute_style(&element("div", None, &["basis"]), &root, None, &[]);
assert_eq!(basis.flex_grow, 1.0);
assert_eq!(basis.flex_shrink, 1.0);
assert_eq!(basis.flex_basis, LengthSpec::Absolute(Pt::from_f32(18.0)));
let auto = resolver.compute_style(&element("div", None, &["auto"]), &root, None, &[]);
assert_eq!(auto.flex_grow, 1.0);
assert_eq!(auto.flex_shrink, 1.0);
assert_eq!(auto.flex_basis, LengthSpec::Auto);
let none = resolver.compute_style(&element("div", None, &["none"]), &root, None, &[]);
assert_eq!(none.flex_grow, 0.0);
assert_eq!(none.flex_shrink, 0.0);
assert_eq!(none.flex_basis, LengthSpec::Auto);
let invalid = resolver.compute_style(&element("div", None, &["invalid"]), &root, None, &[]);
assert_eq!(invalid.flex_grow, 0.0);
assert_eq!(invalid.flex_shrink, 1.0);
assert_eq!(invalid.flex_basis, LengthSpec::Auto);
}
#[test]
fn intrinsic_flex_basis_keywords_remain_distinct() {
let css = ".content { flex: 0 0 content; } \
.min { flex-basis: min-content; } \
.max { flex-basis: max-content; } \
.fit { flex-basis: fit-content; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let basis = |class| {
resolver
.compute_style(&element("div", None, &[class]), &root, None, &[])
.flex_basis
};
assert_eq!(basis("content"), LengthSpec::Content);
assert_eq!(basis("min"), LengthSpec::MinContent);
assert_eq!(basis("max"), LengthSpec::MaxContent);
assert_eq!(basis("fit"), LengthSpec::FitContent);
}
#[test]
fn intrinsic_box_size_keywords_remain_distinct() {
let css = ".min { width: min-content; } \
.max { width: max-content; } \
.fit { width: fit-content; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let width = |class| {
resolver
.compute_style(&element("div", None, &[class]), &root, None, &[])
.width
};
assert_eq!(width("min"), LengthSpec::MinContent);
assert_eq!(width("max"), LengthSpec::MaxContent);
assert_eq!(width("fit"), LengthSpec::FitContent);
}
#[test]
fn flex_flow_shorthand_defaults_missing_component() {
let css = ".a { flex-flow: wrap; } .b { flex-flow: column; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let a_info = element("div", None, &["a"]);
let a_style = resolver.compute_style(&a_info, &root, None, &[]);
assert_eq!(a_style.flex_direction, FlexDirectionMode::Row);
assert_eq!(a_style.flex_wrap, FlexWrapMode::Wrap);
let b_info = element("div", None, &["b"]);
let b_style = resolver.compute_style(&b_info, &root, None, &[]);
assert_eq!(b_style.flex_direction, FlexDirectionMode::Column);
assert_eq!(b_style.flex_wrap, FlexWrapMode::NoWrap);
}
#[test]
fn flex_reverse_keywords_remain_distinct_computed_values() {
let css = ".row { flex-direction: row-reverse; } \
.column { flex-direction: column-reverse; } \
.flow { flex-flow: row-reverse wrap-reverse; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let row = resolver.compute_style(&element("div", None, &["row"]), &root, None, &[]);
assert_eq!(row.flex_direction, FlexDirectionMode::RowReverse);
let column = resolver.compute_style(&element("div", None, &["column"]), &root, None, &[]);
assert_eq!(column.flex_direction, FlexDirectionMode::ColumnReverse);
let flow = resolver.compute_style(&element("div", None, &["flow"]), &root, None, &[]);
assert_eq!(flow.flex_direction, FlexDirectionMode::RowReverse);
assert_eq!(flow.flex_wrap, FlexWrapMode::WrapReverse);
}
#[test]
fn align_content_property_resolves() {
let css = ".x { align-content: flex-end; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_content, AlignContentMode::FlexEnd);
}
#[test]
fn align_content_distribution_keyword_resolves() {
let css = ".x { align-content: space-evenly; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_content, AlignContentMode::SpaceEvenly);
}
#[test]
fn align_self_property_resolves() {
let css = ".x { align-self: flex-end; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_self, AlignSelfMode::FlexEnd);
}
#[test]
fn flex_baseline_alignment_keywords_preserve_first_and_last() {
let css = ".first { align-items: baseline; align-self: first baseline; }\
.last { align-items: last baseline; align-self: last baseline; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let first = resolver.compute_style(&element("div", None, &["first"]), &root, None, &[]);
let last = resolver.compute_style(&element("div", None, &["last"]), &root, None, &[]);
assert_eq!(first.align_items, AlignItemsMode::FirstBaseline);
assert_eq!(first.align_self, AlignSelfMode::FirstBaseline);
assert_eq!(last.align_items, AlignItemsMode::LastBaseline);
assert_eq!(last.align_self, AlignSelfMode::LastBaseline);
}
#[test]
fn aspect_ratio_parses_ratios_auto_and_inherit() {
assert_eq!(
parse_aspect_ratio_spec("2 / 1"),
Some(AspectRatioSpec::Ratio(2.0))
);
let css = ".parent { aspect-ratio: 2 / 1; }\
.inherit { aspect-ratio: inherit; }\
.auto { aspect-ratio: auto; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent = resolver.compute_style(&element("div", None, &["parent"]), &root, None, &[]);
let inherited =
resolver.compute_style(&element("div", None, &["inherit"]), &parent, None, &[]);
let auto = resolver.compute_style(&element("div", None, &["auto"]), &parent, None, &[]);
assert_eq!(parent.aspect_ratio, Some(2.0));
assert_eq!(inherited.aspect_ratio, Some(2.0));
assert_eq!(auto.aspect_ratio, None);
}
#[test]
fn flex_container_and_item_properties_do_not_inherit_to_children() {
let css = ".parent { flex-direction: column; flex-wrap: wrap; flex-basis: 30px;\
order: 4; justify-content: space-between; align-items: center;\
align-content: flex-end; gap: 10px; flex-grow: 3; flex-shrink: 0; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent = resolver.compute_style(&element("div", None, &["parent"]), &root, None, &[]);
let child = resolver.compute_style(&element("div", None, &[]), &parent, None, &[]);
assert_eq!(child.flex_direction, FlexDirectionMode::Row);
assert_eq!(child.flex_wrap, FlexWrapMode::NoWrap);
assert_eq!(child.flex_basis, LengthSpec::Auto);
assert_eq!(child.order, 0);
assert_eq!(child.justify_content, JustifyContentMode::FlexStart);
assert_eq!(child.align_items, AlignItemsMode::Stretch);
assert_eq!(child.align_content, AlignContentMode::Stretch);
assert_eq!(child.row_gap, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(child.gap, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(child.flex_grow, 0.0);
assert_eq!(child.flex_shrink, 1.0);
}
#[test]
fn place_items_shorthand_sets_align_items() {
let css = ".x { place-items: flex-end center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_items, AlignItemsMode::FlexEnd);
assert_eq!(style.justify_items, AlignItemsMode::Center);
}
#[test]
fn place_items_single_value_sets_align_items() {
let css = ".x { place-items: center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_items, AlignItemsMode::Center);
assert_eq!(style.justify_items, AlignItemsMode::Center);
}
#[test]
fn align_self_auto_is_default() {
let css = ".x { align-self: auto; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_self, AlignSelfMode::Auto);
}
#[test]
fn place_self_shorthand_sets_align_self() {
let css = ".x { place-self: flex-end center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_self, AlignSelfMode::FlexEnd);
assert_eq!(style.justify_self, AlignSelfMode::Center);
}
#[test]
fn place_self_single_value_sets_align_self() {
let css = ".x { place-self: center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_self, AlignSelfMode::Center);
}
#[test]
fn justify_content_distribution_keywords_resolve() {
let css = ".a { justify-content: space-around; } .b { justify-content: space-evenly; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let a_info = element("div", None, &["a"]);
let a_style = resolver.compute_style(&a_info, &root, None, &[]);
assert_eq!(a_style.justify_content, JustifyContentMode::SpaceAround);
let b_info = element("div", None, &["b"]);
let b_style = resolver.compute_style(&b_info, &root, None, &[]);
assert_eq!(b_style.justify_content, JustifyContentMode::SpaceEvenly);
}
#[test]
fn justify_content_preserves_safe_center_overflow_alignment() {
let css = ".safe { justify-content: safe center; }\
.unsafe { justify-content: unsafe center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let safe = resolver.compute_style(&element("div", None, &["safe"]), &root, None, &[]);
let unsafe_style =
resolver.compute_style(&element("div", None, &["unsafe"]), &root, None, &[]);
assert_eq!(safe.justify_content, JustifyContentMode::SafeCenter);
assert_eq!(unsafe_style.justify_content, JustifyContentMode::Center);
}
#[test]
fn place_content_shorthand_sets_align_and_justify() {
let css = ".x { place-content: flex-end center; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_content, AlignContentMode::FlexEnd);
assert_eq!(style.justify_content, JustifyContentMode::Center);
}
#[test]
fn place_content_single_value_copies_distribution_keyword() {
let css = ".x { place-content: space-between; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.align_content, AlignContentMode::SpaceBetween);
assert_eq!(style.justify_content, JustifyContentMode::SpaceBetween);
}
#[test]
fn flex_basis_var_fallback_resolves() {
let css = ".x { flex-basis: var(--missing, 24px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.flex_basis, LengthSpec::Absolute(Pt::from_f32(18.0)));
}
#[test]
fn inset_var_resolves_from_custom_length() {
let css = ".x { --l: 8px; left: var(--l); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.inset_left, LengthSpec::Absolute(Pt::from_f32(6.0)));
}
#[test]
fn inset_shorthand_sets_all_sides() {
let css = ".x { inset: 16px 8px 4px 2px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.inset_top, LengthSpec::Absolute(Pt::from_f32(12.0)));
assert_eq!(style.inset_right, LengthSpec::Absolute(Pt::from_f32(6.0)));
assert_eq!(style.inset_bottom, LengthSpec::Absolute(Pt::from_f32(3.0)));
assert_eq!(style.inset_left, LengthSpec::Absolute(Pt::from_f32(1.5)));
}
#[test]
fn inset_shorthand_single_value_sets_uniform_sides() {
let css = ".x { inset: 12px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let expected = LengthSpec::Absolute(Pt::from_f32(9.0));
assert_eq!(style.inset_top, expected);
assert_eq!(style.inset_right, expected);
assert_eq!(style.inset_bottom, expected);
assert_eq!(style.inset_left, expected);
}
#[test]
fn position_fixed_maps_to_fixed_mode() {
let css = ".x { position: fixed; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.position, PositionMode::Fixed);
}
#[test]
fn running_position_and_string_set_compile_as_non_inherited_page_state() {
let css = ".running { position: running(Head); string-set: section attr(data-title); } \
.reset { position: running(old); position: static; string-set: none; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let running = resolver.compute_style(&element("h2", None, &["running"]), &root, None, &[]);
assert_eq!(running.position, PositionMode::Static);
assert_eq!(running.running_name.as_deref(), Some("Head"));
assert_eq!(
running.string_set,
vec![NamedStringAssignment {
name: "section".to_string(),
source: NamedStringSource::Attribute("data-title".to_string()),
}]
);
let child = resolver.compute_style(&element("span", None, &[]), &running, None, &[]);
assert!(child.running_name.is_none());
assert!(child.string_set.is_empty());
let reset = resolver.compute_style(&element("h2", None, &["reset"]), &root, None, &[]);
assert!(reset.running_name.is_none());
assert!(reset.string_set.is_empty());
}
#[test]
fn string_set_accepts_content_text_alias_used_by_paged_templates() {
let resolver = StyleResolver::new(".running { string-set: document-title content(text); }");
let root = resolver.default_style();
let running = resolver.compute_style(&element("h1", None, &["running"]), &root, None, &[]);
assert_eq!(
running.string_set,
vec![NamedStringAssignment {
name: "document-title".to_string(),
source: NamedStringSource::Content,
}]
);
}
#[test]
fn float_and_clear_modes_are_computed() {
let css = ".left { float: left; } .right { float: inline-end; clear: both; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let left = resolver.compute_style(&element("div", None, &["left"]), &root, None, &[]);
assert_eq!(left.float_mode, FloatMode::Left);
assert_eq!(left.clear_mode, ClearMode::None);
let right = resolver.compute_style(&element("div", None, &["right"]), &root, None, &[]);
assert_eq!(right.float_mode, FloatMode::Right);
assert_eq!(right.clear_mode, ClearMode::Both);
}
#[test]
fn footnote_float_policy_display_and_pseudos_compile() {
let css = ".fn { float: footnote; footnote-policy: line; footnote-display: inline; } \
.fn::footnote-call { content: '[' counter(footnote) ']'; color: #ef476f; } \
.fn::footnote-marker { content: 'N' counter(footnote) ': '; color: #1d4ed8; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("span", None, &["fn"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.float_mode, FloatMode::Footnote);
assert_eq!(style.footnote_policy, FootnotePolicy::Line);
assert_eq!(style.footnote_display, FootnoteDisplay::Inline);
let call = resolver
.compute_pseudo_style(&info, &style, &[], PseudoTarget::FootnoteCall)
.expect("footnote call style");
let marker = resolver
.compute_pseudo_style(&info, &style, &[], PseudoTarget::FootnoteMarker)
.expect("footnote marker style");
assert_eq!(
call.color,
Color::rgb(239.0 / 255.0, 71.0 / 255.0, 111.0 / 255.0)
);
assert_eq!(
marker.color,
Color::rgb(29.0 / 255.0, 78.0 / 255.0, 216.0 / 255.0)
);
assert!(call.generated_content.is_some());
assert!(marker.generated_content.is_some());
}
#[test]
fn concrete_inset_overrides_prior_pending_var() {
let css = ".x { left: var(--missing, 40px); left: 5px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.inset_left, LengthSpec::Absolute(Pt::from_f32(3.75)));
}
#[test]
fn concrete_gap_overrides_prior_pending_var() {
let css = ".x { gap: var(--missing, 40px); gap: 5px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(3.75)));
assert_eq!(style.gap, LengthSpec::Absolute(Pt::from_f32(3.75)));
}
#[test]
fn concrete_row_gap_overrides_prior_pending_var() {
let css = ".x { row-gap: var(--missing, 40px); row-gap: 5px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.row_gap, LengthSpec::Absolute(Pt::from_f32(3.75)));
}
#[test]
fn concrete_flex_basis_overrides_prior_pending_var() {
let css = ".x { flex-basis: var(--missing, 40px); flex-basis: 5px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.flex_basis, LengthSpec::Absolute(Pt::from_f32(3.75)));
}
#[test]
fn font_family_var_chain_resolves_custom_stack() {
let css = ".x { --ff-a: var(--ff-b); --ff-b: monospace; font-family: var(--ff-a); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.font_name.as_ref(), "DejaVu Sans Mono");
}
#[test]
fn font_face_alias_lowers_to_the_first_registered_source() {
let font_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("python/fullbleed_assets/fonts/Inter-Variable.ttf");
let mut registry = FontRegistry::new();
registry.register_file(&font_path);
let expected = registry
.resolve("Inter-Variable")
.expect("registered Inter fixture")
.name
.clone();
let css = r#"
@font-face {
font-family: "Matrix Sans";
src: local("Unavailable Matrix"),
url("../fonts/Inter-Variable.ttf") format("truetype");
font-style: normal;
font-weight: 400;
}
.x { font-family: "Matrix Sans", serif; font-style: oblique 20deg; }
"#;
let resolver = StyleResolver::new_with_debug_viewport_and_fonts(
css,
None,
None,
Some(Arc::new(registry)),
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.font_name.as_ref(), expected);
assert_eq!(style.font_stack[0].as_ref(), expected);
assert_eq!(style.font_style, FontStyleMode::Oblique(2000));
assert!(
style
.font_stack
.iter()
.any(|family| family.as_ref() == "Liberation Serif")
);
assert!(style.font_face_satisfies_weight);
assert!(!style.font_face_satisfies_style);
}
#[test]
fn font_face_alias_selects_matching_weight_and_style_descriptors() {
let css = r#"
@font-face {
font-family: VariantPick;
src: url("RegularFace.ttf");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: VariantPick;
src: url("AuthoredBoldItalic.ttf");
font-style: italic;
font-weight: 700;
}
.normal { font-family: VariantPick; font-style: normal; font-weight: 400; }
.variant { font-family: VariantPick; font-style: italic; font-weight: 700; }
.parent { font-family: VariantPick; }
.child { font-style: italic; font-weight: 700; }
"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let normal = resolver.compute_style(&element("div", None, &["normal"]), &root, None, &[]);
let variant = resolver.compute_style(&element("div", None, &["variant"]), &root, None, &[]);
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let inherited_variant = resolver.compute_style(
&element("span", None, &["child"]),
&parent,
None,
&[parent_info],
);
assert_eq!(normal.font_name.as_ref(), "RegularFace");
assert!(normal.font_face_satisfies_weight);
assert!(normal.font_face_satisfies_style);
assert_eq!(variant.font_name.as_ref(), "AuthoredBoldItalic");
assert!(variant.to_text_style().font_face_satisfies_weight);
assert!(variant.to_text_style().font_face_satisfies_style);
assert_eq!(inherited_variant.font_name.as_ref(), "AuthoredBoldItalic");
}
#[test]
fn font_face_size_adjust_scales_glyphs_without_changing_authored_line_height() {
let css = r#"
@font-face {
font-family: AdjustedPick;
src: url("AdjustedSource.ttf");
size-adjust: 150%;
}
.x {
font-family: AdjustedPick;
font-size: 20px;
line-height: 48px;
}
"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
let text_style = style.to_text_style();
assert_eq!(style.font_size, Pt::from_f32(15.0));
assert_eq!(text_style.font_size, Pt::from_f32(22.5));
assert_eq!(text_style.line_height, Pt::from_f32(36.0));
}
#[test]
fn font_face_unicode_ranges_compile_as_a_composite_font_stack() {
let css = r#"
@font-face {
font-family: RangePick;
src: url("SerifForA.ttf");
unicode-range: U+0041;
}
@font-face {
font-family: RangePick;
src: url("SansForB.ttf");
unicode-range: U+0042, U+4??;
}
.x { font-family: RangePick, monospace; }
"#;
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.font_stack[0].as_ref(), "SerifForA");
assert_eq!(style.font_stack[1].as_ref(), "SansForB");
assert_eq!(style.font_face_source_count, 2);
assert_eq!(style.font_unicode_ranges.len(), style.font_stack.len());
assert_eq!(
style.font_unicode_ranges[0].as_deref(),
Some(&[(0x41, 0x41)][..])
);
assert_eq!(
style.font_unicode_ranges[1].as_deref(),
Some(&[(0x42, 0x42), (0x400, 0x4ff)][..])
);
assert!(style.font_unicode_ranges[2].is_none());
assert_eq!(
style.to_text_style().font_unicode_ranges,
style.font_unicode_ranges
);
}
#[test]
fn font_family_var_fallback_resolves_when_missing() {
let css = ".x { font-family: var(--missing, monospace); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.font_name.as_ref(), "DejaVu Sans Mono");
}
#[test]
fn font_family_var_cycle_uses_fallback() {
let css =
".x { --ff-a: var(--ff-b); --ff-b: var(--ff-a); font-family: var(--ff-a, serif); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.font_name.as_ref(), "Liberation Serif");
}
#[test]
fn font_family_preserves_quoted_generic_names_in_vars() {
let css = ".quoted { font-family: var(--missing, \"serif\"); } .unquoted { font-family: var(--missing, serif); } .custom-quoted { --ff: \"monospace\"; font-family: var(--ff); } .custom-unquoted { --ff: monospace; font-family: var(--ff); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let quoted = resolver.compute_style(&element("div", None, &["quoted"]), &root, None, &[]);
let unquoted =
resolver.compute_style(&element("div", None, &["unquoted"]), &root, None, &[]);
let custom_quoted =
resolver.compute_style(&element("div", None, &["custom-quoted"]), &root, None, &[]);
let custom_unquoted = resolver.compute_style(
&element("div", None, &["custom-unquoted"]),
&root,
None,
&[],
);
assert_eq!(quoted.font_name.as_ref(), "serif");
assert_eq!(unquoted.font_name.as_ref(), "Liberation Serif");
assert_eq!(custom_quoted.font_name.as_ref(), "monospace");
assert_eq!(custom_unquoted.font_name.as_ref(), "DejaVu Sans Mono");
}
#[test]
fn font_family_var_declaration_overrides_prior_concrete_value() {
let css = ".parent { font-family: serif; } .child { font-family: Courier; font-family: var(--missing, inherit); } .fallback { font-family: Courier; font-family: var(--missing, serif); } .initial { font-family: Courier; font-family: var(--missing, initial); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let child = resolver.compute_style(
&element("span", None, &["child"]),
&parent,
None,
&[parent_info.clone()],
);
let fallback =
resolver.compute_style(&element("div", None, &["fallback"]), &root, None, &[]);
let initial = resolver.compute_style(&element("div", None, &["initial"]), &root, None, &[]);
assert_eq!(child.font_name.as_ref(), "Liberation Serif");
assert_eq!(fallback.font_name.as_ref(), "Liberation Serif");
assert_eq!(initial.font_name.as_ref(), "Helvetica");
}
#[test]
fn font_shorthand_sets_and_resets_core_longhands() {
let css = "body { font-size: 20px; line-height: 2; font-weight: 700; font-style: italic; font-family: Courier; } .full { font: italic bold 16px/1.5 serif; } .reset { font: 16px serif; } .system { font: caption; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let body_info = element("body", None, &[]);
let body = resolver.compute_style(&body_info, &root, None, &[]);
let ancestors = [body_info];
let full = resolver.compute_style(&element("p", None, &["full"]), &body, None, &ancestors);
assert!((full.font_size.to_f32() - 12.0).abs() < 0.01);
assert!((full.line_height.to_line_height(full.font_size).to_f32() - 18.0).abs() < 0.01);
assert_eq!(full.font_weight, 700);
assert_eq!(full.font_style, FontStyleMode::Italic);
assert_eq!(full.font_name.as_ref(), "Liberation Serif");
let reset =
resolver.compute_style(&element("p", None, &["reset"]), &body, None, &ancestors);
assert!((reset.font_size.to_f32() - 12.0).abs() < 0.01);
assert!((reset.line_height.to_line_height(reset.font_size).to_f32() - 14.4).abs() < 0.01);
assert_eq!(reset.font_weight, 400);
assert_eq!(reset.font_style, FontStyleMode::Normal);
assert_eq!(reset.font_name.as_ref(), "Liberation Serif");
let system =
resolver.compute_style(&element("p", None, &["system"]), &body, None, &ancestors);
assert!((system.font_size.to_f32() - 12.0).abs() < 0.01);
assert!((system.line_height.to_line_height(system.font_size).to_f32() - 14.4).abs() < 0.01);
assert_eq!(system.font_weight, 400);
assert_eq!(system.font_style, FontStyleMode::Normal);
assert_eq!(system.font_name.as_ref(), "Helvetica");
}
#[test]
fn font_shorthand_preserves_oblique_angle_and_quoted_family() {
let css = ".x { font: oblique 20deg 600 18px/120% \"serif\", sans-serif; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("p", None, &["x"]), &root, None, &[]);
assert!((style.font_size.to_f32() - 13.5).abs() < 0.01);
assert!((style.line_height.to_line_height(style.font_size).to_f32() - 16.2).abs() < 0.01);
assert_eq!(style.font_weight, 600);
assert_eq!(oblique_font_style_degrees(style.font_style), Some(20.0));
assert_eq!(style.font_name.as_ref(), "serif");
}
#[test]
fn font_shorthand_var_resolves_custom_fallback_cycle_and_overrides() {
let css = ".direct { --font: italic 600 18px/120% \"serif\", sans-serif; font: var(--font); } .fallback { font: var(--missing, bold 16px/1.5 serif); } .cycle { --a: var(--b); --b: var(--a); font: var(--a, oblique 10deg 16px serif); } .override { --font: italic 700 20px/2 serif; font: var(--font); font-size: 12px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let direct = resolver.compute_style(&element("p", None, &["direct"]), &root, None, &[]);
assert!(
(direct.font_size.to_f32() - 13.5).abs() < 0.01,
"direct font_size={}",
direct.font_size.to_f32()
);
assert!((direct.line_height.to_line_height(direct.font_size).to_f32() - 16.2).abs() < 0.01);
assert_eq!(direct.font_weight, 600);
assert_eq!(direct.font_style, FontStyleMode::Italic);
assert_eq!(direct.font_name.as_ref(), "serif");
let fallback = resolver.compute_style(&element("p", None, &["fallback"]), &root, None, &[]);
assert!((fallback.font_size.to_f32() - 12.0).abs() < 0.01);
assert!(
(fallback
.line_height
.to_line_height(fallback.font_size)
.to_f32()
- 18.0)
.abs()
< 0.01
);
assert_eq!(fallback.font_weight, 700);
assert_eq!(fallback.font_style, FontStyleMode::Normal);
assert_eq!(fallback.font_name.as_ref(), "Liberation Serif");
let cycle = resolver.compute_style(&element("p", None, &["cycle"]), &root, None, &[]);
assert!((cycle.font_size.to_f32() - 12.0).abs() < 0.01);
assert_eq!(cycle.font_weight, 400);
assert_eq!(oblique_font_style_degrees(cycle.font_style), Some(10.0));
assert_eq!(cycle.font_name.as_ref(), "Liberation Serif");
let override_style =
resolver.compute_style(&element("p", None, &["override"]), &root, None, &[]);
assert!((override_style.font_size.to_f32() - 9.0).abs() < 0.01);
assert!(
(override_style
.line_height
.to_line_height(override_style.font_size)
.to_f32()
- 18.0)
.abs()
< 0.01
);
assert_eq!(override_style.font_weight, 700);
assert_eq!(override_style.font_style, FontStyleMode::Italic);
assert_eq!(override_style.font_name.as_ref(), "Liberation Serif");
}
#[test]
fn vertical_align_keywords_and_vars_resolve() {
let css = ".baseline { vertical-align: baseline; } .sub { vertical-align: sub; } .super { vertical-align: super; } .texttop { vertical-align: text-top; } .middle { --va: middle; vertical-align: var(--va); } .textbottom { vertical-align: var(--missing, text-bottom); } .length { vertical-align: 18px; } .percent { vertical-align: 50%; } .varlength { --va: 1.5em; vertical-align: var(--va); } .initial { vertical-align: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
assert_eq!(
resolver
.compute_style(&element("span", None, &["baseline"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Baseline
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["sub"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Sub
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["super"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Super
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["texttop"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::TextTop
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["middle"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Middle
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["textbottom"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::TextBottom
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["length"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Length(LengthSpec::Absolute(Pt::from_f32(13.5)))
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["percent"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Length(LengthSpec::Percent(0.5))
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["varlength"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Length(LengthSpec::Em(1.5))
);
assert_eq!(
resolver
.compute_style(&element("span", None, &["initial"]), &root, None, &[])
.vertical_align,
VerticalAlignMode::Baseline
);
}
#[test]
fn vertical_align_uses_its_non_inherited_initial_value() {
let css = ".parent { vertical-align: top; } \
.inherited { vertical-align: inherit; } \
.parent::before { content: 'x'; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(parent_style.vertical_align, VerticalAlignMode::Top);
let plain = resolver.compute_style(
&element("span", None, &["plain"]),
&parent_style,
None,
std::slice::from_ref(&parent_info),
);
assert_eq!(plain.vertical_align, VerticalAlignMode::Baseline);
let inherited = resolver.compute_style(
&element("span", None, &["inherited"]),
&parent_style,
None,
std::slice::from_ref(&parent_info),
);
assert_eq!(inherited.vertical_align, VerticalAlignMode::Top);
let before = resolver
.compute_pseudo_style(&parent_info, &parent_style, &[], PseudoTarget::Before)
.expect("generated before style");
assert_eq!(before.vertical_align, VerticalAlignMode::Baseline);
}
#[test]
fn text_decoration_parses_and_resets() {
let css = "div { text-decoration: underline; } span { text-decoration: none; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let div_info = element("div", None, &[]);
let div_style = resolver.compute_style(&div_info, &root, None, &[]);
assert!(div_style.text_decoration.underline);
assert!(!div_style.text_decoration.line_through);
let span_info = element("span", None, &[]);
let span_style = resolver.compute_style(&span_info, &div_style, None, &[div_info]);
assert!(span_style.text_decoration.is_none());
}
#[test]
fn active_text_decoration_keeps_its_paint_across_descendant_runs() {
let css = ".parent { text-decoration: underline solid rgb(239, 71, 111) 2px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_style = resolver.compute_style(
&element("span", None, &["child"]),
&parent_style,
None,
&[parent_info],
);
assert!(!parent_style.text_decoration_propagated);
assert!(child_style.text_decoration_propagated);
assert!(child_style.text_decoration.underline);
assert_eq!(
child_style.text_decoration_color,
Color::rgb(239.0 / 255.0, 71.0 / 255.0, 111.0 / 255.0)
);
assert_eq!(
child_style.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(1.5)))
);
assert_eq!(
child_style.text_decoration_style,
TextDecorationStyleMode::Solid
);
}
#[test]
fn text_decoration_color_tracks_current_color_initial() {
let css = ".parent { color: rgb(255, 0, 0); text-decoration-color: red; } .child { color: rgb(0, 0, 255); text-decoration-line: underline; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("span", None, &["child"]);
let child_style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert!(child_style.text_decoration.underline);
assert_eq!(child_style.color, Color::rgb(0.0, 0.0, 1.0));
assert_eq!(child_style.text_decoration_color, Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn text_decoration_color_parses_longhand_shorthand_and_vars() {
let css = ".longhand { text-decoration-line: underline; text-decoration-color: rgb(255, 0, 0); } .shorthand { text-decoration: underline rgb(0, 128, 0); } .reset { color: rgb(0, 0, 255); text-decoration-color: red; text-decoration: underline; } .var { --dec: rgb(160, 0, 210); text-decoration-line: underline; text-decoration-color: var(--dec); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let longhand =
resolver.compute_style(&element("div", None, &["longhand"]), &root, None, &[]);
assert_eq!(longhand.text_decoration_color, Color::rgb(1.0, 0.0, 0.0));
let shorthand =
resolver.compute_style(&element("div", None, &["shorthand"]), &root, None, &[]);
assert_eq!(
shorthand.text_decoration_color,
Color::rgb(0.0, 128.0 / 255.0, 0.0)
);
let reset = resolver.compute_style(&element("div", None, &["reset"]), &root, None, &[]);
assert_eq!(reset.text_decoration_color, Color::rgb(0.0, 0.0, 1.0));
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(
var.text_decoration_color,
Color::rgb(160.0 / 255.0, 0.0, 210.0 / 255.0)
);
}
#[test]
fn text_decoration_thickness_parses_shorthand_and_resets() {
let css = ".longhand { text-decoration-thickness: 8pt; } .shorthand { text-decoration: underline 25% red; } .reset { text-decoration-thickness: 8pt; text-decoration: underline; } .from-font { text-decoration-thickness: from-font; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let longhand =
resolver.compute_style(&element("div", None, &["longhand"]), &root, None, &[]);
assert_eq!(
longhand.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(8.0)))
);
let shorthand =
resolver.compute_style(&element("div", None, &["shorthand"]), &root, None, &[]);
assert_eq!(
shorthand.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Percent(0.25))
);
let reset = resolver.compute_style(&element("div", None, &["reset"]), &root, None, &[]);
assert_eq!(
reset.text_decoration_thickness,
TextDecorationThicknessMode::Auto
);
let from_font =
resolver.compute_style(&element("div", None, &["from-font"]), &root, None, &[]);
assert_eq!(
from_font.text_decoration_thickness,
TextDecorationThicknessMode::FromFont
);
}
#[test]
fn text_decoration_thickness_inherits_percent_and_resolves_vars() {
let css = ".parent { text-decoration-thickness: 20%; } .child { text-decoration-thickness: inherit; } .var { --thick: 6pt; text-decoration-thickness: var(--thick); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child = resolver.compute_style(
&element("span", None, &["child"]),
&parent_style,
None,
&[parent_info],
);
assert_eq!(
child.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Percent(0.2))
);
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(
var.text_decoration_thickness,
TextDecorationThicknessMode::Length(LengthSpec::Absolute(Pt::from_f32(6.0)))
);
}
#[test]
fn text_decoration_style_parses_shorthand_and_resets() {
let css = ".longhand { text-decoration-style: dashed; } .shorthand { text-decoration: underline wavy red 4pt; } .reset { text-decoration-style: dotted; text-decoration: underline; } .double { text-decoration-style: double; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let longhand =
resolver.compute_style(&element("div", None, &["longhand"]), &root, None, &[]);
assert_eq!(
longhand.text_decoration_style,
TextDecorationStyleMode::Dashed
);
let shorthand =
resolver.compute_style(&element("div", None, &["shorthand"]), &root, None, &[]);
assert!(shorthand.text_decoration.underline);
assert_eq!(
shorthand.text_decoration_style,
TextDecorationStyleMode::Wavy
);
let reset = resolver.compute_style(&element("div", None, &["reset"]), &root, None, &[]);
assert!(reset.text_decoration.underline);
assert_eq!(reset.text_decoration_style, TextDecorationStyleMode::Solid);
let double = resolver.compute_style(&element("div", None, &["double"]), &root, None, &[]);
assert_eq!(
double.text_decoration_style,
TextDecorationStyleMode::Double
);
}
#[test]
fn text_decoration_style_is_non_inherited_but_can_inherit() {
let css = ".parent { text-decoration-style: double; } .child { text-decoration-style: inherit; } .initial { text-decoration-style: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let implicit_child = resolver.compute_style(
&element("span", None, &[]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
implicit_child.text_decoration_style,
TextDecorationStyleMode::Solid
);
let inherited_child = resolver.compute_style(
&element("span", None, &["child"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
inherited_child.text_decoration_style,
TextDecorationStyleMode::Double
);
let initial_child = resolver.compute_style(
&element("span", None, &["initial"]),
&parent_style,
None,
&[parent_info],
);
assert_eq!(
initial_child.text_decoration_style,
TextDecorationStyleMode::Solid
);
}
#[test]
fn text_decoration_skip_ink_parses_and_inherits() {
let css = ".parent { text-decoration-skip-ink: none; } .auto { text-decoration-skip-ink: auto; } .all { text-decoration-skip-ink: all; } .initial { text-decoration-skip-ink: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(
parent.text_decoration_skip_ink,
TextDecorationSkipInkMode::None
);
let inherited = resolver.compute_style(
&element("span", None, &[]),
&parent,
None,
&[parent_info.clone()],
);
assert_eq!(
inherited.text_decoration_skip_ink,
TextDecorationSkipInkMode::None
);
let auto = resolver.compute_style(
&element("span", None, &["auto"]),
&parent,
None,
&[parent_info.clone()],
);
assert_eq!(
auto.text_decoration_skip_ink,
TextDecorationSkipInkMode::Auto
);
let all = resolver.compute_style(
&element("span", None, &["all"]),
&parent,
None,
&[parent_info.clone()],
);
assert_eq!(all.text_decoration_skip_ink, TextDecorationSkipInkMode::All);
let initial = resolver.compute_style(
&element("span", None, &["initial"]),
&parent,
None,
&[parent_info],
);
assert_eq!(
initial.text_decoration_skip_ink,
TextDecorationSkipInkMode::Auto
);
}
#[test]
fn text_underline_offset_parses_lengths_percentages_and_vars() {
let css = ".length { text-underline-offset: 6pt; } .percent { text-underline-offset: 20%; } .negative { text-underline-offset: -0.5em; } .var { --off: 4pt; text-underline-offset: var(--off); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let length = resolver.compute_style(&element("div", None, &["length"]), &root, None, &[]);
assert_eq!(
length.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Absolute(Pt::from_f32(6.0)))
);
let percent = resolver.compute_style(&element("div", None, &["percent"]), &root, None, &[]);
assert_eq!(
percent.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Percent(0.2))
);
let negative =
resolver.compute_style(&element("div", None, &["negative"]), &root, None, &[]);
assert_eq!(
negative.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Em(-0.5))
);
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(
var.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Absolute(Pt::from_f32(4.0)))
);
}
#[test]
fn text_underline_offset_inherits_and_initial_resets() {
let css = ".parent { text-underline-offset: 20%; } .unset { text-underline-offset: unset; } .initial { text-underline-offset: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let implicit_child = resolver.compute_style(
&element("span", None, &[]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
implicit_child.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Percent(0.2))
);
let unset_child = resolver.compute_style(
&element("span", None, &["unset"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
unset_child.text_underline_offset,
TextUnderlineOffsetMode::Length(LengthSpec::Percent(0.2))
);
let initial_child = resolver.compute_style(
&element("span", None, &["initial"]),
&parent_style,
None,
&[parent_info],
);
assert_eq!(
initial_child.text_underline_offset,
TextUnderlineOffsetMode::Auto
);
}
#[test]
fn text_underline_position_parses_keyword_combinations_and_vars() {
let css = ".under { text-underline-position: under; } .left { text-underline-position: left; } .combo { text-underline-position: right under; } .font { text-underline-position: from-font left; } .var { --pos: under right; text-underline-position: var(--pos); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let under = resolver.compute_style(&element("div", None, &["under"]), &root, None, &[]);
assert_eq!(
under.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: None
}
);
let left = resolver.compute_style(&element("div", None, &["left"]), &root, None, &[]);
assert_eq!(
left.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Auto,
side: Some(TextUnderlinePositionSide::Left)
}
);
let combo = resolver.compute_style(&element("div", None, &["combo"]), &root, None, &[]);
assert_eq!(
combo.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: Some(TextUnderlinePositionSide::Right)
}
);
let font = resolver.compute_style(&element("div", None, &["font"]), &root, None, &[]);
assert_eq!(
font.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::FromFont,
side: Some(TextUnderlinePositionSide::Left)
}
);
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(
var.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: Some(TextUnderlinePositionSide::Right)
}
);
}
#[test]
fn text_underline_position_inherits_and_survives_text_decoration_shorthand() {
let css = ".parent { text-underline-position: under; } .child { text-decoration: underline red 4pt; } .unset { text-underline-position: unset; } .initial { text-underline-position: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child = resolver.compute_style(
&element("span", None, &["child"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
child.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: None
}
);
let unset = resolver.compute_style(
&element("span", None, &["unset"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(
unset.text_underline_position,
TextUnderlinePositionMode {
line: TextUnderlinePositionLine::Under,
side: None
}
);
let initial = resolver.compute_style(
&element("span", None, &["initial"]),
&parent_style,
None,
&[parent_info],
);
assert_eq!(
initial.text_underline_position,
TextUnderlinePositionMode::auto()
);
}
#[test]
fn text_shadow_parses_lists_currentcolor_and_vars() {
let css = ".current { color: rgb(0, 128, 0); text-shadow: 12px 0 0 currentColor; } .multi { text-shadow: red 3px 4px, 0 0 2px blue; } .var { --shadow: 2px 0 0 rgb(160, 0, 210); text-shadow: var(--shadow); } .colorvar { --shade: rgb(255, 128, 0); text-shadow: 1em -0.5em 0.25em var(--shade); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let current = resolver.compute_style(&element("div", None, &["current"]), &root, None, &[]);
assert_eq!(current.text_shadows.len(), 1);
assert_eq!(
current.text_shadows[0].offset_x,
LengthSpec::Absolute(px_to_pt(12.0))
);
assert!((current.text_shadows[0].color.g - (128.0 / 255.0)).abs() < 0.001);
assert!(current.text_shadows[0].color_var.is_none());
let multi = resolver.compute_style(&element("div", None, &["multi"]), &root, None, &[]);
assert_eq!(multi.text_shadows.len(), 2);
assert_eq!(
multi.text_shadows[0].offset_x,
LengthSpec::Absolute(px_to_pt(3.0))
);
assert_eq!(
multi.text_shadows[0].offset_y,
LengthSpec::Absolute(px_to_pt(4.0))
);
assert!(multi.text_shadows[0].color.r > 0.99);
assert_eq!(
multi.text_shadows[1].blur,
LengthSpec::Absolute(px_to_pt(2.0))
);
assert!(multi.text_shadows[1].color.b > 0.99);
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(var.text_shadows.len(), 1);
assert_eq!(
var.text_shadows[0].offset_x,
LengthSpec::Absolute(px_to_pt(2.0))
);
assert!((var.text_shadows[0].color.r - (160.0 / 255.0)).abs() < 0.001);
assert!((var.text_shadows[0].color.b - (210.0 / 255.0)).abs() < 0.001);
let colorvar =
resolver.compute_style(&element("div", None, &["colorvar"]), &root, None, &[]);
assert_eq!(colorvar.text_shadows.len(), 1);
assert_eq!(colorvar.text_shadows[0].offset_x, LengthSpec::Em(1.0));
assert_eq!(colorvar.text_shadows[0].offset_y, LengthSpec::Em(-0.5));
assert_eq!(colorvar.text_shadows[0].blur, LengthSpec::Em(0.25));
assert!((colorvar.text_shadows[0].color.g - (128.0 / 255.0)).abs() < 0.001);
assert!(colorvar.text_shadows[0].color_var.is_none());
}
#[test]
fn text_shadow_inherits_and_initial_resets_to_none() {
let css = ".parent { text-shadow: 6px 0 0 rgb(0, 0, 255); } .unset { text-shadow: unset; } .initial { text-shadow: initial; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(parent_style.text_shadows.len(), 1);
let child = resolver.compute_style(
&element("span", None, &[]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(child.text_shadows.len(), 1);
assert!(child.text_shadows[0].color.b > 0.99);
let unset = resolver.compute_style(
&element("span", None, &["unset"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(unset.text_shadows.len(), 1);
let initial = resolver.compute_style(
&element("span", None, &["initial"]),
&parent_style,
None,
&[parent_info],
);
assert!(initial.text_shadows.is_empty());
}
#[test]
fn line_break_parses_keywords_inheritance_and_vars() {
let css = ".anywhere { line-break: anywhere; } .loose { line-break: loose; } .parent { line-break: strict; } .unset { line-break: unset; } .initial { line-break: initial; } .var { --lb: anywhere; line-break: var(--lb); } .fallback { line-break: var(--missing, normal); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let anywhere =
resolver.compute_style(&element("div", None, &["anywhere"]), &root, None, &[]);
assert_eq!(anywhere.line_break, LineBreakMode::Anywhere);
let loose = resolver.compute_style(&element("div", None, &["loose"]), &root, None, &[]);
assert_eq!(loose.line_break, LineBreakMode::Loose);
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
assert_eq!(parent_style.line_break, LineBreakMode::Strict);
let inherited = resolver.compute_style(
&element("span", None, &[]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(inherited.line_break, LineBreakMode::Strict);
let unset = resolver.compute_style(
&element("span", None, &["unset"]),
&parent_style,
None,
&[parent_info.clone()],
);
assert_eq!(unset.line_break, LineBreakMode::Strict);
let initial = resolver.compute_style(
&element("span", None, &["initial"]),
&parent_style,
None,
&[parent_info],
);
assert_eq!(initial.line_break, LineBreakMode::Auto);
let var = resolver.compute_style(&element("div", None, &["var"]), &root, None, &[]);
assert_eq!(var.line_break, LineBreakMode::Anywhere);
let fallback =
resolver.compute_style(&element("div", None, &["fallback"]), &root, None, &[]);
assert_eq!(fallback.line_break, LineBreakMode::Normal);
}
#[test]
fn border_collapse_custom_property_applies() {
let css = "table { border-collapse: collapse; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let table_info = element("table", None, &[]);
let table_style = resolver.compute_style(&table_info, &root, None, &[]);
assert_eq!(table_style.border_collapse, BorderCollapseMode::Collapse);
}
#[test]
fn caption_side_custom_property_applies() {
let css = "table { caption-side: bottom; } .caption-top { caption-side: top; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let table_info = element("table", None, &[]);
let table_style = resolver.compute_style(&table_info, &root, None, &[]);
assert_eq!(table_style.caption_side, CaptionSideMode::Bottom);
let table_top_info = element("table", None, &["caption-top"]);
let table_top_style = resolver.compute_style(&table_top_info, &root, None, &[]);
assert_eq!(table_top_style.caption_side, CaptionSideMode::Top);
}
#[test]
fn invalid_caption_side_does_not_override_inherited_value() {
let css = "table { caption-side: bottom; } caption { caption-side: inline-start; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let table_info = element("table", None, &[]);
let table_style = resolver.compute_style(&table_info, &root, None, &[]);
assert_eq!(table_style.caption_side, CaptionSideMode::Bottom);
let caption_info = element("caption", None, &[]);
let caption_style =
resolver.compute_style(&caption_info, &table_style, None, &[table_info]);
assert_eq!(caption_style.caption_side, CaptionSideMode::Bottom);
}
#[test]
fn list_style_position_inherits_and_shorthand_resets_position() {
let css = "ul { list-style-position: inside; } \
.outside { list-style-position: outside; } \
.shorthand { list-style: square inside; } \
.reset { list-style: square; } \
.initial { list-style-position: initial; } \
.inherit { list-style-position: inherit; } \
.unset { list-style-position: unset; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let ul_info = element("ul", None, &[]);
let ul_style = resolver.compute_style(&ul_info, &root, None, &[]);
assert_eq!(ul_style.list_style_position, ListStylePositionMode::Inside);
let inherited = resolver.compute_style(
&element("li", None, &[]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(inherited.list_style_position, ListStylePositionMode::Inside);
let outside = resolver.compute_style(
&element("li", None, &["outside"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(outside.list_style_position, ListStylePositionMode::Outside);
let shorthand = resolver.compute_style(
&element("li", None, &["shorthand"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(shorthand.list_style_type, ListStyleTypeMode::Square);
assert_eq!(shorthand.list_style_position, ListStylePositionMode::Inside);
let reset = resolver.compute_style(
&element("li", None, &["reset"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(reset.list_style_type, ListStyleTypeMode::Square);
assert_eq!(reset.list_style_position, ListStylePositionMode::Outside);
let initial = resolver.compute_style(
&element("li", None, &["initial"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(initial.list_style_position, ListStylePositionMode::Outside);
let inherit = resolver.compute_style(
&element("li", None, &["inherit"]),
&ul_style,
None,
&[ul_info.clone()],
);
assert_eq!(inherit.list_style_position, ListStylePositionMode::Inside);
let unset = resolver.compute_style(
&element("li", None, &["unset"]),
&ul_style,
None,
&[ul_info],
);
assert_eq!(unset.list_style_position, ListStylePositionMode::Inside);
}
#[test]
fn list_style_type_preserves_roman_and_leading_zero_markers() {
let css = ".zero { list-style-type: decimal-leading-zero; } \
.lower { list-style-type: lower-roman; } \
.upper { list-style: upper-roman inside; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let zero = resolver.compute_style(&element("li", None, &["zero"]), &root, None, &[]);
assert_eq!(zero.list_style_type, ListStyleTypeMode::DecimalLeadingZero);
let lower = resolver.compute_style(&element("li", None, &["lower"]), &root, None, &[]);
assert_eq!(lower.list_style_type, ListStyleTypeMode::LowerRoman);
let upper = resolver.compute_style(&element("li", None, &["upper"]), &root, None, &[]);
assert_eq!(upper.list_style_type, ListStyleTypeMode::UpperRoman);
assert_eq!(upper.list_style_position, ListStylePositionMode::Inside);
}
#[test]
fn list_style_type_preserves_disclosure_markers() {
let css = ".open { list-style-type: disclosure-open; } \
.closed { list-style-type: disclosure-closed; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let open = resolver.compute_style(&element("li", None, &["open"]), &root, None, &[]);
let closed = resolver.compute_style(&element("li", None, &["closed"]), &root, None, &[]);
assert_eq!(open.list_style_type, ListStyleTypeMode::DisclosureOpen);
assert_eq!(closed.list_style_type, ListStyleTypeMode::DisclosureClosed);
}
#[test]
fn list_style_type_preserves_lower_greek_marker() {
let css = ".greek { list-style-type: lower-greek; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let greek = resolver.compute_style(&element("li", None, &["greek"]), &root, None, &[]);
assert_eq!(greek.list_style_type, ListStyleTypeMode::LowerGreek);
}
#[test]
fn list_style_type_preserves_numeric_script_markers() {
let css = ".arabic { list-style-type: arabic-indic; } \
.bengali { list-style-type: bengali; } \
.cambodian { list-style-type: cambodian; } \
.khmer { list-style-type: khmer; } \
.cjk { list-style-type: cjk-decimal; } \
.devanagari { list-style-type: devanagari; } \
.gujarati { list-style-type: gujarati; } \
.gurmukhi { list-style-type: gurmukhi; } \
.kannada { list-style-type: kannada; } \
.lao { list-style-type: lao; } \
.malayalam { list-style-type: malayalam; } \
.mongolian { list-style-type: mongolian; } \
.myanmar { list-style-type: myanmar; } \
.oriya { list-style-type: oriya; } \
.persian { list-style-type: persian; } \
.tamil { list-style-type: tamil; } \
.telugu { list-style-type: telugu; } \
.thai { list-style-type: thai; } \
.tibetan { list-style-type: tibetan; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let arabic = resolver.compute_style(&element("li", None, &["arabic"]), &root, None, &[]);
let bengali = resolver.compute_style(&element("li", None, &["bengali"]), &root, None, &[]);
let cambodian =
resolver.compute_style(&element("li", None, &["cambodian"]), &root, None, &[]);
let khmer = resolver.compute_style(&element("li", None, &["khmer"]), &root, None, &[]);
let cjk = resolver.compute_style(&element("li", None, &["cjk"]), &root, None, &[]);
let devanagari =
resolver.compute_style(&element("li", None, &["devanagari"]), &root, None, &[]);
let gujarati =
resolver.compute_style(&element("li", None, &["gujarati"]), &root, None, &[]);
let gurmukhi =
resolver.compute_style(&element("li", None, &["gurmukhi"]), &root, None, &[]);
let kannada = resolver.compute_style(&element("li", None, &["kannada"]), &root, None, &[]);
let lao = resolver.compute_style(&element("li", None, &["lao"]), &root, None, &[]);
let malayalam =
resolver.compute_style(&element("li", None, &["malayalam"]), &root, None, &[]);
let mongolian =
resolver.compute_style(&element("li", None, &["mongolian"]), &root, None, &[]);
let myanmar = resolver.compute_style(&element("li", None, &["myanmar"]), &root, None, &[]);
let oriya = resolver.compute_style(&element("li", None, &["oriya"]), &root, None, &[]);
let persian = resolver.compute_style(&element("li", None, &["persian"]), &root, None, &[]);
let tamil = resolver.compute_style(&element("li", None, &["tamil"]), &root, None, &[]);
let telugu = resolver.compute_style(&element("li", None, &["telugu"]), &root, None, &[]);
let thai = resolver.compute_style(&element("li", None, &["thai"]), &root, None, &[]);
let tibetan = resolver.compute_style(&element("li", None, &["tibetan"]), &root, None, &[]);
assert_eq!(arabic.list_style_type, ListStyleTypeMode::ArabicIndic);
assert_eq!(bengali.list_style_type, ListStyleTypeMode::Bengali);
assert_eq!(cambodian.list_style_type, ListStyleTypeMode::Cambodian);
assert_eq!(khmer.list_style_type, ListStyleTypeMode::Cambodian);
assert_eq!(cjk.list_style_type, ListStyleTypeMode::CjkDecimal);
assert_eq!(devanagari.list_style_type, ListStyleTypeMode::Devanagari);
assert_eq!(gujarati.list_style_type, ListStyleTypeMode::Gujarati);
assert_eq!(gurmukhi.list_style_type, ListStyleTypeMode::Gurmukhi);
assert_eq!(kannada.list_style_type, ListStyleTypeMode::Kannada);
assert_eq!(lao.list_style_type, ListStyleTypeMode::Lao);
assert_eq!(malayalam.list_style_type, ListStyleTypeMode::Malayalam);
assert_eq!(mongolian.list_style_type, ListStyleTypeMode::Mongolian);
assert_eq!(myanmar.list_style_type, ListStyleTypeMode::Myanmar);
assert_eq!(oriya.list_style_type, ListStyleTypeMode::Oriya);
assert_eq!(persian.list_style_type, ListStyleTypeMode::Persian);
assert_eq!(tamil.list_style_type, ListStyleTypeMode::Tamil);
assert_eq!(telugu.list_style_type, ListStyleTypeMode::Telugu);
assert_eq!(thai.list_style_type, ListStyleTypeMode::Thai);
assert_eq!(tibetan.list_style_type, ListStyleTypeMode::Tibetan);
}
#[test]
fn list_style_type_preserves_japanese_alphabetic_markers() {
let css = ".hiragana { list-style-type: hiragana; } \
.hiragana-iroha { list-style-type: hiragana-iroha; } \
.katakana { list-style-type: katakana; } \
.katakana-iroha { list-style-type: katakana-iroha; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let hiragana =
resolver.compute_style(&element("li", None, &["hiragana"]), &root, None, &[]);
let hiragana_iroha =
resolver.compute_style(&element("li", None, &["hiragana-iroha"]), &root, None, &[]);
let katakana =
resolver.compute_style(&element("li", None, &["katakana"]), &root, None, &[]);
let katakana_iroha =
resolver.compute_style(&element("li", None, &["katakana-iroha"]), &root, None, &[]);
assert_eq!(hiragana.list_style_type, ListStyleTypeMode::Hiragana);
assert_eq!(
hiragana_iroha.list_style_type,
ListStyleTypeMode::HiraganaIroha
);
assert_eq!(katakana.list_style_type, ListStyleTypeMode::Katakana);
assert_eq!(
katakana_iroha.list_style_type,
ListStyleTypeMode::KatakanaIroha
);
}
#[test]
fn list_style_type_preserves_cjk_fixed_markers() {
let css = ".branch { list-style-type: cjk-earthly-branch; } \
.stem { list-style-type: cjk-heavenly-stem; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let branch = resolver.compute_style(&element("li", None, &["branch"]), &root, None, &[]);
let stem = resolver.compute_style(&element("li", None, &["stem"]), &root, None, &[]);
assert_eq!(branch.list_style_type, ListStyleTypeMode::CjkEarthlyBranch);
assert_eq!(stem.list_style_type, ListStyleTypeMode::CjkHeavenlyStem);
}
#[test]
fn list_style_type_preserves_armenian_markers() {
let css = ".armenian { list-style-type: armenian; } \
.upper { list-style-type: upper-armenian; } \
.lower { list-style-type: lower-armenian; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let armenian =
resolver.compute_style(&element("li", None, &["armenian"]), &root, None, &[]);
let upper = resolver.compute_style(&element("li", None, &["upper"]), &root, None, &[]);
let lower = resolver.compute_style(&element("li", None, &["lower"]), &root, None, &[]);
assert_eq!(armenian.list_style_type, ListStyleTypeMode::Armenian);
assert_eq!(upper.list_style_type, ListStyleTypeMode::Armenian);
assert_eq!(lower.list_style_type, ListStyleTypeMode::LowerArmenian);
}
#[test]
fn list_style_type_preserves_georgian_marker() {
let css = ".georgian { list-style-type: georgian; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let georgian =
resolver.compute_style(&element("li", None, &["georgian"]), &root, None, &[]);
assert_eq!(georgian.list_style_type, ListStyleTypeMode::Georgian);
}
#[test]
fn list_style_type_preserves_hebrew_marker() {
let css = ".hebrew { list-style-type: hebrew; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let hebrew = resolver.compute_style(&element("li", None, &["hebrew"]), &root, None, &[]);
assert_eq!(hebrew.list_style_type, ListStyleTypeMode::Hebrew);
}
#[test]
fn list_style_type_preserves_japanese_longhand_markers() {
let css = ".informal { list-style-type: japanese-informal; } \
.formal { list-style-type: japanese-formal; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let informal =
resolver.compute_style(&element("li", None, &["informal"]), &root, None, &[]);
let formal = resolver.compute_style(&element("li", None, &["formal"]), &root, None, &[]);
assert_eq!(
informal.list_style_type,
ListStyleTypeMode::JapaneseInformal
);
assert_eq!(formal.list_style_type, ListStyleTypeMode::JapaneseFormal);
}
#[test]
fn list_style_type_preserves_korean_longhand_markers() {
let css = ".hangul { list-style-type: korean-hangul-formal; } \
.hanja-informal { list-style-type: korean-hanja-informal; } \
.hanja-formal { list-style-type: korean-hanja-formal; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let hangul = resolver.compute_style(&element("li", None, &["hangul"]), &root, None, &[]);
let hanja_informal =
resolver.compute_style(&element("li", None, &["hanja-informal"]), &root, None, &[]);
let hanja_formal =
resolver.compute_style(&element("li", None, &["hanja-formal"]), &root, None, &[]);
assert_eq!(
hangul.list_style_type,
ListStyleTypeMode::KoreanHangulFormal
);
assert_eq!(
hanja_informal.list_style_type,
ListStyleTypeMode::KoreanHanjaInformal
);
assert_eq!(
hanja_formal.list_style_type,
ListStyleTypeMode::KoreanHanjaFormal
);
}
#[test]
fn list_style_type_preserves_chinese_longhand_markers() {
let css = ".simp-informal { list-style-type: simp-chinese-informal; } \
.simp-formal { list-style-type: simp-chinese-formal; } \
.trad-informal { list-style-type: trad-chinese-informal; } \
.trad-formal { list-style-type: trad-chinese-formal; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let simp_informal =
resolver.compute_style(&element("li", None, &["simp-informal"]), &root, None, &[]);
let simp_formal =
resolver.compute_style(&element("li", None, &["simp-formal"]), &root, None, &[]);
let trad_informal =
resolver.compute_style(&element("li", None, &["trad-informal"]), &root, None, &[]);
let trad_formal =
resolver.compute_style(&element("li", None, &["trad-formal"]), &root, None, &[]);
assert_eq!(
simp_informal.list_style_type,
ListStyleTypeMode::SimpChineseInformal
);
assert_eq!(
simp_formal.list_style_type,
ListStyleTypeMode::SimpChineseFormal
);
assert_eq!(
trad_informal.list_style_type,
ListStyleTypeMode::TradChineseInformal
);
assert_eq!(
trad_formal.list_style_type,
ListStyleTypeMode::TradChineseFormal
);
}
#[test]
fn list_style_type_preserves_cjk_ideographic_alias_marker() {
let css = ".alias { list-style-type: cjk-ideographic; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let alias = resolver.compute_style(&element("li", None, &["alias"]), &root, None, &[]);
assert_eq!(
alias.list_style_type,
ListStyleTypeMode::TradChineseInformal
);
}
#[test]
fn list_style_type_preserves_symbols_function_markers() {
let css = ".symbolic { list-style-type: symbols(\"*\" \"+\"); } \
.cyclic { list-style-type: symbols(cyclic \"*\" \"+\"); } \
.numeric { list-style-type: symbols(numeric \"0\" \"1\"); } \
.alphabetic { list-style-type: symbols(alphabetic \"A\" \"B\"); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let symbolic =
resolver.compute_style(&element("li", None, &["symbolic"]), &root, None, &[]);
let cyclic = resolver.compute_style(&element("li", None, &["cyclic"]), &root, None, &[]);
let numeric = resolver.compute_style(&element("li", None, &["numeric"]), &root, None, &[]);
let alphabetic =
resolver.compute_style(&element("li", None, &["alphabetic"]), &root, None, &[]);
assert_eq!(
symbolic.list_style_type,
ListStyleTypeMode::AnonymousSymbols
);
assert_eq!(
symbolic
.list_style_symbols
.as_ref()
.map(|symbols| symbols.system),
Some(AnonymousListStyleSymbolsSystem::Symbolic)
);
assert_eq!(cyclic.list_style_type, ListStyleTypeMode::AnonymousSymbols);
assert_eq!(
cyclic
.list_style_symbols
.as_ref()
.map(|symbols| symbols.system),
Some(AnonymousListStyleSymbolsSystem::Cyclic)
);
assert_eq!(numeric.list_style_type, ListStyleTypeMode::AnonymousSymbols);
assert_eq!(
numeric
.list_style_symbols
.as_ref()
.map(|symbols| symbols.system),
Some(AnonymousListStyleSymbolsSystem::Numeric)
);
assert_eq!(
alphabetic.list_style_type,
ListStyleTypeMode::AnonymousSymbols
);
assert_eq!(
alphabetic
.list_style_symbols
.as_ref()
.map(|symbols| symbols.system),
Some(AnonymousListStyleSymbolsSystem::Alphabetic)
);
}
#[test]
fn list_style_type_preserves_custom_counter_style_name_fallback() {
let css = ".custom { list-style-type: made-up-counter; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let custom = resolver.compute_style(&element("li", None, &["custom"]), &root, None, &[]);
assert_eq!(
custom.list_style_type,
ListStyleTypeMode::CustomCounterStyleName
);
assert_eq!(custom.list_style_marker.as_deref(), Some("made-up-counter"));
}
#[test]
fn list_style_type_resolves_counter_style_rule_names() {
let css = ".custom { list-style-type: space-counter; } \
@counter-style space-counter { system: cyclic; symbols: \"A\" \"B\"; suffix: \": \"; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let custom = resolver.compute_style(&element("li", None, &["custom"]), &root, None, &[]);
let symbols = custom
.list_style_symbols
.as_ref()
.expect("named counter style symbols");
assert_eq!(
custom.list_style_type,
ListStyleTypeMode::CustomCounterStyleName
);
assert_eq!(custom.list_style_marker.as_deref(), Some("space-counter"));
assert_eq!(symbols.system, AnonymousListStyleSymbolsSystem::Cyclic);
assert_eq!(symbols.symbols, vec!["A".to_string(), "B".to_string()]);
assert_eq!(symbols.suffix, ": ");
}
#[test]
fn counter_style_extends_decimal_preserves_affixes_negative_and_padding() {
let css = ".custom { list-style-type: bracketed; } \
@counter-style bracketed { system: extends decimal; negative: '(' ')'; \
pad: 2 '0'; prefix: '['; suffix: '] '; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let custom = resolver.compute_style(&element("li", None, &["custom"]), &root, None, &[]);
let spec = custom
.list_style_symbols
.as_ref()
.expect("extends-decimal counter style");
assert_eq!(spec.system, AnonymousListStyleSymbolsSystem::ExtendsDecimal);
assert_eq!(spec.prefix, "[");
assert_eq!(spec.suffix, "] ");
assert_eq!(spec.negative_prefix, "(");
assert_eq!(spec.negative_suffix, ")");
assert_eq!(spec.pad_width, 2);
assert_eq!(spec.pad_symbol, "0");
let generated_resolver = StyleResolver::new(
"@counter-style negwrap { system: extends decimal; negative: '(' ')'; } \
.row::before { content: counter(n, negwrap); }",
);
let generated_root = generated_resolver.default_style();
let row_info = element("div", None, &["row"]);
let row_style = generated_resolver.compute_style(&row_info, &generated_root, None, &[]);
let before = generated_resolver
.compute_pseudo_style(&row_info, &row_style, &[], PseudoTarget::Before)
.expect("generated custom counter style");
assert!(matches!(
before.generated_content.as_deref(),
Some([GeneratedContentPart::Counter(counter)])
if counter.style.symbols.as_ref().is_some_and(|symbols|
symbols.system == AnonymousListStyleSymbolsSystem::ExtendsDecimal
&& symbols.negative_prefix == "("
&& symbols.negative_suffix == ")")
));
}
#[test]
fn list_style_type_preserves_ethiopic_numeric_marker() {
let css = ".ethiopic { list-style-type: ethiopic-numeric; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let ethiopic =
resolver.compute_style(&element("li", None, &["ethiopic"]), &root, None, &[]);
assert_eq!(ethiopic.list_style_type, ListStyleTypeMode::EthiopicNumeric);
}
#[test]
fn border_spacing_two_values_parse() {
let css = "table { border-spacing: 2px 4px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let table_info = element("table", None, &[]);
let table_style = resolver.compute_style(&table_info, &root, None, &[]);
assert_eq!(
table_style.border_spacing.horizontal,
LengthSpec::Absolute(Pt::from_f32(1.5))
);
assert_eq!(
table_style.border_spacing.vertical,
LengthSpec::Absolute(Pt::from_f32(3.0))
);
}
#[test]
fn multicol_count_and_rule_shorthand_parse() {
let resolver = StyleResolver::new(
".x { column-count: 2; column-fill: auto; column-span: all; column-rule: 2px solid #577590; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.column_count, 2);
assert_eq!(style.column_fill, ColumnFillMode::Auto);
assert_eq!(style.column_span, ColumnSpanMode::All);
assert_eq!(
style.column_rule_width,
LengthSpec::Absolute(Pt::from_f32(1.5))
);
assert_eq!(style.column_rule_style, OutlineLineStyle::Solid);
assert_eq!(style.resolved_column_rule_color(), rgb8(0x57, 0x75, 0x90));
assert!(style.column_rule_visible);
}
#[test]
fn multicol_width_and_columns_shorthand_preserve_both_constraints() {
let resolver = StyleResolver::new(
".width { column-width: 140px; } \
.both { columns: 120px 3; } \
.count { columns: 4; }",
);
let root = resolver.default_style();
let width = resolver.compute_style(&element("div", None, &["width"]), &root, None, &[]);
let both = resolver.compute_style(&element("div", None, &["both"]), &root, None, &[]);
let count = resolver.compute_style(&element("div", None, &["count"]), &root, None, &[]);
assert!(width.column_count_auto);
assert_eq!(
width.column_width,
LengthSpec::Absolute(Pt::from_f32(105.0))
);
assert_eq!(both.column_count, 3);
assert!(!both.column_count_auto);
assert_eq!(both.column_width, LengthSpec::Absolute(Pt::from_f32(90.0)));
assert_eq!(count.column_count, 4);
assert!(!count.column_count_auto);
assert_eq!(count.column_width, LengthSpec::Auto);
}
#[test]
fn border_spacing_and_collapse_inherit() {
let resolver =
StyleResolver::new(".table { border-collapse: collapse; border-spacing: 2px 4px; }");
let root = resolver.default_style();
let table_info = element("div", None, &["table"]);
let table_style = resolver.compute_style(&table_info, &root, None, &[]);
let child_info = element("div", None, &[]);
let child = resolver.compute_style(
&child_info,
&table_style,
None,
std::slice::from_ref(&table_info),
);
assert_eq!(child.border_collapse, BorderCollapseMode::Collapse);
assert_eq!(child.border_spacing, table_style.border_spacing);
}
#[test]
fn table_layout_custom_property_applies() {
let css = "table { table-layout: fixed; } .auto { table-layout: auto; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let table_fixed = element("table", None, &[]);
let style_fixed = resolver.compute_style(&table_fixed, &root, None, &[]);
assert_eq!(style_fixed.table_layout, TableLayoutMode::Fixed);
let table_auto = element("table", None, &["auto"]);
let style_auto = resolver.compute_style(&table_auto, &root, None, &[]);
assert_eq!(style_auto.table_layout, TableLayoutMode::Auto);
}
#[test]
fn ua_table_cells_and_captions_match_browser_defaults() {
let resolver = StyleResolver::new("");
let root = resolver.default_style();
let caption = resolver.compute_style(&element("caption", None, &[]), &root, None, &[]);
assert_eq!(caption.display, DisplayMode::TableCaption);
assert_eq!(caption.text_align, TextAlignMode::Center);
let td = resolver.compute_style(&element("td", None, &[]), &root, None, &[]);
assert_eq!(td.display, DisplayMode::TableCell);
let one_px = LengthSpec::Absolute(Pt::from_f32(0.75));
assert_eq!(td.padding.top, one_px);
assert_eq!(td.padding.right, one_px);
assert_eq!(td.padding.bottom, one_px);
assert_eq!(td.padding.left, one_px);
let th = resolver.compute_style(&element("th", None, &[]), &root, None, &[]);
assert_eq!(th.display, DisplayMode::TableCell);
assert_eq!(th.font_weight, 700);
assert_eq!(th.text_align, TextAlignMode::Center);
}
#[test]
fn ua_lists_reserve_the_browser_default_marker_gutter() {
let resolver = StyleResolver::new("");
let root = resolver.default_style();
let ol = resolver.compute_style(&element("ol", None, &[]), &root, None, &[]);
assert_eq!(ol.padding.left, LengthSpec::Absolute(Pt::from_f32(30.0)));
assert_eq!(ol.padding.right, LengthSpec::Absolute(Pt::ZERO));
}
#[test]
fn font_synthesis_none_disables_synthetic_weight() {
let resolver = StyleResolver::new("th { font-synthesis: none; }");
let root = resolver.default_style();
let style = resolver.compute_style(&element("th", None, &[]), &root, None, &[]);
assert_eq!(style.font_weight, 700);
assert!(!style.font_synthesis_weight);
assert!(!style.to_text_style().font_synthesis_weight);
}
#[test]
fn box_shadow_var_parses_for_unparsed_paths() {
let css =
".x { --shade: rgba(0, 0, 0, 0.05); box-shadow: inset 0 0 0 9999px var(--shade); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert!(shadow.inset);
match shadow.spread {
LengthSpec::Absolute(value) => assert!(value > Pt::from_f32(100.0)),
other => panic!("expected absolute spread, got {other:?}"),
}
assert!((shadow.color.r - 0.95).abs() < 0.01);
assert!((shadow.color.g - 0.95).abs() < 0.01);
assert!((shadow.color.b - 0.95).abs() < 0.01);
assert!((shadow.opacity - 1.0).abs() < 0.001);
}
#[test]
fn box_shadow_multi_list_resolves_all_shadows() {
let css = ".x { box-shadow: 20px 0 0 rgba(255, 0, 0, 1), -20px 0 0 rgba(0, 0, 255, 1); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.box_shadows.len(), 2);
assert!(style.box_shadow.is_some());
assert!(
style.box_shadows[0]
.offset_x
.resolve_width(Pt::ZERO, Pt::ZERO, Pt::ZERO)
> Pt::ZERO
);
assert!(
style.box_shadows[1]
.offset_x
.resolve_width(Pt::ZERO, Pt::ZERO, Pt::ZERO)
< Pt::ZERO
);
assert!(style.box_shadows[0].color.r > 0.9);
assert!(style.box_shadows[1].color.b > 0.9);
}
#[test]
fn box_shadow_omitted_color_resolves_current_color() {
let css = ".x { color: rgb(0, 0, 255); box-shadow: 12px 0 0 0; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert!((shadow.color.r - 0.0).abs() < 0.001);
assert!((shadow.color.g - 0.0).abs() < 0.001);
assert!((shadow.color.b - 1.0).abs() < 0.001);
assert!((shadow.opacity - 1.0).abs() < 0.001);
assert!(shadow.color_var.is_none());
}
#[test]
fn box_shadow_currentcolor_resolves_current_color() {
let css = ".x { color: rgb(0, 128, 0); box-shadow: currentColor 12px 0 0 0; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert!((shadow.color.r - 0.0).abs() < 0.001);
assert!((shadow.color.g - (128.0 / 255.0)).abs() < 0.001);
assert!((shadow.color.b - 0.0).abs() < 0.001);
assert!((shadow.opacity - 1.0).abs() < 0.001);
assert!(shadow.color_var.is_none());
}
#[test]
fn box_shadow_modern_rgb_and_mm_lengths_resolve() {
let css = ".x { box-shadow: 6mm 0 0 0 rgb(160 0 210); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert_eq!(
shadow.offset_x,
LengthSpec::Absolute(Pt::from_f32(6.0 * 72.0 / 25.4))
);
assert_eq!(shadow.offset_y, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(shadow.blur, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(shadow.spread, LengthSpec::Absolute(Pt::ZERO));
assert!((shadow.color.r - (160.0 / 255.0)).abs() < 0.001);
assert!(shadow.color.g < 0.001);
assert!((shadow.color.b - (210.0 / 255.0)).abs() < 0.001);
}
#[test]
fn box_shadow_rejects_invalid_shadow_grammar() {
for css in [
".x { box-shadow: 1px 2px 3px 4px 5px red; }",
".x { box-shadow: 1px 2px -1px red; }",
".x { box-shadow: 10% 0 red; }",
".x { box-shadow: 22px 0 0 red, 12 0 blue; }",
".x { box-shadow: red 1px blue 2px; }",
".x { box-shadow: inset outset 1px 2px red; }",
] {
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(style.box_shadow.is_none(), "css should be invalid: {css}");
assert!(style.box_shadows.is_empty(), "css should be invalid: {css}");
}
}
#[test]
fn color_rgba_var_expression_resolves_in_parsed_path() {
let css = ":root { --bs-success-rgb: 25, 135, 84; } .x { --bs-text-opacity: 1; color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
assert!(
root_style.custom_colors.contains_key("--bs-success-rgb"),
"root custom colors missing --bs-success-rgb: {:?}",
root_style.custom_colors.keys().collect::<Vec<_>>()
);
let info = element("p", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
assert!(
style.custom_colors.contains_key("--bs-success-rgb"),
"child custom colors missing --bs-success-rgb: {:?}",
style.custom_colors.keys().collect::<Vec<_>>()
);
assert!(
(style.color.r - (25.0 / 255.0)).abs() < 0.01,
"unexpected r={:?}",
style.color
);
assert!(
(style.color.g - (135.0 / 255.0)).abs() < 0.01,
"unexpected g={:?}",
style.color
);
assert!(
(style.color.b - (84.0 / 255.0)).abs() < 0.01,
"unexpected b={:?}",
style.color
);
}
#[test]
fn modern_rgb_space_syntax_resolves_from_custom_property() {
let css =
":root { --panel: rgb(189 85 218 / 25%); } .x { background-color: var(--panel); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected modern rgb() custom-property background");
assert!(
(bg.r - (238.5 / 255.0)).abs() < 0.01,
"unexpected bg={bg:?}"
);
assert!(
(bg.g - (212.5 / 255.0)).abs() < 0.01,
"unexpected bg={bg:?}"
);
assert!(
(bg.b - (245.75 / 255.0)).abs() < 0.01,
"unexpected bg={bg:?}"
);
}
#[test]
fn modern_rgb_var_triplet_slash_alpha_resolves() {
let css = ":root { --triplet: 25, 135, 84; --fg: rgb(var(--triplet) / 50%); } .x { background-color: var(--fg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected modern rgb() variable background");
assert!((bg.r - 0.549).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.765).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.665).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn legacy_rgb_percent_alpha_resolves() {
let (color, alpha) =
parse_color_string("rgb(74%, 33%, 85%, 80%)").expect("expected legacy rgb parse");
assert!((color.r - 0.74).abs() < 0.001);
assert!((color.g - 0.33).abs() < 0.001);
assert!((color.b - 0.85).abs() < 0.001);
assert!((alpha - 0.8).abs() < 0.001);
}
#[test]
fn modern_rgb_var_triplet_expression_resolver() {
let resolver = StyleResolver::new("");
let mut style = resolver.default_style();
style.custom_colors.insert(
"--triplet".to_string(),
Color::rgb(25.0 / 255.0, 135.0 / 255.0, 84.0 / 255.0),
);
style
.custom_color_alpha
.insert("--triplet".to_string(), 1.0);
let (color, alpha) =
resolve_custom_color_expr_with_alpha(&style, "rgb(var(--triplet) / 50%)")
.expect("expected modern rgb var expression");
assert!((color.r - (25.0 / 255.0)).abs() < 0.01);
assert!((color.g - (135.0 / 255.0)).abs() < 0.01);
assert!((color.b - (84.0 / 255.0)).abs() < 0.01);
assert!((alpha - 0.5).abs() < 0.01);
}
#[test]
fn modern_hsl_space_syntax_resolves_on_background() {
let css = ".x { background-color: hsl(120deg 100% 50% / 50%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let bg = style
.background_color
.expect("expected modern hsl() background");
assert!((bg.r - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 1.0).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.5).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn modern_hsl_var_triplet_slash_alpha_resolves() {
let css = ":root { --hsl-triplet: 240 100% 50%; --tone: hsl(var(--hsl-triplet) / 50%); } .x { background-color: var(--tone); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected modern hsl() variable background");
assert!((bg.r - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 1.0).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn legacy_hsl_comma_alpha_resolves() {
let (color, alpha) =
parse_color_string("hsla(60, 100%, 50%, 0.5)").expect("expected legacy hsl parse");
assert!((color.r - 1.0).abs() < 0.001);
assert!((color.g - 1.0).abs() < 0.001);
assert!(color.b.abs() < 0.001);
assert!((alpha - 0.5).abs() < 0.001);
}
#[test]
fn modern_hwb_space_syntax_resolves_on_background() {
let css = ".x { background-color: hwb(120deg 0% 0% / 50%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let bg = style
.background_color
.expect("expected modern hwb() background");
assert!((bg.r - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 1.0).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.5).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn modern_hwb_var_triplet_slash_alpha_resolves() {
let css = ":root { --hwb-triplet: 240 0% 0%; --tone: hwb(var(--hwb-triplet) / 50%); } .x { background-color: var(--tone); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected modern hwb() variable background");
assert!((bg.r - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 1.0).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn hwb_whiteness_blackness_normalizes_to_gray() {
let (color, alpha) = parse_color_string("hwb(0 60% 60%)").expect("expected hwb parse");
assert!((color.r - 0.5).abs() < 0.001, "unexpected color={color:?}");
assert!((color.g - 0.5).abs() < 0.001, "unexpected color={color:?}");
assert!((color.b - 0.5).abs() < 0.001, "unexpected color={color:?}");
assert!((alpha - 1.0).abs() < 0.001);
}
#[test]
fn lab_family_color_functions_preserve_alpha() {
for raw in [
"lab(0 0 0 / 50%)",
"lch(0 0 0 / 50%)",
"oklab(0 0 0 / 50%)",
"oklch(0 0 0 / 50%)",
"color(srgb 0 0 0 / 50%)",
] {
let (color, alpha) = parse_color_string(raw).expect("expected color function parse");
assert!(color.r.abs() < 0.001, "unexpected {raw} color={color:?}");
assert!(color.g.abs() < 0.001, "unexpected {raw} color={color:?}");
assert!(color.b.abs() < 0.001, "unexpected {raw} color={color:?}");
assert!(
(alpha - 0.5).abs() < 0.003,
"unexpected {raw} alpha={alpha:?}"
);
}
}
#[test]
fn alpha_hex_named_and_standard_color_spaces_parse_natively() {
let (hex, alpha) = parse_color_string("#33669980").expect("alpha hex");
assert!((hex.r - 0.2).abs() < 0.001);
assert!((hex.g - 0.4).abs() < 0.001);
assert!((hex.b - 0.6).abs() < 0.001);
assert!((alpha - (128.0 / 255.0)).abs() < 0.001);
let (named, alpha) = parse_color_string("ReBeCcApUrPlE").expect("named color");
assert!((named.r - (102.0 / 255.0)).abs() < 0.001);
assert!((named.g - (51.0 / 255.0)).abs() < 0.001);
assert!((named.b - (153.0 / 255.0)).abs() < 0.001);
assert_eq!(alpha, 1.0);
for raw in [
"color(srgb 0 0 0)",
"color(srgb-linear 0 0 0)",
"color(display-p3 0 0 0)",
"color(a98-rgb 0 0 0)",
"color(prophoto-rgb 0 0 0)",
"color(rec2020 0 0 0)",
"color(xyz 0 0 0)",
"color(xyz-d65 0 0 0)",
"color(xyz-d50 0 0 0)",
] {
let (color, alpha) = parse_color_string(raw).expect(raw);
assert!(color.r.abs() < 0.001, "unexpected {raw}: {color:?}");
assert!(color.g.abs() < 0.001, "unexpected {raw}: {color:?}");
assert!(color.b.abs() < 0.001, "unexpected {raw}: {color:?}");
assert_eq!(alpha, 1.0);
}
}
#[test]
fn lab_family_custom_property_alpha_blends_background() {
let css = ":root { --tone: lab(0 0 0 / 50%); } .x { background-color: var(--tone); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected lab() variable background");
assert!((bg.r - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.5).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.5).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn relative_color_functions_preserve_alpha() {
for raw in [
"rgb(from rebeccapurple r g b / 50%)",
"hsl(from rebeccapurple h s l / 50%)",
"hwb(from rebeccapurple h w b / 50%)",
] {
let (color, alpha) = parse_color_string(raw).expect("expected relative color parse");
assert!(
(color.r - (102.0 / 255.0)).abs() < 0.01,
"unexpected {raw} color={color:?}"
);
assert!(
(color.g - (51.0 / 255.0)).abs() < 0.01,
"unexpected {raw} color={color:?}"
);
assert!(
(color.b - (153.0 / 255.0)).abs() < 0.01,
"unexpected {raw} color={color:?}"
);
assert!(
(alpha - 0.5).abs() < 0.003,
"unexpected {raw} alpha={alpha:?}"
);
}
}
#[test]
fn relative_lab_and_color_space_functions_preserve_origin_and_alpha() {
for raw in [
"lab(from rebeccapurple l a b / 50%)",
"lch(from rebeccapurple l c h / 50%)",
"oklab(from rebeccapurple l a b / 50%)",
"oklch(from rebeccapurple l c h / 50%)",
"color(from rebeccapurple srgb r g b / 50%)",
"color(from rebeccapurple display-p3 r g b / 50%)",
] {
let (color, alpha) = parse_color_string(raw).expect(raw);
assert!((color.r - (102.0 / 255.0)).abs() < 0.02, "{raw}: {color:?}");
assert!((color.g - (51.0 / 255.0)).abs() < 0.02, "{raw}: {color:?}");
assert!((color.b - (153.0 / 255.0)).abs() < 0.02, "{raw}: {color:?}");
assert!((alpha - 0.5).abs() < 0.003, "{raw}: {alpha}");
}
}
#[test]
fn relative_color_var_origin_and_alpha_resolve() {
let css = ":root { --base: rebeccapurple; --alpha: 0.5; --tone: hwb(from var(--base) h w b / var(--alpha)); } .x { background-color: var(--tone); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected relative color variable background");
assert!(
(bg.r - (178.5 / 255.0)).abs() < 0.01,
"unexpected background={bg:?}"
);
assert!(
(bg.g - (153.0 / 255.0)).abs() < 0.01,
"unexpected background={bg:?}"
);
assert!(
(bg.b - (204.0 / 255.0)).abs() < 0.01,
"unexpected background={bg:?}"
);
}
#[test]
fn box_shadow_var_fallback_chain_resolves() {
let css = ".x { --shade: rgba(0, 0, 0, 0.05); box-shadow: inset 0 0 0 9999px var(--missing, var(--shade)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert!(shadow.inset);
assert!(
(shadow.color.r - 0.95).abs() < 0.01,
"unexpected shadow={shadow:?}"
);
assert!(
(shadow.color.g - 0.95).abs() < 0.01,
"unexpected shadow={shadow:?}"
);
assert!(
(shadow.color.b - 0.95).abs() < 0.01,
"unexpected shadow={shadow:?}"
);
assert!(shadow.opacity > 0.9);
}
#[test]
fn unresolved_box_shadow_var_is_transparent() {
let css = ".x { box-shadow: inset 0 0 0 9999px var(--missing); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let shadow = style.box_shadow.expect("expected box-shadow to parse");
assert!(shadow.inset);
assert!(shadow.opacity <= 0.001);
}
#[test]
fn custom_property_rgba_var_expression_resolves_for_background() {
let css = ":root { --bs-emphasis-color-rgb: 0, 0, 0; --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); } .x { background-color: var(--bs-table-striped-bg); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected background color from custom rgba expression");
assert!((bg.r - 0.95).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.95).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.95).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn custom_property_color_mix_var_expression_resolves_for_background() {
let css = ":root { --panel: #333b52; --mixed: color-mix(in srgb, var(--panel) 78%, #4c6eb4); } .x { background-color: var(--mixed); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let bg = style
.background_color
.expect("expected background color from color-mix var expression");
assert!(
(bg.r - (56.5 / 255.0)).abs() < 0.02,
"unexpected background={bg:?}"
);
assert!(
(bg.g - (70.2 / 255.0)).abs() < 0.02,
"unexpected background={bg:?}"
);
assert!(
(bg.b - (103.56 / 255.0)).abs() < 0.02,
"unexpected background={bg:?}"
);
}
#[test]
fn color_mix_underfilled_percentages_apply_alpha_multiplier() {
let css = ".x { background-color: color-mix(in srgb, red 20%, blue 20%); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
let bg = style
.background_color
.expect("expected background color from direct color-mix expression");
assert!((bg.r - 0.8).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.g - 0.6).abs() < 0.01, "unexpected background={bg:?}");
assert!((bg.b - 0.8).abs() < 0.01, "unexpected background={bg:?}");
}
#[test]
fn color_mix_supports_linear_rgb_and_polar_hue_interpolation() {
let (linear, alpha) =
parse_color_string("color-mix(in srgb-linear, black, white)").expect("linear mix");
assert!((linear.r - 0.735).abs() < 0.01, "{linear:?}");
assert!((linear.g - 0.735).abs() < 0.01, "{linear:?}");
assert!((linear.b - 0.735).abs() < 0.01, "{linear:?}");
assert_eq!(alpha, 1.0);
let (longer, alpha) =
parse_color_string("color-mix(in hsl longer hue, hsl(0 100% 50%), hsl(240 100% 50%))")
.expect("longer hue mix");
assert!(longer.r < 0.01, "{longer:?}");
assert!(longer.g > 0.99, "{longer:?}");
assert!(longer.b < 0.01, "{longer:?}");
assert_eq!(alpha, 1.0);
}
#[test]
fn color_mix_non_srgb_space_resolves_custom_properties() {
let css = ":root { --left: hsl(0 100% 50%); --mixed: color-mix(in hsl longer hue, var(--left), hsl(240 100% 50%)); } .x { background-color: var(--mixed); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let style = resolver.compute_style(
&element("div", None, &["x"]),
&root_style,
None,
&[root_info],
);
let bg = style
.background_color
.expect("expected dynamic non-sRGB mix");
assert!(bg.r < 0.01, "{bg:?}");
assert!(bg.g > 0.99, "{bg:?}");
assert!(bg.b < 0.01, "{bg:?}");
}
#[test]
fn border_color_rgba_var_expression_resolves_for_unparsed_property() {
let css = ":root { --bs-success-rgb: 25, 135, 84; --bs-border-opacity: 1; } .x { border-width: 1px; border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
let border = style
.border_color
.expect("expected resolved border-color expression");
assert!((border.r - (25.0 / 255.0)).abs() < 0.01);
assert!((border.g - (135.0 / 255.0)).abs() < 0.01);
assert!((border.b - (84.0 / 255.0)).abs() < 0.01);
}
#[test]
fn border_color_four_value_shorthand_maps_clockwise() {
let css = ".x { border: 12px solid; border-color: rgb(200, 20, 20) rgb(20, 160, 40) rgb(30, 70, 200) rgb(220, 180, 30); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(style.border_color.is_none());
assert_eq!(
style.border_top_color,
Some(Color::rgb(200.0 / 255.0, 20.0 / 255.0, 20.0 / 255.0))
);
assert_eq!(
style.border_right_color,
Some(Color::rgb(20.0 / 255.0, 160.0 / 255.0, 40.0 / 255.0))
);
assert_eq!(
style.border_bottom_color,
Some(Color::rgb(30.0 / 255.0, 70.0 / 255.0, 200.0 / 255.0))
);
assert_eq!(
style.border_left_color,
Some(Color::rgb(220.0 / 255.0, 180.0 / 255.0, 30.0 / 255.0))
);
}
#[test]
fn border_rgba_preserves_source_color_and_per_edge_alpha() {
let resolver = StyleResolver::new(
".uniform { border: 20px solid rgba(0, 0, 0, .35); } .sides { border-style: solid; border-color: rgba(213, 0, 0, .58) rgba(0, 121, 107, .58) rgba(41, 98, 255, .58) rgba(255, 109, 0, .58); }",
);
let root = resolver.default_style();
let uniform = resolver.compute_style(&element("div", None, &["uniform"]), &root, None, &[]);
let sides = resolver.compute_style(&element("div", None, &["sides"]), &root, None, &[]);
assert_eq!(uniform.border_top_color, Some(Color::BLACK));
assert!((uniform.border_top_alpha - 0.35).abs() < 1.0e-6);
assert_eq!(
sides.border_top_color,
Some(Color::rgb(213.0 / 255.0, 0.0, 0.0))
);
assert_eq!(
sides.border_right_color,
Some(Color::rgb(0.0, 121.0 / 255.0, 107.0 / 255.0))
);
assert!(
[
sides.border_top_alpha,
sides.border_right_alpha,
sides.border_bottom_alpha,
sides.border_left_alpha,
]
.into_iter()
.all(|alpha| (alpha - 0.58).abs() < 1.0e-6)
);
}
#[test]
fn border_width_keywords_match_css_pixel_contract() {
let css = ".x { border-style: solid; border-width: thin medium thick 8px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_width.top,
LengthSpec::Absolute(Pt::from_f32(0.75))
);
assert_eq!(
style.border_width.right,
LengthSpec::Absolute(Pt::from_f32(2.25))
);
assert_eq!(
style.border_width.bottom,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(6.0))
);
}
#[test]
fn border_radius_four_value_shorthand_maps_corners_clockwise() {
let css = ".x { border-radius: 10px 5px 0 2px; }
.variable { --radius: 8px 4px; border-radius: var(--radius); }
.fallback { border-radius: var(--missing, 6px / 3px); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(7.5))
);
assert_eq!(
style.border_radius.horizontal.top_right,
LengthSpec::Absolute(Pt::from_f32(3.75))
);
assert_eq!(
style.border_radius.horizontal.bottom_right,
LengthSpec::Absolute(Pt::ZERO)
);
assert_eq!(
style.border_radius.horizontal.bottom_left,
LengthSpec::Absolute(Pt::from_f32(1.5))
);
assert_eq!(style.border_radius.horizontal, style.border_radius.vertical);
let variable = element("div", None, &["variable"]);
let variable_style = resolver.compute_style(&variable, &root, None, &[]);
assert_eq!(
variable_style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(6.0))
);
assert_eq!(
variable_style.border_radius.horizontal.top_right,
LengthSpec::Absolute(Pt::from_f32(3.0))
);
assert_eq!(
variable_style.border_radius.horizontal,
variable_style.border_radius.vertical
);
let fallback = element("div", None, &["fallback"]);
let fallback_style = resolver.compute_style(&fallback, &root, None, &[]);
assert_eq!(
fallback_style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(4.5))
);
assert_eq!(
fallback_style.border_radius.vertical.top_left,
LengthSpec::Absolute(Pt::from_f32(2.25))
);
}
#[test]
fn border_radius_slash_syntax_maps_elliptical_axes() {
let css = ".x { border-radius: 10px 20px / 30px 40px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(7.5))
);
assert_eq!(
style.border_radius.horizontal.top_right,
LengthSpec::Absolute(Pt::from_f32(15.0))
);
assert_eq!(
style.border_radius.horizontal.bottom_right,
LengthSpec::Absolute(Pt::from_f32(7.5))
);
assert_eq!(
style.border_radius.horizontal.bottom_left,
LengthSpec::Absolute(Pt::from_f32(15.0))
);
assert_eq!(
style.border_radius.vertical.top_left,
LengthSpec::Absolute(Pt::from_f32(22.5))
);
assert_eq!(
style.border_radius.vertical.top_right,
LengthSpec::Absolute(Pt::from_f32(30.0))
);
assert_eq!(
style.border_radius.vertical.bottom_right,
LengthSpec::Absolute(Pt::from_f32(22.5))
);
assert_eq!(
style.border_radius.vertical.bottom_left,
LengthSpec::Absolute(Pt::from_f32(30.0))
);
}
#[test]
fn border_radius_invalid_unitless_value_preserves_prior_declaration() {
let css = ".x { border-radius: 32px; border-radius: 12; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(24.0))
);
assert_eq!(style.border_radius.horizontal, style.border_radius.vertical);
}
#[test]
fn border_radius_corner_longhands_update_only_the_target_corners() {
let css = ".x { border-radius: 4px; border-top-left-radius: 50px; border-bottom-right-radius: 30px 20px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(37.5))
);
assert_eq!(
style.border_radius.horizontal.bottom_right,
LengthSpec::Absolute(Pt::from_f32(22.5))
);
assert_eq!(
style.border_radius.vertical.bottom_right,
LengthSpec::Absolute(Pt::from_f32(15.0))
);
assert_eq!(
style.border_radius.horizontal.top_right,
LengthSpec::Absolute(Pt::from_f32(3.0))
);
}
#[test]
fn logical_border_radius_longhands_follow_writing_mode_and_declaration_order() {
let css = ".x { writing-mode: vertical-lr; border-start-start-radius: 24px 14px; border-top-left-radius: 7px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_radius.horizontal.top_left,
LengthSpec::Absolute(Pt::from_f32(5.25))
);
assert_eq!(
style.border_radius.vertical.top_left,
LengthSpec::Absolute(Pt::from_f32(5.25))
);
}
#[test]
fn background_image_set_selects_the_one_x_candidate() {
let css = ".x { background-image: image-set(url(\"low.png\") 1x, url(\"high.png\") 2x); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert!(matches!(
style.background_paints.first(),
Some(BackgroundPaint::Image { source }) if source == "low.png"
));
}
#[test]
fn border_none_and_hidden_preserve_style_and_zero_width() {
let css = ".x { border-width: 12px; border-style: solid none hidden solid; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_width.top,
LengthSpec::Absolute(Pt::from_f32(9.0))
);
assert_eq!(style.border_width.right, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.bottom, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(9.0))
);
assert_eq!(
style.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
assert_eq!(style.border_style.right, BorderLineStyle::None);
assert_eq!(style.border_style.bottom, BorderLineStyle::Hidden);
assert_eq!(
style.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
}
#[test]
fn border_shorthand_without_style_defaults_to_none_and_zero_width() {
let css = ".x { border: 12px rgb(20, 40, 80); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.border_width.top, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.right, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.bottom, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.left, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_style.top, BorderLineStyle::None);
assert_eq!(style.border_style.right, BorderLineStyle::None);
assert_eq!(style.border_style.bottom, BorderLineStyle::None);
assert_eq!(style.border_style.left, BorderLineStyle::None);
assert_eq!(
style.border_color,
Some(Color::rgb(20.0 / 255.0, 40.0 / 255.0, 80.0 / 255.0))
);
}
#[test]
fn border_inline_start_var_width_and_color_resolve() {
let css = ":root { --bs-border-width: 1px; --bs-border-color: #dee2e6; } .x { border-inline-start: var(--bs-border-width) solid var(--bs-border-color); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let mut root_info = element("html", None, &[]);
root_info.is_root = true;
let root_style = resolver.compute_style(&root_info, &root, None, &[]);
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root_style, None, &[root_info]);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(0.75))
);
assert_eq!(style.border_width.top, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.right, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.bottom, LengthSpec::Absolute(Pt::ZERO));
let border = style
.border_color
.expect("expected border color from border-inline-start");
assert!((border.r - (0xde as f32 / 255.0)).abs() < 0.01);
assert!((border.g - (0xe2 as f32 / 255.0)).abs() < 0.01);
assert!((border.b - (0xe6 as f32 / 255.0)).abs() < 0.01);
}
#[test]
fn direction_rtl_remaps_logical_inline_border_edges() {
let css = ".parent { direction: rtl; } .child { border-inline-start: 4px solid red; border-inline-end: 2px solid blue; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.direction, DirectionMode::Rtl);
assert_eq!(style.border_width.top, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(
style.border_width.right,
LengthSpec::Absolute(Pt::from_f32(3.0))
);
assert_eq!(style.border_width.bottom, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(1.5))
);
assert_eq!(style.border_right_color, Some(Color::rgb(1.0, 0.0, 0.0)));
assert_eq!(style.border_left_color, Some(Color::rgb(0.0, 0.0, 1.0)));
assert!(style.border_color.is_none());
}
#[test]
fn writing_mode_vertical_rl_remaps_logical_border_edges() {
let css = ".parent { writing-mode: vertical-rl; } .child { border-inline-start: 0.125in solid red; border-inline-end: 0.0625in solid blue; border-block-start: 0.09375in solid yellow; border-block-end: 0.03125in solid rgb(255, 0, 255); }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let parent_info = element("div", None, &["parent"]);
let parent_style = resolver.compute_style(&parent_info, &root, None, &[]);
let child_info = element("div", None, &["child"]);
let style = resolver.compute_style(&child_info, &parent_style, None, &[parent_info]);
assert_eq!(style.writing_mode, WritingModeMode::VerticalRl);
assert_eq!(
style.border_width.top,
LengthSpec::Absolute(Pt::from_f32(9.0))
);
assert_eq!(
style.border_width.right,
LengthSpec::Absolute(Pt::from_f32(6.75))
);
assert_eq!(
style.border_width.bottom,
LengthSpec::Absolute(Pt::from_f32(4.5))
);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(2.25))
);
assert_eq!(style.border_top_color, Some(Color::rgb(1.0, 0.0, 0.0)));
assert_eq!(style.border_right_color, Some(Color::rgb(1.0, 1.0, 0.0)));
assert_eq!(style.border_bottom_color, Some(Color::rgb(0.0, 0.0, 1.0)));
assert_eq!(style.border_left_color, Some(Color::rgb(1.0, 0.0, 1.0)));
assert_eq!(
style.border_style.top,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
assert_eq!(
style.border_style.right,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
assert_eq!(
style.border_style.bottom,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
assert_eq!(
style.border_style.left,
BorderLineStyle::Style(OutlineLineStyle::Solid)
);
}
#[test]
fn logical_and_physical_border_longhands_share_declaration_order() {
let css = ".x { border: 8px solid #d9dee3; border-inline-start: 14px solid #e63946; border-left-color: #315fce; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(10.5))
);
assert_eq!(
style.border_left_color,
Some(Color::rgb(49.0 / 255.0, 95.0 / 255.0, 206.0 / 255.0))
);
assert_eq!(
style.border_top_color,
Some(Color::rgb(217.0 / 255.0, 222.0 / 255.0, 227.0 / 255.0))
);
}
#[test]
fn sideways_lr_maps_inline_start_to_bottom_for_ltr_content() {
let css = ".x { writing-mode: sideways-lr; border-block-start: 13px solid #f4a261; border-inline-start: 11px solid #264653; border-start-start-radius: 24px 14px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(9.75))
);
assert_eq!(
style.border_width.bottom,
LengthSpec::Absolute(Pt::from_f32(8.25))
);
assert_eq!(
style.border_radius.horizontal.bottom_left,
LengthSpec::Absolute(Pt::from_f32(18.0))
);
assert_eq!(
style.border_radius.vertical.bottom_left,
LengthSpec::Absolute(Pt::from_f32(10.5))
);
}
#[test]
fn bootstrap_border_start_with_border_width_keeps_only_start_edge() {
let css =
".border-start { border-left: 1px solid #198754; } .border-3 { border-width: 3px; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["border-start", "border-3"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(style.border_width.top, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.right, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(style.border_width.bottom, LengthSpec::Absolute(Pt::ZERO));
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(2.25))
);
}
#[test]
fn border_color_is_not_promoted_when_other_active_edge_has_no_explicit_color() {
let css = ".x { border-left: 1px solid red; border-right-width: 1px; border-right-style: solid; }";
let resolver = StyleResolver::new(css);
let root = resolver.default_style();
let info = element("div", None, &["x"]);
let style = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(
style.border_width.left,
LengthSpec::Absolute(Pt::from_f32(0.75))
);
assert_eq!(
style.border_width.right,
LengthSpec::Absolute(Pt::from_f32(0.75))
);
assert!(style.border_color.is_none());
assert!(style.border_left_color.is_some());
assert!(style.border_right_color.is_none());
}
#[test]
fn inline_style_matches_stylesheet_for_table_custom_properties() {
let sheet_resolver = StyleResolver::new(
".from-sheet { border-collapse: collapse; caption-side: bottom; border-spacing: 2px 4px; table-layout: fixed; }",
);
let sheet_root = sheet_resolver.default_style();
let sheet_table = element("table", None, &["from-sheet"]);
let from_sheet = sheet_resolver.compute_style(&sheet_table, &sheet_root, None, &[]);
let inline_resolver = StyleResolver::new("");
let inline_root = inline_resolver.default_style();
let inline_table = element("table", None, &[]);
let from_inline = inline_resolver.compute_style(
&inline_table,
&inline_root,
Some(
"border-collapse: collapse; caption-side: bottom; border-spacing: 2px 4px; table-layout: fixed;",
),
&[],
);
assert_eq!(from_inline.border_collapse, from_sheet.border_collapse);
assert_eq!(from_inline.caption_side, from_sheet.caption_side);
assert_eq!(
from_inline.border_spacing.horizontal,
from_sheet.border_spacing.horizontal
);
assert_eq!(
from_inline.border_spacing.vertical,
from_sheet.border_spacing.vertical
);
assert_eq!(from_inline.table_layout, from_sheet.table_layout);
}
#[test]
fn inline_style_matches_stylesheet_for_pagination_properties() {
let sheet_resolver = StyleResolver::new(
".from-sheet { break-before: page; break-after: page; break-inside: avoid-page; orphans: 4; widows: 5; }",
);
let sheet_root = sheet_resolver.default_style();
let sheet_block = element("div", None, &["from-sheet"]);
let from_sheet = sheet_resolver.compute_style(&sheet_block, &sheet_root, None, &[]);
let inline_resolver = StyleResolver::new("");
let inline_root = inline_resolver.default_style();
let inline_block = element("div", None, &[]);
let from_inline = inline_resolver.compute_style(
&inline_block,
&inline_root,
Some(
"break-before: page; break-after: page; break-inside: avoid-page; orphans: 4; widows: 5;",
),
&[],
);
assert_eq!(from_inline.pagination, from_sheet.pagination);
}
#[test]
fn orphans_widows_inherit_and_resolve_css_wide_values() {
let resolver = StyleResolver::new(
".parent { break-before: page; break-after: page; break-inside: avoid-page; orphans: 4; widows: 5; } \
.child { } \
.override { orphans: 3; widows: 6; } \
.initial { orphans: initial; widows: initial; } \
.inherit { orphans: inherit; widows: inherit; } \
.unset { orphans: unset; widows: unset; } \
.invalid { orphans: 0; widows: -1; }",
);
let root = resolver.default_style();
let parent_info = element("section", None, &["parent"]);
let parent = resolver.compute_style(&parent_info, &root, None, &[]);
let ancestors = [parent_info.clone()];
let child =
resolver.compute_style(&element("p", None, &["child"]), &parent, None, &ancestors);
assert_eq!(child.pagination.orphans, 4);
assert_eq!(child.pagination.widows, 5);
assert_eq!(child.pagination.break_before, BreakBefore::Auto);
assert_eq!(child.pagination.break_after, BreakAfter::Auto);
assert_eq!(child.pagination.break_inside, BreakInside::Auto);
let override_style = resolver.compute_style(
&element("p", None, &["override"]),
&parent,
None,
&ancestors,
);
assert_eq!(override_style.pagination.orphans, 3);
assert_eq!(override_style.pagination.widows, 6);
let initial =
resolver.compute_style(&element("p", None, &["initial"]), &parent, None, &ancestors);
assert_eq!(initial.pagination.orphans, 2);
assert_eq!(initial.pagination.widows, 2);
let inherit =
resolver.compute_style(&element("p", None, &["inherit"]), &parent, None, &ancestors);
assert_eq!(inherit.pagination.orphans, 4);
assert_eq!(inherit.pagination.widows, 5);
let unset =
resolver.compute_style(&element("p", None, &["unset"]), &parent, None, &ancestors);
assert_eq!(unset.pagination.orphans, 4);
assert_eq!(unset.pagination.widows, 5);
let invalid =
resolver.compute_style(&element("p", None, &["invalid"]), &parent, None, &ancestors);
assert_eq!(invalid.pagination.orphans, 4);
assert_eq!(invalid.pagination.widows, 5);
}
#[test]
fn break_before_after_preserve_mdn_keywords_and_legacy_alias_subset() {
let cases = [
("avoid", BreakBefore::Avoid, BreakAfter::Avoid, false),
("always", BreakBefore::Always, BreakAfter::Always, true),
("all", BreakBefore::All, BreakAfter::All, true),
(
"avoid-page",
BreakBefore::AvoidPage,
BreakAfter::AvoidPage,
false,
),
("page", BreakBefore::Page, BreakAfter::Page, true),
("left", BreakBefore::Left, BreakAfter::Left, true),
("right", BreakBefore::Right, BreakAfter::Right, true),
("recto", BreakBefore::Recto, BreakAfter::Recto, true),
("verso", BreakBefore::Verso, BreakAfter::Verso, true),
(
"avoid-column",
BreakBefore::AvoidColumn,
BreakAfter::AvoidColumn,
false,
),
("column", BreakBefore::Column, BreakAfter::Column, false),
(
"avoid-region",
BreakBefore::AvoidRegion,
BreakAfter::AvoidRegion,
false,
),
("region", BreakBefore::Region, BreakAfter::Region, false),
];
for (value, expected_before, expected_after, forces_page) in cases {
let css = format!(".x {{ break-before: {value}; break-after: {value}; }}");
let resolver = StyleResolver::new(&css);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert_eq!(style.pagination.break_before, expected_before, "{value}");
assert_eq!(style.pagination.break_after, expected_after, "{value}");
assert_eq!(
style.pagination.break_before.forces_page(),
forces_page,
"{value}"
);
assert_eq!(
style.pagination.break_after.forces_page(),
forces_page,
"{value}"
);
}
let resolver = StyleResolver::new(
".left-right { page-break-before: left; page-break-after: right; } \
.always { page-break-before: always; page-break-after: always; } \
.avoid { page-break-before: avoid; page-break-after: avoid; } \
.invalid { break-before: column; break-after: region; page-break-before: page; page-break-after: column; }",
);
let root = resolver.default_style();
let left_right =
resolver.compute_style(&element("div", None, &["left-right"]), &root, None, &[]);
assert_eq!(left_right.pagination.break_before, BreakBefore::Left);
assert_eq!(left_right.pagination.break_after, BreakAfter::Right);
assert!(left_right.pagination.break_before.forces_page());
assert!(left_right.pagination.break_after.forces_page());
let always = resolver.compute_style(&element("div", None, &["always"]), &root, None, &[]);
assert_eq!(always.pagination.break_before, BreakBefore::Page);
assert_eq!(always.pagination.break_after, BreakAfter::Page);
let avoid = resolver.compute_style(&element("div", None, &["avoid"]), &root, None, &[]);
assert_eq!(avoid.pagination.break_before, BreakBefore::Avoid);
assert_eq!(avoid.pagination.break_after, BreakAfter::Avoid);
let invalid = resolver.compute_style(&element("div", None, &["invalid"]), &root, None, &[]);
assert_eq!(invalid.pagination.break_before, BreakBefore::Column);
assert_eq!(invalid.pagination.break_after, BreakAfter::Region);
}
#[test]
fn break_inside_preserves_mdn_keywords_and_legacy_alias_subset() {
let resolver = StyleResolver::new(
".avoid { break-inside: avoid; } \
.page { break-inside: avoid-page; } \
.column { break-inside: avoid-column; } \
.region { break-inside: avoid-region; } \
.legacy { page-break-inside: avoid; } \
.legacy-invalid { break-inside: avoid; page-break-inside: avoid-page; }",
);
let root = resolver.default_style();
let avoid = resolver.compute_style(&element("div", None, &["avoid"]), &root, None, &[]);
assert_eq!(avoid.pagination.break_inside, BreakInside::Avoid);
let page = resolver.compute_style(&element("div", None, &["page"]), &root, None, &[]);
assert_eq!(page.pagination.break_inside, BreakInside::AvoidPage);
let column = resolver.compute_style(&element("div", None, &["column"]), &root, None, &[]);
assert_eq!(column.pagination.break_inside, BreakInside::AvoidColumn);
let region = resolver.compute_style(&element("div", None, &["region"]), &root, None, &[]);
assert_eq!(region.pagination.break_inside, BreakInside::AvoidRegion);
let legacy = resolver.compute_style(&element("div", None, &["legacy"]), &root, None, &[]);
assert_eq!(legacy.pagination.break_inside, BreakInside::Avoid);
let legacy_invalid =
resolver.compute_style(&element("div", None, &["legacy-invalid"]), &root, None, &[]);
assert_eq!(legacy_invalid.pagination.break_inside, BreakInside::Avoid);
}
#[test]
fn box_decoration_break_parses_clone_slice_and_webkit_alias() {
let resolver = StyleResolver::new(
".clone { box-decoration-break: clone; } \
.slice { box-decoration-break: slice; } \
.webkit { -webkit-box-decoration-break: clone; }",
);
let root = resolver.default_style();
let clone = resolver.compute_style(&element("div", None, &["clone"]), &root, None, &[]);
assert_eq!(clone.box_decoration_break, BoxDecorationBreak::Clone);
let slice = resolver.compute_style(&element("div", None, &["slice"]), &root, None, &[]);
assert_eq!(slice.box_decoration_break, BoxDecorationBreak::Slice);
let webkit = resolver.compute_style(&element("div", None, &["webkit"]), &root, None, &[]);
assert_eq!(webkit.box_decoration_break, BoxDecorationBreak::Clone);
}
#[test]
fn border_image_shorthand_parses_nine_slice_components() {
let resolver = StyleResolver::new(
".x { border: 8px solid transparent; border-image: linear-gradient(to right, red, blue) 10% 20% fill / 3 4px / .25em 2 round repeat; }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(matches!(
style.border_image.source,
Some(BackgroundPaint::LinearGradient { .. })
));
assert_eq!(
style.border_image.slice,
[
BorderImageSliceValue::Percent(0.1),
BorderImageSliceValue::Percent(0.2),
BorderImageSliceValue::Percent(0.1),
BorderImageSliceValue::Percent(0.2),
]
);
assert!(style.border_image.fill);
assert_eq!(
style.border_image.width,
[
BorderImageWidthValue::Number(3.0),
BorderImageWidthValue::Length(LengthSpec::Absolute(Pt::from_f32(3.0))),
BorderImageWidthValue::Number(3.0),
BorderImageWidthValue::Length(LengthSpec::Absolute(Pt::from_f32(3.0))),
]
);
assert!(matches!(
style.border_image.outset[0],
BorderImageOutsetValue::Length(LengthSpec::Em(value)) if (value - 0.25).abs() < 1.0e-6
));
assert_eq!(
style.border_image.outset[1],
BorderImageOutsetValue::Number(2.0)
);
assert_eq!(style.border_image.repeat_x, BorderImageRepeatMode::Round);
assert_eq!(style.border_image.repeat_y, BorderImageRepeatMode::Repeat);
}
#[test]
fn border_shorthand_resets_border_image_before_later_source_longhand() {
let resolver = StyleResolver::new(
".x { border-image: linear-gradient(red, blue) 12 / 5 / 3 round; border: 10px solid #123; border-image-source: linear-gradient(orange, green); }",
);
let root = resolver.default_style();
let style = resolver.compute_style(&element("div", None, &["x"]), &root, None, &[]);
assert!(matches!(
style.border_image.source,
Some(BackgroundPaint::LinearGradient { .. })
));
assert_eq!(
style.border_image.slice,
[BorderImageSliceValue::Percent(1.0); 4]
);
assert_eq!(
style.border_image.width,
[BorderImageWidthValue::Number(1.0); 4]
);
assert_eq!(
style.border_image.outset,
[BorderImageOutsetValue::Number(0.0); 4]
);
assert_eq!(style.border_image.repeat_x, BorderImageRepeatMode::Stretch);
assert_eq!(style.border_image.repeat_y, BorderImageRepeatMode::Stretch);
}
#[test]
fn text_emphasis_shorthand_position_and_inheritance_compute() {
let resolver = StyleResolver::new(
".em { color: #111; text-emphasis: filled dot #d7263d; text-emphasis-position: under right; } \
.current { color: rgb(0, 128, 0); text-emphasis: filled dot; }",
);
let root = resolver.default_style();
let emphasized = resolver.compute_style(&element("span", None, &["em"]), &root, None, &[]);
assert_eq!(
emphasized.text_emphasis_style,
TextEmphasisStyleMode::FilledDot
);
assert_eq!(
emphasized.text_emphasis_position,
TextEmphasisPositionMode::Under
);
assert_eq!(
emphasized.text_emphasis_color,
Color::rgb(215.0 / 255.0, 38.0 / 255.0, 61.0 / 255.0)
);
let inherited = resolver.compute_style(&element("span", None, &[]), &emphasized, None, &[]);
assert_eq!(
inherited.text_emphasis_style,
TextEmphasisStyleMode::FilledDot
);
assert_eq!(
inherited.text_emphasis_position,
TextEmphasisPositionMode::Under
);
assert_eq!(
inherited.text_emphasis_color,
emphasized.text_emphasis_color
);
let current =
resolver.compute_style(&element("span", None, &["current"]), &root, None, &[]);
assert_eq!(current.text_emphasis_color, current.color);
}
#[test]
fn first_letter_initial_letter_value_is_non_inherited() {
let resolver = StyleResolver::new(
"p::first-letter { initial-letter: 2; color: #d7263d; font-weight: bold; }",
);
let root = resolver.default_style();
let info = element("p", None, &[]);
let paragraph = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(paragraph.initial_letter, None);
let first = resolver
.compute_pseudo_style(&info, ¶graph, &[], PseudoTarget::FirstLetter)
.expect("first-letter style");
assert_eq!(
first.initial_letter,
Some(InitialLetterValue { size: 2.0, sink: 2 })
);
assert_eq!(first.font_weight, 700);
assert_eq!(
first.color,
Color::rgb(215.0 / 255.0, 38.0 / 255.0, 61.0 / 255.0)
);
let child = resolver.compute_style(&element("span", None, &[]), &first, None, &[]);
assert_eq!(child.initial_letter, None);
}
#[test]
fn first_line_pseudo_resolves_text_metrics_paint_and_transform() {
let resolver = StyleResolver::new(
"p { font-size: 18px; line-height: 26px; color: #111111; } \
p::first-line { font-size: 34px; line-height: 42px; color: #d7263d; \
background: #ffeb3b; letter-spacing: 5px; text-transform: uppercase; }",
);
let root = resolver.default_style();
let info = element("p", None, &[]);
let paragraph = resolver.compute_style(&info, &root, None, &[]);
assert_eq!(paragraph.font_size, Pt::from_f32(13.5));
assert_eq!(paragraph.background_color, None);
assert_eq!(paragraph.text_transform, TextTransformMode::None);
let first = resolver
.compute_pseudo_style(&info, ¶graph, &[], PseudoTarget::FirstLine)
.expect("first-line style");
let first_text = first.to_text_style();
assert_eq!(first_text.font_size, Pt::from_f32(25.5));
assert_eq!(first_text.line_height, Pt::from_f32(31.5));
assert_eq!(first_text.letter_spacing, Pt::from_f32(3.75));
assert_eq!(first.text_transform, TextTransformMode::Uppercase);
assert_eq!(
first.color,
Color::rgb(215.0 / 255.0, 38.0 / 255.0, 61.0 / 255.0)
);
assert_eq!(
first.background_color,
Some(Color::rgb(1.0, 235.0 / 255.0, 59.0 / 255.0))
);
}
}