Skip to content

Commit 6240ed5

Browse files
authored
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
1 parent 2291c6e commit 6240ed5

File tree

23 files changed

+357
-234
lines changed

23 files changed

+357
-234
lines changed

Cargo.lock

+14-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ time = "0.3"
2222
name = "zola"
2323

2424
[dependencies]
25-
atty = "0.2.11"
2625
clap = { version = "3", features = ["derive"] }
27-
termcolor = "1.0.4"
2826
# Below is for the serve cmd
2927
hyper = { version = "0.14.1", default-features = false, features = ["runtime", "server", "http2", "http1"] }
3028
tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs", "time"] }
@@ -39,6 +37,7 @@ mime_guess = "2.0"
3937

4038
site = { path = "components/site" }
4139
errors = { path = "components/errors" }
40+
console = { path = "components/console" }
4241
utils = { path = "components/utils" }
4342
libs = { path = "components/libs" }
4443

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
use serde::{Deserialize, Serialize};
22

3+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4+
pub enum LinkCheckerLevel {
5+
#[serde(rename = "error")]
6+
Error,
7+
#[serde(rename = "warn")]
8+
Warn,
9+
}
10+
11+
impl Default for LinkCheckerLevel {
12+
fn default() -> Self {
13+
Self::Error
14+
}
15+
}
16+
317
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
418
#[serde(default)]
519
pub struct LinkChecker {
620
/// Skip link checking for these URL prefixes
721
pub skip_prefixes: Vec<String>,
822
/// Skip anchor checking for these URL prefixes
923
pub skip_anchor_prefixes: Vec<String>,
24+
/// Emit either "error" or "warn" for broken internal links (including anchor links).
25+
pub internal_level: LinkCheckerLevel,
26+
/// Emit either "error" or "warn" for broken external links (including anchor links).
27+
pub external_level: LinkCheckerLevel,
1028
}

components/config/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ mod theme;
55
use std::path::Path;
66

77
pub use crate::config::{
8-
languages::LanguageOptions, link_checker::LinkChecker, search::Search, slugify::Slugify,
9-
taxonomies::TaxonomyConfig, Config,
8+
languages::LanguageOptions, link_checker::LinkChecker, link_checker::LinkCheckerLevel,
9+
search::Search, slugify::Slugify, taxonomies::TaxonomyConfig, Config,
1010
};
1111
use errors::Result;
1212

components/console/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "console"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
errors = { path = "../errors" }
8+
libs = { path = "../libs" }

components/console/src/lib.rs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::env;
2+
use std::io::Write;
3+
4+
use libs::atty;
5+
use libs::once_cell::sync::Lazy;
6+
use libs::termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
7+
8+
/// Termcolor color choice.
9+
/// We do not rely on ColorChoice::Auto behavior
10+
/// as the check is already performed by has_color.
11+
static COLOR_CHOICE: Lazy<ColorChoice> =
12+
Lazy::new(|| if has_color() { ColorChoice::Always } else { ColorChoice::Never });
13+
14+
pub fn info(message: &str) {
15+
colorize(message, ColorSpec::new().set_bold(true), StandardStream::stdout(*COLOR_CHOICE));
16+
}
17+
18+
pub fn warn(message: &str) {
19+
colorize(
20+
&format!("{}{}", "Warning: ", message),
21+
ColorSpec::new().set_bold(true).set_fg(Some(Color::Yellow)),
22+
StandardStream::stdout(*COLOR_CHOICE),
23+
);
24+
}
25+
26+
pub fn success(message: &str) {
27+
colorize(
28+
message,
29+
ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)),
30+
StandardStream::stdout(*COLOR_CHOICE),
31+
);
32+
}
33+
34+
pub fn error(message: &str) {
35+
colorize(
36+
&format!("{}{}", "Error: ", message),
37+
ColorSpec::new().set_bold(true).set_fg(Some(Color::Red)),
38+
StandardStream::stderr(*COLOR_CHOICE),
39+
);
40+
}
41+
42+
/// Print a colorized message to stdout
43+
fn colorize(message: &str, color: &ColorSpec, mut stream: StandardStream) {
44+
stream.set_color(color).unwrap();
45+
write!(stream, "{}", message).unwrap();
46+
stream.set_color(&ColorSpec::new()).unwrap();
47+
writeln!(stream).unwrap();
48+
}
49+
50+
/// Check whether to output colors
51+
fn has_color() -> bool {
52+
let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0"
53+
&& env::var("NO_COLOR").is_err();
54+
let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_| "0".to_string()) != "0";
55+
56+
force_colors || use_colors && atty::is(atty::Stream::Stdout)
57+
}

