1use super::builder::resolve_css_len;
4use crate::engine::animation::types::KeyframeStop as RuntimeKeyframeStop;
5use crate::engine::animation::types::KeyframesRule as RuntimeKeyframesRule;
6use crate::engine::css::parser::{AtQuery, ComplexSelector, CssNode, CssNodeType, RangeOperator};
7use crate::engine::css::values::{CssIdent, CssValue};
8use crate::engine::layouter::types::ColorScheme;
9
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13pub(super) type Properties = HashMap<String, ResolvedDeclaration>;
14
15struct Declaration {
16 name: String,
17 value: CssValue,
18 important: bool,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum StyleOrigin {
24 UserAgent,
26 Author,
28}
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
36pub enum PseudoElement {
37 #[default]
38 None,
39 Before,
40 After,
41}
42
43impl PseudoElement {
44 pub fn from_name(name: &str) -> Option<Self> {
47 match name.to_ascii_lowercase().as_str() {
48 "before" => Some(Self::Before),
49 "after" => Some(Self::After),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
74pub struct ResolvedDeclaration {
75 pub selector: Arc<ComplexSelector>,
76 pub pseudo: PseudoElement,
78 pub name: String,
79 pub value: CssValue,
80 pub specificity: (u32, u32, u32),
81 pub order: usize,
82 pub important: bool,
83 pub origin: StyleOrigin,
84 pub media_queries: Arc<Vec<AtQuery>>,
87}
88
89pub type ResolvedStyles = Vec<ResolvedDeclaration>;
90
91pub type KeyframesRegistry = HashMap<String, RuntimeKeyframesRule>;
93
94#[derive(Debug, Clone, Default)]
96pub struct StylesheetResolution {
97 pub declarations: ResolvedStyles,
98 pub keyframes: KeyframesRegistry,
99}
100
101impl StylesheetResolution {
102 pub fn append(&mut self, mut other: StylesheetResolution) {
103 append_resolved_styles(
104 &mut self.declarations,
105 std::mem::take(&mut other.declarations),
106 );
107 for (name, rule) in other.keyframes {
110 self.keyframes.insert(name, rule);
111 }
112 }
113}
114
115#[derive(Debug, Clone, Default)]
130pub struct RuleSet {
131 declarations: Vec<ResolvedDeclaration>,
132 groups: Vec<SelectorGroup>,
135 id_rules: HashMap<String, Vec<usize>>,
136 class_rules: HashMap<String, Vec<usize>>,
137 tag_rules: HashMap<String, Vec<usize>>,
138 attribute_rules: HashMap<String, Vec<usize>>,
139 universal_rules: Vec<usize>,
140}
141
142#[derive(Debug, Clone)]
146pub struct SelectorGroup {
147 pub selector: Arc<ComplexSelector>,
148 pub decls: Vec<usize>,
149}
150
151impl RuleSet {
152 pub fn from_declarations(
154 declarations: &[ResolvedDeclaration],
155 media_env: &MediaEnvironment,
156 ) -> Self {
157 let mut groups = Vec::<SelectorGroup>::new();
161 let mut group_by_selector: HashMap<Arc<ComplexSelector>, usize> = HashMap::new();
162 for (idx, decl) in declarations.iter().enumerate() {
163 if !decl.matches_media(media_env) {
165 continue;
166 }
167 match group_by_selector.get(&decl.selector) {
168 Some(&group_idx) => groups[group_idx].decls.push(idx),
169 None => {
170 let group_idx = groups.len();
171 groups.push(SelectorGroup {
172 selector: Arc::clone(&decl.selector),
173 decls: vec![idx],
174 });
175 group_by_selector.insert(Arc::clone(&decl.selector), group_idx);
176 }
177 }
178 }
179
180 let mut id_rules: HashMap<String, Vec<usize>> = HashMap::new();
181 let mut class_rules: HashMap<String, Vec<usize>> = HashMap::new();
182 let mut tag_rules: HashMap<String, Vec<usize>> = HashMap::new();
183 let mut attribute_rules: HashMap<String, Vec<usize>> = HashMap::new();
184 let mut universal_rules = Vec::new();
185
186 for (group_idx, group) in groups.iter().enumerate() {
187 if let Some(subject_part) = group.selector.parts.first() {
189 let sel = &subject_part.selector;
190 if let Some(id) = &sel.id {
191 id_rules.entry(id.clone()).or_default().push(group_idx);
192 } else if let Some(first_class) = sel.classes.first() {
193 class_rules
195 .entry(first_class.clone())
196 .or_default()
197 .push(group_idx);
198 } else if let Some(tag) = &sel.tag {
199 tag_rules.entry(tag.clone()).or_default().push(group_idx);
200 } else if let Some(first_attr) = sel.attributes.first() {
201 attribute_rules
202 .entry(first_attr.name.clone())
203 .or_default()
204 .push(group_idx);
205 } else {
206 universal_rules.push(group_idx);
207 }
208 } else {
209 universal_rules.push(group_idx);
210 }
211 }
212
213 Self {
214 declarations: declarations.to_vec(),
215 groups,
216 id_rules,
217 class_rules,
218 tag_rules,
219 attribute_rules,
220 universal_rules,
221 }
222 }
223
224 pub fn query_candidates(
227 &self,
228 element: &crate::engine::css::matcher::ElementInfo,
229 ) -> impl Iterator<Item = &SelectorGroup> {
230 let universal = self.universal_rules.iter().map(|&g| &self.groups[g]);
231
232 let id = element
233 .id
234 .as_ref()
235 .and_then(|id_str| self.id_rules.get(id_str.as_ref()))
236 .into_iter()
237 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
238
239 let tag = self
240 .tag_rules
241 .get(element.tag_name.as_ref())
242 .into_iter()
243 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
244
245 let classes = element
246 .classes
247 .iter()
248 .filter_map(|class_str| self.class_rules.get(class_str.as_ref()))
249 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
250
251 let attributes = element
252 .attributes
253 .iter()
254 .filter_map(|(name, _)| self.attribute_rules.get(name.as_ref()))
255 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
256
257 universal
258 .chain(id)
259 .chain(tag)
260 .chain(classes)
261 .chain(attributes)
262 }
263
264 pub fn declarations(&self) -> &[ResolvedDeclaration] {
265 &self.declarations
266 }
267}
268
269impl ResolvedDeclaration {
270 pub fn outranks(&self, other: &Self) -> bool {
272 (self.important, self.origin, self.specificity, self.order)
273 > (
274 other.important,
275 other.origin,
276 other.specificity,
277 other.order,
278 )
279 }
280
281 pub fn matches_media(&self, environment: &MediaEnvironment) -> bool {
283 self.media_queries
284 .iter()
285 .all(|query| MediaEvaluator::evaluate(query, environment))
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq)]
291pub struct MediaEnvironment {
292 pub viewport_width: f32,
294 pub viewport_height: f32,
296 pub color_scheme: ColorScheme,
298}
299
300impl MediaEnvironment {
301 pub fn new(viewport: (f32, f32), color_scheme: ColorScheme) -> Self {
302 Self {
303 viewport_width: viewport.0,
304 viewport_height: viewport.1,
305 color_scheme,
306 }
307 }
308
309 pub fn viewport(&self) -> (f32, f32) {
311 (self.viewport_width, self.viewport_height)
312 }
313}
314
315pub fn evaluate_media_query(query: &AtQuery, environment: &MediaEnvironment) -> bool {
319 MediaEvaluator::evaluate(query, environment)
320}
321
322pub fn filter_media<'a>(
324 styles: &'a ResolvedStyles,
325 environment: &'a MediaEnvironment,
326) -> impl Iterator<Item = &'a ResolvedDeclaration> {
327 styles
328 .iter()
329 .filter(move |declaration| declaration.matches_media(environment))
330}
331
332pub fn append_resolved_styles(target: &mut ResolvedStyles, mut incoming: ResolvedStyles) {
334 let next_order = target
335 .iter()
336 .map(|declaration| declaration.order)
337 .max()
338 .map_or(0, |order| order + 1);
339 for declaration in &mut incoming {
340 declaration.order += next_order;
341 }
342 target.extend(incoming);
343}
344
345pub fn resolve_inline_style(style_attr: &str) -> Vec<(String, CssValue, bool)> {
352 let mut parser = crate::engine::css::parser::Parser::new(style_attr);
353 let Ok(nodes) = parser.parse_declarations() else {
354 return Vec::new();
355 };
356
357 let declarations = DeclarationResolver::collect(&nodes);
358 let mut custom_properties = Properties::new();
359 for declaration in &declarations {
360 if declaration.name.starts_with("--") {
361 set_inline_custom_property(
362 &mut custom_properties,
363 declaration.name.clone(),
364 declaration.value.clone(),
365 declaration.important,
366 );
367 }
368 }
369
370 declarations
371 .into_iter()
372 .map(|declaration| {
373 let value = if declaration.name.starts_with("--") {
374 declaration.value
375 } else {
376 DeclarationResolver::resolve_var(
377 &declaration.value,
378 &custom_properties,
379 &mut HashSet::new(),
380 )
381 .unwrap_or(declaration.value)
382 };
383 (declaration.name, value, declaration.important)
384 })
385 .collect()
386}
387
388pub fn resolve_inline_value(value: &str) -> Option<CssValue> {
389 let mut tokenizer = crate::engine::css::tokenizer::Tokenizer::new(value);
390 let mut tokens = Vec::new();
391
392 loop {
393 let token = tokenizer.next_token();
394 if token == crate::engine::css::tokenizer::Token::EOF {
395 break;
396 }
397
398 tokens.push(token);
399 }
400
401 let Ok(value) = crate::engine::css::parser::Parser::parse_tokens_to_css_value(tokens) else {
402 return None;
403 };
404
405 Some(value)
406}
407
408pub(super) fn set_inline_custom_property(
412 properties: &mut Properties,
413 name: String,
414 value: CssValue,
415 important: bool,
416) {
417 if properties
418 .get(&name)
419 .is_some_and(|current| current.important && !important)
420 {
421 return;
422 }
423
424 properties.insert(
425 name.clone(),
426 ResolvedDeclaration {
427 selector: Arc::new(ComplexSelector { parts: Vec::new() }),
428 pseudo: PseudoElement::None,
429 name,
430 value,
431 specificity: (u32::MAX, u32::MAX, u32::MAX),
432 order: usize::MAX,
433 important,
434 origin: StyleOrigin::Author,
435 media_queries: Arc::new(Vec::new()),
436 },
437 );
438}
439
440pub struct CssResolver;
445
446impl CssResolver {
447 pub fn resolve(stylesheet: &CssNode) -> ResolvedStyles {
449 Self::resolve_stylesheet(stylesheet).declarations
450 }
451
452 pub fn resolve_with_origin(stylesheet: &CssNode, origin: StyleOrigin) -> ResolvedStyles {
454 Self::resolve_stylesheet_with_origin(stylesheet, origin).declarations
455 }
456
457 pub fn resolve_stylesheet(stylesheet: &CssNode) -> StylesheetResolution {
459 Self::resolve_stylesheet_with_origin(stylesheet, StyleOrigin::Author)
460 }
461
462 pub fn resolve_stylesheet_with_origin(
464 stylesheet: &CssNode,
465 origin: StyleOrigin,
466 ) -> StylesheetResolution {
467 let mut styles = Vec::new();
468 let mut keyframes = KeyframesRegistry::new();
469 let mut order = 0;
470 Self::walk(
471 stylesheet,
472 &mut styles,
473 &mut keyframes,
474 &mut order,
475 origin,
476 &mut Vec::new(),
477 &[],
478 );
479 StylesheetResolution {
480 declarations: styles,
481 keyframes,
482 }
483 }
484
485 pub fn resolve_inline_style(style: &str) -> ResolvedStyles {
491 let source = format!("* {{ {style} }}");
492 let Ok(stylesheet) = crate::engine::css::parser::Parser::new(&source).parse() else {
493 return Vec::new();
494 };
495 let mut declarations = Self::resolve_with_origin(&stylesheet, StyleOrigin::Author);
496 for declaration in &mut declarations {
497 declaration.specificity = (u32::MAX, u32::MAX, u32::MAX);
498 declaration.order = usize::MAX;
499 }
500 declarations
501 }
502
503 fn walk(
504 node: &CssNode,
505 styles: &mut ResolvedStyles,
506 keyframes: &mut KeyframesRegistry,
507 order: &mut usize,
508 origin: StyleOrigin,
509 media_queries: &mut Vec<AtQuery>,
510 parent_selectors: &[ComplexSelector],
511 ) {
512 match node.node() {
513 CssNodeType::Keyframes { name, stops } => {
514 if !name.is_empty() {
515 let runtime_stops = Self::convert_keyframe_stops(stops);
516 keyframes.insert(
517 name.clone(),
518 RuntimeKeyframesRule {
519 name: name.clone(),
520 stops: runtime_stops,
521 },
522 );
523 }
524 return;
525 }
526 CssNodeType::AtRule { name, params } => {
527 if name.eq_ignore_ascii_case("supports") && !SupportsEvaluator::evaluate(params) {
528 return;
529 }
530
531 let is_media = name.eq_ignore_ascii_case("media");
532 if is_media {
533 media_queries.push(params.clone());
534 }
535 for child in node.children() {
536 Self::walk(
537 child,
538 styles,
539 keyframes,
540 order,
541 origin,
542 media_queries,
543 parent_selectors,
544 );
545 }
546 if is_media {
547 media_queries.pop();
548 }
549 return;
550 }
551 _ => {}
552 }
553
554 let resolved_selectors =
555 Self::resolve_rule(node, styles, order, origin, media_queries, parent_selectors);
556
557 for child in node.children() {
558 Self::walk(
559 child,
560 styles,
561 keyframes,
562 order,
563 origin,
564 media_queries,
565 &resolved_selectors,
566 );
567 }
568 }
569
570 fn convert_keyframe_stops(
571 stops: &[crate::engine::css::parser::KeyframeStop],
572 ) -> Vec<RuntimeKeyframeStop> {
573 let mut out = Vec::new();
574 for stop in stops {
575 let declarations: Vec<(String, CssValue)> = stop
576 .declarations
577 .iter()
578 .filter(|(_, _, important)| !*important)
579 .map(|(name, value, _)| (name.clone(), value.clone()))
580 .collect();
581 for selector in &stop.selectors {
582 if let Some(offset) = Self::keyframe_selector_offset(&selector.0) {
583 out.push(RuntimeKeyframeStop {
584 offset,
585 declarations: declarations.clone(),
586 });
587 }
588 }
589 }
590 out.sort_by(|a, b| {
591 a.offset
592 .partial_cmp(&b.offset)
593 .unwrap_or(std::cmp::Ordering::Equal)
594 });
595 out
596 }
597
598 fn keyframe_selector_offset(value: &CssValue) -> Option<f32> {
599 match value {
600 CssValue::Keyword(k) if k.eq_ignore_ascii_case("from") => Some(0.0),
601 CssValue::Keyword(k) if k.eq_ignore_ascii_case("to") => Some(1.0),
602 CssValue::Length(v, crate::engine::css::values::Unit::Percent) => {
603 Some((*v / 100.0).clamp(0.0, 1.0))
604 }
605 CssValue::Number(v) => {
606 if *v <= 1.0 {
608 Some(v.clamp(0.0, 1.0))
609 } else {
610 Some((v / 100.0).clamp(0.0, 1.0))
611 }
612 }
613 _ => None,
614 }
615 }
616
617 fn resolve_rule(
618 node: &CssNode,
619 styles: &mut ResolvedStyles,
620 order: &mut usize,
621 origin: StyleOrigin,
622 media_queries: &[AtQuery],
623 parent_selectors: &[ComplexSelector],
624 ) -> Vec<ComplexSelector> {
625 let CssNodeType::Rule { selectors } = node.node() else {
626 return vec![];
627 };
628
629 let resolved_selectors: Vec<ComplexSelector> = selectors
630 .iter()
631 .flat_map(|child| {
632 if parent_selectors.is_empty() {
633 vec![child.clone()]
634 } else {
635 parent_selectors
636 .iter()
637 .map(|parent| parent.nest(child))
638 .collect::<Vec<_>>()
639 }
640 })
641 .collect();
642
643 let declarations = DeclarationResolver::collect(node.children());
644
645 for selector in resolved_selectors.iter() {
650 let subject_pseudo = selector
651 .parts
652 .first()
653 .and_then(|part| part.selector.pseudo_element.as_deref())
654 .and_then(PseudoElement::from_name)
655 .filter(|_| {
659 selector.parts[1..]
660 .iter()
661 .all(|part| part.selector.pseudo_element.is_none())
662 });
663 let selector_for_push = if subject_pseudo.is_some() {
664 let mut selector = selector.clone();
665 for part in &mut selector.parts {
666 part.selector.pseudo_element = None;
667 }
668 selector
669 } else {
670 selector.clone()
671 };
672 Self::push_resolved(
673 &selector_for_push,
674 subject_pseudo.unwrap_or(PseudoElement::None),
675 &declarations,
676 styles,
677 order,
678 origin,
679 media_queries,
680 );
681 }
682
683 resolved_selectors
684 }
685
686 fn push_resolved(
687 selector: &ComplexSelector,
688 pseudo: PseudoElement,
689 declarations: &[Declaration],
690 styles: &mut ResolvedStyles,
691 order: &mut usize,
692 origin: StyleOrigin,
693 media_queries: &[AtQuery],
694 ) {
695 let specificity = selector.specificity();
696 let selector = Arc::new(selector.clone());
697 let media_queries = Arc::new(media_queries.to_vec());
698
699 for decl in declarations {
700 styles.push(ResolvedDeclaration {
701 selector: Arc::clone(&selector),
702 pseudo,
703 name: decl.name.clone(),
704 value: decl.value.clone(),
705 specificity,
706 order: *order,
707 important: decl.important,
708 origin,
709 media_queries: Arc::clone(&media_queries),
710 });
711 *order += 1;
712 }
713 }
714}
715
716struct MediaEvaluator;
717
718impl MediaEvaluator {
719 fn evaluate(query: &AtQuery, environment: &MediaEnvironment) -> bool {
720 match query {
721 AtQuery::Group(items) => Self::evaluate_group(items, environment),
722 item => Self::evaluate_clause(std::slice::from_ref(item), environment),
723 }
724 }
725
726 fn evaluate_clause(items: &[AtQuery], environment: &MediaEnvironment) -> bool {
727 if items.is_empty() {
728 return false;
729 }
730 let negate = matches!(items.first(), Some(AtQuery::Keyword(keyword)) if keyword.eq_ignore_ascii_case("not"));
731 let mut saw_operand = false;
732 let matches = items
733 .iter()
734 .skip(usize::from(negate))
735 .all(|item| match item {
736 AtQuery::Keyword(keyword)
737 if keyword.eq_ignore_ascii_case("and")
738 || keyword.eq_ignore_ascii_case("only") =>
739 {
740 true
741 }
742 AtQuery::Keyword(keyword)
743 if keyword.eq_ignore_ascii_case("all")
744 || keyword.eq_ignore_ascii_case("screen") =>
745 {
746 saw_operand = true;
747 true
748 }
749 AtQuery::Keyword(keyword) if keyword.eq_ignore_ascii_case("print") => {
750 saw_operand = true;
751 false
752 }
753 AtQuery::Keyword(_) => {
754 saw_operand = true;
755 false
756 }
757 AtQuery::Condition { name, value } => {
758 saw_operand = true;
759 Self::evaluate_condition(name, value, environment)
760 }
761 AtQuery::Range { left, name, right } => {
762 saw_operand = true;
763 Self::evaluate_range(left.as_ref(), name, right.as_ref(), environment)
764 }
765 AtQuery::Group(group) => {
766 saw_operand = true;
767 Self::evaluate_group(group, environment)
768 }
769 });
770 if !saw_operand {
771 return false;
772 }
773 if negate { !matches } else { matches }
774 }
775
776 fn evaluate_group(items: &[AtQuery], environment: &MediaEnvironment) -> bool {
777 items
778 .split(|item| matches!(item, AtQuery::Keyword(keyword) if keyword == ","))
779 .any(|clause| Self::evaluate_clause(clause, environment))
780 }
781
782 fn evaluate_range(
783 left: Option<&(CssValue, RangeOperator)>,
784 name: &str,
785 right: Option<&(RangeOperator, CssValue)>,
786 environment: &MediaEnvironment,
787 ) -> bool {
788 let name = name.to_ascii_lowercase();
789
790 let actual = match name.as_str() {
791 "width" => environment.viewport_width,
792 "height" => environment.viewport_height,
793 _ => return false,
794 };
795
796 if let Some((value, operator)) = left {
797 let Some(expected) = Self::length_px(value, environment) else {
798 return false;
799 };
800
801 if !Self::compare_range(expected, *operator, actual) {
802 return false;
803 }
804 }
805
806 if let Some((operator, value)) = right {
807 let Some(expected) = Self::length_px(value, environment) else {
808 return false;
809 };
810
811 if !Self::compare_range(actual, *operator, expected) {
812 return false;
813 }
814 }
815
816 true
817 }
818
819 fn compare_range(actual: f32, operator: RangeOperator, expected: f32) -> bool {
820 match operator {
821 RangeOperator::Less => actual < expected,
822 RangeOperator::LessEqual => actual <= expected,
823 RangeOperator::Equal => (actual - expected).abs() <= f32::EPSILON,
824 RangeOperator::GreaterEqual => actual >= expected,
825 RangeOperator::Greater => actual > expected,
826 }
827 }
828
829 fn evaluate_condition(name: &str, value: &CssValue, environment: &MediaEnvironment) -> bool {
830 let name = name.to_ascii_lowercase();
831 match name.as_str() {
832 "width" | "min-width" | "max-width" => {
833 Self::compare_length(&name, value, environment.viewport_width, environment)
834 }
835 "height" | "min-height" | "max-height" => {
836 Self::compare_length(&name, value, environment.viewport_height, environment)
837 }
838 "orientation" => match value {
839 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("portrait") => {
840 environment.viewport_height >= environment.viewport_width
841 }
842 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("landscape") => {
843 environment.viewport_width > environment.viewport_height
844 }
845 _ => false,
846 },
847 "prefers-color-scheme" => match value {
848 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("dark") => {
849 environment.color_scheme == ColorScheme::Dark
850 }
851 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("light") => {
852 environment.color_scheme == ColorScheme::Light
853 }
854 _ => false,
855 },
856 _ => false,
857 }
858 }
859
860 fn compare_length(
861 name: &str,
862 value: &CssValue,
863 actual: f32,
864 environment: &MediaEnvironment,
865 ) -> bool {
866 let Some(expected) = Self::length_px(value, environment) else {
867 return false;
868 };
869 if name.starts_with("min-") {
870 actual >= expected
871 } else if name.starts_with("max-") {
872 actual <= expected
873 } else {
874 (actual - expected).abs() <= f32::EPSILON
875 }
876 }
877
878 fn length_px(value: &CssValue, environment: &MediaEnvironment) -> Option<f32> {
879 let text_flow = crate::engine::layouter::types::TextFlowStyle::default();
880 let len = resolve_css_len("media", std::slice::from_ref(value), &text_flow)?;
881 len.resolve_with(
882 None,
883 environment.viewport_width,
884 environment.viewport_height,
885 )
886 }
887}
888
889struct SupportsEvaluator;
894
895impl SupportsEvaluator {
896 fn evaluate(query: &AtQuery) -> bool {
898 match query {
899 AtQuery::Group(items) => Self::evaluate_group(items),
901 AtQuery::Condition { name, value } => Self::is_supported(name, value),
903 AtQuery::Range { .. } => false,
905 AtQuery::Keyword(_) => false,
907 }
908 }
909
910 fn evaluate_group(items: &[AtQuery]) -> bool {
918 if items.is_empty() {
919 return false;
920 }
921
922 if matches!(items.first(), Some(AtQuery::Keyword(k)) if k.eq_ignore_ascii_case("not")) {
924 return items.len() > 1 && !Self::evaluate(&AtQuery::Group(items[1..].to_vec()));
925 }
926
927 if let Some(operands) = Self::split_by_keyword(items, "and") {
929 operands.iter().all(|g| Self::evaluate(g))
930 } else if let Some(operands) = Self::split_by_keyword(items, "or") {
932 operands.iter().any(|g| Self::evaluate(g))
933 } else if items.len() == 1 {
935 Self::evaluate(&items[0])
936 } else {
937 items.iter().all(Self::evaluate)
938 }
939 }
940
941 fn is_supported(name: &str, value: &CssValue) -> bool {
942 super::builder::apply_declaration(
943 name,
944 value,
945 &mut ui_layout::Style::default(),
946 &mut super::types::ContainerStyle::default(),
947 &mut super::types::TextStyle::default(),
948 &mut super::types::TextFlowStyle::default(),
949 &ui_layout::Style::default(),
950 &super::types::ContainerStyle::default(),
951 &super::types::TextStyle::default(),
952 &super::types::TextFlowStyle::default(),
953 &mut super::types::Overflow::default(),
954 super::types::ColorScheme::Light,
955 )
956 .is_some()
957 }
958
959 fn split_by_keyword<'a>(items: &'a [AtQuery], keyword: &str) -> Option<Vec<&'a AtQuery>> {
960 let has = items
961 .iter()
962 .any(|item| matches!(item, AtQuery::Keyword(k) if k.eq_ignore_ascii_case(keyword)));
963 if !has {
964 return None;
965 }
966 Some(
967 items
968 .iter()
969 .filter(
970 |item| !matches!(item, AtQuery::Keyword(k) if k.eq_ignore_ascii_case(keyword)),
971 )
972 .collect(),
973 )
974 }
975}
976
977pub(super) struct DeclarationResolver;
982
983impl DeclarationResolver {
984 fn collect(children: &[CssNode]) -> Vec<Declaration> {
985 let mut result = Vec::new();
986
987 for child in children {
988 let CssNodeType::Declaration { name, value } = &child.node() else {
989 continue;
990 };
991
992 let (value, important) = Self::extract_important(value);
993
994 result.push(Declaration {
995 name: name.clone(),
996 value,
997 important,
998 });
999 }
1000
1001 result
1002 }
1003
1004 fn extract_important(value: &CssValue) -> (CssValue, bool) {
1009 match value {
1010 CssValue::List(list) if list.len() >= 2 => {
1011 let len = list.len();
1012 let is_important = matches!(
1013 (&list[len - 2], &list[len - 1]),
1014 (
1015 CssValue::Keyword(bang),
1016 CssValue::Keyword(ident)
1017 )
1018 if bang == "!" && ident.eq_ignore_ascii_case("important")
1019 );
1020
1021 if is_important {
1022 let value = if len - 2 == 1 {
1023 list.iter().next().unwrap().clone()
1024 } else {
1025 CssValue::List(list[..len - 2].to_vec())
1026 };
1027 (value, true)
1028 } else {
1029 (value.clone(), false)
1030 }
1031 }
1032 _ => (value.clone(), false),
1033 }
1034 }
1035
1036 pub fn resolve_var(
1037 value: &CssValue,
1038 custom_props: &Properties,
1039 visited: &mut HashSet<CssIdent>,
1040 ) -> Option<CssValue> {
1041 match value {
1042 CssValue::Function(name, args) if name == "var" => {
1044 let var_name = match args.first().and_then(|argument| argument.first()) {
1045 Some(CssValue::Keyword(name)) => name,
1046 _ => return None,
1047 };
1048
1049 if !visited.insert(var_name.clone()) {
1050 return None;
1051 }
1052
1053 let result = if let Some(v) = custom_props.get(var_name.as_str()) {
1054 Self::resolve_var(&v.value, custom_props, visited)
1055 } else if let Some(fallback) = args.get(1).and_then(|argument| argument.first()) {
1056 Self::resolve_var(fallback, custom_props, visited)
1057 } else {
1058 None
1059 };
1060
1061 visited.remove(var_name);
1062 result
1063 }
1064
1065 CssValue::Function(name, args) if name == "env" => {
1072 match args.first().and_then(|argument| argument.first()) {
1073 Some(CssValue::Keyword(_)) => {}
1074 _ => return None,
1075 }
1076
1077 let fallback: Vec<CssValue> = args[1..]
1081 .iter()
1082 .flatten()
1083 .map(|v| Self::resolve_var(v, custom_props, &mut visited.clone()))
1084 .collect::<Option<_>>()?;
1085 match fallback.len() {
1086 0 => None,
1087 1 => Some(fallback.into_iter().next().unwrap()),
1088 _ => Some(CssValue::List(fallback)),
1089 }
1090 }
1091
1092 CssValue::Function(name, args) => {
1094 let resolved_args = args
1095 .iter()
1096 .map(|argument| {
1097 argument
1098 .iter()
1099 .map(|v| Self::resolve_var(v, custom_props, &mut visited.clone()))
1100 .collect::<Option<Vec<_>>>()
1101 })
1102 .collect::<Option<Vec<_>>>()?;
1103 Some(CssValue::Function(name.clone(), resolved_args))
1104 }
1105
1106 CssValue::List(list) => {
1108 let resolved = list
1109 .iter()
1110 .map(|v| Self::resolve_var(v, custom_props, &mut visited.clone()))
1111 .collect::<Option<Vec<_>>>()?;
1112 Some(CssValue::List(resolved))
1113 }
1114
1115 _ => Some(value.clone()),
1117 }
1118 }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use super::*;
1124 use crate::engine::css::parser::Parser;
1125 use crate::engine::css::values::Unit;
1126
1127 fn resolve(css: &str, origin: StyleOrigin) -> ResolvedStyles {
1128 let stylesheet = Parser::new(css).parse().unwrap();
1129 CssResolver::resolve_with_origin(&stylesheet, origin)
1130 }
1131
1132 #[test]
1133 fn author_declaration_outranks_more_specific_user_agent_declaration() {
1134 let user_agent = resolve(
1135 r#"input[type="text"] { display: inline-block; }"#,
1136 StyleOrigin::UserAgent,
1137 );
1138 let author = resolve("input { display: block; }", StyleOrigin::Author);
1139
1140 assert!(author[0].outranks(&user_agent[0]));
1141 }
1142
1143 #[test]
1144 fn append_rebases_order_across_stylesheets() {
1145 let mut styles = resolve("input { display: inline; }", StyleOrigin::Author);
1146 let later = resolve("input { display: block; }", StyleOrigin::Author);
1147 append_resolved_styles(&mut styles, later);
1148
1149 assert!(styles[1].order > styles[0].order);
1150 assert!(styles[1].outranks(&styles[0]));
1151 }
1152
1153 #[test]
1154 fn resolve_inline_style_extracts_declarations_and_important() {
1155 let decls = resolve_inline_style("color: red; margin: 4px 8px !important; width: 10px");
1156 assert_eq!(decls.len(), 3);
1157
1158 assert_eq!(decls[0].0, "color");
1159 assert!(!decls[0].2);
1160
1161 assert_eq!(decls[1].0, "margin");
1162 assert!(decls[1].2);
1163
1164 assert_eq!(decls[2].0, "width");
1165 }
1166
1167 #[test]
1168 fn resolve_inline_style_tolerates_empty_and_malformed_input() {
1169 assert!(resolve_inline_style("").is_empty());
1170 assert!(resolve_inline_style(";;;").is_empty());
1171 }
1172
1173 #[test]
1174 fn resolve_inline_style_resolves_var() {
1175 let decls = resolve_inline_style("--accent: blue; color: var(--accent)");
1176 assert_eq!(decls.len(), 2);
1177
1178 let (_, color_value, _) = decls.iter().find(|(n, _, _)| n == "color").unwrap();
1179 assert_eq!(color_value, &CssValue::Keyword("blue".into()));
1180 }
1181
1182 #[test]
1183 fn media_width_conditions_follow_viewport() {
1184 let styles = resolve(
1185 "@media screen and (max-width: 600px) { div { color: red; } }",
1186 StyleOrigin::Author,
1187 );
1188 let narrow = MediaEnvironment::new((600.0, 800.0), ColorScheme::Light);
1189 let wide = MediaEnvironment::new((601.0, 800.0), ColorScheme::Light);
1190
1191 assert_eq!(filter_media(&styles, &narrow).count(), 1);
1192 assert_eq!(filter_media(&styles, &wide).count(), 0);
1193 }
1194
1195 #[test]
1196 fn scratch_style_adjacent_media_conditions_follow_desktop_viewport() {
1197 let stylesheet = Parser::new(
1198 "@media only screen and (max-width : 479px){#view{text-align:center}.inner{margin:0 auto;width:100%}}@media only screen and (min-width : 480px)and (max-width : 767px){#view{text-align:center}.inner{margin:0 auto;width:480px}}@media only screen and (min-width : 768px)and (max-width : 941px){#view{text-align:center}.inner{margin:0 auto;width:768px}}@media only screen and (min-width : 942px){.inner{margin:0 auto;width:942px}}html,body{display:block}",
1199 )
1200 .parse_lossy();
1201 let styles = CssResolver::resolve_with_origin(&stylesheet, StyleOrigin::Author);
1202 let desktop = MediaEnvironment::new((1280.0, 800.0), ColorScheme::Light);
1203 let declarations = filter_media(&styles, &desktop)
1204 .filter(|declaration| declaration.name == "width")
1205 .collect::<Vec<_>>();
1206
1207 assert_eq!(declarations.len(), 1);
1208 assert_eq!(declarations[0].name, "width");
1209 assert_eq!(declarations[0].value, CssValue::Length(942.0, Unit::Px));
1210 }
1211
1212 #[test]
1213 fn media_query_lists_use_or_semantics() {
1214 let styles = resolve(
1215 "@media print, (orientation: landscape) { div { color: red; } }",
1216 StyleOrigin::Author,
1217 );
1218 let landscape = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1219 let portrait = MediaEnvironment::new((600.0, 800.0), ColorScheme::Light);
1220
1221 assert_eq!(filter_media(&styles, &landscape).count(), 1);
1222 assert_eq!(filter_media(&styles, &portrait).count(), 0);
1223 }
1224
1225 #[test]
1226 fn media_color_scheme_matches_system_preference() {
1227 let styles = resolve(
1228 "@media (prefers-color-scheme: dark) { div { color: white; } }",
1229 StyleOrigin::Author,
1230 );
1231 let light = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1232 let dark = MediaEnvironment::new((800.0, 600.0), ColorScheme::Dark);
1233
1234 assert_eq!(filter_media(&styles, &light).count(), 0);
1235 assert_eq!(filter_media(&styles, &dark).count(), 1);
1236 }
1237
1238 #[test]
1239 fn empty_media_query_does_not_match() {
1240 let styles = resolve("@media { div { color: red; } }", StyleOrigin::Author);
1241 let environment = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1242
1243 assert_eq!(filter_media(&styles, &environment).count(), 0);
1244 }
1245
1246 #[test]
1247 fn test_rule_set_partitioning_and_querying() {
1248 let styles = resolve(
1249 r#"
1250 * { margin: 0; }
1251 #header { color: red; }
1252 .btn { display: inline-block; }
1253 span { font-size: 12px; }
1254 div.container { padding: 10px; }
1255 "#,
1256 StyleOrigin::Author,
1257 );
1258 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1259 let rule_set = RuleSet::from_declarations(&styles, &env);
1260 let decls_of = |group: &SelectorGroup| -> Vec<&ResolvedDeclaration> {
1261 group
1262 .decls
1263 .iter()
1264 .map(|&idx| &rule_set.declarations()[idx])
1265 .collect()
1266 };
1267
1268 let el1 = crate::engine::css::matcher::ElementInfo {
1270 tag_name: "div".into(),
1271 id: Some("header".into()),
1272 classes: vec!["btn".into()].into(),
1273 ..Default::default()
1274 };
1275 let candidates1: Vec<_> = rule_set.query_candidates(&el1).flat_map(decls_of).collect();
1276 assert!(candidates1.iter().any(|d| d.name == "margin"));
1278 assert!(candidates1.iter().any(|d| d.name == "color"));
1279 assert!(candidates1.iter().any(|d| d.name == "display"));
1280 assert!(!candidates1.iter().any(|d| d.name == "font-size"));
1282
1283 let el2 = crate::engine::css::matcher::ElementInfo {
1285 tag_name: "span".into(),
1286 id: None,
1287 classes: vec!["other".into()].into(),
1288 ..Default::default()
1289 };
1290 let candidates2: Vec<_> = rule_set.query_candidates(&el2).flat_map(decls_of).collect();
1291 assert!(candidates2.iter().any(|d| d.name == "margin"));
1293 assert!(candidates2.iter().any(|d| d.name == "font-size"));
1294 assert!(!candidates2.iter().any(|d| d.name == "color"));
1296 assert!(!candidates2.iter().any(|d| d.name == "display"));
1297 }
1298
1299 #[test]
1300 fn rule_set_groups_shared_selectors() {
1301 let styles = resolve(
1302 r#"
1303 div { color: red; font-size: 12px; }
1304 div { margin: 0; }
1305 p { padding: 1px; }
1306 p.padded { padding: 2px; }
1307 "#,
1308 StyleOrigin::Author,
1309 );
1310 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1311 let rule_set = RuleSet::from_declarations(&styles, &env);
1312
1313 let div = crate::engine::css::matcher::ElementInfo {
1316 tag_name: "div".into(),
1317 ..Default::default()
1318 };
1319 let groups: Vec<_> = rule_set.query_candidates(&div).collect();
1320 assert_eq!(groups.len(), 1);
1321 assert_eq!(groups[0].decls.len(), 3);
1322
1323 let p = crate::engine::css::matcher::ElementInfo {
1325 tag_name: "p".into(),
1326 ..Default::default()
1327 };
1328 assert_eq!(rule_set.query_candidates(&p).count(), 1);
1329
1330 let padded = crate::engine::css::matcher::ElementInfo {
1332 tag_name: "p".into(),
1333 classes: vec!["padded".into()].into(),
1334 ..Default::default()
1335 };
1336 assert_eq!(rule_set.query_candidates(&padded).count(), 2);
1337 }
1338
1339 #[test]
1340 fn rule_set_indexes_attribute_subjects() {
1341 let styles = resolve(
1342 r#"
1343 [hidden] { display: none; }
1344 [data-tip] { position: relative; }
1345 "#,
1346 StyleOrigin::Author,
1347 );
1348 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
1349 let rule_set = RuleSet::from_declarations(&styles, &env);
1350
1351 let hidden = crate::engine::css::matcher::ElementInfo {
1352 tag_name: "div".into(),
1353 attributes: vec![("hidden".into(), "".into())].into(),
1354 ..Default::default()
1355 };
1356 assert_eq!(
1357 rule_set
1358 .query_candidates(&hidden)
1359 .flat_map(|group| group.decls.iter())
1360 .count(),
1361 1,
1362 "only the [hidden] group is queried"
1363 );
1364
1365 let plain = crate::engine::css::matcher::ElementInfo {
1366 tag_name: "div".into(),
1367 ..Default::default()
1368 };
1369 assert_eq!(
1370 rule_set
1371 .query_candidates(&plain)
1372 .flat_map(|group| group.decls.iter())
1373 .count(),
1374 0,
1375 "no candidates for attribute-less element"
1376 );
1377 }
1378
1379 #[test]
1380 fn media_width_calc_rem_is_resolved() {
1381 let styles = resolve(
1382 "@media (width > calc(19rem)) { div { color: red; } }",
1383 StyleOrigin::Author,
1384 );
1385 let wide = MediaEnvironment::new((400.0, 800.0), ColorScheme::Light);
1387 let narrow = MediaEnvironment::new((200.0, 800.0), ColorScheme::Light);
1388 assert_eq!(filter_media(&styles, &wide).count(), 1);
1389 assert_eq!(filter_media(&styles, &narrow).count(), 0);
1390 }
1391
1392 #[test]
1393 fn media_range_calc_vw() {
1394 let styles = resolve(
1395 "@media (width > calc(100vw - 40px)) { div { color: red; } }",
1396 StyleOrigin::Author,
1397 );
1398 let env = MediaEnvironment::new((100.0, 800.0), ColorScheme::Light);
1400 assert_eq!(filter_media(&styles, &env).count(), 1);
1402 let env2 = MediaEnvironment::new((30.0, 800.0), ColorScheme::Light);
1403 assert_eq!(filter_media(&styles, &env2).count(), 1);
1405 }
1406
1407 #[test]
1408 fn media_min_width_rem() {
1409 let styles = resolve(
1410 "@media (min-width: 2rem) { div { color: red; } }",
1411 StyleOrigin::Author,
1412 );
1413 let env = MediaEnvironment::new((40.0, 800.0), ColorScheme::Light);
1415 assert_eq!(filter_media(&styles, &env).count(), 1);
1416 let env2 = MediaEnvironment::new((20.0, 800.0), ColorScheme::Light);
1417 assert_eq!(filter_media(&styles, &env2).count(), 0);
1418 }
1419
1420 #[test]
1421 fn media_width_calc_min_function() {
1422 let styles = resolve(
1423 "@media (width > min(800px, 90vw)) { div { color: red; } }",
1424 StyleOrigin::Author,
1425 );
1426 let env = MediaEnvironment::new((1000.0, 800.0), ColorScheme::Light);
1428 assert_eq!(filter_media(&styles, &env).count(), 1);
1429 let env2 = MediaEnvironment::new((500.0, 800.0), ColorScheme::Light);
1431 assert_eq!(filter_media(&styles, &env2).count(), 1);
1432 let env3 = MediaEnvironment::new((400.0, 800.0), ColorScheme::Light);
1434 assert_eq!(filter_media(&styles, &env3).count(), 1);
1435 let env4 = MediaEnvironment::new((300.0, 800.0), ColorScheme::Light);
1437 assert_eq!(filter_media(&styles, &env4).count(), 1);
1438 }
1439}