Skip to content

kitsuyui/sxd_html

 
 

sxd_html

crates.io tests Coverage license

Uses the html5ever to parse html and convert it to a sxd_document::Package to use with sxd_xpath.

parse_html and parse_html_fragment intentionally return only the parsed sxd_document::Package. If callers need to inspect html5ever parse errors, use parse_html_with_errors or parse_html_fragment_with_errors:

let (package, errors) = sxd_html::parse_html_with_errors(contents);
for error in &errors {
    eprintln!("line {}: {}", error.line(), error.message());
}

Example

use sxd_xpath::{nodeset::Node, Context, Error, Factory, Value};

fn main() -> anyhow::Result<()> {
    let contents = r#"
<!doctype html>
<html lang="en">
  <body>
    <article>
      <h1>
        <a href="/rust-lang/rust"><span>rust-lang</span> /rust</a>
      </h1>
    </article>
    <article>
      <h1>
        <a href="/tokio-rs/tokio"><span>tokio-rs</span> /tokio</a>
      </h1>
    </article>
    <article>
      <h1>
        <a href="/hyperium/hyper"><span>hyperium</span> /hyper</a>
      </h1>
    </article>
  </body>
</html>
"#;
    let package = sxd_html::parse_html(contents);
    let document = package.as_document();

    let mut trending_repos: Vec<String> = Default::default();
    let repo_as = match evaluate_xpath_node(document.root(), "//article/h1/a")? {
        Value::Nodeset(set) => set,
        _ => panic!("Expected node set"),
    }
    .document_order();

    for repo_a in repo_as {
        let user = evaluate_xpath_node(repo_a, "./span/text()")?.into_string();
        let name = repo_a
            .children()
            .last()
            .unwrap()
            .text()
            .map(|t| t.text())
            .unwrap_or_default();
        trending_repos.push(format!("{}{}", user.trim(), name.trim()));
    }

    println!("Trending Repos :");
    for name in trending_repos {
        println!("\t{name}");
    }

    Ok(())
}

fn evaluate_xpath_node<'d>(node: impl Into<Node<'d>>, expr: &str) -> Result<Value<'d>, Error> {
    let factory = Factory::new();
    let expression = factory.build(expr)?;
    let expression = expression.ok_or(Error::NoXPath)?;

    let context = Context::new();

    expression
        .evaluate(&context, node.into())
        .map_err(Into::into)
}

Output:

Trending Repos :
	rust-lang/rust
	tokio-rs/tokio
	hyperium/hyper

Note

This library uses html5ever to parse html. So you need to follow the constraints of html5ever. For example, you need to be careful about how to handle <table> tags.

<table>
    <tr>
        <td>1</td>
        <td>2</td>
    </tr>
    <tr>
        <td>3</td>
        <td>4</td>
    </tr>
</table>

will be interpreted as

<table>
    <tbody>
        <tr>
            <td>1</td>
            <td>2</td>
        </tr>
        <tr>
            <td>3</td>
            <td>4</td>
        </tr>
    </tbody>
</table>

So if you run the query //table/tr/td, you will not get the td hidden by tbody. You need to run the query //table/tbody/tr/td.

HTML elements are stored without the XHTML namespace so existing unprefixed XPath queries such as //article/h1/a keep working. Embedded SVG and MathML elements keep their HTML5 namespaces. Register the namespace prefix on the XPath context before querying those elements:

let mut context = Context::new();
context.set_namespace("svg", "http://www.w3.org/2000/svg");

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Helper crate to parse html into sxd_document::Package

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 95.1%
  • Shell 4.9%