components/libs/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2021"
66
[dependencies]
77
ahash = "0.7.6"
88
ammonia = "3"
9+
atty = "0.2.11"
910
base64 = "0.13"
1011
csv = "1"
1112
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"
3435
svg_metadata = "0.4"
3536
syntect = "5"
3637
tera = { version = "1", features = ["preserve_order"] }
38+
termcolor = "1.0.4"
3739
time = "0.3"
3840
toml = "0.5"
3941
unic-langid = "0.9"

components/libs/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
pub use ahash;
88
pub use ammonia;
9+
pub use atty;
910
pub use base64;
1011
pub use csv;
1112
pub use elasticlunr;
@@ -34,6 +35,7 @@ pub use slug;
3435
pub use svg_metadata;
3536
pub use syntect;
3637
pub use tera;
38+
pub use termcolor;
3739
pub use time;
3840
pub use toml;
3941
pub use unic_langid;

components/markdown/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pest_derive = "2"
1111
errors = { path = "../errors" }
1212
utils = { path = "../utils" }
1313
config = { path = "../config" }
14+
console = { path = "../console" }
1415
libs = { path = "../libs" }
1516

1617
[dev-dependencies]

components/markdown/src/markdown.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::fmt::Write;
22

3+
use errors::bail;
34
use libs::gh_emoji::Replacer as EmojiReplacer;
45
use libs::once_cell::sync::Lazy;
56
use libs::pulldown_cmark as cmark;
67
use libs::tera;
78

89
use crate::context::RenderContext;
9-
use errors::{anyhow, Context, Error, Result};
10+
use errors::{Context, Error, Result};
1011
use libs::pulldown_cmark::escape::escape_html;
1112
use utils::site::resolve_internal_link;
1213
use utils::slugs::slugify_anchors;
@@ -139,7 +140,18 @@ fn fix_link(
139140
resolved.permalink
140141
}
141142
Err(_) => {
142-
return Err(anyhow!("Relative link {} not found.", link));
143+
let msg = format!(
144+
"Broken relative link `{}` in {}",
145+
link,
146+
context.current_page_path.unwrap_or("unknown"),
147+
);
148+
match context.config.link_checker.internal_level {
149+
config::LinkCheckerLevel::Error => bail!(msg),
150+
config::LinkCheckerLevel::Warn => {
151+
console::warn(&msg);
152+
link.to_string()
153+
}
154+
}
143155
}
144156
}
145157
} else if is_external_link(link) {

components/site/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ serde = { version = "1.0", features = ["derive"] }
1010

1111
errors = { path = "../errors" }
1212
config = { path = "../config" }
13+
console = { path = "../console" }
1314
utils = { path = "../utils" }
1415
templates = { path = "../templates" }
1516
search = { path = "../search" }

components/site/src/lib.rs

+37-2
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,45 @@ impl Site {
295295
tpls::register_tera_global_fns(self);
296296

297297
// Needs to be done after rendering markdown as we only get the anchors at that point
298-
link_checking::check_internal_links_with_anchors(self)?;
298+
let internal_link_messages = link_checking::check_internal_links_with_anchors(self);
299+
300+
// log any broken internal links and error out if needed
301+
if let Err(messages) = internal_link_messages {
302+
let messages: Vec<String> = messages
303+
.iter()
304+
.enumerate()
305+
.map(|(i, msg)| format!(" {}. {}", i + 1, msg))
306+
.collect();
307+
let msg = format!(
308+
"Found {} broken internal anchor link(s)\n{}",
309+
messages.len(),
310+
messages.join("\n")
311+
);
312+
match self.config.link_checker.internal_level {
313+
config::LinkCheckerLevel::Warn => console::warn(&msg),
314+
config::LinkCheckerLevel::Error => return Err(anyhow!(msg.clone())),
315+
}
316+
}
299317

318+
// check external links, log the results, and error out if needed
300319
if self.config.is_in_check_mode() {
301-
link_checking::check_external_links(self)?;
320+
let external_link_messages = link_checking::check_external_links(self);
321+
if let Err(messages) = external_link_messages {
322+
let messages: Vec<String> = messages
323+
.iter()
324+
.enumerate()
325+
.map(|(i, msg)| format!(" {}. {}", i + 1, msg))
326+
.collect();
327+
let msg = format!(
328+
"Found {} broken external link(s)\n{}",
329+
messages.len(),
330+
messages.join("\n")
331+
);
332+
match self.config.link_checker.external_level {
333+
config::LinkCheckerLevel::Warn => console::warn(&msg),
334+
config::LinkCheckerLevel::Error => return Err(anyhow!(msg.clone())),
335+
}
336+
}
302337
}
303338

304339
Ok(())

0 commit comments

Comments
 (0)