pub mod cli;
pub mod counter;
pub mod output;
pub mod world;
use anyhow::{Context, Result};
use cli::Cli;
use counter::Count;
use std::path::Path;
use typst::{World, layout::PagedDocument};
pub fn compile_document(path: &Path, exclude_imports: bool) -> Result<Count> {
let world = world::SimpleWorld::new(path)
.with_context(|| format!("Failed to load {}", path.display()))?;
let main_file_id = world.main();
let result = typst::compile(&world);
let document: PagedDocument = result.output.map_err(|errors| {
let error_msg = errors
.iter()
.map(|e| format!("{}", e.message))
.collect::<Vec<_>>()
.join(", ");
anyhow::anyhow!("Failed to compile {}: {}", path.display(), error_msg)
})?;
Ok(counter::count_document(
&document.introspector,
exclude_imports,
main_file_id,
))
}
pub fn process_files(args: &Cli) -> Result<Vec<(String, Count)>> {
args.input
.iter()
.map(|path| {
compile_document(path, args.exclude_imports)
.map(|count| (path.display().to_string(), count))
})
.collect()
}
pub fn check_limits(args: &Cli, total: &Count) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if let Some(max) = args.max_words
&& total.words > max
{
errors.push(format!(
"Word count exceeds maximum ({} > {})",
total.words, max
));
}
if let Some(min) = args.min_words
&& total.words < min
{
errors.push(format!(
"Word count below minimum ({} < {})",
total.words, min
));
}
if let Some(max) = args.max_characters
&& total.characters > max
{
errors.push(format!(
"Character count exceeds maximum ({} > {})",
total.characters, max
));
}
if let Some(min) = args.min_characters
&& total.characters < min
{
errors.push(format!(
"Character count below minimum ({} < {})",
total.characters, min
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{Cli, CountMode, DisplayMode, OutputFormat};
fn make_test_cli() -> Cli {
Cli {
input: vec![],
format: OutputFormat::Human,
mode: CountMode::Both,
output: None,
display: DisplayMode::Auto,
exclude_imports: false,
max_words: None,
min_words: None,
max_characters: None,
min_characters: None,
}
}
#[test]
fn test_check_limits_no_limits() {
let args = make_test_cli();
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_max_words_ok() {
let mut args = make_test_cli();
args.max_words = Some(200);
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_max_words_exceeded() {
let mut args = make_test_cli();
args.max_words = Some(50);
let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("exceeds maximum"));
assert!(errors[0].contains("100 > 50"));
}
#[test]
fn test_check_limits_min_words_ok() {
let mut args = make_test_cli();
args.min_words = Some(50);
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_min_words_below() {
let mut args = make_test_cli();
args.min_words = Some(200);
let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("below minimum"));
assert!(errors[0].contains("100 < 200"));
}
#[test]
fn test_check_limits_max_characters_ok() {
let mut args = make_test_cli();
args.max_characters = Some(1000);
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_max_characters_exceeded() {
let mut args = make_test_cli();
args.max_characters = Some(300);
let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("exceeds maximum"));
assert!(errors[0].contains("500 > 300"));
}
#[test]
fn test_check_limits_min_characters_ok() {
let mut args = make_test_cli();
args.min_characters = Some(100);
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_min_characters_below() {
let mut args = make_test_cli();
args.min_characters = Some(1000);
let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("below minimum"));
assert!(errors[0].contains("500 < 1000"));
}
#[test]
fn test_check_limits_multiple_violations() {
let mut args = make_test_cli();
args.max_words = Some(50);
args.min_words = Some(200);
args.max_characters = Some(300);
args.min_characters = Some(1000);
let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 4);
}
#[test]
fn test_check_limits_boundary_values() {
let mut args = make_test_cli();
args.max_words = Some(100);
args.min_words = Some(100);
let count = Count {
words: 100,
characters: 500,
};
assert!(check_limits(&args, &count).is_ok());
}
#[test]
fn test_check_limits_mixed_ok_and_violations() {
let mut args = make_test_cli();
args.max_words = Some(200); args.min_words = Some(50); args.max_characters = Some(300); args.min_characters = Some(100); let count = Count {
words: 100,
characters: 500,
};
let result = check_limits(&args, &count);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("Character count exceeds maximum"));
}
}