use clap::{arg, command};
use regex::Regex;
use scraper::{Html, Selector};
pub struct Etymology {
pub word: String,
pub label: String,
pub etymology: String,
}
impl Etymology {
pub fn new(word: &str) -> Etymology {
let results_html = query_etym_online(word);
let etymology_html = Etymology::extract_etymology_html(&results_html);
let etymology = Etymology::beautify(&etymology_html);
let label = Etymology::extract_word_name(&results_html);
Etymology {
word: word.to_owned(),
label,
etymology,
}
}
pub fn beautify(etym_html: &str) -> String {
let re_italics =
Regex::new(r#"<span class="\w+ notranslate">(?P<word>[^<]+)</span>"#).unwrap();
let e: String = re_italics
.replace_all(etym_html, "\x1b[0;3m${word}\x1b[23m")
.to_string();
let html = Html::parse_fragment(&e);
let sel = Selector::parse("div").unwrap();
html.select(&sel).next().unwrap().text().collect::<String>()
}
pub fn extract_etymology_html(raw_html: &str) -> String {
let d = Html::parse_document(raw_html);
let section_selector = Selector::parse("section").unwrap();
for x in d.select(§ion_selector) {
if let Some(y) = x.value().attr("class") {
if y.starts_with("word__def") {
let etym_html = format!("<div>{}</div>", x.inner_html());
return etym_html;
}
}
}
raw_html.to_string()
}
pub fn extract_word_name(raw_html: &str) -> String {
let d = Html::parse_document(raw_html);
let section_selector = Selector::parse("a").unwrap();
for x in d.select(§ion_selector) {
if let Some(y) = x.value().attr("class") {
if y.starts_with("word__name") {
let word_name = x.text().collect::<String>();
return word_name;
}
}
}
raw_html.to_string()
}
}
fn query_etym_online(word: &str) -> String {
let url = format!("https://www.etymonline.com/search?q={}", word);
match ureq::get(&url).call() {
Ok(response) => response.into_string().unwrap(),
Err(ureq::Error::Status(code, _response)) => {
panic!("Encountered HTTP error {} when looking up {}", code, word);
}
Err(_) => {
panic!("Failed to look up {}", word);
}
}
}
fn main() {
let matches = command!()
.arg(arg!([word] "Word for which etymology is desired"))
.arg(arg!(
-d --debug ... "Turn debugging information on"
))
.get_matches();
if let Some(word) = matches.value_of("word") {
let e = Etymology::new(word);
println!("\x1b[1m{}\x1b[22m", e.label);
let w = textwrap::termwidth();
let s = textwrap::fill(&e.etymology, w);
println!("{}", s);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_import_fixture() {
let raw_html = include_str!("../tests/fixture-viking.html");
assert!(raw_html != "foo");
}
#[test]
fn can_parse_html() {
let raw_html = include_str!("../tests/fixture-viking.html");
let document = Html::parse_document(raw_html);
let selector = Selector::parse("div#root").unwrap();
let root_div = document.select(&selector).next().unwrap();
assert!(root_div.value().name() == "div");
assert!(root_div.value().id() == Some("root"));
let root_text = root_div.text().collect::<String>();
assert!(root_text.starts_with("Adver"));
}
#[test]
fn html_markup_removed_from_etym() {
let raw_html = include_str!("../tests/fixture-viking.html");
let _h = Etymology::extract_etymology_html(&raw_html);
let e = Etymology::new("viking");
assert!(e.word == "viking");
assert!(!e.etymology.contains("<span class=\"foreign notranslate\">"));
}
}