mdbook-linkcheck2 0.12.0

A backend for `mdbook` which will check your links for you.
Documentation
use anyhow::{Context, Error};
use codespan_reporting::term::termcolor::ColorChoice;
use mdbook_driver::MDBook;
use mdbook_renderer::RenderContext;
use std::{io, path::PathBuf};
use structopt::StructOpt;

fn main() -> Result<(), Error> {
    env_logger::init();
    let args = Args::from_args();

    // get a `RenderContext`, either from stdin (because we're used as a plugin)
    // or by instrumenting MDBook directly (in standalone mode).
    let ctx: RenderContext = if args.standalone {
        let md = MDBook::load(dunce::canonicalize(&args.root)?).map_err(to_sync)?;
        let destination = md.build_dir_for("linkcheck");
        RenderContext::new(md.root, md.book, md.config, destination)
    } else {
        serde_json::from_reader(io::stdin()).context("Unable to parse RenderContext")?
    };

    let cache_file = ctx.destination.join("cache.json");
    let cache_file = if args.no_cache {
        None
    } else {
        Some(cache_file.as_path())
    };
    mdbook_linkcheck2::run(cache_file, args.colour, &ctx, args.selected_files)
}

#[derive(Debug, Clone, StructOpt)]
struct Args {
    #[structopt(
        short = "s",
        long = "standalone",
        help = "Run standalone (i.e. not as a mdbook plugin)"
    )]
    standalone: bool,
    #[structopt(help = "The book to render.", parse(from_os_str), default_value = ".")]
    root: PathBuf,
    #[structopt(
        short = "c",
        long = "colour",
        help = "Output colouring",
        parse(try_from_str = parse_colour),
        default_value = "auto",
        possible_values = &["always", "auto", "never"]
    )]
    colour: ColorChoice,
    #[structopt(
        short = "f",
        long = "files",
        help = "Limit checking of web links to only the given files (check all files if omitted).
        Internal links to other Markdown files are always checked in all files, even when this flag is passed."
    )]
    selected_files: Option<Vec<String>>,
    #[structopt(
        long = "no-cache",
        help = "Ignore any existing cache, neither using nor updating it."
    )]
    no_cache: bool,
}

fn parse_colour(raw: &str) -> Result<ColorChoice, Error> {
    let lower = raw.to_lowercase();
    match lower.as_str() {
        "auto" => Ok(ColorChoice::Auto),
        "never" => Ok(ColorChoice::Never),
        "always" => Ok(ColorChoice::Always),
        _ => Err(Error::msg("Unknown colour choice")),
    }
}

fn to_sync(err: mdbook_renderer::errors::Error) -> Error {
    use std::{
        fmt::{self, Display, Formatter},
        sync::Mutex,
    };

    #[derive(Debug)]
    struct Synchronised(Mutex<mdbook_renderer::errors::Error>);

    impl Display for Synchronised {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            self.0.lock().expect("lock was poisoned").fmt(f)
        }
    }

    impl std::error::Error for Synchronised {}

    Error::from(Synchronised(Mutex::new(err)))
}