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 `not(preceding-sibling::*[1])`). Only
9//! `:only-child`/`:only-of-type` need their own translation.
10//!
11//! # Positional tests, not `count()`
12//!
13//! Every bound on the number of siblings is written as a positional
14//! predicate — see [`push_sibling_count`]. `count()` materialises the whole
15//! axis, so selecting over *n* siblings costs O(n²); `axis::T[k]` lets the
16//! engine stop at the *k*-th node. Only the `mod a` congruence of a
17//! repeating series still has to count.
18
19use std::fmt::{self, Write as _};
20
21use selectors::parser::{NthSelectorData, NthType, Selector};
22
23use super::Translator;
24use super::error::Error;
25use super::xpath_expr::{Condition, XPathExpr};
26use crate::parser::CssToXpathImpl;
27
28/// The maximum `An+B of S` nesting depth accepted.
29///
30/// XPath 1.0 has no variables, so `S` must be written out more than once:
31/// to filter the siblings being tested — twice when a test bounds the
32/// sibling count from both sides, as `:nth-child(2 of S)` or
33/// `:nth-child(2n+3 of S)` does — and once more to constrain the element
34/// being matched. An `of S` list nested inside another therefore appears
35/// in every copy, and the output doubles or triples per level — a
36/// ~500-byte selector nesting 30 deep asks for far more memory than
37/// exists. The duplication is inherent, so only a depth limit can bound
38/// it, with [`MAX_NTH_OF_BYTES`] behind it for the arguments the depth
39/// alone does not keep small. Nothing hand-written nests `of S` at all.
40///
41/// This is far below [`MAX_NESTING_DEPTH`](crate::MAX_NESTING_DEPTH), which bounds
42/// *recursion* rather than output size and so can afford to be generous.
43pub const MAX_NTH_OF_DEPTH: usize = 8;
44
45/// The maximum size of one `of S` translation, a last line of defence
46/// behind [`MAX_NTH_OF_DEPTH`](crate::MAX_NTH_OF_DEPTH): the duplication is bounded by the depth
47/// limit, but the argument it copies is bounded only by the length of
48/// the selector, so cap the product too. Checked per nesting level, which
49/// caps the largest string ever built at roughly three times this.
50pub const MAX_NTH_OF_BYTES: usize = 1 << 20;
51
52/// A Level 4 `of S` argument list, carried together with how many other
53/// such lists it is nested inside — the two are only ever meaningful
54/// together, since the depth exists to bound this list's duplication.
55#[derive(Clone, Copy)]
56struct OfList<'a> {
57    selectors: &'a [Selector<CssToXpathImpl>],
58    depth: usize,
59}
60
61impl Translator {
62    /// Route one `NthSelectorData` (with Servo's pre-parsed `(a, b)`) to
63    /// the matching translation. `selector_list` carries the Level 4
64    /// `of S` arguments when present (`Component::NthOf`).
65    pub(crate) fn apply_nth(
66        &self,
67        xpath: &mut XPathExpr,
68        data: &NthSelectorData,
69        selector_list: Option<&[Selector<CssToXpathImpl>]>,
70        of_depth: usize,
71    ) -> Result<(), Error> {
72        let a = data.an_plus_b.0;
73        let b = data.an_plus_b.1;
74        let of = selector_list.map(|selectors| OfList {
75            selectors,
76            depth: of_depth,
77        });
78        match data.ty {
79            // :only-child — sibling tests rather than
80            // count(parent::*/child::*) = 1, so the root element (whose
81            // parent is the document node, not an element) matches, the
82            // same way the equivalent :first-child:last-child does.
83            NthType::OnlyChild => {
84                let mut cond = String::new();
85                push_sibling_count(&mut cond, "preceding-sibling::*", Bound::Exactly, 0);
86                cond.push_str(" and ");
87                push_sibling_count(&mut cond, "following-sibling::*", Bound::Exactly, 0);
88                xpath.add_condition(cond);
89                Ok(())
90            }
91            // :only-of-type
92            NthType::OnlyOfType => {
93                let nodetest = xpath.same_type_nodetest().ok_or_else(|| {
94                    Error::unsupported("`:only-of-type` on the universal selector `*`")
95                })?;
96                let mut cond = String::new();
97                for (i, last) in [false, true].into_iter().enumerate() {
98                    if i > 0 {
99                        cond.push_str(" and ");
100                    }
101                    let siblings = Siblings::new(last, &nodetest, None);
102                    push_sibling_count(&mut cond, siblings, Bound::Exactly, 0);
103                }
104                xpath.add_condition(cond);
105                Ok(())
106            }
107            // :first-child / :last-child / :nth-child() / :nth-last-child()
108            NthType::Child | NthType::LastChild => self.xpath_nth_child(
109                xpath,
110                a,
111                b,
112                /* last = */ data.ty == NthType::LastChild,
113                /* nodetest = */ "*",
114                of,
115            ),
116            // :first-of-type / :last-of-type / :nth-of-type() /
117            // :nth-last-of-type() — none are implemented on the universal
118            // selector `*`.
119            NthType::OfType | NthType::LastOfType => {
120                let nodetest = xpath.same_type_nodetest().ok_or_else(|| {
121                    Error::unsupported("an of-type pseudo-class on the universal selector `*`")
122                })?;
123                self.xpath_nth_child(
124                    xpath,
125                    a,
126                    b,
127                    /* last = */ data.ty == NthType::LastOfType,
128                    &nodetest,
129                    of,
130                )
131            }
132        }
133    }
134
135    /// The general an+b translation, derived from
136    /// https://www.w3.org/TR/selectors-4/#structural-pseudos.
137    ///
138    /// `nodetest` selects which siblings are counted: `*` for the child
139    /// pseudos, the same-type node test for the of-type pseudos.
140    fn xpath_nth_child(
141        &self,
142        xpath: &mut XPathExpr,
143        a: i32,
144        b: i32,
145        last: bool,
146        nodetest: &str,
147        of: Option<OfList<'_>>,
148    ) -> Result<(), Error> {
149        // i64 throughout: `-(b-1)` / `abs(a)` must not overflow for
150        // extreme i32 inputs.
151        let a = i64::from(a);
152        let b = i64::from(b);
153
154        // work with b-1 instead
155        let b_min_1 = b - 1;
156
157        // CSS Level 4: when a selector list is provided, the current
158        // element must match it too. The same OR-joined condition is
159        // appended in every branch *and* rendered into the sibling
160        // predicate below, so each level of `of S` nesting doubles or
161        // triples the output: both limits guard that growth.
162        // A trivially-true list (it contains a universal argument)
163        // constrains nothing, like a plain :nth-child.
164        let current_element_check = match of {
165            Some(of) => {
166                if of.depth >= MAX_NTH_OF_DEPTH {
167                    return Err(Error::unsupported(format!(
168                        "`An+B of S` selector lists nested more than \
169                         {MAX_NTH_OF_DEPTH} levels deep"
170                    )));
171                }
172                let check = self
173                    .arg_conditions(of.selectors, ":nth-child(... of S)", of.depth + 1)?
174                    .and_then(Condition::join_or);
175                if check
176                    .as_ref()
177                    .is_some_and(|c| c.expr.len() > MAX_NTH_OF_BYTES)
178                {
179                    return Err(Error::unsupported(format!(
180                        "an `An+B of S` selector list translating to more than \
181                         {MAX_NTH_OF_BYTES} bytes"
182                    )));
183                }
184                check
185            }
186            None => None,
187        };
188
189        // early-exit condition 1:
190        // ~~~~~~~~~~~~~~~~~~~~~~~
191        // for a == 1, nth-*(an+b) means n+b-1 siblings before/after, and
192        // since n is a non-negative integer, if b-1<=0 there is always an
193        // "n" matching any number of siblings (maybe none)
194        if a == 1 && b_min_1 <= 0 {
195            if let Some(check) = current_element_check {
196                xpath.push_condition(check);
197            }
198            return Ok(());
199        }
200        // early-exit condition 2:
201        // ~~~~~~~~~~~~~~~~~~~~~~~
202        // an+b-1 siblings with (b-1)<0 needs a>0 to reach zero, so for
203        // a<=0 nothing can match. Writing it as `0` rather than letting
204        // the a==0 branch below ask for a negative sibling count says so
205        // plainly.
206        if a <= 0 && b_min_1 < 0 {
207            if let Some(check) = current_element_check {
208                xpath.push_condition(check);
209            }
210            xpath.add_condition("0");
211            return Ok(());
212        }
213
214        // The siblings before or after the element, filtered by `S` (CSS
215        // Level 4 `of S`) — the same OR-joined conditions as the
216        // current-element check. Every term below is written straight
217        // into one buffer: `S` can be up to `MAX_NTH_OF_BYTES`, and any
218        // intermediate string holding it multiplies what a nested `of S`
219        // allocates.
220        let siblings = Siblings::new(
221            last,
222            nodetest,
223            current_element_check.as_ref().map(|c| c.expr.as_str()),
224        );
225        // At most two copies of `siblings` (an exact position, or a bound
226        // and the congruence), plus the fixed text and up to three
227        // integers around them.
228        let mut cond = String::with_capacity(2 * siblings.len() + 96);
229
230        // special case of fixed position: nth-*(0n+b)
231        if a == 0 {
232            push_sibling_count(&mut cond, siblings, Bound::Exactly, b_min_1);
233        } else {
234            if a > 0 {
235                // siblings count, an+b-1, is always >= 0, so if a>0 and
236                // (b-1)<=0 an "n" exists to satisfy this; the predicate is
237                // only interesting if (b-1)>0. Nor is it if (b-1)<a: the
238                // smallest count congruent to b-1 modulo a is then b-1
239                // itself, so the `mod` test below already implies the
240                // bound.
241                if b_min_1 >= a {
242                    push_sibling_count(&mut cond, siblings, Bound::AtLeast, b_min_1);
243                }
244            } else {
245                // a<0 with (b-1)<0 was the early exit above; otherwise:
246                push_sibling_count(&mut cond, siblings, Bound::AtMost, b_min_1);
247            }
248
249            // operations modulo 1 or -1 are simpler: the >=/<= test above
250            // already covers them
251            if a.abs() != 1 {
252                if !cond.is_empty() {
253                    cond.push_str(" and ");
254                }
255                // count(***-sibling::***) - (b-1) = 0 (mod a) — the one
256                // test with no positional equivalent.
257                //
258                // Apply "modulo a" on the 2nd term, -(b-1), to simplify
259                // things like "(... +6) % -3", and also make it positive
260                // with |a| (`rem_euclid`).
261                let b_neg = (-b_min_1).rem_euclid(a.abs());
262                let written = if b_neg == 0 {
263                    write!(cond, "count({siblings}) mod {a} = 0")
264                } else {
265                    write!(cond, "(count({siblings}) + {b_neg}) mod {a} = 0")
266                };
267                written.expect("writing to a String cannot fail");
268            }
269        }
270
271        // The current-element check goes before the sibling test: `and`
272        // evaluates left to right and stops at the first false operand,
273        // so a candidate that does not match `S` skips the sibling walk.
274        if let Some(check) = current_element_check {
275            xpath.push_condition(check);
276        }
277        if !cond.is_empty() {
278            xpath.add_condition(cond);
279        }
280
281        Ok(())
282    }
283}
284
285/// How [`push_sibling_count`] bounds the number of siblings.
286#[derive(Clone, Copy)]
287enum Bound {
288    Exactly,
289    AtLeast,
290    AtMost,
291}
292
293/// A sibling axis step, `{axis}-sibling::{nodetest}` with an optional
294/// `[{filter}]`, written in place wherever it is formatted rather than
295/// built once and copied.
296#[derive(Clone, Copy)]
297struct Siblings<'a> {
298    axis: &'static str,
299    nodetest: &'a str,
300    filter: Option<&'a str>,
301}
302
303impl<'a> Siblings<'a> {
304    /// The siblings after the element when `last`, else those before it.
305    const fn new(last: bool, nodetest: &'a str, filter: Option<&'a str>) -> Self {
306        let axis = if last {
307            "following-sibling::"
308        } else {
309            "preceding-sibling::"
310        };
311        Self {
312            axis,
313            nodetest,
314            filter,
315        }
316    }
317
318    /// The formatted length in bytes.
319    fn len(&self) -> usize {
320        self.axis.len() + self.nodetest.len() + self.filter.map_or(0, |f| f.len() + 2)
321    }
322}
323
324impl fmt::Display for Siblings<'_> {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        f.write_str(self.axis)?;
327        f.write_str(self.nodetest)?;
328        if let Some(filter) = self.filter {
329            f.write_str("[")?;
330            f.write_str(filter)?;
331            f.write_str("]")?;
332        }
333        Ok(())
334    }
335}
336
337/// Append to `out` a test that the node-set `siblings` — a reverse or
338/// forward sibling axis step, predicates and all — has `Exactly`,
339/// `AtLeast` or `AtMost` `k` members, written with positional predicates
340/// rather than `count()`.
341///
342/// `siblings[k]` is non-empty exactly when there are at least `k`
343/// siblings: on either sibling axis, position counts outwards from the
344/// context node. A trailing `[S]` in `siblings` filters before the
345/// position applies, so `[S][k]` is the `k`-th sibling matching `S`.
346///
347/// The `[1]` in the zero case is load-bearing: libxml2 takes a
348/// pathological path on a bare `not(preceding-sibling::*)` — minutes for a
349/// list `count()` answers in seconds — which the positional form avoids.
350fn push_sibling_count(out: &mut String, siblings: impl fmt::Display, bound: Bound, k: i64) {
351    debug_assert!(k >= 0, "a sibling count is never negative");
352    let written = match bound {
353        Bound::Exactly if k == 0 => write!(out, "not({siblings}[1])"),
354        Bound::Exactly => write!(out, "{siblings}[{k}] and not({siblings}[{}])", k + 1),
355        Bound::AtLeast => {
356            debug_assert!(k > 0, "at least zero siblings is no test at all");
357            write!(out, "{siblings}[{k}]")
358        }
359        Bound::AtMost => write!(out, "not({siblings}[{}])", k + 1),
360    };
361    written.expect("writing to a String cannot fail");
362}