diff --git a/Cargo.toml b/Cargo.toml index 9b37f4a..3ab4163 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,20 +17,20 @@ readme = './README.md' [dependencies] async-trait = "0.1.56" -tokio = { version = "1", features = ["full"] } -clap = { version = "3.2.8", features = ["cargo"] } +tokio = { version = "1.19.2", features = ["full"] } +clap = { version = "3.2.10", features = ["cargo"] } colored = "2.0.0" dirs = "4.0.0" env_logger = "0.9.0" -keyring = "1.1.2" +keyring = "1.2.0" log = "0.4.17" -openssl = "0.10.40" +openssl = "0.10.41" pyo3 = { version = "0.16.5", optional = true } rand = "0.8.5" -serde = { version = "1.0.138", features = ["derive"] } +serde = { version = "1.0.139", features = ["derive"] } serde_json = "1.0.82" toml = "0.5.9" -regex = "1.5.6" +regex = "1.6.0" scraper = "0.13.0" [dependencies.diesel] diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 2eec037..345b3ea 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -6,7 +6,6 @@ mod sql; use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; -use crate::cmds::{CODE_END, CODE_START}; use crate::helper::test_cases_path; use crate::{cfg, err::Error, plugins::LeetCode}; use colored::Colorize; @@ -285,10 +284,16 @@ impl Cache { File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; - let begin = code.find(CODE_START).unwrap_or(0); - let end = code.find(CODE_END).unwrap_or(code.len()); - let code = if let Some(solution) = code.get(begin..end) { - solution.to_string() + let code = if conf.code.edit_code_marker { + let begin = code.find(&conf.code.start_marker); + let end = code.find(&conf.code.end_marker); + if let (Some(l), Some(r)) = (begin, end) { + code.get((l + conf.code.start_marker.len())..r) + .map(|s| s.to_string()) + .unwrap_or_else(|| code) + } else { + code + } } else { code }; diff --git a/src/cache/models.rs b/src/cache/models.rs index c1d192d..0a9a6ab 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -39,11 +39,12 @@ impl Problem { _ => "Unknown", } } - pub fn desc_comment(&self) -> String { + pub fn desc_comment(&self, conf: &Config) -> String { let mut res = String::new(); - res += format!("// Category: {}\n", self.category).as_str(); - res += format!("// Level: {}\n", self.display_level(),).as_str(); - res += format!("// Percent: {}%\n\n", self.percent).as_str(); + let comment_leading = &conf.code.comment_leading; + res += format!("{} Category: {}\n", comment_leading, self.category).as_str(); + res += format!("{} Level: {}\n", comment_leading, self.display_level(),).as_str(); + res += format!("{} Percent: {}%\n\n", comment_leading, self.percent).as_str(); res + "\n" } @@ -148,13 +149,13 @@ impl Question { self.content.render() } - pub fn desc_comment(&self) -> String { + pub fn desc_comment(&self, conf: &Config) -> String { let desc = self.content.render(); - let mut res = desc - .lines() - .fold("/*\n".to_string(), |acc, e| acc + " * " + e + "\n"); - res += " */\n"; + let mut res = desc.lines().fold("\n".to_string(), |acc, e| { + acc + " " + conf.code.comment_leading.as_str() + " " + e + "\n" + }); + res += " \n"; res } @@ -481,7 +482,9 @@ impl std::fmt::Display for VerifyResult { } } +use crate::Config; use verify::*; + mod verify { use super::super::parser::ssr; use serde::Deserialize; diff --git a/src/cfg.rs b/src/cfg.rs index 21079e4..184e3e0 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -9,7 +9,7 @@ use crate::Error; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf}; -const DEFAULT_CONFIG: &str = r##" +pub const DEFAULT_CONFIG: &str = r##" # usually you don't wanna change those [sys] categories = [ @@ -57,6 +57,12 @@ favorite_delete = "https://leetcode.com/list/api/questions/$hash/$id" [code] editor = "vim" lang = "rust" +edit_code_marker = false +comment_problem_desc = false +comment_leading = "///" +start_marker = "@lc code=start" +end_marker = "@lc code=start" +test = true pick = "${fid}.${slug}" submission = "${fid}.${slug}.${sid}.${ac}" @@ -66,10 +72,10 @@ session = "" [storage] root = "~/.leetcode" -cache = "Problems" scripts = "scripts" -# absolutely path for the code, other use root as parent dir code = "code" +# absolutely path for the cache, other use root as parent dir +cache = "~/.cache/leetcode" "##; /// Sync with `~/.leetcode/config.toml` @@ -134,6 +140,12 @@ pub struct Code { pub editor: String, #[serde(rename(serialize = "editor-args", deserialize = "editor-args"))] pub editor_args: Option>, + pub edit_code_marker: bool, + pub start_marker: String, + pub end_marker: String, + pub comment_problem_desc: bool, + pub comment_leading: String, + pub test: bool, pub lang: String, pub pick: String, pub submission: String, @@ -163,16 +175,23 @@ impl Storage { /// get cache path pub fn cache(&self) -> Result { - let root = &self.root()?; - Ok(PathBuf::from(root) - .join(&self.cache) + let home = dirs::home_dir() + .ok_or(Error::NoneError)? .to_string_lossy() - .to_string()) + .to_string(); + let path = PathBuf::from(self.cache.replace('~', &home)); + if !path.is_dir() { + info!("Generate cache dir at {:?}.", &path); + fs::DirBuilder::new().recursive(true).create(&path)?; + } + + Ok(path.join("Problems").to_string_lossy().to_string()) } /// get code path pub fn code(&self) -> Result { - let p = PathBuf::from(&self.code); + let root = &self.root()?; + let p = PathBuf::from(root).join(&self.code); if !PathBuf::from(&p).exists() { fs::create_dir(&p)? } diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 039ad2e..a6ec981 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -22,9 +22,6 @@ use clap::{Arg, ArgMatches, Command as ClapCommand}; /// ``` pub struct EditCommand; -pub const CODE_START: &str = r#"// @lc code=start"#; -pub const CODE_END: &str = r#"// @lc code=end"#; - #[async_trait] impl Command for EditCommand { /// `edit` usage @@ -45,13 +42,6 @@ impl Command for EditCommand { .required(true) .help("question id"), ) - .arg( - Arg::with_name("test") - .long("test") - .short('t') - .required(false) - .help("write test file"), - ) } /// `edit` handler @@ -66,16 +56,16 @@ impl Command for EditCommand { let problem = cache.get_problem(id)?; let mut conf = cache.to_owned().0.conf; - let test_flag = m.contains_id("test"); + let test_flag = conf.code.test; - let p_desc_comment = problem.desc_comment(); + let p_desc_comment = problem.desc_comment(&conf); // condition language if m.contains_id("lang") { conf.code.lang = m.value_of("lang").ok_or(Error::NoneError)?.to_string(); conf.sync()?; } - let lang = conf.code.lang; + let lang = &conf.code.lang; let path = crate::helper::code_path(&problem, Some(lang.to_owned()))?; if !Path::new(&path).exists() { @@ -87,22 +77,40 @@ impl Command for EditCommand { let question: Question = qr?; let mut file_code = File::create(&path)?; - let question_desc = question.desc_comment() + "\n"; + let question_desc = question.desc_comment(&conf) + "\n"; let test_path = crate::helper::test_cases_path(&problem)?; - let mut file_tests = File::create(&test_path)?; let mut flag = false; for d in question.defs.0 { - if d.value == lang { + if d.value == *lang { flag = true; - file_code.write_all(p_desc_comment.as_bytes())?; - file_code.write_all(question_desc.as_bytes())?; - file_code.write_all((CODE_START.to_string() + "\n").as_bytes())?; + if conf.code.comment_problem_desc { + file_code.write_all(p_desc_comment.as_bytes())?; + file_code.write_all(question_desc.as_bytes())?; + } + if conf.code.edit_code_marker { + file_code.write_all( + (conf.code.comment_leading.clone() + + " " + + &conf.code.start_marker + + "\n") + .as_bytes(), + )?; + } file_code.write_all((d.code.to_string() + "\n").as_bytes())?; - file_code.write_all((CODE_END.to_string() + "\n").as_bytes())?; + if conf.code.edit_code_marker { + file_code.write_all( + (conf.code.comment_leading.clone() + + " " + + &conf.code.end_marker + + "\n") + .as_bytes(), + )?; + } if test_flag { + let mut file_tests = File::create(&test_path)?; file_tests.write_all(question.all_cases.as_bytes())?; } } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 30e5841..58a9dc3 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -38,5 +38,3 @@ pub use list::ListCommand; pub use pick::PickCommand; pub use stat::StatCommand; pub use test::TestCommand; - -pub use edit::{CODE_END, CODE_START}; diff --git a/src/err.rs b/src/err.rs index 3ae8cc1..5f007e9 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1,4 +1,5 @@ //! Errors in leetcode-cli +use crate::cfg::{root, DEFAULT_CONFIG}; use crate::cmds::{Command, DataCommand}; use colored::Colorize; use std::fmt; @@ -98,19 +99,31 @@ impl std::convert::From for Error { // toml impl std::convert::From for Error { - fn from(err: toml::de::Error) -> Self { - Error::ParseError(format!( - "{}, {}{}{}{}{}{}{}{}", - err, + fn from(_err: toml::de::Error) -> Self { + let conf = root().unwrap().join("leetcode_tmp.toml"); + std::fs::write(&conf, &DEFAULT_CONFIG[1..]).unwrap(); + #[cfg(debug_assertions)] + let err_msg = format!( + "{}, {}{}{}{}{}{}", + _err, "Parse config file failed, ", "leetcode-cli has just generated a new leetcode.toml at ", "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), " the current one at ", "~/.leetcode/leetcode.toml".yellow().bold().underline(), - "seems missing some keys, please compare to ", - "the tmp one and add them up : )\n", - ".", - )) + " seems missing some keys, Please compare the new file and add the missing keys.\n", + ); + #[cfg(not(debug_assertions))] + let err_msg = format!( + "{}{}{}{}{}{}", + "Parse config file failed, ", + "leetcode-cli has just generated a new leetcode.toml at ", + "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), + " the current one at ", + "~/.leetcode/leetcode.toml".yellow().bold().underline(), + " seems missing some keys, Please compare the new file and add the missing keys.\n", + ); + Error::ParseError(err_msg) } } diff --git a/src/helper.rs b/src/helper.rs index c4a70a3..60efcd8 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -90,9 +90,46 @@ mod filter { } } +pub fn superscript(n: u8) -> String { + match n { + x if x >= 10 => format!("{}{}", superscript(n / 10), superscript(n % 10)), + 0 => "⁰".to_string(), + 1 => "¹".to_string(), + 2 => "²".to_string(), + 3 => "³".to_string(), + 4 => "⁴".to_string(), + 5 => "⁵".to_string(), + 6 => "⁶".to_string(), + 7 => "⁷".to_string(), + 8 => "⁸".to_string(), + 9 => "⁹".to_string(), + _ => n.to_string(), + } +} + +pub fn subscript(n: u8) -> String { + match n { + x if x >= 10 => format!("{}{}", subscript(n / 10), subscript(n % 10)), + 0 => "₀".to_string(), + 1 => "₁".to_string(), + 2 => "₂".to_string(), + 3 => "₃".to_string(), + 4 => "₄".to_string(), + 5 => "₅".to_string(), + 6 => "₆".to_string(), + 7 => "₇".to_string(), + 8 => "₈".to_string(), + 9 => "₉".to_string(), + _ => n.to_string(), + } +} + /// Render html to command-line mod html { + use crate::helper::{subscript, superscript}; + use regex::Captures; use scraper::Html; + /// Html render plugin pub trait HTML { fn render(&self) -> String; @@ -100,12 +137,20 @@ mod html { impl HTML for String { fn render(&self) -> String { - let rep = self - .replace(r#""#, "") - .replace(r#""#, "^") - .replace(r#""#, "") - .replace(r#""#, "_"); - let frag = Html::parse_fragment(rep.as_str()); + let sup_re = regex::Regex::new(r"(?P[0-9]*)").unwrap(); + let sub_re = regex::Regex::new(r"(?P[0-9]*)").unwrap(); + + let res = sup_re.replace_all(self, |cap: &Captures| { + let num: u8 = cap["num"].to_string().parse().unwrap(); + superscript(num) + }); + + let res = sub_re.replace_all(&res, |cap: &Captures| { + let num: u8 = cap["num"].to_string().parse().unwrap(); + subscript(num) + }); + + let frag = Html::parse_fragment(&res); let res = frag .root_element()