Skip to main content

css_to_xpath/translate/
nth.rs

1//! The nth-child arithmetic and the structural pseudo-classes Servo folds
2//! into `NthSelectorData`.
3//!
4//! Servo parses `:first-child` as `nth-child` data with `(a, b) = (0, 1)`,
5//! `:last-child` as `nth-last-child(0n+1)`, and so on. That collapse is
6//! lossless for translation: a dedicated `:first-child` translation would
7//! produce byte-identical output to the general an+b form on the same
8//! `(a, b)` (e.g. both give `count(preceding-sibling::*) = 0`). Only
9//! `:only-child`/`:only-of-type` need their own translation.
10
11use selectors::parser::{NthSelectorData, NthType, Selector};
12
13use super::Translator;
14use super::error::Error;
15use super::xpath_expr::{Condition, XPathExpr};
16use crate::parser::CssToXpathImpl;
17
18/// The maximum `An+B of S` nesting depth accepted.
19///
20/// XPath 1.0 has no variables, so `S` must be written out twice: once to
21/// filter the siblings being counted, once to constrain the element being
22/// matched. An `of S` list nested inside another therefore appears in both
23/// copies, and the output doubles per level — a ~500-byte selector nesting
24/// 30 deep asks for tens of gigabytes. The duplication is inherent, so
25/// only a depth limit can bound it. At 8 levels the doubling costs at most
26/// a few hundred times the argument's own translation, and nothing
27/// hand-written nests `of S` at all.
28///
29/// This is far below [`MAX_NESTING_DEPTH`](crate::MAX_NESTING_DEPTH), which bounds
30/// *recursion* rather than output size and so can afford to be generous.
31pub const MAX_NTH_OF_DEPTH: usize = 8;
32
33/// The maximum size of one `of S` translation, a last line of defence
34/// behind [`MAX_NTH_OF_DEPTH`](crate::MAX_NTH_OF_DEPTH): the doubling is bounded by the depth
35/// limit, but the argument it doubles is bounded only by the length of
36/// the selector, so cap the product too. Checked per nesting level, which
37/// caps the largest string ever built at roughly twice this.
38pub const MAX_NTH_OF_BYTES: usize = 1 << 20;
39
40/// A Level 4 `of S` argument list, carried together with how many other
41/// such lists it is nested inside — the two are only ever meaningful
42/// together, since the depth exists to bound this list's duplication.
43#[derive(Clone, Copy)]
44struct OfList<'a> {
45    selectors: &'a [Selector<CssToXpathImpl>],
46    depth: usize,
47}
48
49impl Translator {
50    /// Route one `NthSelectorData` (with Servo's pre-parsed `(a, b)`) to
51    /// the matching translation. `selector_list` carries the Level 4
52    /// `of S` arguments when present (`Component::NthOf`).
53    pub(crate) fn apply_nth(
54        &self,
55        xpath: &mut XPathExpr,
56        data: &NthSelectorData,
57        selector_list: Option<&[Selector<CssToXpathImpl>]>,
58        of_depth: usize,
59    ) -> Result<(), Error> {
60        let a = data.an_plus_b.0;
61        let b = data.an_plus_b.1;
62        let of = selector_list.map(|selectors| OfList {
63            selectors,
64            depth: of_depth,
65        });
66        match data.ty {
67            // :only-child — sibling counts rather than
68            // count(parent::*/child::*) = 1, so the root element (whose
69            // parent is the document node, not an element) matches, the
70            // same way the equivalent :first-child:last-child does.
71            NthType::OnlyChild => {
72                xpath.add_condition(
73                    "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0",
74                );
75                Ok(())
76            }
77            // :only-of-type
78            NthType::OnlyOfType => {
79                let nodetest = xpath.same_type_nodetest().ok_or_else(|| {
80                    Error::unsupported("`:only-of-type` on the universal selector `*`")
81                })?;
82                xpath.add_condition(&format!(
83                    "count(preceding-sibling::{nodetest}) = 0 \
84                     and count(following-sibling::{nodetest}) = 0"
85                ));
86                Ok(())
87            }
88            // :first-child / :last-child / :nth-child() / :nth-last-child()
89            NthType::Child | NthType::LastChild => self.xpath_nth_child(
90                xpath,
91                a,
92                b,
93                /* last = */ data.ty == NthType::LastChild,
94                /* nodetest = */ "*",
95                of,
96            ),
97            // :first-of-type / :last-of-type / :nth-of-type() /
98            // :nth-last-of-type() — none are implemented on the universal
99            // selector `*`.
100            NthType::OfType | NthType::LastOfType => {
101                let nodetest = xpath.same_type_nodetest().ok_or_else(|| {
102                    Error::unsupported("an of-type pseudo-class on the universal selector `*`")
103                })?;
104                self.xpath_nth_child(
105                    xpath,
106                    a,
107                    b,
108                    /* last = */ data.ty == NthType::LastOfType,
109                    &nodetest,
110                    of,
111                )
112            }
113        }
114    }
115
116    /// The general an+b translation, derived from
117    /// https://www.w3.org/TR/selectors-4/#structural-pseudos.
118    ///
119    /// `nodetest` selects which siblings are counted: `*` for the child
120    /// pseudos, the same-type node test for the of-type pseudos.
121    fn xpath_nth_child(
122        &self,
123        xpath: &mut XPathExpr,
124        a: i32,
125        b: i32,
126        last: bool,
127        nodetest: &str,
128        of: Option<OfList<'_>>,
129    ) -> Result<(), Error> {
130        // i64 throughout: `-(b-1)` / `abs(a)` must not overflow for
131        // extreme i32 inputs.
132        let a = i64::from(a);
133        let b = i64::from(b);
134
135        // work with b-1 instead
136        let b_min_1 = b - 1;
137
138        // CSS Level 4: when a selector list is provided, the current
139        // element must match it too. The same OR-joined condition is
140        // appended in every branch *and* rendered into the sibling
141        // predicate below, so each level of `of S` nesting doubles the
142        // output: both limits guard that doubling.
143        // A trivially-true list (it contains a universal argument)
144        // constrains nothing, like a plain :nth-child.
145        let current_element_check = match of {
146            Some(of) => {
147                if of.depth >= MAX_NTH_OF_DEPTH {
148                    return Err(Error::unsupported(format!(
149                        "`An+B of S` selector lists nested more than \
150                         {MAX_NTH_OF_DEPTH} levels deep"
151                    )));
152                }
153                let check = self
154                    .arg_conditions(of.selectors, ":nth-child(... of S)", of.depth + 1)?
155                    .and_then(|conditions| Condition::join_or(&conditions));
156                if check
157                    .as_ref()
158                    .is_some_and(|c| c.expr.len() > MAX_NTH_OF_BYTES)
159                {
160                    return Err(Error::unsupported(format!(
161                        "an `An+B of S` selector list translating to more than \
162                         {MAX_NTH_OF_BYTES} bytes"
163                    )));
164                }
165                check
166            }
167            None => None,
168        };
169
170        // early-exit condition 1:
171        // ~~~~~~~~~~~~~~~~~~~~~~~
172        // for a == 1, nth-*(an+b) means n+b-1 siblings before/after, and
173        // since n is a non-negative integer, if b-1<=0 there is always an
174        // "n" matching any number of siblings (maybe none)
175        if a == 1 && b_min_1 <= 0 {
176            if let Some(check) = current_element_check {
177                xpath.push_condition(check);
178            }
179            return Ok(());
180        }
181        // early-exit condition 2:
182        // ~~~~~~~~~~~~~~~~~~~~~~~
183        // an+b-1 siblings with (b-1)<0 needs a>0 to reach zero, so for
184        // a<=0 nothing can match. Writing it as `0` rather than letting
185        // the a==0 branch below emit `count(...) = -1` says so plainly.
186        if a <= 0 && b_min_1 < 0 {
187            xpath.add_condition("0");
188            if let Some(check) = current_element_check {
189                xpath.push_condition(check);
190            }
191            return Ok(());
192        }
193
194        // The predicate filtering counted siblings (CSS Level 4 `of S`) —
195        // the same OR-joined conditions as the current-element check.
196        let selector_predicate = match current_element_check {
197            Some(ref check) => format!("[{}]", check.expr),
198            None => String::new(),
199        };
200
201        // count siblings before or after the element
202        let axis = if last { "following" } else { "preceding" };
203        let siblings_count = format!("count({axis}-sibling::{nodetest}{selector_predicate})");
204
205        // special case of fixed position: nth-*(0n+b)
206        if a == 0 {
207            xpath.add_condition(&format!("{siblings_count} = {b_min_1}"));
208            if let Some(check) = current_element_check {
209                xpath.push_condition(check);
210            }
211            return Ok(());
212        }
213
214        let mut expr: Vec<String> = Vec::new();
215
216        if a > 0 {
217            // siblings count, an+b-1, is always >= 0, so if a>0 and
218            // (b-1)<=0 an "n" exists to satisfy this; the predicate is
219            // only interesting if (b-1)>0
220            if b_min_1 > 0 {
221                expr.push(format!("{siblings_count} >= {b_min_1}"));
222            }
223        } else {
224            // a<0 with (b-1)<0 was the early exit above; otherwise:
225            expr.push(format!("{siblings_count} <= {b_min_1}"));
226        }
227
228        // operations modulo 1 or -1 are simpler: the >=/<= test above
229        // already covers them
230        if a.abs() != 1 {
231            // count(***-sibling::***) - (b-1) = 0 (mod a)
232            let mut left = siblings_count;
233
234            // apply "modulo a" on the 2nd term, -(b-1), to simplify things
235            // like "(... +6) % -3", and also make it positive with |a|
236            // (`rem_euclid`)
237            let b_neg = (-b_min_1).rem_euclid(a.abs());
238
239            if b_neg != 0 {
240                left = format!("({left} + {b_neg})");
241            }
242
243            expr.push(format!("{left} mod {a} = 0"));
244        }
245
246        if !expr.is_empty() {
247            xpath.add_condition(&expr.join(" and "));
248        }
249
250        if let Some(check) = current_element_check {
251            xpath.push_condition(check);
252        }
253
254        Ok(())
255    }
256}