hpx-browser 2.4.23

Headless browser engine for hpx: HTML parsing, rendering, CDP, and canvas support
Documentation
use crate::{
    css_parser::{Token, TokenKind},
    css_selectors::{ast::NthExpr, error::SelectorParseError},
};

pub fn parse_nth(tokens: &[Token<'_>]) -> Result<NthExpr, SelectorParseError> {
    let tokens: Vec<&Token<'_>> = tokens.iter().filter(|t| !t.kind.is_whitespace()).collect();

    if tokens.is_empty() {
        return Err(SelectorParseError::InvalidNth("empty expression".into()));
    }

    if tokens.len() == 1 {
        if let TokenKind::Ident(name) = &tokens[0].kind {
            match name.to_ascii_lowercase().as_str() {
                "odd" => return Ok(NthExpr { a: 2, b: 1 }),
                "even" => return Ok(NthExpr { a: 2, b: 0 }),
                "n" => return Ok(NthExpr { a: 1, b: 0 }),
                "-n" => return Ok(NthExpr { a: -1, b: 0 }),
                _ => {}
            }
        }
        if let TokenKind::Number {
            int_value: Some(v), ..
        } = &tokens[0].kind
        {
            return Ok(NthExpr { a: 0, b: *v as i32 });
        }
    }

    let mut repr = String::new();
    for t in &tokens {
        match &t.kind {
            TokenKind::Ident(s) => repr.push_str(s),
            TokenKind::Number {
                int_value: Some(v),
                has_sign,
                ..
            } => {
                if *has_sign && *v >= 0 {
                    repr.push('+');
                }
                repr.push_str(&v.to_string());
            }
            TokenKind::Number {
                value, has_sign, ..
            } => {
                if *has_sign && *value >= 0.0 {
                    repr.push('+');
                }
                repr.push_str(&(*value as i64).to_string());
            }
            TokenKind::Dimension {
                int_value: Some(v),
                unit,
                ..
            } => {
                repr.push_str(&v.to_string());
                repr.push_str(unit);
            }
            TokenKind::Delim('+') => repr.push('+'),
            TokenKind::Delim('-') => repr.push('-'),
            _ => {}
        }
    }

    parse_nth_string(&repr)
}

fn parse_nth_string(s: &str) -> Result<NthExpr, SelectorParseError> {
    let s = s.to_ascii_lowercase();
    let s = s.trim();

    match s {
        "odd" => return Ok(NthExpr { a: 2, b: 1 }),
        "even" => return Ok(NthExpr { a: 2, b: 0 }),
        _ => {}
    }

    let n_pos_opt = s
        .char_indices()
        .find(|&(i, c)| {
            c == 'n' && {
                if i > 0 {
                    let prev = s.as_bytes()[i - 1];
                    !prev.is_ascii_alphabetic()
                } else {
                    true
                }
            }
        })
        .map(|(i, _)| i);

    if let Some(n_pos) = n_pos_opt {
        let a_part = &s[..n_pos];
        let b_part = s[n_pos + 1..].trim();

        let a = match a_part {
            "" | "+" => 1,
            "-" => -1,
            _ => a_part
                .parse::<i32>()
                .map_err(|_| SelectorParseError::InvalidNth(s.to_string()))?,
        };

        let b = if b_part.is_empty() {
            0
        } else {
            b_part
                .replace(' ', "")
                .parse::<i32>()
                .map_err(|_| SelectorParseError::InvalidNth(s.to_string()))?
        };

        Ok(NthExpr { a, b })
    } else {
        let b = s
            .parse::<i32>()
            .map_err(|_| SelectorParseError::InvalidNth(s.to_string()))?;
        Ok(NthExpr { a: 0, b })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse(input: &str) -> NthExpr {
        parse_nth_string(input).unwrap()
    }

    #[test]
    fn odd() {
        assert_eq!(parse("odd"), NthExpr { a: 2, b: 1 });
    }

    #[test]
    fn even() {
        assert_eq!(parse("even"), NthExpr { a: 2, b: 0 });
    }

    #[test]
    fn plain_number() {
        assert_eq!(parse("5"), NthExpr { a: 0, b: 5 });
    }

    #[test]
    fn three_n_plus_one() {
        assert_eq!(parse("3n+1"), NthExpr { a: 3, b: 1 });
    }

    #[test]
    fn neg_n_plus_six() {
        assert_eq!(parse("-n+6"), NthExpr { a: -1, b: 6 });
    }

    #[test]
    fn nth_matches() {
        let expr = NthExpr { a: 2, b: 1 };
        assert!(expr.matches(1));
        assert!(!expr.matches(2));
        assert!(expr.matches(3));
    }
}