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}