1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use regex::Regex;

/// Convert the Xpath selectors to CSS selector
///
/// ## Things it supports
/// Any tag (including *), contains(), text(), indexed, any attribute (@id, @class, @anything).
///
/// ## Things it does not support
/// Move up to a parent tag (//a/../p) and maybe something else I'm not aware of...
///
/// ## Examples
///
/// Basic usage:
/// ```
/// let result = cssifier::cssifier("//a/p");
/// assert_eq!(result, Some("a p".to_string()));
///
/// let result = cssifier::cssifier("//a/p[@id='hello']");
/// assert_eq!(result, Some("a p#hello".to_string()));
///
/// let result = cssifier::cssifier("//a/p[contains(text(), 'hello')]");
/// assert_eq!(result, Some("a p:contains(hello)".to_string()));
///
/// let result = cssifier::cssifier("*random selector//*"); // Invalid selectors throw a empty string (WIP)
/// assert_eq!(result, Some("".to_string()));
/// ```
pub fn cssifier<S: AsRef<str>>(xpath: S) -> Option<String> {
    // Trait to &str
    let xpath = xpath.as_ref();

    // Ultra magic regex to parse XPath selectors
    let reg = Regex::new(r#"(?P<node>(^id\(["']?(?P<idvalue>\s*[\w/:][-/\w\s,:;.]*)["']?\)|(?P<nav>//?)(?P<tag>([a-zA-Z][a-zA-Z0-9]{0,10}|\*))(\[((?P<matched>(?P<mattr>@?[.a-zA-Z_:][-\w:.]*(\(\))?)=["'](?P<mvalue>\s*[\w/:][-/\w\s,:;.]*))["']|(?P<contained>contains\((?P<cattr>@?[.a-zA-Z_:][-\w:.]*(\(\))?),\s*["'](?P<cvalue>\s*[\w/:][-/\w\s,:;.]*)["']\)))\])?(\[(?P<nth>\d)\])?))"#).unwrap();
    let mut css = String::new();
    let mut position = 0;

    while position < xpath.len() {
        let node = reg.captures(&xpath[position..])?;
        let find = reg.find(&xpath[position..])?;

        // See the nav identifier
        let nav = match position {
            0 => "",
            _ => {
                if node.name("nav")?.as_str() != "//" {
                    " "
                } else {
                    " > "
                }
            }
        };

        // See the tag name
        let tag = if node.name("tag")?.as_str() == "*" {
            ""
        } else {
            match node.name("tag") {
                Some(tag) => tag.as_str(),
                _ => "",
            }
        };

        // See the idenfitier attribute of the tag
        let attr = if node.name("idvalue").is_some() {
            format!("#{}", node.name("idvalue")?.as_str().replace(" ", "#"))
        } else if node.name("matched").is_some() {
            let mattr = node.name("mattr")?.as_str();
            let mvalue = node.name("mvalue")?.as_str();

            if mattr == "@id" {
                format!("#{}", mvalue.replace(" ", "#"))
            } else if mattr == "@class" {
                format!(".{}", mvalue.replace(" ", "."))
            } else if mattr == "text()" || mattr == "." {
                format!(":contains(^{}$)", mvalue)
            } else if mattr != "" {
                let new_mvalue = if mvalue.find(" ").is_some() {
                    format!("\"{}\"", mvalue)
                } else {
                    mvalue.to_string()
                };
                format!("[{}={}]", mattr.replace("@", ""), new_mvalue)
            } else {
                String::from("")
            }
        } else if node.name("contained").is_some() {
            let cattr = node.name("cattr")?.as_str();
            let cvalue = node.name("cvalue")?.as_str();

            if cattr.starts_with("@") {
                format!("[{}*={}]", cattr.replace("@", ""), cvalue)
            } else if cattr == "text()" {
                format!(":contains({})", cvalue)
            } else {
                String::from("")
            }
        } else {
            String::from("")
        };

        // See the child type
        let nth = if node.name("nth").is_some() {
            format!(":nth-of-type({})", node.name("nth")?.as_str())
        } else {
            String::from("")
        };

        // Paste all the magic :sparkles:
        css = format!("{}{}{}{}{}", css, nav, tag, attr, nth);
        position += find.end();
    }

    Some(css)
}

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

    #[test]
    fn it_works() {
        assert_eq!(cssifier("//a/b").unwrap(), "a b");
        assert_eq!(cssifier("//a/b[@id='hello']").unwrap(), "a b#hello");
        assert_eq!(
            cssifier("//a/b[contains(text(), 'hello')]").unwrap(),
            "a b:contains(hello)"
        );
        assert_eq!(cssifier("*random shit//*").unwrap(), "");
    }
}