From 667457a16c64580e0e5545cc7797a4bb1ba25aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Thu, 8 Nov 2018 23:07:40 +0000 Subject: [PATCH] style: Split up push_applicable_declarations. Introduce RuleCollector, which contains all the state we need during the cascade, and allows to reuse a bit of code. Differential Revision: https://phabricator.services.mozilla.com/D11233 --- components/style/lib.rs | 1 + components/style/rule_collector.rs | 391 +++++++++++++++++++++++++++++ components/style/stylist.rs | 327 +++--------------------- 3 files changed, 426 insertions(+), 293 deletions(-) create mode 100644 components/style/rule_collector.rs diff --git a/components/style/lib.rs b/components/style/lib.rs index 6694f776ab8..77927826523 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -143,6 +143,7 @@ pub mod media_queries; pub mod parallel; pub mod parser; pub mod rule_cache; +pub mod rule_collector; pub mod rule_tree; pub mod scoped_tls; pub mod selector_map; diff --git a/components/style/rule_collector.rs b/components/style/rule_collector.rs new file mode 100644 index 00000000000..b975beca652 --- /dev/null +++ b/components/style/rule_collector.rs @@ -0,0 +1,391 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Collects a series of applicable rules for a given element. + +use applicable_declarations::{ApplicableDeclarationBlock, ApplicableDeclarationList}; +use dom::{TElement, TShadowRoot}; +use properties::{AnimationRules, PropertyDeclarationBlock}; +use rule_tree::{CascadeLevel, ShadowCascadeOrder}; +use selector_map::SelectorMap; +use selector_parser::PseudoElement; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use servo_arc::ArcBorrow; +use shared_lock::Locked; +use smallvec::SmallVec; +use stylesheets::Origin; +use stylist::{AuthorStylesEnabled, Rule, RuleInclusion, Stylist}; + +/// An object that we use with all the intermediate state needed for the +/// cascade. +/// +/// This is done basically to be able to organize the cascade in smaller +/// functions, and be able to reason about it easily. +pub struct RuleCollector<'a, 'b: 'a, E, F: 'a> +where + E: TElement, +{ + element: E, + rule_hash_target: E, + stylist: &'a Stylist, + pseudo_element: Option<&'a PseudoElement>, + style_attribute: Option>>, + smil_override: Option>>, + animation_rules: AnimationRules, + rule_inclusion: RuleInclusion, + rules: &'a mut ApplicableDeclarationList, + context: &'a mut MatchingContext<'b, E::Impl>, + flags_setter: &'a mut F, + shadow_cascade_order: ShadowCascadeOrder, + matches_user_and_author_rules: bool, + matches_document_author_rules: bool, +} + +impl<'a, 'b: 'a, E, F: 'a> RuleCollector<'a, 'b, E, F> +where + E: TElement, + F: FnMut(&E, ElementSelectorFlags), +{ + /// Trivially construct a new collector. + pub fn new( + stylist: &'a Stylist, + element: E, + pseudo_element: Option<&'a PseudoElement>, + style_attribute: Option>>, + smil_override: Option>>, + animation_rules: AnimationRules, + rule_inclusion: RuleInclusion, + rules: &'a mut ApplicableDeclarationList, + context: &'a mut MatchingContext<'b, E::Impl>, + flags_setter: &'a mut F, + ) -> Self { + let rule_hash_target = element.rule_hash_target(); + let matches_user_and_author_rules = rule_hash_target.matches_user_and_author_rules(); + + // Gecko definitely has pseudo-elements with style attributes, like + // ::-moz-color-swatch. + debug_assert!( + cfg!(feature = "gecko") || style_attribute.is_none() || pseudo_element.is_none(), + "Style attributes do not apply to pseudo-elements" + ); + debug_assert!(pseudo_element.map_or(true, |p| !p.is_precomputed())); + + Self { + element, + rule_hash_target, + stylist, + pseudo_element, + style_attribute, + smil_override, + animation_rules, + rule_inclusion, + context, + flags_setter, + rules, + matches_user_and_author_rules, + shadow_cascade_order: 0, + matches_document_author_rules: matches_user_and_author_rules, + } + } + + fn collect_stylist_rules(&mut self, origin: Origin) { + let cascade_level = match origin { + Origin::UserAgent => CascadeLevel::UANormal, + Origin::User => CascadeLevel::UserNormal, + Origin::Author => CascadeLevel::SameTreeAuthorNormal, + }; + + let cascade_data = self.stylist.cascade_data().borrow_for_origin(origin); + let map = match cascade_data.normal_rules(self.pseudo_element) { + Some(m) => m, + None => return, + }; + + map.get_all_matching_rules( + self.element, + self.rule_hash_target, + self.rules, + self.context, + self.flags_setter, + cascade_level, + 0, + ); + } + + fn collect_user_agent_rules(&mut self) { + self.collect_stylist_rules(Origin::UserAgent); + } + + fn collect_user_rules(&mut self) { + if !self.matches_user_and_author_rules { + return; + } + + self.collect_stylist_rules(Origin::User); + } + + /// Presentational hints. + /// + /// These go before author rules, but after user rules, see: + /// https://drafts.csswg.org/css-cascade/#preshint + fn collect_presentational_hints(&mut self) { + if self.pseudo_element.is_some() { + return; + } + + let length_before_preshints = self.rules.len(); + self.element + .synthesize_presentational_hints_for_legacy_attributes( + self.context.visited_handling(), + self.rules, + ); + if cfg!(debug_assertions) { + if self.rules.len() != length_before_preshints { + for declaration in &self.rules[length_before_preshints..] { + assert_eq!(declaration.level(), CascadeLevel::PresHints); + } + } + } + } + + fn collect_rules_in_shadow_tree( + &mut self, + shadow_host: E, + map: &SelectorMap, + cascade_level: CascadeLevel, + ) { + debug_assert!(shadow_host.shadow_root().is_some()); + let element = self.element; + let rule_hash_target = self.rule_hash_target; + let rules = &mut self.rules; + let flags_setter = &mut self.flags_setter; + let shadow_cascade_order = self.shadow_cascade_order; + self.context.with_shadow_host(Some(shadow_host), |context| { + map.get_all_matching_rules( + element, + rule_hash_target, + rules, + context, + flags_setter, + cascade_level, + shadow_cascade_order, + ); + }); + self.shadow_cascade_order += 1; + } + + /// Collects the rules for the ::slotted pseudo-element. + fn collect_slotted_rules(&mut self) { + let mut slots = SmallVec::<[_; 3]>::new(); + let mut current = self.rule_hash_target.assigned_slot(); + while let Some(slot) = current { + debug_assert!( + self.matches_user_and_author_rules, + "We should not slot NAC anywhere" + ); + slots.push(slot); + current = slot.assigned_slot(); + } + + // Match slotted rules in reverse order, so that the outer slotted rules + // come before the inner rules (and thus have less priority). + for slot in slots.iter().rev() { + let shadow = slot.containing_shadow().unwrap(); + let data = match shadow.style_data() { + Some(d) => d, + None => continue, + }; + let slotted_rules = match data.slotted_rules(self.pseudo_element) { + Some(r) => r, + None => continue, + }; + self.collect_rules_in_shadow_tree( + shadow.host(), + slotted_rules, + CascadeLevel::InnerShadowNormal, + ); + } + } + + fn collect_normal_rules_from_containing_shadow_tree(&mut self) { + if !self.matches_user_and_author_rules { + return; + } + + let mut current_containing_shadow = self.rule_hash_target.containing_shadow(); + while let Some(containing_shadow) = current_containing_shadow { + let cascade_data = containing_shadow.style_data(); + let host = containing_shadow.host(); + if let Some(map) = cascade_data.and_then(|data| data.normal_rules(self.pseudo_element)) + { + self.collect_rules_in_shadow_tree(host, map, CascadeLevel::SameTreeAuthorNormal); + } + let host_is_svg_use_element = + host.is_svg_element() && host.local_name() == &*local_name!("use"); + if !host_is_svg_use_element { + self.matches_document_author_rules = false; + break; + } + + debug_assert!( + cascade_data.is_none(), + "We allow no stylesheets in subtrees" + ); + + // NOTE(emilio): Hack so matches the rules of the + // enclosing tree. + // + // This is not a problem for invalidation and that kind of stuff + // because they still don't match rules based on elements + // outside of the shadow tree, and because the + // subtrees are immutable and recreated each time the source + // tree changes. + // + // We historically allow cross-document to have these + // rules applied, but I think that's not great. Gecko is the + // only engine supporting that. + // + // See https://github.com/w3c/svgwg/issues/504 for the relevant + // spec discussion. + current_containing_shadow = host.containing_shadow(); + self.matches_document_author_rules = current_containing_shadow.is_none(); + } + } + + /// Collects the rules for the :host pseudo-class. + fn collect_host_rules(&mut self) { + let shadow = match self.rule_hash_target.shadow_root() { + Some(s) => s, + None => return, + }; + + debug_assert!( + self.matches_user_and_author_rules, + "NAC should not be a shadow host" + ); + + let style_data = match shadow.style_data() { + Some(d) => d, + None => return, + }; + + let host_rules = match style_data.host_rules(self.pseudo_element) { + Some(rules) => rules, + None => return, + }; + + let rule_hash_target = self.rule_hash_target; + self.collect_rules_in_shadow_tree( + rule_hash_target, + host_rules, + CascadeLevel::InnerShadowNormal, + ); + } + + fn collect_document_author_rules(&mut self) { + if !self.matches_document_author_rules { + return; + } + + self.collect_stylist_rules(Origin::Author); + } + + fn collect_xbl_rules(&mut self) { + let element = self.element; + let cut_xbl_binding_inheritance = + element.each_xbl_cascade_data(|cascade_data, quirks_mode| { + let map = match cascade_data.normal_rules(self.pseudo_element) { + Some(m) => m, + None => return, + }; + + // NOTE(emilio): This is needed because the XBL stylist may + // think it has a different quirks mode than the document. + let mut matching_context = MatchingContext::new( + self.context.matching_mode(), + self.context.bloom_filter, + self.context.nth_index_cache.as_mut().map(|s| &mut **s), + quirks_mode, + ); + matching_context.pseudo_element_matching_fn = + self.context.pseudo_element_matching_fn; + + // SameTreeAuthorNormal instead of InnerShadowNormal to + // preserve behavior, though that's kinda fishy... + map.get_all_matching_rules( + self.element, + self.rule_hash_target, + self.rules, + &mut matching_context, + self.flags_setter, + CascadeLevel::SameTreeAuthorNormal, + self.shadow_cascade_order, + ); + }); + + self.matches_document_author_rules &= !cut_xbl_binding_inheritance; + } + + fn collect_style_attribute_and_animation_rules(&mut self) { + if let Some(sa) = self.style_attribute { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + sa.clone_arc(), + CascadeLevel::StyleAttributeNormal, + )); + } + + if let Some(so) = self.smil_override { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + so.clone_arc(), + CascadeLevel::SMILOverride, + )); + } + + // The animations sheet (CSS animations, script-generated + // animations, and CSS transitions that are no longer tied to CSS + // markup). + if let Some(anim) = self.animation_rules.0.take() { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + anim, + CascadeLevel::Animations, + )); + } + + // The transitions sheet (CSS transitions that are tied to CSS + // markup). + if let Some(anim) = self.animation_rules.1.take() { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + anim, + CascadeLevel::Transitions, + )); + } + } + + /// Collects all the rules, leaving the result in `self.rules`. + /// + /// Note that `!important` rules are handled during rule tree insertion. + pub fn collect_all(mut self) { + self.collect_user_agent_rules(); + self.collect_user_rules(); + if self.rule_inclusion == RuleInclusion::DefaultOnly { + return; + } + self.collect_presentational_hints(); + // FIXME(emilio): Should the author styles enabled stuff avoid the + // presentational hints from getting pushed? See bug 1505770. + if self.stylist.author_styles_enabled() == AuthorStylesEnabled::No { + return; + } + self.collect_host_rules(); + self.collect_slotted_rules(); + self.collect_normal_rules_from_containing_shadow_tree(); + self.collect_xbl_rules(); + self.collect_document_author_rules(); + self.collect_style_attribute_and_animation_rules(); + } +} diff --git a/components/style/stylist.rs b/components/style/stylist.rs index 749ba82ccb2..7698eebe21a 100644 --- a/components/style/stylist.rs +++ b/components/style/stylist.rs @@ -17,6 +17,7 @@ use crate::media_queries::Device; use crate::properties::{self, CascadeMode, ComputedValues}; use crate::properties::{AnimationRules, PropertyDeclarationBlock}; use crate::rule_cache::{RuleCache, RuleCacheConditions}; +use crate::rule_collector::RuleCollector; use crate::rule_tree::{CascadeLevel, RuleTree, ShadowCascadeOrder, StrongRuleNode, StyleSource}; use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet, SelectorMap, SelectorMapEntry}; use crate::selector_parser::{PerPseudoElementMap, PseudoElement, SelectorImpl, SnapshotMap}; @@ -47,7 +48,6 @@ use selectors::visitor::SelectorVisitor; use selectors::NthIndexCache; use servo_arc::{Arc, ArcBorrow}; use smallbitvec::SmallBitVec; -use smallvec::SmallVec; use std::ops; use std::sync::Mutex; use style_traits::viewport::ViewportConstraints; @@ -180,10 +180,11 @@ impl UserAgentCascadeData { } } -/// All the computed information for a stylesheet. +/// All the computed information for all the stylesheets that apply to the +/// document. #[derive(Default)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] -struct DocumentCascadeData { +pub struct DocumentCascadeData { #[cfg_attr( feature = "servo", ignore_malloc_size_of = "Arc, owned by UserAgentCascadeDataCache" @@ -210,7 +211,9 @@ impl<'a> Iterator for DocumentCascadeDataIter<'a> { } impl DocumentCascadeData { - fn borrow_for_origin(&self, origin: Origin) -> &CascadeData { + /// Borrows the cascade data for a given origin. + #[inline] + pub fn borrow_for_origin(&self, origin: Origin) -> &CascadeData { match origin { Origin::UserAgent => &self.user_agent.cascade_data, Origin::Author => &self.author, @@ -412,10 +415,16 @@ impl Stylist { } } - /// Returns the cascade data for the author level. + /// Returns the document cascade data. #[inline] - pub fn author_cascade_data(&self) -> &CascadeData { - &self.cascade_data.author + pub fn cascade_data(&self) -> &DocumentCascadeData { + &self.cascade_data + } + + /// Returns whether author styles are enabled or not. + #[inline] + pub fn author_styles_enabled(&self) -> AuthorStylesEnabled { + self.author_styles_enabled } /// Iterate through all the cascade datas from the document. @@ -1098,11 +1107,6 @@ impl Stylist { } /// Returns the applicable CSS declarations for the given element. - /// - /// This corresponds to `ElementRuleCollector` in WebKit, and should push to - /// elements in the list in the order defined by: - /// - /// https://drafts.csswg.org/css-cascade/#cascade-origin pub fn push_applicable_declarations( &self, element: E, @@ -1118,284 +1122,18 @@ impl Stylist { E: TElement, F: FnMut(&E, ElementSelectorFlags), { - // Gecko definitely has pseudo-elements with style attributes, like - // ::-moz-color-swatch. - debug_assert!( - cfg!(feature = "gecko") || style_attribute.is_none() || pseudo_element.is_none(), - "Style attributes do not apply to pseudo-elements" - ); - debug_assert!(pseudo_element.map_or(true, |p| !p.is_precomputed())); - - let rule_hash_target = element.rule_hash_target(); - - let matches_user_and_author_rules = rule_hash_target.matches_user_and_author_rules(); - - // Normal user-agent rules. - if let Some(map) = self - .cascade_data - .user_agent - .cascade_data - .normal_rules(pseudo_element) - { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::UANormal, - 0, - ); - } - - // NB: the following condition, although it may look somewhat - // inaccurate, would be equivalent to something like: - // - // element.matches_user_and_author_rules() || - // (is_implemented_pseudo && - // rule_hash_target.matches_user_and_author_rules()) - // - // Which may be more what you would probably expect. - if matches_user_and_author_rules { - // User normal rules. - if let Some(map) = self.cascade_data.user.normal_rules(pseudo_element) { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::UserNormal, - 0, - ); - } - } - - if rule_inclusion == RuleInclusion::DefaultOnly { - return; - } - - if pseudo_element.is_none() { - // Presentational hints. - // - // These go before author rules, but after user rules, see: - // https://drafts.csswg.org/css-cascade/#preshint - let length_before_preshints = applicable_declarations.len(); - element.synthesize_presentational_hints_for_legacy_attributes( - context.visited_handling(), - applicable_declarations, - ); - if cfg!(debug_assertions) { - if applicable_declarations.len() != length_before_preshints { - for declaration in &applicable_declarations[length_before_preshints..] { - assert_eq!(declaration.level(), CascadeLevel::PresHints); - } - } - } - } - - if self.author_styles_enabled == AuthorStylesEnabled::No { - return; - } - - let mut match_document_author_rules = matches_user_and_author_rules; - let mut shadow_cascade_order = 0; - - // XBL / Shadow DOM rules, which are author rules too. - if let Some(shadow) = rule_hash_target.shadow_root() { - debug_assert!( - matches_user_and_author_rules, - "NAC should not be a shadow host" - ); - if let Some(map) = shadow - .style_data() - .and_then(|data| data.host_rules(pseudo_element)) - { - context.with_shadow_host(Some(rule_hash_target), |context| { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::InnerShadowNormal, - shadow_cascade_order, - ); - }); - shadow_cascade_order += 1; - } - } - - // Match slotted rules in reverse order, so that the outer slotted - // rules come before the inner rules (and thus have less priority). - let mut slots = SmallVec::<[_; 3]>::new(); - let mut current = rule_hash_target.assigned_slot(); - while let Some(slot) = current { - debug_assert!( - matches_user_and_author_rules, - "We should not slot NAC anywhere" - ); - slots.push(slot); - current = slot.assigned_slot(); - } - - for slot in slots.iter().rev() { - let shadow = slot.containing_shadow().unwrap(); - if let Some(map) = shadow - .style_data() - .and_then(|data| data.slotted_rules(pseudo_element)) - { - context.with_shadow_host(Some(shadow.host()), |context| { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::InnerShadowNormal, - shadow_cascade_order, - ); - }); - shadow_cascade_order += 1; - } - } - - if matches_user_and_author_rules { - let mut current_containing_shadow = rule_hash_target.containing_shadow(); - while let Some(containing_shadow) = current_containing_shadow { - let cascade_data = containing_shadow.style_data(); - let host = containing_shadow.host(); - if let Some(map) = cascade_data.and_then(|data| data.normal_rules(pseudo_element)) { - context.with_shadow_host(Some(host), |context| { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::SameTreeAuthorNormal, - shadow_cascade_order, - ); - }); - shadow_cascade_order += 1; - } - - let host_is_svg_use_element = - host.is_svg_element() && host.local_name() == &*local_name!("use"); - - if !host_is_svg_use_element { - match_document_author_rules = false; - break; - } - - debug_assert!( - cascade_data.is_none(), - "We allow no stylesheets in subtrees" - ); - - // NOTE(emilio): Hack so matches the rules of the - // enclosing tree. - // - // This is not a problem for invalidation and that kind of stuff - // because they still don't match rules based on elements - // outside of the shadow tree, and because the - // subtrees are immutable and recreated each time the source - // tree changes. - // - // We historically allow cross-document to have these - // rules applied, but I think that's not great. Gecko is the - // only engine supporting that. - // - // See https://github.com/w3c/svgwg/issues/504 for the relevant - // spec discussion. - current_containing_shadow = host.containing_shadow(); - match_document_author_rules = current_containing_shadow.is_none(); - } - } - - let cut_xbl_binding_inheritance = - element.each_xbl_cascade_data(|cascade_data, quirks_mode| { - if let Some(map) = cascade_data.normal_rules(pseudo_element) { - // NOTE(emilio): This is needed because the XBL stylist may - // think it has a different quirks mode than the document. - let mut matching_context = MatchingContext::new( - context.matching_mode(), - context.bloom_filter, - context.nth_index_cache.as_mut().map(|s| &mut **s), - quirks_mode, - ); - matching_context.pseudo_element_matching_fn = - context.pseudo_element_matching_fn; - - // SameTreeAuthorNormal instead of InnerShadowNormal to - // preserve behavior, though that's kinda fishy... - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - &mut matching_context, - flags_setter, - CascadeLevel::SameTreeAuthorNormal, - shadow_cascade_order, - ); - } - }); - - match_document_author_rules &= !cut_xbl_binding_inheritance; - - if match_document_author_rules { - // Author normal rules. - if let Some(map) = self.cascade_data.author.normal_rules(pseudo_element) { - map.get_all_matching_rules( - element, - rule_hash_target, - applicable_declarations, - context, - flags_setter, - CascadeLevel::SameTreeAuthorNormal, - shadow_cascade_order, - ); - } - } - - // Style attribute ("Normal override declarations"). - if let Some(sa) = style_attribute { - applicable_declarations.push(ApplicableDeclarationBlock::from_declarations( - sa.clone_arc(), - CascadeLevel::StyleAttributeNormal, - )); - } - - // Declarations from SVG SMIL animation elements. - if let Some(so) = smil_override { - applicable_declarations.push(ApplicableDeclarationBlock::from_declarations( - so.clone_arc(), - CascadeLevel::SMILOverride, - )); - } - - // The animations sheet (CSS animations, script-generated - // animations, and CSS transitions that are no longer tied to CSS - // markup). - if let Some(anim) = animation_rules.0 { - applicable_declarations.push(ApplicableDeclarationBlock::from_declarations( - anim.clone(), - CascadeLevel::Animations, - )); - } - - // - // !important rules are handled during rule tree insertion. - // - - // The transitions sheet (CSS transitions that are tied to CSS - // markup). - if let Some(anim) = animation_rules.1 { - applicable_declarations.push(ApplicableDeclarationBlock::from_declarations( - anim.clone(), - CascadeLevel::Transitions, - )); - } + RuleCollector::new( + self, + element, + pseudo_element, + style_attribute, + smil_override, + animation_rules, + rule_inclusion, + applicable_declarations, + context, + flags_setter, + ).collect_all(); } /// Given an id, returns whether there might be any rules for that id in any @@ -2100,18 +1838,21 @@ impl CascadeData { self.attribute_dependencies.contains(local_name) } + /// Returns the normal rule map for a given pseudo-element. #[inline] - fn normal_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { + pub fn normal_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { self.normal_rules.rules(pseudo) } + /// Returns the host pseudo rule map for a given pseudo-element. #[inline] - fn host_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { + pub fn host_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { self.host_rules.as_ref().and_then(|d| d.rules(pseudo)) } + /// Returns the slotted rule map for a given pseudo-element. #[inline] - fn slotted_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { + pub fn slotted_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap> { self.slotted_rules.as_ref().and_then(|d| d.rules(pseudo)) }