From 6240ed5469b302399445d80309a3a5a825465555 Mon Sep 17 00:00:00 2001 From: Michael Clayton Date: Wed, 11 May 2022 15:54:34 -0400 Subject: [PATCH] add link_checker settings for external_level and internal_level (#1848) * add external_level and internal_level * remove unnecessary debug derive on LinkDef * clarify doc comment about link check levels * simplify link checker logging * add missing warn prefix * simplify link level logging, remove "Level" from linklevel variants * remove link level config from test site * switch back to using bail! from get_link_domain * move console's deps to libs * remove unnecessary reference * calling console::error/warn directly * emit one error, or one warning, per link checker run * various link checker level changes * add docs about link checker levels * remove accidentally committed test site * remove completed TODO --- Cargo.lock | 17 +- Cargo.toml | 3 +- components/config/src/config/link_checker.rs | 18 +++ components/config/src/lib.rs | 4 +- components/console/Cargo.toml | 8 + components/console/src/lib.rs | 57 +++++++ components/libs/Cargo.toml | 2 + components/libs/src/lib.rs | 2 + components/markdown/Cargo.toml | 1 + components/markdown/src/markdown.rs | 16 +- components/site/Cargo.toml | 1 + components/site/src/lib.rs | 39 ++++- components/site/src/link_checking.rs | 152 ++++++++++-------- docs/content/documentation/content/linking.md | 2 + .../getting-started/configuration.md | 6 + src/cmd/build.rs | 6 +- src/cmd/check.rs | 6 +- src/cmd/init.rs | 1 - src/cmd/serve.rs | 14 +- src/console.rs | 134 --------------- src/main.rs | 14 +- src/messages.rs | 79 +++++++++ src/prompt.rs | 1 - 23 files changed, 353 insertions(+), 230 deletions(-) create mode 100644 components/console/Cargo.toml create mode 100644 components/console/src/lib.rs delete mode 100644 src/console.rs create mode 100644 src/messages.rs diff --git a/Cargo.lock b/Cargo.lock index 91e8a1f1..1df38680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,6 +385,14 @@ dependencies = [ "utils", ] +[[package]] +name = "console" +version = "0.1.0" +dependencies = [ + "errors", + "libs", +] + [[package]] name = "console" version = "0.15.0" @@ -1244,7 +1252,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "689960f187c43c01650c805fb6bc6f55ab944499d86d4ffe9474ad78991d8e94" dependencies = [ - "console", + "console 0.15.0", "once_cell", "serde", "serde_json", @@ -1392,6 +1400,7 @@ version = "0.1.0" dependencies = [ "ahash", "ammonia", + "atty", "base64", "csv", "elasticlunr-rs", @@ -1420,6 +1429,7 @@ dependencies = [ "svg_metadata", "syntect", "tera", + "termcolor", "time", "toml", "unic-langid", @@ -1579,6 +1589,7 @@ name = "markdown" version = "0.1.0" dependencies = [ "config", + "console 0.1.0", "errors", "insta", "libs", @@ -2811,6 +2822,7 @@ name = "site" version = "0.1.0" dependencies = [ "config", + "console 0.1.0", "content", "errors", "imageproc", @@ -3811,9 +3823,9 @@ dependencies = [ name = "zola" version = "0.16.0" dependencies = [ - "atty", "clap 3.1.17", "clap_complete", + "console 0.1.0", "ctrlc", "errors", "hyper", @@ -3824,7 +3836,6 @@ dependencies = [ "pathdiff", "same-file", "site", - "termcolor", "time", "tokio", "utils", diff --git a/Cargo.toml b/Cargo.toml index ac0c05a9..3ac77856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,7 @@ time = "0.3" name = "zola" [dependencies] -atty = "0.2.11" clap = { version = "3", features = ["derive"] } -termcolor = "1.0.4" # Below is for the serve cmd hyper = { version = "0.14.1", default-features = false, features = ["runtime", "server", "http2", "http1"] } tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs", "time"] } @@ -39,6 +37,7 @@ mime_guess = "2.0" site = { path = "components/site" } errors = { path = "components/errors" } +console = { path = "components/console" } utils = { path = "components/utils" } libs = { path = "components/libs" } diff --git a/components/config/src/config/link_checker.rs b/components/config/src/config/link_checker.rs index 53d353c9..85012879 100644 --- a/components/config/src/config/link_checker.rs +++ b/components/config/src/config/link_checker.rs @@ -1,5 +1,19 @@ use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum LinkCheckerLevel { + #[serde(rename = "error")] + Error, + #[serde(rename = "warn")] + Warn, +} + +impl Default for LinkCheckerLevel { + fn default() -> Self { + Self::Error + } +} + #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct LinkChecker { @@ -7,4 +21,8 @@ pub struct LinkChecker { pub skip_prefixes: Vec, /// Skip anchor checking for these URL prefixes pub skip_anchor_prefixes: Vec, + /// Emit either "error" or "warn" for broken internal links (including anchor links). + pub internal_level: LinkCheckerLevel, + /// Emit either "error" or "warn" for broken external links (including anchor links). + pub external_level: LinkCheckerLevel, } diff --git a/components/config/src/lib.rs b/components/config/src/lib.rs index 23ec6f45..59c6568b 100644 --- a/components/config/src/lib.rs +++ b/components/config/src/lib.rs @@ -5,8 +5,8 @@ mod theme; use std::path::Path; pub use crate::config::{ - languages::LanguageOptions, link_checker::LinkChecker, search::Search, slugify::Slugify, - taxonomies::TaxonomyConfig, Config, + languages::LanguageOptions, link_checker::LinkChecker, link_checker::LinkCheckerLevel, + search::Search, slugify::Slugify, taxonomies::TaxonomyConfig, Config, }; use errors::Result; diff --git a/components/console/Cargo.toml b/components/console/Cargo.toml new file mode 100644 index 00000000..931400e6 --- /dev/null +++ b/components/console/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "console" +version = "0.1.0" +edition = "2021" + +[dependencies] +errors = { path = "../errors" } +libs = { path = "../libs" } diff --git a/components/console/src/lib.rs b/components/console/src/lib.rs new file mode 100644 index 00000000..e480c8f3 --- /dev/null +++ b/components/console/src/lib.rs @@ -0,0 +1,57 @@ +use std::env; +use std::io::Write; + +use libs::atty; +use libs::once_cell::sync::Lazy; +use libs::termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +/// Termcolor color choice. +/// We do not rely on ColorChoice::Auto behavior +/// as the check is already performed by has_color. +static COLOR_CHOICE: Lazy = + Lazy::new(|| if has_color() { ColorChoice::Always } else { ColorChoice::Never }); + +pub fn info(message: &str) { + colorize(message, ColorSpec::new().set_bold(true), StandardStream::stdout(*COLOR_CHOICE)); +} + +pub fn warn(message: &str) { + colorize( + &format!("{}{}", "Warning: ", message), + ColorSpec::new().set_bold(true).set_fg(Some(Color::Yellow)), + StandardStream::stdout(*COLOR_CHOICE), + ); +} + +pub fn success(message: &str) { + colorize( + message, + ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)), + StandardStream::stdout(*COLOR_CHOICE), + ); +} + +pub fn error(message: &str) { + colorize( + &format!("{}{}", "Error: ", message), + ColorSpec::new().set_bold(true).set_fg(Some(Color::Red)), + StandardStream::stderr(*COLOR_CHOICE), + ); +} + +/// Print a colorized message to stdout +fn colorize(message: &str, color: &ColorSpec, mut stream: StandardStream) { + stream.set_color(color).unwrap(); + write!(stream, "{}", message).unwrap(); + stream.set_color(&ColorSpec::new()).unwrap(); + writeln!(stream).unwrap(); +} + +/// Check whether to output colors +fn has_color() -> bool { + let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0" + && env::var("NO_COLOR").is_err(); + let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_| "0".to_string()) != "0"; + + force_colors || use_colors && atty::is(atty::Stream::Stdout) +} diff --git a/components/libs/Cargo.toml b/components/libs/Cargo.toml index 55e2828b..87d02a1c 100644 --- a/components/libs/Cargo.toml +++ b/components/libs/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] ahash = "0.7.6" ammonia = "3" +atty = "0.2.11" base64 = "0.13" csv = "1" elasticlunr-rs = {version = "2", default-features = false, features = ["da", "no", "de", "du", "es", "fi", "fr", "it", "pt", "ro", "ru", "sv", "tr"] } @@ -34,6 +35,7 @@ slug = "0.1" svg_metadata = "0.4" syntect = "5" tera = { version = "1", features = ["preserve_order"] } +termcolor = "1.0.4" time = "0.3" toml = "0.5" unic-langid = "0.9" diff --git a/components/libs/src/lib.rs b/components/libs/src/lib.rs index d2cb9981..a1afcbb4 100644 --- a/components/libs/src/lib.rs +++ b/components/libs/src/lib.rs @@ -6,6 +6,7 @@ pub use ahash; pub use ammonia; +pub use atty; pub use base64; pub use csv; pub use elasticlunr; @@ -34,6 +35,7 @@ pub use slug; pub use svg_metadata; pub use syntect; pub use tera; +pub use termcolor; pub use time; pub use toml; pub use unic_langid; diff --git a/components/markdown/Cargo.toml b/components/markdown/Cargo.toml index 123c0d47..d1226ee8 100644 --- a/components/markdown/Cargo.toml +++ b/components/markdown/Cargo.toml @@ -11,6 +11,7 @@ pest_derive = "2" errors = { path = "../errors" } utils = { path = "../utils" } config = { path = "../config" } +console = { path = "../console" } libs = { path = "../libs" } [dev-dependencies] diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs index 92740dbe..1e33d3c1 100644 --- a/components/markdown/src/markdown.rs +++ b/components/markdown/src/markdown.rs @@ -1,12 +1,13 @@ use std::fmt::Write; +use errors::bail; use libs::gh_emoji::Replacer as EmojiReplacer; use libs::once_cell::sync::Lazy; use libs::pulldown_cmark as cmark; use libs::tera; use crate::context::RenderContext; -use errors::{anyhow, Context, Error, Result}; +use errors::{Context, Error, Result}; use libs::pulldown_cmark::escape::escape_html; use utils::site::resolve_internal_link; use utils::slugs::slugify_anchors; @@ -139,7 +140,18 @@ fn fix_link( resolved.permalink } Err(_) => { - return Err(anyhow!("Relative link {} not found.", link)); + let msg = format!( + "Broken relative link `{}` in {}", + link, + context.current_page_path.unwrap_or("unknown"), + ); + match context.config.link_checker.internal_level { + config::LinkCheckerLevel::Error => bail!(msg), + config::LinkCheckerLevel::Warn => { + console::warn(&msg); + link.to_string() + } + } } } } else if is_external_link(link) { diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml index 3fa0391e..ab96c3b0 100644 --- a/components/site/Cargo.toml +++ b/components/site/Cargo.toml @@ -10,6 +10,7 @@ serde = { version = "1.0", features = ["derive"] } errors = { path = "../errors" } config = { path = "../config" } +console = { path = "../console" } utils = { path = "../utils" } templates = { path = "../templates" } search = { path = "../search" } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 3872cc1a..6615bfce 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -295,10 +295,45 @@ impl Site { tpls::register_tera_global_fns(self); // Needs to be done after rendering markdown as we only get the anchors at that point - link_checking::check_internal_links_with_anchors(self)?; + let internal_link_messages = link_checking::check_internal_links_with_anchors(self); + // log any broken internal links and error out if needed + if let Err(messages) = internal_link_messages { + let messages: Vec = messages + .iter() + .enumerate() + .map(|(i, msg)| format!(" {}. {}", i + 1, msg)) + .collect(); + let msg = format!( + "Found {} broken internal anchor link(s)\n{}", + messages.len(), + messages.join("\n") + ); + match self.config.link_checker.internal_level { + config::LinkCheckerLevel::Warn => console::warn(&msg), + config::LinkCheckerLevel::Error => return Err(anyhow!(msg.clone())), + } + } + + // check external links, log the results, and error out if needed if self.config.is_in_check_mode() { - link_checking::check_external_links(self)?; + let external_link_messages = link_checking::check_external_links(self); + if let Err(messages) = external_link_messages { + let messages: Vec = messages + .iter() + .enumerate() + .map(|(i, msg)| format!(" {}. {}", i + 1, msg)) + .collect(); + let msg = format!( + "Found {} broken external link(s)\n{}", + messages.len(), + messages.join("\n") + ); + match self.config.link_checker.external_level { + config::LinkCheckerLevel::Warn => console::warn(&msg), + config::LinkCheckerLevel::Error => return Err(anyhow!(msg.clone())), + } + } } Ok(()) diff --git a/components/site/src/link_checking.rs b/components/site/src/link_checking.rs index 899b01a2..589927d8 100644 --- a/components/site/src/link_checking.rs +++ b/components/site/src/link_checking.rs @@ -1,9 +1,10 @@ use core::time; use std::{collections::HashMap, path::PathBuf, thread}; +use config::LinkCheckerLevel; use libs::rayon::prelude::*; -use crate::{anyhow, Site}; +use crate::Site; use errors::{bail, Result}; use libs::rayon; use libs::url::Url; @@ -11,8 +12,10 @@ use libs::url::Url; /// Check whether all internal links pointing to explicit anchor fragments are valid. /// /// This is very similar to `check_external_links`, although internal links checking -/// is always performed (while external ones only conditionally in `zola check`). -pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> { +/// is always performed (while external ones only conditionally in `zola check`). If broken links +/// are encountered, the `internal_level` setting in config.toml will determine whether they are +/// treated as warnings or errors. +pub fn check_internal_links_with_anchors(site: &Site) -> Result<(), Vec> { println!("Checking all internal links with anchors."); let library = site.library.write().expect("Get lock for check_internal_links_with_anchors"); @@ -73,7 +76,7 @@ pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> { }); // Format faulty entries into error messages, and collect them. - let errors = missing_targets + let messages = missing_targets .map(|(page_path, md_path, anchor)| { format!( "The anchor in the link `@/{}#{}` in {} does not exist.", @@ -85,7 +88,7 @@ pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> { .collect::>(); // Finally emit a summary, and return overall anchors-checking result. - match errors.len() { + match messages.len() { 0 => { println!("> Successfully checked {} internal link(s) with anchors.", anchors_total); Ok(()) @@ -95,7 +98,7 @@ pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> { "> Checked {} internal link(s) with anchors: {} target(s) missing.", anchors_total, errors_total, ); - Err(anyhow!(errors.join("\n"))) + Err(messages) } } } @@ -114,21 +117,22 @@ fn get_link_domain(link: &str) -> Result { }; } -pub fn check_external_links(site: &Site) -> Result<()> { +pub fn check_external_links(site: &Site) -> Result<(), Vec> { let library = site.library.write().expect("Get lock for check_external_links"); struct LinkDef { file_path: PathBuf, external_link: String, - domain: String, + domain: Result, } impl LinkDef { - pub fn new(file_path: PathBuf, external_link: String, domain: String) -> Self { + pub fn new(file_path: PathBuf, external_link: String, domain: Result) -> Self { Self { file_path, external_link, domain } } } + let mut messages: Vec = vec![]; let mut checked_links: Vec = vec![]; let mut skipped_link_count: u32 = 0; @@ -137,7 +141,7 @@ pub fn check_external_links(site: &Site) -> Result<()> { if should_skip_by_prefix(&external_link, &site.config.link_checker.skip_prefixes) { skipped_link_count += 1; } else { - let domain = get_link_domain(&external_link)?; + let domain = get_link_domain(&external_link); checked_links.push(LinkDef::new(p.file.path.clone(), external_link, domain)); } } @@ -148,24 +152,45 @@ pub fn check_external_links(site: &Site) -> Result<()> { if should_skip_by_prefix(&external_link, &site.config.link_checker.skip_prefixes) { skipped_link_count += 1; } else { - let domain = get_link_domain(&external_link)?; + let domain = get_link_domain(&external_link); checked_links.push(LinkDef::new(s.file.path.clone(), external_link, domain)); } } } + // separate the links with valid domains from the links with invalid domains + let (checked_links, invalid_url_links): (Vec<&LinkDef>, Vec<&LinkDef>) = + checked_links.iter().partition(|link| link.domain.is_ok()); + println!( - "Checking {} external link(s). Skipping {} external link(s).", + "Checking {} external link(s). Skipping {} external link(s).{}", checked_links.len(), - skipped_link_count + skipped_link_count, + if invalid_url_links.is_empty() { + "".to_string() + } else { + format!(" {} link(s) had unparseable URLs.", invalid_url_links.len()) + } ); + for err in invalid_url_links.into_iter() { + let msg = err.domain.as_ref().unwrap_err().to_string(); + messages.push(msg); + } + + // error out if we're in error mode and any external URLs couldn't be parsed + match site.config.link_checker.external_level { + LinkCheckerLevel::Error if messages.len() > 0 => return Err(messages), + _ => (), + } + let mut links_by_domain: HashMap> = HashMap::new(); for link in checked_links.iter() { - links_by_domain.entry(link.domain.to_string()).or_default(); + let domain = link.domain.as_ref().unwrap(); + links_by_domain.entry(domain.to_string()).or_default(); // Insert content path and link under the domain key - links_by_domain.get_mut(&link.domain).unwrap().push(link); + links_by_domain.get_mut(domain).unwrap().push(link); } if checked_links.is_empty() { @@ -176,62 +201,63 @@ pub fn check_external_links(site: &Site) -> Result<()> { // (almost) all pages simultaneously, limiting all links for a single // domain to one thread to avoid rate-limiting let threads = std::cmp::min(links_by_domain.len(), 8); - let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build()?; + let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build(); - let errors = pool.install(|| { - links_by_domain - .par_iter() - .map(|(_domain, links)| { - let mut links_to_process = links.len(); - links - .iter() - .filter_map(move |link_def| { - links_to_process -= 1; + match pool { + Ok(pool) => { + let errors = pool.install(|| { + links_by_domain + .par_iter() + .map(|(_domain, links)| { + let mut links_to_process = links.len(); + links + .iter() + .filter_map(move |link_def| { + links_to_process -= 1; - let res = link_checker::check_url( - &link_def.external_link, - &site.config.link_checker, - ); + let res = link_checker::check_url( + &link_def.external_link, + &site.config.link_checker, + ); - if links_to_process > 0 { - // Prevent rate-limiting, wait before next crawl unless we're done with this domain - thread::sleep(time::Duration::from_millis(500)); - } + if links_to_process > 0 { + // Prevent rate-limiting, wait before next crawl unless we're done with this domain + thread::sleep(time::Duration::from_millis(500)); + } - if link_checker::is_valid(&res) { - None - } else { - Some((&link_def.file_path, &link_def.external_link, res)) - } + if link_checker::is_valid(&res) { + None + } else { + Some((&link_def.file_path, &link_def.external_link, res)) + } + }) + .collect::>() }) + .flatten() .collect::>() - }) - .flatten() - .collect::>() - }); + }); - println!( - "> Checked {} external link(s): {} error(s) found.", - checked_links.len(), - errors.len() - ); + println!( + "> Checked {} external link(s): {} error(s) found.", + checked_links.len(), + errors.len() + ); - if errors.is_empty() { - return Ok(()); + if errors.is_empty() { + return Ok(()); + } + + for (page_path, link, check_res) in errors.iter() { + messages.push(format!( + "Broken link in {} to {}: {}", + page_path.to_string_lossy(), + link, + link_checker::message(check_res) + )); + } + } + Err(pool_err) => messages.push(pool_err.to_string()), } - let msg = errors - .into_iter() - .map(|(page_path, link, check_res)| { - format!( - "Dead link in {} to {}: {}", - page_path.to_string_lossy(), - link, - link_checker::message(&check_res) - ) - }) - .collect::>() - .join("\n"); - - Err(anyhow!(msg)) + Err(messages) } diff --git a/docs/content/documentation/content/linking.md b/docs/content/documentation/content/linking.md index a48857e0..03c84c8b 100644 --- a/docs/content/documentation/content/linking.md +++ b/docs/content/documentation/content/linking.md @@ -54,3 +54,5 @@ to link to. The path to the file starts from the `content` directory. For example, linking to a file located at `content/pages/about.md` would be `[my link](@/pages/about.md)`. You can still link to an anchor directly; `[my link](@/pages/about.md#example)` will work as expected. + +By default, broken internal links are treated as errors. To treat them as warnings instead, visit the `[link_checker]` section of `config.toml` and set `internal_level = "warn"`. Note: treating broken links as warnings allows the site to be built with broken links intact, so a link such as `[my link](@/pages/whoops.md)` will be rendered to HTML as ``. diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 1ecc07c2..4ddaac28 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -130,6 +130,12 @@ skip_anchor_prefixes = [ "https://caniuse.com/", ] +# Treat internal link problems as either "error" or "warn", default is "error" +internal_level = "error" + +# Treat external link problems as either "error" or "warn", default is "error" +external_level = "error" + # Various slugification strategies, see below for details # Defaults to everything being a slug [slugify] diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 21dd456a..6bfe603d 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -3,7 +3,7 @@ use std::path::Path; use errors::{Error, Result}; use site::Site; -use crate::console; +use crate::messages; use crate::prompt::ask_bool_timeout; const BUILD_PROMPT_TIMEOUT_MILLIS: u64 = 10_000; @@ -47,7 +47,7 @@ pub fn build( site.include_drafts(); } site.load()?; - console::notify_site_size(&site); - console::warn_about_ignored_pages(&site); + messages::notify_site_size(&site); + messages::warn_about_ignored_pages(&site); site.build() } diff --git a/src/cmd/check.rs b/src/cmd/check.rs index 1ad57b92..46522dea 100644 --- a/src/cmd/check.rs +++ b/src/cmd/check.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use errors::Result; use site::Site; -use crate::console; +use crate::messages; pub fn check( root_dir: &Path, @@ -23,7 +23,7 @@ pub fn check( site.include_drafts(); } site.load()?; - console::check_site_summary(&site); - console::warn_about_ignored_pages(&site); + messages::check_site_summary(&site); + messages::warn_about_ignored_pages(&site); Ok(()) } diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 579e7b7e..3af0f89f 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -4,7 +4,6 @@ use std::path::Path; use errors::{bail, Result}; use utils::fs::create_file; -use crate::console; use crate::prompt::{ask_bool, ask_url}; const CONFIG: &str = r#" diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index ce73ec71..fc4612c8 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -49,7 +49,7 @@ use site::sass::compile_sass; use site::{Site, SITE_CONTENT}; use utils::fs::copy_file; -use crate::console; +use crate::messages; use std::ffi::OsStr; #[derive(Debug, PartialEq)] @@ -228,7 +228,7 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st )) .unwrap(); } - Err(e) => console::unravel_errors("Failed to build the site", &e), + Err(e) => messages::unravel_errors("Failed to build the site", &e), } } @@ -274,8 +274,8 @@ fn create_new_site( } else { site.enable_live_reload(interface_port); } - console::notify_site_size(&site); - console::warn_about_ignored_pages(&site); + messages::notify_site_size(&site); + messages::warn_about_ignored_pages(&site); site.build()?; Ok((site, address)) } @@ -304,7 +304,7 @@ pub fn serve( include_drafts, None, )?; - console::report_elapsed_time(start); + messages::report_elapsed_time(start); // Stop right there if we can't bind to the address let bind_address: SocketAddrV4 = match address.parse() { @@ -509,7 +509,7 @@ pub fn serve( Some(s) } Err(e) => { - console::unravel_errors("Failed to build the site", &e); + messages::unravel_errors("Failed to build the site", &e); None } }; @@ -628,7 +628,7 @@ pub fn serve( } } }; - console::report_elapsed_time(start); + messages::report_elapsed_time(start); } _ => {} } diff --git a/src/console.rs b/src/console.rs deleted file mode 100644 index a4729de4..00000000 --- a/src/console.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::io::Write; -use std::time::Instant; -use std::{convert::TryInto, env}; - -use libs::once_cell::sync::Lazy; -use libs::time::Duration; -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; - -use errors::Error; -use site::Site; - -/// Termcolor color choice. -/// We do not rely on ColorChoice::Auto behavior -/// as the check is already performed by has_color. -static COLOR_CHOICE: Lazy = - Lazy::new(|| if has_color() { ColorChoice::Always } else { ColorChoice::Never }); - -pub fn info(message: &str) { - colorize(message, ColorSpec::new().set_bold(true), StandardStream::stdout(*COLOR_CHOICE)); -} - -pub fn warn(message: &str) { - colorize( - message, - ColorSpec::new().set_bold(true).set_fg(Some(Color::Yellow)), - StandardStream::stdout(*COLOR_CHOICE), - ); -} - -pub fn success(message: &str) { - colorize( - message, - ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)), - StandardStream::stdout(*COLOR_CHOICE), - ); -} - -pub fn error(message: &str) { - colorize( - message, - ColorSpec::new().set_bold(true).set_fg(Some(Color::Red)), - StandardStream::stderr(*COLOR_CHOICE), - ); -} - -/// Print a colorized message to stdout -fn colorize(message: &str, color: &ColorSpec, mut stream: StandardStream) { - stream.set_color(color).unwrap(); - write!(stream, "{}", message).unwrap(); - stream.set_color(&ColorSpec::new()).unwrap(); - writeln!(stream).unwrap(); -} - -/// Display in the console the number of pages/sections in the site -pub fn notify_site_size(site: &Site) { - let library = site.library.read().unwrap(); - println!( - "-> Creating {} pages ({} orphan) and {} sections", - library.pages.len(), - library.get_all_orphan_pages().len(), - library.sections.len() - 1, // -1 since we do not count the index as a section there - ); -} - -/// Display in the console only the number of pages/sections in the site -pub fn check_site_summary(site: &Site) { - let library = site.library.read().unwrap(); - let orphans = library.get_all_orphan_pages(); - println!( - "-> Site content: {} pages ({} orphan), {} sections", - library.pages.len(), - orphans.len(), - library.sections.len() - 1, // -1 since we do not count the index as a section there - ); - - for orphan in orphans { - warn(&format!("Orphan page found: {}", orphan.path)); - } -} - -/// Display a warning in the console if there are ignored pages in the site -pub fn warn_about_ignored_pages(site: &Site) { - let library = site.library.read().unwrap(); - let ignored_pages: Vec<_> = library - .sections - .values() - .flat_map(|s| s.ignored_pages.iter().map(|k| library.pages[k].file.path.clone())) - .collect(); - - if !ignored_pages.is_empty() { - warn(&format!( - "{} page(s) ignored (missing date or weight in a sorted section):", - ignored_pages.len() - )); - for path in ignored_pages { - warn(&format!("- {}", path.display())); - } - } -} - -/// Print the time elapsed rounded to 1 decimal -pub fn report_elapsed_time(instant: Instant) { - let duration: Duration = instant.elapsed().try_into().unwrap(); - let duration_ms = duration.whole_milliseconds() as f64; - - if duration_ms < 1000.0 { - success(&format!("Done in {}ms.\n", duration_ms)); - } else { - let duration_sec = duration_ms / 1000.0; - success(&format!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0))); - } -} - -/// Display an error message and the actual error(s) -pub fn unravel_errors(message: &str, error: &Error) { - if !message.is_empty() { - self::error(message); - } - self::error(&format!("Error: {}", error)); - let mut cause = error.source(); - while let Some(e) = cause { - self::error(&format!("Reason: {}", e)); - cause = e.source(); - } -} - -/// Check whether to output colors -fn has_color() -> bool { - let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0" - && env::var("NO_COLOR").is_err(); - let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_| "0".to_string()) != "0"; - - force_colors || use_colors && atty::is(atty::Stream::Stdout) -} diff --git a/src/main.rs b/src/main.rs index add5546d..6710020f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use time::UtcOffset; mod cli; mod cmd; -mod console; +mod messages; mod prompt; fn get_config_file_path(dir: &Path, config_path: &Path) -> (PathBuf, PathBuf) { @@ -35,7 +35,7 @@ fn main() { match cli.command { Command::Init { name, force } => { if let Err(e) = cmd::create_new_project(&name, force) { - console::unravel_errors("Failed to create the project", &e); + messages::unravel_errors("Failed to create the project", &e); std::process::exit(1); } } @@ -50,9 +50,9 @@ fn main() { output_dir.as_deref(), drafts, ) { - Ok(()) => console::report_elapsed_time(start), + Ok(()) => messages::report_elapsed_time(start), Err(e) => { - console::unravel_errors("Failed to build the site", &e); + messages::unravel_errors("Failed to build the site", &e); std::process::exit(1); } } @@ -84,7 +84,7 @@ fn main() { fast, UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC), ) { - console::unravel_errors("Failed to serve the site", &e); + messages::unravel_errors("Failed to serve the site", &e); std::process::exit(1); } } @@ -93,9 +93,9 @@ fn main() { let start = Instant::now(); let (root_dir, config_file) = get_config_file_path(&cli_dir, &cli.config); match cmd::check(&root_dir, &config_file, None, None, drafts) { - Ok(()) => console::report_elapsed_time(start), + Ok(()) => messages::report_elapsed_time(start), Err(e) => { - console::unravel_errors("Failed to check the site", &e); + messages::unravel_errors("Failed to check the site", &e); std::process::exit(1); } } diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 00000000..1ad4d7d4 --- /dev/null +++ b/src/messages.rs @@ -0,0 +1,79 @@ +use libs::time::Duration; +use std::convert::TryInto; +use std::time::Instant; + +use errors::Error; +use site::Site; + +/// Display in the console the number of pages/sections in the site +pub fn notify_site_size(site: &Site) { + let library = site.library.read().unwrap(); + println!( + "-> Creating {} pages ({} orphan) and {} sections", + library.pages.len(), + library.get_all_orphan_pages().len(), + library.sections.len() - 1, // -1 since we do not count the index as a section there + ); +} + +/// Display in the console only the number of pages/sections in the site +pub fn check_site_summary(site: &Site) { + let library = site.library.read().unwrap(); + let orphans = library.get_all_orphan_pages(); + println!( + "-> Site content: {} pages ({} orphan), {} sections", + library.pages.len(), + orphans.len(), + library.sections.len() - 1, // -1 since we do not count the index as a section there + ); + + for orphan in orphans { + console::warn(&format!("Orphan page found: {}", orphan.path)); + } +} + +/// Display a warning in the console if there are ignored pages in the site +pub fn warn_about_ignored_pages(site: &Site) { + let library = site.library.read().unwrap(); + let ignored_pages: Vec<_> = library + .sections + .values() + .flat_map(|s| s.ignored_pages.iter().map(|k| library.pages[k].file.path.clone())) + .collect(); + + if !ignored_pages.is_empty() { + console::warn(&format!( + "{} page(s) ignored (missing date or weight in a sorted section):", + ignored_pages.len() + )); + for path in ignored_pages { + console::warn(&format!("- {}", path.display())); + } + } +} + +/// Print the time elapsed rounded to 1 decimal +pub fn report_elapsed_time(instant: Instant) { + let duration: Duration = instant.elapsed().try_into().unwrap(); + let duration_ms = duration.whole_milliseconds() as f64; + + if duration_ms < 1000.0 { + console::success(&format!("Done in {}ms.\n", duration_ms)); + } else { + let duration_sec = duration_ms / 1000.0; + console::success(&format!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0))); + } +} + +/// Display an error message and the actual error(s) +pub fn unravel_errors(message: &str, error: &Error) { + if !message.is_empty() { + console::error(message); + } + console::error(&error.to_string()); + let mut cause = error.source(); + while let Some(e) = cause { + console::error(&format!("Reason: {}", e)); + cause = e.source(); + } +} diff --git a/src/prompt.rs b/src/prompt.rs index b4a68e4c..2820f48f 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -3,7 +3,6 @@ use std::time::Duration; use libs::url::Url; -use crate::console; use errors::{anyhow, Result}; /// Wait for user input and return what they typed