use lazy_static::lazy_static;
use scraper::Selector;
use std::collections::HashMap;
use std::sync::RwLock;
use crate::types::{ParserError, ParserResult};
pub struct CachedSelectors {
pub title: Selector,
pub meta: Selector,
pub link: Selector,
pub base: Selector,
pub html: Selector,
pub body: Selector,
pub article: Selector,
pub main: Selector,
pub main_role: Selector,
pub h1: Selector,
pub h2: Selector,
pub h3: Selector,
pub h4: Selector,
pub h5: Selector,
pub h6: Selector,
pub p: Selector,
pub blockquote: Selector,
pub pre: Selector,
pub pre_code: Selector,
pub code: Selector,
pub ul: Selector,
pub ol: Selector,
pub li: Selector,
pub dl: Selector,
pub dt: Selector,
pub dd: Selector,
pub table: Selector,
pub thead: Selector,
pub tbody: Selector,
pub tfoot: Selector,
pub tr: Selector,
pub th: Selector,
pub td: Selector,
pub caption: Selector,
pub a: Selector,
pub img: Selector,
pub picture: Selector,
pub source: Selector,
pub figure: Selector,
pub figcaption: Selector,
pub script: Selector,
pub style: Selector,
pub noscript: Selector,
pub nav: Selector,
pub header: Selector,
pub footer: Selector,
pub aside: Selector,
pub json_ld: Selector,
pub microdata: Selector,
}
impl CachedSelectors {
fn new() -> Self {
Self {
title: Selector::parse("title").unwrap(),
meta: Selector::parse("meta").unwrap(),
link: Selector::parse("link").unwrap(),
base: Selector::parse("base").unwrap(),
html: Selector::parse("html").unwrap(),
body: Selector::parse("body").unwrap(),
article: Selector::parse("article").unwrap(),
main: Selector::parse("main").unwrap(),
main_role: Selector::parse("[role=main]").unwrap(),
h1: Selector::parse("h1").unwrap(),
h2: Selector::parse("h2").unwrap(),
h3: Selector::parse("h3").unwrap(),
h4: Selector::parse("h4").unwrap(),
h5: Selector::parse("h5").unwrap(),
h6: Selector::parse("h6").unwrap(),
p: Selector::parse("p").unwrap(),
blockquote: Selector::parse("blockquote").unwrap(),
pre: Selector::parse("pre").unwrap(),
pre_code: Selector::parse("pre code").unwrap(),
code: Selector::parse("code").unwrap(),
ul: Selector::parse("ul").unwrap(),
ol: Selector::parse("ol").unwrap(),
li: Selector::parse("li").unwrap(),
dl: Selector::parse("dl").unwrap(),
dt: Selector::parse("dt").unwrap(),
dd: Selector::parse("dd").unwrap(),
table: Selector::parse("table").unwrap(),
thead: Selector::parse("thead").unwrap(),
tbody: Selector::parse("tbody").unwrap(),
tfoot: Selector::parse("tfoot").unwrap(),
tr: Selector::parse("tr").unwrap(),
th: Selector::parse("th").unwrap(),
td: Selector::parse("td").unwrap(),
caption: Selector::parse("caption").unwrap(),
a: Selector::parse("a").unwrap(),
img: Selector::parse("img").unwrap(),
picture: Selector::parse("picture").unwrap(),
source: Selector::parse("source").unwrap(),
figure: Selector::parse("figure").unwrap(),
figcaption: Selector::parse("figcaption").unwrap(),
script: Selector::parse("script").unwrap(),
style: Selector::parse("style").unwrap(),
noscript: Selector::parse("noscript").unwrap(),
nav: Selector::parse("nav").unwrap(),
header: Selector::parse("header").unwrap(),
footer: Selector::parse("footer").unwrap(),
aside: Selector::parse("aside").unwrap(),
json_ld: Selector::parse("script[type='application/ld+json']").unwrap(),
microdata: Selector::parse("[itemscope]").unwrap(),
}
}
}
lazy_static! {
pub static ref SELECTORS: CachedSelectors = CachedSelectors::new();
}
lazy_static! {
static ref SELECTOR_CACHE: RwLock<HashMap<String, Selector>> =
RwLock::new(HashMap::new());
}
pub fn get_or_create_selector(selector_str: &str) -> ParserResult<Selector> {
{
let cache = SELECTOR_CACHE.read().unwrap();
if let Some(sel) = cache.get(selector_str) {
return Ok(sel.clone());
}
}
let selector = parse_selector(selector_str)?;
{
let mut cache = SELECTOR_CACHE.write().unwrap();
cache.insert(selector_str.to_string(), selector.clone());
}
Ok(selector)
}
pub fn parse_selector(selector_str: &str) -> ParserResult<Selector> {
Selector::parse(selector_str)
.map_err(|_| ParserError::SelectorError(selector_str.to_string()))
}
pub fn try_parse_selector(selector_str: &str) -> Option<Selector> {
Selector::parse(selector_str).ok()
}
pub fn heading_selector(level: u8) -> &'static Selector {
match level {
1 => &SELECTORS.h1,
2 => &SELECTORS.h2,
3 => &SELECTORS.h3,
4 => &SELECTORS.h4,
5 => &SELECTORS.h5,
6 => &SELECTORS.h6,
_ => &SELECTORS.h1,
}
}
pub const CONTENT_SELECTORS: &[&str] = &[
"article",
"main",
"[role=main]",
".content",
".post-content",
".entry-content",
".article-content",
".post-body",
".article-body",
"#content",
"#main-content",
];
pub const BOILERPLATE_SELECTORS: &[&str] = &[
"script",
"style",
"noscript",
"iframe",
"object",
"embed",
"nav",
"header:not(article header)",
"footer:not(article footer)",
"aside",
".sidebar",
".navigation",
".nav",
".menu",
".advertisement",
".ad",
".ads",
".social-share",
".social-buttons",
".comments",
".comment-form",
".related-posts",
".recommended",
"[role=navigation]",
"[role=banner]",
"[role=contentinfo]",
"[role=complementary]",
"[aria-hidden=true]",
];
pub const INLINE_ELEMENTS: &[&str] = &[
"a", "span", "em", "strong", "b", "i", "u", "s",
"mark", "small", "sub", "sup", "code", "kbd", "samp", "var",
"abbr", "cite", "dfn", "time", "q",
];
pub const BLOCK_ELEMENTS: &[&str] = &[
"p", "div", "h1", "h2", "h3", "h4", "h5", "h6",
"blockquote", "pre", "ul", "ol", "li", "dl", "dt", "dd",
"table", "tr", "th", "td", "article", "section", "aside",
"header", "footer", "nav", "main", "figure", "figcaption",
"address", "hr", "br",
];
pub fn attr_selector(element: &str, attr: &str, value: &str) -> String {
format!("{}[{}='{}']", element, attr, value)
}
pub fn attr_contains_selector(element: &str, attr: &str, value: &str) -> String {
format!("{}[{}*='{}']", element, attr, value)
}
pub fn attr_starts_with_selector(element: &str, attr: &str, value: &str) -> String {
format!("{}[{}^='{}']", element, attr, value)
}
pub fn class_selector(element: &str, class: &str) -> String {
format!("{}.{}", element, class)
}
pub fn id_selector(element: &str, id: &str) -> String {
format!("{}#{}", element, id)
}
pub fn descendant_selector(ancestor: &str, descendant: &str) -> String {
format!("{} {}", ancestor, descendant)
}
pub fn child_selector(parent: &str, child: &str) -> String {
format!("{} > {}", parent, child)
}
pub fn multi_selector(selectors: &[&str]) -> String {
selectors.join(", ")
}
pub fn meta_name_selector(name: &str) -> String {
format!("meta[name='{}']", name)
}
pub fn meta_property_selector(property: &str) -> String {
format!("meta[property='{}']", property)
}
pub fn link_rel_selector(rel: &str) -> String {
format!("link[rel='{}']", rel)
}
#[cfg(test)]
mod tests {
use super::*;
use scraper::Html;
#[test]
fn test_cached_selectors_exist() {
let _ = &SELECTORS.title;
let _ = &SELECTORS.body;
let _ = &SELECTORS.h1;
}
#[test]
fn test_cached_selectors_work() {
let html = Html::parse_document("<html><body><h1>Test</h1></body></html>");
let h1 = html.select(&SELECTORS.h1).next();
assert!(h1.is_some());
}
#[test]
fn test_parse_selector_success() {
let sel = parse_selector("div.class").unwrap();
let html = Html::parse_document("<div class='class'>Test</div>");
assert!(html.select(&sel).next().is_some());
}
#[test]
fn test_parse_selector_failure() {
let result = parse_selector("div[[[invalid");
assert!(result.is_err());
if let Err(ParserError::SelectorError(s)) = result {
assert!(s.contains("invalid"));
}
}
#[test]
fn test_try_parse_selector() {
assert!(try_parse_selector("div").is_some());
assert!(try_parse_selector("div[[[").is_none());
}
#[test]
fn test_get_or_create_selector() {
let sel1 = get_or_create_selector("div.test-class").unwrap();
let sel2 = get_or_create_selector("div.test-class").unwrap();
let html = Html::parse_document("<div class='test-class'>Hello</div>");
assert!(html.select(&sel1).next().is_some());
assert!(html.select(&sel2).next().is_some());
}
#[test]
fn test_heading_selector() {
assert!(std::ptr::eq(heading_selector(1), &SELECTORS.h1));
assert!(std::ptr::eq(heading_selector(2), &SELECTORS.h2));
assert!(std::ptr::eq(heading_selector(6), &SELECTORS.h6));
assert!(std::ptr::eq(heading_selector(99), &SELECTORS.h1)); }
#[test]
fn test_attr_selector() {
let sel = attr_selector("input", "type", "text");
assert_eq!(sel, "input[type='text']");
let selector = parse_selector(&sel).unwrap();
let html = Html::parse_document("<input type='text'>");
assert!(html.select(&selector).next().is_some());
}
#[test]
fn test_attr_contains_selector() {
let sel = attr_contains_selector("a", "href", "example");
assert_eq!(sel, "a[href*='example']");
}
#[test]
fn test_attr_starts_with_selector() {
let sel = attr_starts_with_selector("a", "href", "https");
assert_eq!(sel, "a[href^='https']");
}
#[test]
fn test_class_selector() {
let sel = class_selector("div", "container");
assert_eq!(sel, "div.container");
}
#[test]
fn test_id_selector() {
let sel = id_selector("div", "main");
assert_eq!(sel, "div#main");
}
#[test]
fn test_descendant_selector() {
let sel = descendant_selector("article", "p");
assert_eq!(sel, "article p");
}
#[test]
fn test_child_selector() {
let sel = child_selector("ul", "li");
assert_eq!(sel, "ul > li");
}
#[test]
fn test_multi_selector() {
let sel = multi_selector(&["h1", "h2", "h3"]);
assert_eq!(sel, "h1, h2, h3");
}
#[test]
fn test_meta_name_selector() {
let sel = meta_name_selector("description");
assert_eq!(sel, "meta[name='description']");
let selector = parse_selector(&sel).unwrap();
let html = Html::parse_document("<meta name='description' content='Test'>");
assert!(html.select(&selector).next().is_some());
}
#[test]
fn test_meta_property_selector() {
let sel = meta_property_selector("og:title");
assert_eq!(sel, "meta[property='og:title']");
}
#[test]
fn test_link_rel_selector() {
let sel = link_rel_selector("canonical");
assert_eq!(sel, "link[rel='canonical']");
}
#[test]
fn test_boilerplate_selectors_valid() {
for sel_str in BOILERPLATE_SELECTORS {
assert!(
try_parse_selector(sel_str).is_some(),
"Invalid selector: {}", sel_str
);
}
}
#[test]
fn test_content_selectors_valid() {
for sel_str in CONTENT_SELECTORS {
assert!(
try_parse_selector(sel_str).is_some(),
"Invalid selector: {}", sel_str
);
}
}
}