mod assertions;
mod checker;
mod error;
mod format;
mod language_server;
mod parser;
mod path_util;
mod scoring;
mod validator;
mod wiki;
use crate::{
checker::{analyze, check},
error::{Error, format_errors},
format::CodePath,
path_util::relative_path,
wiki::WIKI_EXTENSION,
};
use clap::{ArgAction, Parser, Subcommand as ClapSubcommand};
use std::{
env, fs,
path::{Path, PathBuf},
process::exit,
rc::Rc,
};
#[derive(Parser)]
#[command(
about = concat!(
env!("CARGO_PKG_DESCRIPTION"),
"\n\n",
"More information can be found at: ",
env!("CARGO_PKG_HOMEPAGE"),
),
version,
disable_version_flag = true
)]
struct Cli {
#[arg(short, long, help = "Print version", action = ArgAction::Version)]
_version: Option<bool>,
#[arg(long, value_name = "PATH", help = "Specify the path to the wiki")]
path: Option<PathBuf>,
#[command(subcommand)]
command: Option<Subcommand>,
}
#[derive(ClapSubcommand)]
enum Subcommand {
#[command(about = "Check a wiki")]
Check,
#[command(about = "Fix a wiki (default)")]
Fix,
#[command(about = "Start the language server")]
LanguageServer,
}
fn find_wiki() -> Result<PathBuf, Error> {
let current_directory = env::current_dir().map_err(|error| {
Error::new(
"Unable to determine the current directory.",
None,
None,
Some(Rc::new(error)),
)
})?;
for directory in current_directory.ancestors() {
let entries = fs::read_dir(directory).map_err(|error| {
Error::new(
&format!("Unable to read {}.", directory.code_path()),
None,
None,
Some(Rc::new(error)),
)
})?;
let mut wikis = Vec::<PathBuf>::new();
for entry in entries {
let entry = entry.map_err(|error| {
Error::new(
&format!("Unable to read an entry in {}.", directory.code_path()),
None,
None,
Some(Rc::new(error)),
)
})?;
let path = entry.path();
let has_wiki_extension = path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case(WIKI_EXTENSION));
if has_wiki_extension {
let metadata = fs::metadata(&path).map_err(|error| {
Error::new(
&format!("Unable to inspect {}.", path.code_path()),
None,
None,
Some(Rc::new(error)),
)
})?;
if metadata.is_file() {
wikis.push(path);
}
}
}
wikis.sort();
if wikis.len() > 1 {
let file_names = wikis
.iter()
.filter_map(|path| path.file_name())
.map(|file_name| Path::new(file_name).code_path().to_string())
.collect::<Vec<String>>()
.join(", ");
return Err(Error::new(
&format!(
"Found multiple wikis in {}: {file_names}",
directory.code_path(),
),
None,
None,
None,
));
}
if let Some(wiki) = wikis.into_iter().next() {
return Ok(wiki);
}
}
Err(Error::new(
&format!(
"No wiki found in {} or its ancestors.",
current_directory.code_path(),
),
None,
None,
None,
))
}
async fn entry() -> Result<(), Vec<Error>> {
let cli = Cli::parse();
let should_fix = match cli.command.unwrap_or(Subcommand::Fix) {
Subcommand::Check => false,
Subcommand::Fix => true,
Subcommand::LanguageServer => {
language_server::run().await;
return Ok(());
}
};
let wiki_path = relative_path(
&env::current_dir().map_err(|error| {
vec![Error::new(
"Unable to determine the current directory.",
None,
None,
Some(Rc::new(error)),
)]
})?,
&cli.path
.map_or_else(find_wiki, Ok)
.map_err(|error| vec![error])?,
)
.to_owned();
let wiki_bytes = fs::read(&wiki_path).map_err(|error| {
vec![Error::new(
"Unable to read the wiki.",
Some(&wiki_path),
None,
Some(Rc::new(error)),
)]
})?;
let wiki_contents = String::from_utf8(wiki_bytes).map_err(|error| {
vec![Error::new(
"The wiki is not valid UTF-8.",
Some(&wiki_path),
None,
Some(Rc::new(error)),
)]
})?;
let wiki = if should_fix {
analyze(Some(&wiki_path), &wiki_contents)
} else {
check(Some(&wiki_path), &wiki_contents)
}?;
let rendered_wiki = wiki.to_string();
if should_fix {
if wiki_contents == rendered_wiki {
println!("Wiki {} looks good.", wiki_path.code_path());
} else {
fs::write(&wiki_path, rendered_wiki).map_err(|error| {
vec![Error::new(
"Unable to write the wiki.",
Some(&wiki_path),
None,
Some(Rc::new(error)),
)]
})?;
println!("Fixed {}.", wiki_path.code_path());
}
} else {
println!("Wiki {} looks good.", wiki_path.code_path());
}
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(errors) = entry().await {
eprintln!("{}", format_errors(&errors));
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::{Cli, Subcommand};
use clap::{CommandFactory, Parser};
use std::path::PathBuf;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
#[test]
fn parse_subcommands() {
assert!(matches!(
Cli::try_parse_from(["mull", "check"]).unwrap().command,
Some(Subcommand::Check),
));
assert!(matches!(
Cli::try_parse_from(["mull", "fix"]).unwrap().command,
Some(Subcommand::Fix),
));
assert!(matches!(
Cli::try_parse_from(["mull", "language-server"])
.unwrap()
.command,
Some(Subcommand::LanguageServer),
));
}
#[test]
fn parse_path() {
let cli = Cli::try_parse_from(["mull", "--path", "notes.mull", "check"]).unwrap();
assert_eq!(cli.path, Some(PathBuf::from("notes.mull")));
}
}