1use crate::constants::{APPLICATION, VALE_RELEASES_URL, VALE_VERSION};
5use crate::util::{
6 checksum, command_exists, download_binary, extension, make_executable, path_to_string, pretty_print, standard_project_folder, to_string,
7 Constant, Label, ProgrammingLanguage, SemanticVersion,
8};
9use color_eyre::owo_colors::OwoColorize;
10use duct::cmd;
11use flate2::read::GzDecoder;
12use ini::Ini;
13use std::collections::HashMap;
14use std::fs::File;
15use std::fs::{create_dir_all, remove_file};
16use std::io::prelude::*;
17use std::path::PathBuf;
18use tar::Archive;
19use titlecase::Titlecase;
20use tracing::{debug, error, info, trace};
21use which::which;
22
23pub mod readability;
24pub mod vale;
25
26use vale::{parse_vale_output, print_vale_output, Vale, ValeConfig};
27
28pub trait StaticAnalyzer {
30 fn command(self) -> String;
32 fn analyze(&self, id: String, content: String, output: Option<String>) -> usize;
34 fn download(self, config: Option<ValeConfig>) -> Self;
36 fn download_checksums(self) -> Result<HashMap<String, String>, String>;
38 fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf;
40 fn sync(self) -> Result<(), std::io::Error>;
42 fn with_binary(self, path: PathBuf) -> Self;
44 fn with_config(self, value: ValeConfig) -> Self;
46 fn with_system_command(self) -> Self;
48 fn with_version(self, value: String) -> Self;
50}
51pub trait StaticAnalyzerConfig {
53 fn default() -> ValeConfig;
55 fn ini(self) -> Ini;
57 fn save(self) -> ValeConfig;
59}
60impl StaticAnalyzer for Vale {
61 fn command(self) -> String {
62 "vale".to_string()
63 }
64 fn analyze(&self, id: String, content: String, output: Option<String>) -> usize {
65 let root = standard_project_folder("check", None);
66 match create_dir_all(root.clone()) {
67 | Ok(_) => {}
68 | Err(why) => error!(path = path_to_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
69 }
70 let path = root.join(&id);
71 let mut file = match File::create(&path) {
72 | Ok(file) => file,
73 | Err(why) => panic!("=> {} Create file {} - {}", Label::fail(), path.display(), why),
74 };
75 file.write_all(content.as_bytes())
76 .expect("Unable to write to cache directory project file");
77 let binary = match &self.binary {
78 | Some(value) => value,
79 | None => {
80 error!("=> {} {} binary", Label::not_found(), self.clone().command());
81 std::process::exit(exitcode::UNAVAILABLE);
82 }
83 };
84 match &self.config {
85 | Some(config) => {
86 let result = match output {
87 | Some(value) => cmd!(
88 binary,
89 "--no-wrap",
90 "--config",
91 config.clone().path,
92 "--output",
93 value,
94 path.clone(),
95 "--ext",
96 ".md",
97 "--no-exit",
98 )
99 .read(),
100 | None => cmd!(
101 binary,
102 "--no-wrap",
103 "--config",
104 config.clone().path,
105 path.clone(),
106 "--ext",
107 ".md",
108 "--no-exit"
109 )
110 .read(),
111 };
112 match result {
113 | Ok(output) => {
114 let parsed = parse_vale_output(path, &output);
115 if parsed.is_empty() {
116 let message = format!("=> {} {} has {}", Label::pass(), id.to_string().underline(), "no prose issues".green(),);
117 info!("{}", message);
118 0
119 } else {
120 error!("=> {} {} issues found in {}", Label::fail(), parsed.len(), id.to_string().underline());
121 print_vale_output(parsed.clone());
122 let highlight = parsed.clone().into_iter().map(|item| item.line as usize).collect::<Vec<_>>();
123 println!();
124 pretty_print(&content, ProgrammingLanguage::Markdown, highlight);
125 println!("\n");
126 parsed.len()
127 }
128 }
129 | Err(output) => {
130 error!("=> {} Analyze - {}", Label::fail(), output);
131 1
132 }
133 }
134 }
135 | None => {
136 let title = self.clone().command().titlecase();
137 error!("=> {} {} configuration", Label::not_found(), title);
138 std::process::exit(exitcode::UNAVAILABLE);
139 }
140 }
141 }
142 fn download(self, config: Option<ValeConfig>) -> Vale {
144 let os = std::env::consts::OS.to_lowercase();
146 let platform = match os.as_str() {
147 | "linux" => "Linux_64-bit.tar.gz",
148 | "macos" | "apple" => "macOS_64-bit.tar.gz",
149 | "windows" => "Windows_64-bit.zip",
150 | _ => {
151 error!(os, "=> {}", Label::not_found());
152 std::process::exit(exitcode::UNAVAILABLE);
153 }
154 };
155 let release = match self.version {
156 | Some(value) => value,
157 | None => SemanticVersion::from_string(VALE_VERSION),
158 };
159 let url = format!(
160 "{}/download/v{}/{}_{}_{}",
161 VALE_RELEASES_URL,
162 release,
163 self.clone().command(),
164 release,
165 platform
166 );
167 info!(url, "=> {} Vale release v{}", Label::using(), release);
168 let binary = match download_binary(&url, ".") {
169 | Ok(path) => {
170 let dowloaded_checksums = match self.clone().download_checksums() {
171 | Ok(value) => value.get(platform).unwrap().to_string(),
172 | Err(_) => "".to_string(),
173 };
174 let calculated = checksum(path.clone());
175 if !dowloaded_checksums.eq(&calculated) {
176 error!(dowloaded_checksums, calculated, "=> {}", Label::invalid());
177 let _cleanup = remove_file(path);
178 std::process::exit(exitcode::USAGE);
179 } else {
180 info!(dowloaded_checksums, "=> {}", Label::pass());
181 }
182 let destination = match config.clone() {
184 | Some(value) => value.path.parent().unwrap().to_path_buf(),
185 | None => PathBuf::from("./.vale/"),
186 };
187 let binary = self.clone().extract(path.clone(), Some(destination));
188 if make_executable(&binary) {
189 let _cleanup = remove_file(path);
190 Some(binary)
191 } else {
192 error!("=> {} {} not executable", Label::fail(), self.command());
193 None
194 }
195 }
196 | Err(error) => {
197 error!(error, url, "=> {} {} download", Label::fail(), self.command());
198 None
199 }
200 };
201 let builder = Vale::init().version(release).maybe_binary(binary);
202 match config {
203 | Some(value) => builder.config(value).build(),
204 | None => {
205 let config = ValeConfig::default();
206 builder.config(config).build()
207 }
208 }
209 }
210 fn download_checksums(self) -> Result<HashMap<String, String>, String> {
211 let release = match self.version {
212 | Some(value) => value,
213 | None => SemanticVersion::from_string(VALE_VERSION),
214 };
215 let url = format!(
216 "{}/download/v{}/{}_{}_checksums.txt",
217 VALE_RELEASES_URL,
218 release,
219 self.clone().command(),
220 release
221 );
222 let client = reqwest::blocking::Client::new();
223 let response = client.get(url).send().unwrap();
224 let content = response.text().unwrap();
225 let checksums = content.lines().clone().fold(HashMap::new(), |mut acc: HashMap<String, String>, line| {
226 let mut values = line.split(" ").collect::<Vec<&str>>();
227 let key = values.pop().unwrap()["vale_#.#.#_".len()..].to_string();
228 let value = values.pop().unwrap().to_string();
229 acc.insert(key, value);
230 acc
231 });
232 debug!(
233 "=> {} {} checksums {:#?}",
234 Label::using(),
235 self.command().titlecase(),
236 checksums.dimmed().cyan()
237 );
238 Ok(checksums)
239 }
240 fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf {
241 match extension(&path).as_str() {
242 | "zip" => unimplemented!(),
243 | _ => {
244 let tar_gz = File::open(path).unwrap();
245 let tar = GzDecoder::new(tar_gz);
246 let mut archive = Archive::new(tar);
247 let parent = match destination {
248 | Some(value) => path_to_string(value),
249 | None => "./.vale/".to_string(),
250 };
251 let message = format!("Unable to extract {} binary", self.clone().command());
252 archive.unpack(parent.clone()).expect(&message);
253 debug!(parent, "=> {} Extracted {} binary", Label::using(), self.command());
254 PathBuf::from(format!("{parent}/vale"))
255 }
256 }
257 }
258 fn sync(self) -> Result<(), std::io::Error> {
259 let path = match self.binary {
260 | Some(value) => value,
261 | None => {
262 error!("=> {} {} binary", Label::not_found(), self.command());
263 std::process::exit(exitcode::UNAVAILABLE);
264 }
265 };
266 let config_path = self.config.unwrap().path;
267 let result = cmd!(path.clone(), "--config", config_path.clone(), "sync").run();
268 match result {
269 | Ok(_) => {
270 let parent = format!("{}/styles/config/vocabularies/{}", config_path.parent().unwrap().display(), APPLICATION);
271 debug!(parent, "=> {} Vocabularies", Label::using());
272 match create_dir_all(parent.clone()) {
273 | Ok(_) => {}
274 | Err(why) => error!(directory = parent, "=> {} Create - {}", Label::fail(), why),
275 }
276 match File::create(format!("{parent}/accept.txt")) {
277 | Ok(mut file) => {
278 let acronyms = Constant::last_values("acronyms");
280 let partners = Constant::last_values("partners");
281 let sponsors = Constant::last_values("sponsors");
282 let words = Constant::read_lines("accept.txt");
283 let content = acronyms.chain(partners).chain(sponsors).chain(words).collect::<Vec<String>>().join("\n");
284 file.write_all(content.as_bytes()).expect("Unable to write to accept.txt");
285 }
286 | Err(why) => panic!("=> {} Create accept.txt - {}", Label::fail(), why),
287 }
288 match File::create(format!("{parent}/reject.txt")) {
289 | Ok(mut file) => {
290 let content = Constant::read_lines("reject.txt").join("\n");
291 file.write_all(content.as_bytes()).expect("Unable to write to reject.txt");
292 }
293 | Err(why) => panic!("=> {} Create reject.txt - {}", Label::fail(), why),
294 }
295 Ok(())
296 }
297 | Err(why) => {
298 error!(config = path_to_string(config_path), "=> {} Vale sync - {}", Label::fail(), why);
299 std::process::exit(exitcode::SOFTWARE);
300 }
301 }
302 }
303 fn with_binary(mut self, path: PathBuf) -> Self {
304 self.binary = Some(path);
305 self
306 }
307 fn with_config(mut self, value: ValeConfig) -> Self {
308 self.config = Some(value);
309 self
310 }
311 fn with_system_command(mut self) -> Self {
312 let name = self.clone().command();
313 if command_exists(name.clone()) {
314 let path = which(name.clone()).unwrap().to_path_buf();
315 self.binary = Some(path.clone());
316 let offset = "vale version ".len();
317 let version = cmd!(name.clone(), "--version").read().unwrap()[offset..].to_string();
318 self.version = Some(SemanticVersion::from_string(&version));
319 debug!(
320 path = path_to_string(path),
321 "=> {} System {} (v{}) command",
322 Label::using(),
323 name.green().bold(),
324 version
325 );
326 }
327 self
328 }
329 fn with_version(mut self, value: String) -> Self {
330 self.version = Some(SemanticVersion::from_string(&value));
331 self
332 }
333}
334impl StaticAnalyzerConfig for ValeConfig {
335 fn default() -> Self {
336 let config = ValeConfig::init()
337 .packages(to_string(vec!["Google", "proselint", "write-good", "Joblint"]))
338 .vocabularies(to_string(vec![APPLICATION]))
339 .disabled(to_string(vec![
340 "Vale.Terms",
341 "Google.EmDash",
342 "Google.Contractions",
343 "Google.GenderBias",
344 "Google.Headings",
345 "Google.Parens",
346 "Google.Quotes",
347 "Google.We",
348 "Joblint.Competitive",
349 "proselint.GenderBias",
350 "write-good.E-Prime",
351 "write-good.Passive",
352 "write-good.TooWordy",
353 "write-good.Weasel",
354 ]))
355 .build();
356 trace!("=> {} Default - {:#?}", Label::using(), config.dimmed().cyan());
357 config
358 }
359 fn ini(self) -> Ini {
360 let ValeConfig {
361 packages,
362 vocabularies,
363 disabled,
364 ..
365 } = self;
366 let mut conf = Ini::new();
367 conf.with_section::<String>(None)
369 .set("StylesPath", "styles")
370 .set("Vocab", vocabularies.join(", "))
371 .set("Packages", packages.join(", "));
372 conf.with_section(Some("*"))
373 .set("BasedOnStyles", format!("Vale, {}", packages.join(", ")));
374 disabled.iter().for_each(|rule| {
375 conf.with_section(Some("*")).set(rule, "NO");
376 });
377 conf
378 }
379 fn save(self) -> ValeConfig {
380 let path = self.clone().path;
381 let parent = path.parent().unwrap().to_path_buf();
382 match create_dir_all(parent.clone()) {
383 | Ok(_) => {}
384 | Err(why) => error!(directory = path_to_string(parent), "=> {} Create - {}", Label::fail(), why),
385 }
386 match self.clone().ini().write_to_file(path.clone()) {
387 | Ok(_) => {
388 debug!(path = path_to_string(path), "=> {} Saved configuration", Label::using());
389 }
390 | Err(why) => {
391 error!("=> {} Save configuration - {}", Label::fail(), why);
392 std::process::exit(exitcode::SOFTWARE);
393 }
394 }
395 self
396 }
397}
398
399#[cfg(test)]
400mod tests;