diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1113dc4..dad2041 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,11 @@ jobs: - uses: taiki-e/create-gh-release-action@v1 with: # (optional) Path to changelog. +<<<<<<< HEAD # changelog: CHANGELOG.md +======= + changelog: CHANGELOG.md +>>>>>>> d13247f479d853832f107b1db5963787494deffc # (required) GitHub token for creating GitHub Releases. token: ${{ secrets.GITHUB_TOKEN }} @@ -38,7 +42,11 @@ jobs: with: # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. # Note that glob pattern is not supported yet. +<<<<<<< HEAD bin: rae-cli +======= + bin: ... +>>>>>>> d13247f479d853832f107b1db5963787494deffc # (optional) Target triple, default is host triple. # This is optional but it is recommended that this always be set to # clarify which target you are building for if macOS is included in diff --git a/Cargo.lock b/Cargo.lock index 7ce92a6..bdd77c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,55 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -108,6 +157,59 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -603,6 +705,12 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -1029,6 +1137,8 @@ dependencies = [ name = "rae-cli" version = "0.1.0" dependencies = [ + "clap", + "const_format", "html2text", "inquire", "reqwest", @@ -1436,6 +1546,12 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1693,6 +1809,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -1716,6 +1838,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 413a8b6..18332a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +clap = "4.5.20" +const_format = "0.2.33" html2text = { version = "0.13.2", features = ["css"] } inquire = "0.7.5" reqwest = { version = "0.12.8", features = ["blocking"] } diff --git a/README.md b/README.md index b50783e..025a967 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,17 @@ A rather primitive tool to search and display spanish-spanish translations of wo ## Usage - -Search for a word: ```sh -rae-cli [word] +buschar palabras en real Real Academia Española. + +Usage: rae-cli + +Arguments: + palabra para buschar + +Options: + -h, --help Print help + -V, --version Print version ``` If the word if found information will be displayed diff --git a/src/main.rs b/src/main.rs index d8b167c..0cc4f9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ use std::env; +use std::fmt::Display; +use const_format::concatcp; +use inquire::InquireError; +use reqwest::header::USER_AGENT; use std::str; use scraper::selectable::Selectable; use scraper::ElementRef; use scraper::{Html, Selector}; -use reqwest; +use reqwest::{self, StatusCode}; use html2text; use html2text::config; use html2text::render::RichAnnotation; @@ -12,10 +16,73 @@ use termion::color::*; use std::cell::LazyCell; +use clap::{arg, Command}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const NAME: &str = env!("CARGO_PKG_NAME"); +const CLI_USER_AGENT: &str = concatcp!(NAME, "/", VERSION); const DIV_RESULTS_SELECTOR: LazyCell = LazyCell::new(|| { Selector::parse(r#"div[id="resultados"]"#).unwrap() }); const RESULT_OR_SUGGESTION_SELECTOR: LazyCell = LazyCell::new(|| { Selector::parse(r#"article, div[class="item-list"]"#).unwrap() }); const OPTIONS_SELECTOR: LazyCell = LazyCell::new(|| { Selector::parse("a").unwrap() }); +#[derive(Debug)] +enum RaeError { + RequestError(reqwest::Error), + ResponseError(StatusCode), + HtmlParseError(html2text::Error), + SelectError(InquireError), + UnexpectedSiteStructure +} + +enum RaeSuccess { + Definicion(String), + NoEncontrado +} + +type RaeResult = std::result::Result; + +impl From for RaeError { + fn from(r_error: reqwest::Error) -> Self { + Self::RequestError(r_error) + } +} + +impl From for RaeError { + fn from(s_code: StatusCode) -> Self { + Self::ResponseError(s_code) + } + +} + +impl From for RaeError { + fn from(s_code: html2text::Error) -> Self { + Self::HtmlParseError(s_code) + } + +} + +impl From for RaeError { + fn from(s_code: InquireError) -> Self { + Self::SelectError(s_code) + } + +} + +impl Display for RaeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RaeError::UnexpectedSiteStructure => write!(f, "[Error]: Unexpected site structre. latest version? something changed? please report :/"), + RaeError::RequestError(r_e) => write!(f, "[Error]: {}, internet avialable? site down?", r_e), + RaeError::ResponseError(r_e) => write!(f, "[Error]: {}, latest version?", r_e), + RaeError::SelectError(s_e) => write!(f, "[Error]: {}", s_e), + RaeError::HtmlParseError(h_e) => write!(f, "[Error]: {}", h_e), + } + } +} + +impl std::error::Error for RaeError {} + + // TODO: either work with adapted colours or get rid fn default_colour_map( annotations: &[RichAnnotation], @@ -51,75 +118,89 @@ fn default_colour_map( result } -fn imprimir_palabra(definicion_html: ElementRef) { - - let co = config::rich(); // .use_doc_css(); + +fn imprimir_palabra(definicion_html: ElementRef) -> RaeResult { + let (width, _) = termion::terminal_size().unwrap(); + let co = config::rich(); let mut redader = std::io::Cursor::new(definicion_html.inner_html()); - let d = co.coloured(&mut redader, 100, default_colour_map).unwrap(); + let d = co.coloured(&mut redader, usize::from(width), default_colour_map)?; - print!("{}", d); + Ok(RaeSuccess::Definicion(d)) } -fn print_options(options_list: ElementRef) { +fn print_options(options_list: ElementRef) -> RaeResult { use inquire::Select; - let options_list = options_list.select(&*&OPTIONS_SELECTOR).filter_map(|x| x.text().next()).collect::>(); - if options_list.len() == 1 { + let options_list = options_list + .select(&*&OPTIONS_SELECTOR) + .filter_map(|x| x.text().next()) + .collect::>(); + if options_list.len() == 1 { println!("La palabra hacar no está en el Diccionario. Las entradas que se muestran a continuación podrían estar relacionadas: {}", options_list[0]); println!(); - buschar_palabra(options_list[0]); - return - } - - - let ans = Select::new("La palabra hacar no está en el Diccionario. Las entradas que se muestran a continuación podrían estar relacionadas:", options_list).prompt(); - - match ans { - Ok(choice) => buschar_palabra(choice), - Err(_) => println!("There was an error, please try again"), + buschar_palabra(options_list[0]) + } else { + let choice = Select::new("La palabra hacar no está en el Diccionario. Las entradas que se muestran a continuación podrían estar relacionadas:", options_list).prompt()?; + buschar_palabra(choice) } } -fn print_definition_or_options(word: &str, page_core: ElementRef) { +fn print_definition_or_options(page_core: ElementRef) -> RaeResult { match page_core.select(&*RESULT_OR_SUGGESTION_SELECTOR).next() { Some(w) => match w.value().name() { "article" => imprimir_palabra(page_core), "div" => print_options(w), - _ => println!("La palabra {} no está en el Diccionario.", word), + _ => Ok(RaeSuccess::NoEncontrado), }, - _ => println!("La palabra {} no está en el Diccionario.", word), + _ => Ok(RaeSuccess::NoEncontrado), } } -// TODO: implement return codes or similiar insted of passing the f*cking word around -fn buschar_palabra(palabra: &str){ +fn buschar_palabra(palabra: &str) -> RaeResult { let client = reqwest::blocking::Client::new(); - let pagina = client.get(format!("https://dle.rae.es/{}", palabra)).header("User-Agent", "mitk").send().expect("no url"); - - let raw_page = pagina.text().expect("stupid"); - let dom_fragment = Html::parse_document(&raw_page); - // let results_selector = Selector::parse(r#"div[id="resultados"]"#).unwrap(); - - - match dom_fragment.select(&*DIV_RESULTS_SELECTOR).next() { - Some(c) => { - print_definition_or_options(palabra, c); - }, - _ => println!("La palabra {} no está en el Diccionario.", palabra), + let url = format!("https://dle.rae.es/{}", palabra); + println!("Datos de: {}", url); + let response = client + .get(url) + .header(USER_AGENT, CLI_USER_AGENT) + .send()?; + + + if !response.status().is_success() { + Err(RaeError::ResponseError(response.status())) + } else { // I hate it that i have to use else here + let raw_page = response.text()?; + let dom_fragment = Html::parse_document(&raw_page); + + match dom_fragment.select(&*DIV_RESULTS_SELECTOR).next() { + Some(c) => print_definition_or_options(c), + _ => Err(RaeError::UnexpectedSiteStructure), + } } - } -fn main() { - let args: Vec = env::args().collect(); - if args.len() < 2 { - // TODO: proper help/usage whatever, or just leave it ;) - panic!("you are stupid..."); - } - let palabra = args[1].clone(); - buschar_palabra(&palabra); +fn main() { + let matches = Command::new(NAME) + .arg_required_else_help(true) + .name(NAME) + .version(VERSION) + .about("buschar palabras en real Real Academia Española.") + .arg(arg!([palabra] "palabra para buschar").required(true)) + .get_matches(); + + let p = matches + .get_one::("palabra") + .unwrap(); // required so unwrap is safe + + match buschar_palabra(&p) { + Ok(s) => match s { + RaeSuccess::Definicion(d) => println!("{}", d), + RaeSuccess::NoEncontrado => println!("La palabra {} no está en el Diccionario.", p) + }, + Err(e) => eprintln!("{}", e), + } }