nom_html_parser 0.1.1

A parser to convert HTML string to HTML tree structure written with Nom
Documentation
use nom::{
  IResult,
  bytes::complete::{take, take_until, tag, take_while},
  sequence::{delimited},
  character::complete::{char},
  multi::{many0},
  combinator::{opt},
  error::{context, ParseError, VerboseError},
};

pub mod errors;

use super::errors::NEW_LINE_ERROR;
use errors::*;

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Identifier {
  Id(String),
  Class(String),
  Host(String),
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CssRule {
  identifier: Identifier,
  selector: String,
  value: String,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CssRoot {
    rules: Vec<CssRule>,
}

pub fn identifier() -> impl Fn(&str) -> IResult<&str, Identifier, nom::error::VerboseError<&str>> {
  move |input| {
    let (input, ident): (&str, &str) = context(
      IDENTIFIER_ERROR,
      take(1usize)
    )(input)?;
    let ident = match ident {
      "#" => Ok(Identifier::Id(String::from(ident))),
      "." => Ok(Identifier::Class(String::from(ident))),
      ":" => Ok(Identifier::Host(String::from(ident))),
      c => {
        Err(nom::Err::Error(VerboseError::from_char(input, c.chars().take(1).collect::<Vec<char>>().first().unwrap().to_owned())))
      }
    }?;
    Ok((input, ident))
  } 
}

pub fn css_rule(input: &str) -> IResult<&str, CssRule, VerboseError<&str>> {
  // println!("css_rule input start: {:?}", input);
  let input = input.trim_start_matches('\n');
  let (input, space_before) = take_while(move |s| s == ' ')(input)?;
  let (input, identifier) = identifier()(input)?;  
  let (input, selector) = take_until("{")(input)?;
  let selector = selector.trim_end();

  // println!("css rule input before");
  // println!("{}", input);
  let opening_tag = format!("{}", '{');
  let closing_tag = format!("{}{}", space_before, '}');
  // println!("css rule opening tag: {}", opening_tag);
  // println!("css rule closing tag: {}", closing_tag);
  let (input, value) = delimited(
    tag(opening_tag.as_str()),
    take_until(closing_tag.as_str()),
    tag(closing_tag.as_str())
  )(input)?;

  // println!("css rule value: {}", value);
  // println!("css rule input after: {}", input);

  Ok((input, CssRule {
    identifier,
    selector: String::from(selector),
    value: String::from(value),
  }))
}

pub fn css(input: &str) -> IResult<&str, CssRoot, VerboseError<&str>> {
    // println!("first input: {:?}", input);

    // trim starting newlines
    let input =input.trim_end();
    let input =input.trim_end_matches('\n');
    let input =input.trim_end_matches(' ');

    // println!("after trimming: {:?}", input);

    let (input, rules) = many0(css_rule)(input)?; 
    
    let (input, _rest) = nom::character::complete::multispace0(input)?;
    // println!("rest of data after ending tag: {:?}", rest);
    // println!("after rest: {:?}", input);

    Ok((input, CssRoot {
        rules,
    }))
}

#[test]
fn parse_css_rule_test() -> Result<(), String> {
    let css_string = r#"
        .test-class {
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rules_test() -> Result<(), String> {
    let css_string = r#"
        .test-class {
            display: block;
            background-color: rgb(0,0,0,0);
        }
        #test-id {
            display: block;
            background-color: rgb(0,0,0,0);
            background-image: url({{#test}}{{/test}});
        }
        "#;

    let result = css(css_string).map_err(|e| e.to_string())?;

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          },
          CssRule {
            identifier: Identifier::Id("#".to_string()),
            selector: "test-id".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);
            background-image: url({{#test}}{{/test}});\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rule_with_hover_pseudo_selector_test() -> Result<(), String> {
    let css_string = r#"
        .test-class:hover {
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class:hover".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rule_with_first_child_selector_test() -> Result<(), String> {
    let css_string = r#"
        .test-class:first-child {
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class:first-child".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rule_with_nth_child_selector_test() -> Result<(), String> {
    let css_string = r#"
        .test-class:nth-child(2){
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class:nth-child(2)".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rule_with_after_selector_test() -> Result<(), String> {
    let css_string = r#"
        .test-class::after{
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class::after".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}

#[test]
fn parse_css_rule_with_before_selector_test() -> Result<(), String> {
    let css_string = r#"
        .test-class::before {
            display: block;
            background-color: rgb(0,0,0,0);
        }
        "#;

    let result = css(css_string).unwrap();

    assert_eq!(result.1, CssRoot {
        rules: vec![
          CssRule {
            identifier: Identifier::Class(".".to_string()),
            selector: "test-class::before".to_string(),
            value: "\n            display: block;
            background-color: rgb(0,0,0,0);\n".to_string(),
          }
        ],
    });
    assert_eq!(result.0, "");
    Ok(())
}