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}