Skip to main content

tl/queryselector/
parser.rs

1#[cfg(not(feature = "std"))]
2use crate::ParseError;
3use crate::{stream::Stream, util};
4
5use super::Selector;
6
7/// A query selector parser
8pub struct Parser<'a> {
9    stream: Stream<'a, u8>,
10}
11
12impl<'a> Parser<'a> {
13    /// Creates a new query selector parser
14    pub fn new(input: &'a [u8]) -> Self {
15        Self {
16            stream: Stream::new(input),
17        }
18    }
19
20    #[cfg(not(feature = "std"))]
21    fn skip_whitespaces(&mut self) -> bool {
22        let has_whitespace = self.stream.expect_and_skip_cond(b' ');
23        while !self.stream.is_eof() {
24            if self.stream.expect_and_skip(b' ').is_none() {
25                break;
26            }
27        }
28        has_whitespace
29    }
30
31    fn read_identifier(&mut self) -> &'a [u8] {
32        let start = self.stream.idx;
33
34        while !self.stream.is_eof() {
35            let is_ident = self.stream.current().copied().is_some_and(util::is_ident);
36            if !is_ident {
37                break;
38            } else {
39                self.stream.advance();
40            }
41        }
42
43        self.stream.slice(start, self.stream.idx)
44    }
45
46    #[cfg(feature = "std")]
47    fn read_css_identifier(&mut self) -> &'a [u8] {
48        let start = self.stream.idx;
49        while self
50            .stream
51            .current_cpy()
52            .is_some_and(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c >= 128)
53        {
54            self.stream.advance();
55        }
56        self.stream.slice(start, self.stream.idx)
57    }
58
59    #[cfg(not(feature = "std"))]
60    fn parse_combinator<const MAX_SELECTOR_NODES: usize>(
61        &mut self,
62        left: Selector<'a, MAX_SELECTOR_NODES>,
63    ) -> Result<Selector<'a, MAX_SELECTOR_NODES>, ParseError> {
64        let has_whitespaces = self.skip_whitespaces();
65        if self.stream.current_cpy().is_none() {
66            return Ok(left);
67        }
68        if has_whitespaces || matches!(self.stream.current_cpy(), Some(b',' | b'>')) {
69            return Err(ParseError::SelectorCapacityExceeded);
70        }
71        Err(ParseError::SelectorCapacityExceeded)
72    }
73
74    fn parse_attribute<const MAX_SELECTOR_NODES: usize>(
75        &mut self,
76    ) -> Option<Selector<'a, MAX_SELECTOR_NODES>> {
77        let attribute = self.read_identifier();
78        if attribute.is_empty() {
79            return None;
80        }
81        let ty = match self.stream.current_cpy() {
82            Some(b']') => {
83                self.stream.advance();
84                Selector::Attribute(attribute)
85            }
86            Some(b'=') => {
87                self.stream.advance();
88                let quote = self.stream.expect_oneof_and_skip(b"\"'");
89                let value = if let Some(q) = quote {
90                    let start = self.stream.idx;
91                    while self
92                        .stream
93                        .current_cpy()
94                        .is_some_and(|c| c != q && c != b'\\')
95                    {
96                        self.stream.advance();
97                    }
98                    self.stream.slice(start, self.stream.idx)
99                } else {
100                    self.read_identifier()
101                };
102                if let Some(quote) = quote {
103                    // Only require the given quote if the value starts with a quote
104                    self.stream.expect_and_skip(quote)?;
105                }
106                self.stream.expect_and_skip(b']')?;
107                Selector::AttributeValue(attribute, value)
108            }
109            Some(c @ b'~' | c @ b'^' | c @ b'$' | c @ b'*') => {
110                self.stream.advance();
111                self.stream.expect_and_skip(b'=')?;
112                let quote = self.stream.expect_oneof_and_skip(b"\"'");
113                let value = if let Some(q) = quote {
114                    let start = self.stream.idx;
115                    while self
116                        .stream
117                        .current_cpy()
118                        .is_some_and(|c| c != q && c != b'\\')
119                    {
120                        self.stream.advance();
121                    }
122                    self.stream.slice(start, self.stream.idx)
123                } else {
124                    self.read_identifier()
125                };
126                if let Some(quote) = quote {
127                    // Only require the given quote if the value starts with a quote
128                    self.stream.expect_and_skip(quote)?;
129                }
130                self.stream.expect_and_skip(b']')?;
131                match c {
132                    b'~' => Selector::AttributeValueWhitespacedContains(attribute, value),
133                    b'^' => Selector::AttributeValueStartsWith(attribute, value),
134                    b'$' => Selector::AttributeValueEndsWith(attribute, value),
135                    b'*' => Selector::AttributeValueSubstring(attribute, value),
136                    _ => unreachable!(),
137                }
138            }
139            _ => return None,
140        };
141        Some(ty)
142    }
143
144    /// Parses a full selector, respecting compound, combinator and list precedence.
145    #[cfg(feature = "std")]
146    pub fn selector(&mut self) -> Option<Selector<'a>> {
147        css(self.stream.data())
148    }
149
150    /// Parses a full selector without allocation.
151    #[cfg(not(feature = "std"))]
152    pub fn selector<const MAX_SELECTOR_NODES: usize>(
153        &mut self,
154    ) -> Result<Selector<'a, MAX_SELECTOR_NODES>, ParseError> {
155        self.skip_whitespaces();
156        let tok = self
157            .stream
158            .current_cpy()
159            .ok_or(ParseError::SelectorCapacityExceeded)?;
160
161        let left = match tok {
162            b'#' => {
163                self.stream.advance();
164                let id = self.read_identifier();
165                Selector::Id(id)
166            }
167            b'.' => {
168                self.stream.advance();
169                let class = self.read_identifier();
170                Selector::Class(class)
171            }
172            b'*' => {
173                self.stream.advance();
174                Selector::All
175            }
176            b'[' => {
177                self.stream.advance();
178                self.parse_attribute::<MAX_SELECTOR_NODES>()
179                    .ok_or(ParseError::SelectorCapacityExceeded)?
180            }
181            _ if util::is_ident(tok) => {
182                let tag = self.read_identifier();
183                Selector::Tag(tag)
184            }
185            _ => return Err(ParseError::SelectorCapacityExceeded),
186        };
187
188        self.parse_combinator(left)
189    }
190}
191
192#[cfg(feature = "std")]
193fn css(input: &[u8]) -> Option<Selector<'_>> {
194    css_at(input, 0)
195}
196
197#[cfg(feature = "std")]
198fn css_at(input: &[u8], depth_limit: usize) -> Option<Selector<'_>> {
199    if depth_limit >= 64 {
200        return None;
201    }
202    let input = trim(input);
203    if input.is_empty() {
204        return None;
205    }
206    // Split only outside attributes, functional pseudo-classes and quoted strings.
207    let mut depth = 0usize;
208    let mut quote = None;
209    let mut relations = Vec::new();
210    let mut commas = Vec::new();
211    for (i, &c) in input.iter().enumerate() {
212        if let Some(q) = quote {
213            if c == q {
214                quote = None;
215            }
216            continue;
217        }
218        match c {
219            b'\'' | b'"' => quote = Some(c),
220            b'[' | b'(' => depth += 1,
221            b']' | b')' => depth = depth.checked_sub(1)?,
222            b',' if depth == 0 => commas.push(i),
223            b'>' | b'+' | b'~' if depth == 0 => relations.push((i, c)),
224            c if c.is_ascii_whitespace() && depth == 0 => relations.push((i, b' ')),
225            _ => {}
226        }
227    }
228    if depth != 0 || quote.is_some() {
229        return None;
230    }
231    if let Some(&i) = commas.first() {
232        return Some(Selector::Or(
233            Box::new(css_at(&input[..i], depth_limit + 1)?),
234            Box::new(css_at(&input[i + 1..], depth_limit + 1)?),
235        ));
236    }
237    // Rightmost relation gives left-associative chains; spaces surrounding an
238    // explicit combinator are not descendant combinators.
239    for &(i, c) in relations.iter().rev() {
240        let left = trim(&input[..i]);
241        let right = trim(&input[i + 1..]);
242        if c == b' '
243            && (left.is_empty()
244                || right.is_empty()
245                || left.last().is_some_and(|c| b">+~".contains(c))
246                || right.first().is_some_and(|c| b">+~".contains(c)))
247        {
248            continue;
249        }
250        let left = Box::new(css_at(left, depth_limit + 1)?);
251        let right = Box::new(css_at(right, depth_limit + 1)?);
252        return Some(match c {
253            b'>' => Selector::Parent(left, right),
254            b'+' => Selector::Adjacent(left, right),
255            b'~' => Selector::Sibling(left, right),
256            _ => Selector::Descendant(left, right),
257        });
258    }
259    let mut parser = Parser::new(input);
260    let mut result = None;
261    let mut compounds = 0;
262    while let Some(tok) = parser.stream.current_cpy() {
263        compounds += 1;
264        if compounds >= 64 {
265            return None;
266        }
267        let simple = match tok {
268            b'#' | b'.' => {
269                parser.stream.advance();
270                let ident = parser.read_css_identifier();
271                if ident.is_empty() {
272                    return None;
273                }
274                if tok == b'#' {
275                    Selector::Id(ident)
276                } else {
277                    Selector::Class(ident)
278                }
279            }
280            b'*' => {
281                parser.stream.advance();
282                Selector::All
283            }
284            b'[' => {
285                parser.stream.advance();
286                parser.parse_attribute::<0>()?
287            }
288            b':' => {
289                parser.stream.advance();
290                let name = parser.read_css_identifier();
291                match name {
292                    b"first-child" => Selector::NthChild(0, 1),
293                    b"nth-child" | b"not" | b"has" => {
294                        parser.stream.expect_and_skip(b'(')?;
295                        let start = parser.stream.idx;
296                        let mut depth = 1;
297                        let mut quote = None;
298                        while let Some(c) = parser.stream.current_cpy() {
299                            if let Some(q) = quote {
300                                if c == q {
301                                    quote = None;
302                                }
303                            } else {
304                                match c {
305                                    b'\'' | b'"' => quote = Some(c),
306                                    b'(' => depth += 1,
307                                    b')' => {
308                                        depth -= 1;
309                                        if depth == 0 {
310                                            break;
311                                        }
312                                    }
313                                    _ => {}
314                                }
315                            }
316                            parser.stream.advance();
317                        }
318                        let arg = trim(parser.stream.slice(start, parser.stream.idx));
319                        parser.stream.expect_and_skip(b')')?;
320                        match name {
321                            b"not" => Selector::Not(Box::new(css_at(arg, depth_limit + 1)?)),
322                            b"has" => Selector::Has(Box::new(css_at(arg, depth_limit + 1)?)),
323                            _ => {
324                                let text = core::str::from_utf8(arg).ok()?;
325                                let (a, b) = match text {
326                                    "odd" => (2, 1),
327                                    "even" => (2, 0),
328                                    _ if text.contains('n') => {
329                                        let (a, b) = text.split_once('n')?;
330                                        let a = match a.trim() {
331                                            "" | "+" => 1,
332                                            "-" => -1,
333                                            a => a.parse::<i32>().ok()?,
334                                        };
335                                        let b = b.trim();
336                                        let b = if b.is_empty() {
337                                            0
338                                        } else {
339                                            if !b.starts_with('+') && !b.starts_with('-') {
340                                                return None;
341                                            }
342                                            let compact: String = b
343                                                .chars()
344                                                .filter(|c| !c.is_ascii_whitespace())
345                                                .collect();
346                                            compact.parse::<i32>().ok()?
347                                        };
348                                        (a, b)
349                                    }
350                                    _ => (0, text.parse::<i32>().ok()?),
351                                };
352                                Selector::NthChild(a, b)
353                            }
354                        }
355                    }
356                    _ => return None,
357                }
358            }
359            _ if tok.is_ascii_alphanumeric() || tok == b'_' || tok == b'-' || tok >= 128 => {
360                // A type selector must start a compound selector.
361                if result.is_some() {
362                    return None;
363                }
364                Selector::Tag(parser.read_css_identifier())
365            }
366            _ => return None,
367        };
368        result = Some(match result {
369            None => simple,
370            Some(left) => Selector::And(Box::new(left), Box::new(simple)),
371        });
372    }
373    result
374}
375
376#[cfg(feature = "std")]
377fn trim(mut input: &[u8]) -> &[u8] {
378    while input.first().is_some_and(u8::is_ascii_whitespace) {
379        input = &input[1..];
380    }
381    while input.last().is_some_and(u8::is_ascii_whitespace) {
382        input = &input[..input.len() - 1];
383    }
384    input
385}