1use super::builder::resolve_css_len;
4use crate::engine::css::parser::{AtQuery, ComplexSelector, CssNode, CssNodeType, RangeOperator};
5use crate::engine::css::values::{CssIdent, CssValue};
6use crate::engine::layouter::types::ColorScheme;
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11pub(super) type Properties = HashMap<String, ResolvedDeclaration>;
12
13struct Declaration {
14 name: String,
15 value: CssValue,
16 important: bool,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum StyleOrigin {
22 UserAgent,
24 Author,
26}
27
28#[derive(Debug, Clone)]
47pub struct ResolvedDeclaration {
48 pub selector: Arc<ComplexSelector>,
49 pub name: String,
50 pub value: CssValue,
51 pub specificity: (u32, u32, u32),
52 pub order: usize,
53 pub important: bool,
54 pub origin: StyleOrigin,
55 pub media_queries: Arc<Vec<AtQuery>>,
58}
59
60pub type ResolvedStyles = Vec<ResolvedDeclaration>;
61
62#[derive(Debug, Clone, Default)]
77pub struct RuleSet {
78 declarations: Vec<ResolvedDeclaration>,
79 groups: Vec<SelectorGroup>,
82 id_rules: HashMap<String, Vec<usize>>,
83 class_rules: HashMap<String, Vec<usize>>,
84 tag_rules: HashMap<String, Vec<usize>>,
85 attribute_rules: HashMap<String, Vec<usize>>,
86 universal_rules: Vec<usize>,
87}
88
89#[derive(Debug, Clone)]
93pub struct SelectorGroup {
94 pub selector: Arc<ComplexSelector>,
95 pub decls: Vec<usize>,
96}
97
98impl RuleSet {
99 pub fn from_declarations(
101 declarations: &[ResolvedDeclaration],
102 media_env: &MediaEnvironment,
103 ) -> Self {
104 let mut groups = Vec::<SelectorGroup>::new();
108 let mut group_by_selector: HashMap<Arc<ComplexSelector>, usize> = HashMap::new();
109 for (idx, decl) in declarations.iter().enumerate() {
110 if !decl.matches_media(media_env) {
112 continue;
113 }
114 match group_by_selector.get(&decl.selector) {
115 Some(&group_idx) => groups[group_idx].decls.push(idx),
116 None => {
117 let group_idx = groups.len();
118 groups.push(SelectorGroup {
119 selector: Arc::clone(&decl.selector),
120 decls: vec![idx],
121 });
122 group_by_selector.insert(Arc::clone(&decl.selector), group_idx);
123 }
124 }
125 }
126
127 let mut id_rules: HashMap<String, Vec<usize>> = HashMap::new();
128 let mut class_rules: HashMap<String, Vec<usize>> = HashMap::new();
129 let mut tag_rules: HashMap<String, Vec<usize>> = HashMap::new();
130 let mut attribute_rules: HashMap<String, Vec<usize>> = HashMap::new();
131 let mut universal_rules = Vec::new();
132
133 for (group_idx, group) in groups.iter().enumerate() {
134 if let Some(subject_part) = group.selector.parts.first() {
136 let sel = &subject_part.selector;
137 if let Some(id) = &sel.id {
138 id_rules.entry(id.clone()).or_default().push(group_idx);
139 } else if let Some(first_class) = sel.classes.first() {
140 class_rules
142 .entry(first_class.clone())
143 .or_default()
144 .push(group_idx);
145 } else if let Some(tag) = &sel.tag {
146 tag_rules.entry(tag.clone()).or_default().push(group_idx);
147 } else if let Some(first_attr) = sel.attributes.first() {
148 attribute_rules
149 .entry(first_attr.name.clone())
150 .or_default()
151 .push(group_idx);
152 } else {
153 universal_rules.push(group_idx);
154 }
155 } else {
156 universal_rules.push(group_idx);
157 }
158 }
159
160 Self {
161 declarations: declarations.to_vec(),
162 groups,
163 id_rules,
164 class_rules,
165 tag_rules,
166 attribute_rules,
167 universal_rules,
168 }
169 }
170
171 pub fn query_candidates(
174 &self,
175 element: &crate::engine::css::matcher::ElementInfo,
176 ) -> impl Iterator<Item = &SelectorGroup> {
177 let universal = self.universal_rules.iter().map(|&g| &self.groups[g]);
178
179 let id = element
180 .id
181 .as_ref()
182 .and_then(|id_str| self.id_rules.get(id_str))
183 .into_iter()
184 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
185
186 let tag = self
187 .tag_rules
188 .get(&element.tag_name)
189 .into_iter()
190 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
191
192 let classes = element
193 .classes
194 .iter()
195 .filter_map(|class_str| self.class_rules.get(class_str))
196 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
197
198 let attributes = element
199 .attributes
200 .iter()
201 .filter_map(|(name, _)| self.attribute_rules.get(name))
202 .flat_map(|indices| indices.iter().map(|&g| &self.groups[g]));
203
204 universal
205 .chain(id)
206 .chain(tag)
207 .chain(classes)
208 .chain(attributes)
209 }
210
211 pub fn declarations(&self) -> &[ResolvedDeclaration] {
212 &self.declarations
213 }
214}
215
216impl ResolvedDeclaration {
217 pub fn outranks(&self, other: &Self) -> bool {
219 (self.important, self.origin, self.specificity, self.order)
220 > (
221 other.important,
222 other.origin,
223 other.specificity,
224 other.order,
225 )
226 }
227
228 pub fn matches_media(&self, environment: &MediaEnvironment) -> bool {
230 self.media_queries
231 .iter()
232 .all(|query| MediaEvaluator::evaluate(query, environment))
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq)]
238pub struct MediaEnvironment {
239 pub viewport_width: f32,
241 pub viewport_height: f32,
243 pub color_scheme: ColorScheme,
245}
246
247impl MediaEnvironment {
248 pub fn new(viewport: (f32, f32), color_scheme: ColorScheme) -> Self {
249 Self {
250 viewport_width: viewport.0,
251 viewport_height: viewport.1,
252 color_scheme,
253 }
254 }
255}
256
257pub fn filter_media<'a>(
259 styles: &'a ResolvedStyles,
260 environment: &'a MediaEnvironment,
261) -> impl Iterator<Item = &'a ResolvedDeclaration> {
262 styles
263 .iter()
264 .filter(move |declaration| declaration.matches_media(environment))
265}
266
267pub fn append_resolved_styles(target: &mut ResolvedStyles, mut incoming: ResolvedStyles) {
269 let next_order = target
270 .iter()
271 .map(|declaration| declaration.order)
272 .max()
273 .map_or(0, |order| order + 1);
274 for declaration in &mut incoming {
275 declaration.order += next_order;
276 }
277 target.extend(incoming);
278}
279
280pub fn resolve_inline_style(style_attr: &str) -> Vec<(String, CssValue, bool)> {
287 let mut parser = crate::engine::css::parser::Parser::new(style_attr);
288 let Ok(nodes) = parser.parse_declarations() else {
289 return Vec::new();
290 };
291
292 let declarations = DeclarationResolver::collect(&nodes);
293 let mut custom_properties = Properties::new();
294 for declaration in &declarations {
295 if declaration.name.starts_with("--") {
296 set_inline_custom_property(
297 &mut custom_properties,
298 declaration.name.clone(),
299 declaration.value.clone(),
300 declaration.important,
301 );
302 }
303 }
304
305 declarations
306 .into_iter()
307 .map(|declaration| {
308 let value = if declaration.name.starts_with("--") {
309 declaration.value
310 } else {
311 DeclarationResolver::resolve_var(
312 &declaration.value,
313 &custom_properties,
314 &mut HashSet::new(),
315 )
316 .unwrap_or(declaration.value)
317 };
318 (declaration.name, value, declaration.important)
319 })
320 .collect()
321}
322
323pub fn resolve_inline_value(value: &str) -> Option<CssValue> {
324 let mut tokenizer = crate::engine::css::tokenizer::Tokenizer::new(value);
325 let mut tokens = Vec::new();
326
327 loop {
328 let token = tokenizer.next_token();
329 if token == crate::engine::css::tokenizer::Token::EOF {
330 break;
331 }
332
333 tokens.push(token);
334 }
335
336 let Ok(value) = crate::engine::css::parser::Parser::parse_tokens_to_css_value(tokens) else {
337 return None;
338 };
339
340 Some(value)
341}
342
343pub(super) fn set_inline_custom_property(
347 properties: &mut Properties,
348 name: String,
349 value: CssValue,
350 important: bool,
351) {
352 if properties
353 .get(&name)
354 .is_some_and(|current| current.important && !important)
355 {
356 return;
357 }
358
359 properties.insert(
360 name.clone(),
361 ResolvedDeclaration {
362 selector: Arc::new(ComplexSelector { parts: Vec::new() }),
363 name,
364 value,
365 specificity: (u32::MAX, u32::MAX, u32::MAX),
366 order: usize::MAX,
367 important,
368 origin: StyleOrigin::Author,
369 media_queries: Arc::new(Vec::new()),
370 },
371 );
372}
373
374pub struct CssResolver;
379
380impl CssResolver {
381 pub fn resolve(stylesheet: &CssNode) -> ResolvedStyles {
383 Self::resolve_with_origin(stylesheet, StyleOrigin::Author)
384 }
385
386 pub fn resolve_with_origin(stylesheet: &CssNode, origin: StyleOrigin) -> ResolvedStyles {
388 let mut styles = Vec::new();
389 let mut order = 0;
390 Self::walk(
391 stylesheet,
392 &mut styles,
393 &mut order,
394 origin,
395 &mut Vec::new(),
396 &[],
397 );
398 styles
399 }
400
401 pub fn resolve_inline_style(style: &str) -> ResolvedStyles {
407 let source = format!("* {{ {style} }}");
408 let Ok(stylesheet) = crate::engine::css::parser::Parser::new(&source).parse() else {
409 return Vec::new();
410 };
411 let mut declarations = Self::resolve_with_origin(&stylesheet, StyleOrigin::Author);
412 for declaration in &mut declarations {
413 declaration.specificity = (u32::MAX, u32::MAX, u32::MAX);
414 declaration.order = usize::MAX;
415 }
416 declarations
417 }
418
419 fn walk(
420 node: &CssNode,
421 styles: &mut ResolvedStyles,
422 order: &mut usize,
423 origin: StyleOrigin,
424 media_queries: &mut Vec<AtQuery>,
425 parent_selectors: &[ComplexSelector],
426 ) {
427 if let CssNodeType::AtRule { name, params } = node.node() {
428 if name.eq_ignore_ascii_case("supports") && !SupportsEvaluator::evaluate(params) {
429 return;
430 }
431
432 let is_media = name.eq_ignore_ascii_case("media");
433 if is_media {
434 media_queries.push(params.clone());
435 }
436 for child in node.children() {
437 Self::walk(
438 child,
439 styles,
440 order,
441 origin,
442 media_queries,
443 parent_selectors,
444 );
445 }
446 if is_media {
447 media_queries.pop();
448 }
449 return;
450 }
451
452 let resolved_selectors =
453 Self::resolve_rule(node, styles, order, origin, media_queries, parent_selectors);
454
455 for child in node.children() {
456 Self::walk(
457 child,
458 styles,
459 order,
460 origin,
461 media_queries,
462 &resolved_selectors,
463 );
464 }
465 }
466
467 fn resolve_rule(
468 node: &CssNode,
469 styles: &mut ResolvedStyles,
470 order: &mut usize,
471 origin: StyleOrigin,
472 media_queries: &[AtQuery],
473 parent_selectors: &[ComplexSelector],
474 ) -> Vec<ComplexSelector> {
475 let CssNodeType::Rule { selectors } = node.node() else {
476 return vec![];
477 };
478
479 let resolved_selectors: Vec<ComplexSelector> = selectors
480 .iter()
481 .flat_map(|child| {
482 if parent_selectors.is_empty() {
483 vec![child.clone()]
484 } else {
485 parent_selectors
486 .iter()
487 .map(|parent| parent.nest(child))
488 .collect::<Vec<_>>()
489 }
490 })
491 .collect();
492
493 let declarations = DeclarationResolver::collect(node.children());
494
495 for selector in resolved_selectors.iter() {
496 Self::push_resolved(
497 selector,
498 &declarations,
499 styles,
500 order,
501 origin,
502 media_queries,
503 );
504 }
505
506 resolved_selectors
507 }
508
509 fn push_resolved(
510 selector: &ComplexSelector,
511 declarations: &[Declaration],
512 styles: &mut ResolvedStyles,
513 order: &mut usize,
514 origin: StyleOrigin,
515 media_queries: &[AtQuery],
516 ) {
517 let specificity = selector.specificity();
518 let selector = Arc::new(selector.clone());
519 let media_queries = Arc::new(media_queries.to_vec());
520
521 for decl in declarations {
522 styles.push(ResolvedDeclaration {
523 selector: Arc::clone(&selector),
524 name: decl.name.clone(),
525 value: decl.value.clone(),
526 specificity,
527 order: *order,
528 important: decl.important,
529 origin,
530 media_queries: Arc::clone(&media_queries),
531 });
532 *order += 1;
533 }
534 }
535}
536
537struct MediaEvaluator;
538
539impl MediaEvaluator {
540 fn evaluate(query: &AtQuery, environment: &MediaEnvironment) -> bool {
541 match query {
542 AtQuery::Group(items) => Self::evaluate_group(items, environment),
543 item => Self::evaluate_clause(std::slice::from_ref(item), environment),
544 }
545 }
546
547 fn evaluate_clause(items: &[AtQuery], environment: &MediaEnvironment) -> bool {
548 if items.is_empty() {
549 return false;
550 }
551 let negate = matches!(items.first(), Some(AtQuery::Keyword(keyword)) if keyword.eq_ignore_ascii_case("not"));
552 let mut saw_operand = false;
553 let matches = items
554 .iter()
555 .skip(usize::from(negate))
556 .all(|item| match item {
557 AtQuery::Keyword(keyword)
558 if keyword.eq_ignore_ascii_case("and")
559 || keyword.eq_ignore_ascii_case("only") =>
560 {
561 true
562 }
563 AtQuery::Keyword(keyword)
564 if keyword.eq_ignore_ascii_case("all")
565 || keyword.eq_ignore_ascii_case("screen") =>
566 {
567 saw_operand = true;
568 true
569 }
570 AtQuery::Keyword(keyword) if keyword.eq_ignore_ascii_case("print") => {
571 saw_operand = true;
572 false
573 }
574 AtQuery::Keyword(_) => {
575 saw_operand = true;
576 false
577 }
578 AtQuery::Condition { name, value } => {
579 saw_operand = true;
580 Self::evaluate_condition(name, value, environment)
581 }
582 AtQuery::Range { left, name, right } => {
583 saw_operand = true;
584 Self::evaluate_range(left.as_ref(), name, right.as_ref(), environment)
585 }
586 AtQuery::Group(group) => {
587 saw_operand = true;
588 Self::evaluate_group(group, environment)
589 }
590 });
591 if !saw_operand {
592 return false;
593 }
594 if negate { !matches } else { matches }
595 }
596
597 fn evaluate_group(items: &[AtQuery], environment: &MediaEnvironment) -> bool {
598 items
599 .split(|item| matches!(item, AtQuery::Keyword(keyword) if keyword == ","))
600 .any(|clause| Self::evaluate_clause(clause, environment))
601 }
602
603 fn evaluate_range(
604 left: Option<&(CssValue, RangeOperator)>,
605 name: &str,
606 right: Option<&(RangeOperator, CssValue)>,
607 environment: &MediaEnvironment,
608 ) -> bool {
609 let name = name.to_ascii_lowercase();
610
611 let actual = match name.as_str() {
612 "width" => environment.viewport_width,
613 "height" => environment.viewport_height,
614 _ => return false,
615 };
616
617 if let Some((value, operator)) = left {
618 let Some(expected) = Self::length_px(value, environment) else {
619 return false;
620 };
621
622 if !Self::compare_range(expected, *operator, actual) {
623 return false;
624 }
625 }
626
627 if let Some((operator, value)) = right {
628 let Some(expected) = Self::length_px(value, environment) else {
629 return false;
630 };
631
632 if !Self::compare_range(actual, *operator, expected) {
633 return false;
634 }
635 }
636
637 true
638 }
639
640 fn compare_range(actual: f32, operator: RangeOperator, expected: f32) -> bool {
641 match operator {
642 RangeOperator::Less => actual < expected,
643 RangeOperator::LessEqual => actual <= expected,
644 RangeOperator::Equal => (actual - expected).abs() <= f32::EPSILON,
645 RangeOperator::GreaterEqual => actual >= expected,
646 RangeOperator::Greater => actual > expected,
647 }
648 }
649
650 fn evaluate_condition(name: &str, value: &CssValue, environment: &MediaEnvironment) -> bool {
651 let name = name.to_ascii_lowercase();
652 match name.as_str() {
653 "width" | "min-width" | "max-width" => {
654 Self::compare_length(&name, value, environment.viewport_width, environment)
655 }
656 "height" | "min-height" | "max-height" => {
657 Self::compare_length(&name, value, environment.viewport_height, environment)
658 }
659 "orientation" => match value {
660 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("portrait") => {
661 environment.viewport_height >= environment.viewport_width
662 }
663 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("landscape") => {
664 environment.viewport_width > environment.viewport_height
665 }
666 _ => false,
667 },
668 "prefers-color-scheme" => match value {
669 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("dark") => {
670 environment.color_scheme == ColorScheme::Dark
671 }
672 CssValue::Keyword(keyword) if keyword.eq_ignore_ascii_case("light") => {
673 environment.color_scheme == ColorScheme::Light
674 }
675 _ => false,
676 },
677 _ => false,
678 }
679 }
680
681 fn compare_length(
682 name: &str,
683 value: &CssValue,
684 actual: f32,
685 environment: &MediaEnvironment,
686 ) -> bool {
687 let Some(expected) = Self::length_px(value, environment) else {
688 return false;
689 };
690 if name.starts_with("min-") {
691 actual >= expected
692 } else if name.starts_with("max-") {
693 actual <= expected
694 } else {
695 (actual - expected).abs() <= f32::EPSILON
696 }
697 }
698
699 fn length_px(value: &CssValue, environment: &MediaEnvironment) -> Option<f32> {
700 let text_flow = crate::engine::layouter::types::TextFlowStyle::default();
701 let len = resolve_css_len("media", std::slice::from_ref(value), &text_flow)?;
702 len.resolve_with(
703 None,
704 environment.viewport_width,
705 environment.viewport_height,
706 )
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::engine::css::parser::Parser;
714 use crate::engine::css::values::Unit;
715
716 fn resolve(css: &str, origin: StyleOrigin) -> ResolvedStyles {
717 let stylesheet = Parser::new(css).parse().unwrap();
718 CssResolver::resolve_with_origin(&stylesheet, origin)
719 }
720
721 #[test]
722 fn author_declaration_outranks_more_specific_user_agent_declaration() {
723 let user_agent = resolve(
724 r#"input[type="text"] { display: inline-block; }"#,
725 StyleOrigin::UserAgent,
726 );
727 let author = resolve("input { display: block; }", StyleOrigin::Author);
728
729 assert!(author[0].outranks(&user_agent[0]));
730 }
731
732 #[test]
733 fn append_rebases_order_across_stylesheets() {
734 let mut styles = resolve("input { display: inline; }", StyleOrigin::Author);
735 let later = resolve("input { display: block; }", StyleOrigin::Author);
736 append_resolved_styles(&mut styles, later);
737
738 assert!(styles[1].order > styles[0].order);
739 assert!(styles[1].outranks(&styles[0]));
740 }
741
742 #[test]
743 fn resolve_inline_style_extracts_declarations_and_important() {
744 let decls = resolve_inline_style("color: red; margin: 4px 8px !important; width: 10px");
745 assert_eq!(decls.len(), 3);
746
747 assert_eq!(decls[0].0, "color");
748 assert!(!decls[0].2);
749
750 assert_eq!(decls[1].0, "margin");
751 assert!(decls[1].2);
752
753 assert_eq!(decls[2].0, "width");
754 }
755
756 #[test]
757 fn resolve_inline_style_tolerates_empty_and_malformed_input() {
758 assert!(resolve_inline_style("").is_empty());
759 assert!(resolve_inline_style(";;;").is_empty());
760 }
761
762 #[test]
763 fn resolve_inline_style_resolves_var() {
764 let decls = resolve_inline_style("--accent: blue; color: var(--accent)");
765 assert_eq!(decls.len(), 2);
766
767 let (_, color_value, _) = decls.iter().find(|(n, _, _)| n == "color").unwrap();
768 assert_eq!(color_value, &CssValue::Keyword("blue".into()));
769 }
770
771 #[test]
772 fn media_width_conditions_follow_viewport() {
773 let styles = resolve(
774 "@media screen and (max-width: 600px) { div { color: red; } }",
775 StyleOrigin::Author,
776 );
777 let narrow = MediaEnvironment::new((600.0, 800.0), ColorScheme::Light);
778 let wide = MediaEnvironment::new((601.0, 800.0), ColorScheme::Light);
779
780 assert_eq!(filter_media(&styles, &narrow).count(), 1);
781 assert_eq!(filter_media(&styles, &wide).count(), 0);
782 }
783
784 #[test]
785 fn scratch_style_adjacent_media_conditions_follow_desktop_viewport() {
786 let stylesheet = Parser::new(
787 "@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}",
788 )
789 .parse_lossy();
790 let styles = CssResolver::resolve_with_origin(&stylesheet, StyleOrigin::Author);
791 let desktop = MediaEnvironment::new((1280.0, 800.0), ColorScheme::Light);
792 let declarations = filter_media(&styles, &desktop)
793 .filter(|declaration| declaration.name == "width")
794 .collect::<Vec<_>>();
795
796 assert_eq!(declarations.len(), 1);
797 assert_eq!(declarations[0].name, "width");
798 assert_eq!(declarations[0].value, CssValue::Length(942.0, Unit::Px));
799 }
800
801 #[test]
802 fn media_query_lists_use_or_semantics() {
803 let styles = resolve(
804 "@media print, (orientation: landscape) { div { color: red; } }",
805 StyleOrigin::Author,
806 );
807 let landscape = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
808 let portrait = MediaEnvironment::new((600.0, 800.0), ColorScheme::Light);
809
810 assert_eq!(filter_media(&styles, &landscape).count(), 1);
811 assert_eq!(filter_media(&styles, &portrait).count(), 0);
812 }
813
814 #[test]
815 fn media_color_scheme_matches_system_preference() {
816 let styles = resolve(
817 "@media (prefers-color-scheme: dark) { div { color: white; } }",
818 StyleOrigin::Author,
819 );
820 let light = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
821 let dark = MediaEnvironment::new((800.0, 600.0), ColorScheme::Dark);
822
823 assert_eq!(filter_media(&styles, &light).count(), 0);
824 assert_eq!(filter_media(&styles, &dark).count(), 1);
825 }
826
827 #[test]
828 fn empty_media_query_does_not_match() {
829 let styles = resolve("@media { div { color: red; } }", StyleOrigin::Author);
830 let environment = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
831
832 assert_eq!(filter_media(&styles, &environment).count(), 0);
833 }
834
835 #[test]
836 fn test_rule_set_partitioning_and_querying() {
837 let styles = resolve(
838 r#"
839 * { margin: 0; }
840 #header { color: red; }
841 .btn { display: inline-block; }
842 span { font-size: 12px; }
843 div.container { padding: 10px; }
844 "#,
845 StyleOrigin::Author,
846 );
847 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
848 let rule_set = RuleSet::from_declarations(&styles, &env);
849 let decls_of = |group: &SelectorGroup| -> Vec<&ResolvedDeclaration> {
850 group
851 .decls
852 .iter()
853 .map(|&idx| &rule_set.declarations()[idx])
854 .collect()
855 };
856
857 let el1 = crate::engine::css::matcher::ElementInfo {
859 tag_name: "div".to_string(),
860 id: Some("header".to_string()),
861 classes: vec!["btn".to_string()],
862 ..Default::default()
863 };
864 let candidates1: Vec<_> = rule_set.query_candidates(&el1).flat_map(decls_of).collect();
865 assert!(candidates1.iter().any(|d| d.name == "margin"));
867 assert!(candidates1.iter().any(|d| d.name == "color"));
868 assert!(candidates1.iter().any(|d| d.name == "display"));
869 assert!(!candidates1.iter().any(|d| d.name == "font-size"));
871
872 let el2 = crate::engine::css::matcher::ElementInfo {
874 tag_name: "span".to_string(),
875 id: None,
876 classes: vec!["other".to_string()],
877 ..Default::default()
878 };
879 let candidates2: Vec<_> = rule_set.query_candidates(&el2).flat_map(decls_of).collect();
880 assert!(candidates2.iter().any(|d| d.name == "margin"));
882 assert!(candidates2.iter().any(|d| d.name == "font-size"));
883 assert!(!candidates2.iter().any(|d| d.name == "color"));
885 assert!(!candidates2.iter().any(|d| d.name == "display"));
886 }
887
888 #[test]
889 fn rule_set_groups_shared_selectors() {
890 let styles = resolve(
891 r#"
892 div { color: red; font-size: 12px; }
893 div { margin: 0; }
894 p { padding: 1px; }
895 p.padded { padding: 2px; }
896 "#,
897 StyleOrigin::Author,
898 );
899 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
900 let rule_set = RuleSet::from_declarations(&styles, &env);
901
902 let div = crate::engine::css::matcher::ElementInfo {
905 tag_name: "div".to_string(),
906 ..Default::default()
907 };
908 let groups: Vec<_> = rule_set.query_candidates(&div).collect();
909 assert_eq!(groups.len(), 1);
910 assert_eq!(groups[0].decls.len(), 3);
911
912 let p = crate::engine::css::matcher::ElementInfo {
914 tag_name: "p".to_string(),
915 ..Default::default()
916 };
917 let groups: Vec<_> = rule_set.query_candidates(&p).collect();
918 assert_eq!(groups.len(), 1);
919
920 let padded = crate::engine::css::matcher::ElementInfo {
922 tag_name: "p".to_string(),
923 classes: vec!["padded".to_string()],
924 ..Default::default()
925 };
926 let groups: Vec<_> = rule_set.query_candidates(&padded).collect();
927 assert_eq!(groups.len(), 2);
928 }
929
930 #[test]
931 fn rule_set_indexes_attribute_subjects() {
932 let styles = resolve(
933 r#"
934 [hidden] { display: none; }
935 [data-tip] { position: relative; }
936 "#,
937 StyleOrigin::Author,
938 );
939 let env = MediaEnvironment::new((800.0, 600.0), ColorScheme::Light);
940 let rule_set = RuleSet::from_declarations(&styles, &env);
941
942 let hidden = crate::engine::css::matcher::ElementInfo {
943 tag_name: "div".to_string(),
944 attributes: vec![("hidden".to_string(), String::new())],
945 ..Default::default()
946 };
947 let candidates: Vec<_> = rule_set
948 .query_candidates(&hidden)
949 .flat_map(|group| group.decls.iter())
950 .collect();
951 assert_eq!(candidates.len(), 1, "only the [hidden] group is queried");
952
953 let plain = crate::engine::css::matcher::ElementInfo {
954 tag_name: "div".to_string(),
955 ..Default::default()
956 };
957 let candidates: Vec<_> = rule_set
958 .query_candidates(&plain)
959 .flat_map(|group| group.decls.iter())
960 .collect();
961 assert_eq!(
962 candidates.len(),
963 0,
964 "no candidates for attribute-less element"
965 );
966 }
967
968 #[test]
969 fn media_width_calc_rem_is_resolved() {
970 let styles = resolve(
971 "@media (width > calc(19rem)) { div { color: red; } }",
972 StyleOrigin::Author,
973 );
974 let wide = MediaEnvironment::new((400.0, 800.0), ColorScheme::Light);
976 let narrow = MediaEnvironment::new((200.0, 800.0), ColorScheme::Light);
977 assert_eq!(filter_media(&styles, &wide).count(), 1);
978 assert_eq!(filter_media(&styles, &narrow).count(), 0);
979 }
980
981 #[test]
982 fn media_range_calc_vw() {
983 let styles = resolve(
984 "@media (width > calc(100vw - 40px)) { div { color: red; } }",
985 StyleOrigin::Author,
986 );
987 let env = MediaEnvironment::new((100.0, 800.0), ColorScheme::Light);
989 assert_eq!(filter_media(&styles, &env).count(), 1);
991 let env2 = MediaEnvironment::new((30.0, 800.0), ColorScheme::Light);
992 assert_eq!(filter_media(&styles, &env2).count(), 1);
994 }
995
996 #[test]
997 fn media_min_width_rem() {
998 let styles = resolve(
999 "@media (min-width: 2rem) { div { color: red; } }",
1000 StyleOrigin::Author,
1001 );
1002 let env = MediaEnvironment::new((40.0, 800.0), ColorScheme::Light);
1004 assert_eq!(filter_media(&styles, &env).count(), 1);
1005 let env2 = MediaEnvironment::new((20.0, 800.0), ColorScheme::Light);
1006 assert_eq!(filter_media(&styles, &env2).count(), 0);
1007 }
1008
1009 #[test]
1010 fn media_width_calc_min_function() {
1011 let styles = resolve(
1012 "@media (width > min(800px, 90vw)) { div { color: red; } }",
1013 StyleOrigin::Author,
1014 );
1015 let env = MediaEnvironment::new((1000.0, 800.0), ColorScheme::Light);
1017 assert_eq!(filter_media(&styles, &env).count(), 1);
1018 let env2 = MediaEnvironment::new((500.0, 800.0), ColorScheme::Light);
1020 assert_eq!(filter_media(&styles, &env2).count(), 1);
1021 let env3 = MediaEnvironment::new((400.0, 800.0), ColorScheme::Light);
1023 assert_eq!(filter_media(&styles, &env3).count(), 1);
1024 let env4 = MediaEnvironment::new((300.0, 800.0), ColorScheme::Light);
1026 assert_eq!(filter_media(&styles, &env4).count(), 1);
1027 }
1028}
1029
1030struct SupportsEvaluator;
1035
1036impl SupportsEvaluator {
1037 fn evaluate(query: &AtQuery) -> bool {
1039 match query {
1040 AtQuery::Group(items) => Self::evaluate_group(items),
1042 AtQuery::Condition { name, value } => Self::is_supported(name, value),
1044 AtQuery::Range { .. } => false,
1046 AtQuery::Keyword(_) => false,
1048 }
1049 }
1050
1051 fn evaluate_group(items: &[AtQuery]) -> bool {
1059 if items.is_empty() {
1060 return false;
1061 }
1062
1063 if matches!(items.first(), Some(AtQuery::Keyword(k)) if k.eq_ignore_ascii_case("not")) {
1065 return items.len() > 1 && !Self::evaluate(&AtQuery::Group(items[1..].to_vec()));
1066 }
1067
1068 if let Some(operands) = Self::split_by_keyword(items, "and") {
1070 operands.iter().all(|g| Self::evaluate(g))
1071 } else if let Some(operands) = Self::split_by_keyword(items, "or") {
1073 operands.iter().any(|g| Self::evaluate(g))
1074 } else if items.len() == 1 {
1076 Self::evaluate(&items[0])
1077 } else {
1078 items.iter().all(Self::evaluate)
1079 }
1080 }
1081
1082 fn is_supported(name: &str, value: &CssValue) -> bool {
1083 super::builder::apply_declaration(
1084 name,
1085 value,
1086 &mut ui_layout::Style::default(),
1087 &mut super::types::ContainerStyle::default(),
1088 &mut super::types::TextStyle::default(),
1089 &mut super::types::TextFlowStyle::default(),
1090 &ui_layout::Style::default(),
1091 &super::types::ContainerStyle::default(),
1092 &super::types::TextStyle::default(),
1093 &super::types::TextFlowStyle::default(),
1094 &mut super::types::Overflow::default(),
1095 super::types::ColorScheme::Light,
1096 )
1097 .is_some()
1098 }
1099
1100 fn split_by_keyword<'a>(items: &'a [AtQuery], keyword: &str) -> Option<Vec<&'a AtQuery>> {
1101 let has = items
1102 .iter()
1103 .any(|item| matches!(item, AtQuery::Keyword(k) if k.eq_ignore_ascii_case(keyword)));
1104 if !has {
1105 return None;
1106 }
1107 Some(
1108 items
1109 .iter()
1110 .filter(
1111 |item| !matches!(item, AtQuery::Keyword(k) if k.eq_ignore_ascii_case(keyword)),
1112 )
1113 .collect(),
1114 )
1115 }
1116}
1117
1118pub(super) struct DeclarationResolver;
1123
1124impl DeclarationResolver {
1125 fn collect(children: &[CssNode]) -> Vec<Declaration> {
1126 let mut result = Vec::new();
1127
1128 for child in children {
1129 let CssNodeType::Declaration { name, value } = &child.node() else {
1130 continue;
1131 };
1132
1133 let (value, important) = Self::extract_important(value);
1134
1135 result.push(Declaration {
1136 name: name.clone(),
1137 value,
1138 important,
1139 });
1140 }
1141
1142 result
1143 }
1144
1145 fn extract_important(value: &CssValue) -> (CssValue, bool) {
1150 match value {
1151 CssValue::List(list) if list.len() >= 2 => {
1152 let len = list.len();
1153 let is_important = matches!(
1154 (&list[len - 2], &list[len - 1]),
1155 (
1156 CssValue::Keyword(bang),
1157 CssValue::Keyword(ident)
1158 )
1159 if bang == "!" && ident.eq_ignore_ascii_case("important")
1160 );
1161
1162 if is_important {
1163 let value = if len - 2 == 1 {
1164 list.iter().next().unwrap().clone()
1165 } else {
1166 CssValue::List(list[..len - 2].to_vec())
1167 };
1168 (value, true)
1169 } else {
1170 (value.clone(), false)
1171 }
1172 }
1173 _ => (value.clone(), false),
1174 }
1175 }
1176
1177 pub fn resolve_var(
1178 value: &CssValue,
1179 custom_props: &Properties,
1180 visited: &mut HashSet<CssIdent>,
1181 ) -> Option<CssValue> {
1182 match value {
1183 CssValue::Function(name, args) if name == "var" => {
1185 let var_name = match args.first().and_then(|argument| argument.first()) {
1186 Some(CssValue::Keyword(name)) => name,
1187 _ => return None,
1188 };
1189
1190 if !visited.insert(var_name.clone()) {
1191 return None;
1192 }
1193
1194 let result = if let Some(v) = custom_props.get(var_name.as_str()) {
1195 Self::resolve_var(&v.value, custom_props, visited)
1196 } else if let Some(fallback) = args.get(1).and_then(|argument| argument.first()) {
1197 Self::resolve_var(fallback, custom_props, visited)
1198 } else {
1199 None
1200 };
1201
1202 visited.remove(var_name);
1203 result
1204 }
1205
1206 CssValue::Function(name, args) => {
1208 let resolved_args = args
1209 .iter()
1210 .map(|argument| {
1211 argument
1212 .iter()
1213 .map(|v| Self::resolve_var(v, custom_props, &mut visited.clone()))
1214 .collect::<Option<Vec<_>>>()
1215 })
1216 .collect::<Option<Vec<_>>>()?;
1217 Some(CssValue::Function(name.clone(), resolved_args))
1218 }
1219
1220 CssValue::List(list) => {
1222 let resolved = list
1223 .iter()
1224 .map(|v| Self::resolve_var(v, custom_props, &mut visited.clone()))
1225 .collect::<Option<Vec<_>>>()?;
1226 Some(CssValue::List(resolved))
1227 }
1228
1229 _ => Some(value.clone()),
1231 }
1232 }
1233}