1pub mod cli;
29pub mod counter;
30pub mod output;
31pub mod world;
32
33use anyhow::{Context, Result};
34use cli::Cli;
35use counter::Count;
36use std::path::Path;
37use typst::{World, layout::PagedDocument};
38
39pub fn compile_document(path: &Path, exclude_imports: bool) -> Result<Count> {
76 let world = world::SimpleWorld::new(path)
77 .with_context(|| format!("Failed to load {}", path.display()))?;
78 let main_file_id = world.main();
79
80 let result = typst::compile(&world);
81 let document: PagedDocument = result.output.map_err(|errors| {
82 let error_msg = errors
83 .iter()
84 .map(|e| format!("{}", e.message))
85 .collect::<Vec<_>>()
86 .join(", ");
87 anyhow::anyhow!("Failed to compile {}: {}", path.display(), error_msg)
88 })?;
89
90 Ok(counter::count_document(
91 &document.introspector,
92 exclude_imports,
93 main_file_id,
94 ))
95}
96
97pub fn process_files(args: &Cli) -> Result<Vec<(String, Count)>> {
133 args.input
134 .iter()
135 .map(|path| {
136 compile_document(path, args.exclude_imports)
137 .map(|count| (path.display().to_string(), count))
138 })
139 .collect()
140}
141
142pub fn check_limits(args: &Cli, total: &Count) -> Result<(), Vec<String>> {
185 let mut errors = Vec::new();
186
187 if let Some(max) = args.max_words
188 && total.words > max
189 {
190 errors.push(format!(
191 "Word count exceeds maximum ({} > {})",
192 total.words, max
193 ));
194 }
195
196 if let Some(min) = args.min_words
197 && total.words < min
198 {
199 errors.push(format!(
200 "Word count below minimum ({} < {})",
201 total.words, min
202 ));
203 }
204
205 if let Some(max) = args.max_characters
206 && total.characters > max
207 {
208 errors.push(format!(
209 "Character count exceeds maximum ({} > {})",
210 total.characters, max
211 ));
212 }
213
214 if let Some(min) = args.min_characters
215 && total.characters < min
216 {
217 errors.push(format!(
218 "Character count below minimum ({} < {})",
219 total.characters, min
220 ));
221 }
222
223 if errors.is_empty() {
224 Ok(())
225 } else {
226 Err(errors)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::cli::{Cli, CountMode, DisplayMode, OutputFormat};
234
235 fn make_test_cli() -> Cli {
236 Cli {
237 input: vec![],
238 format: OutputFormat::Human,
239 mode: CountMode::Both,
240 output: None,
241 display: DisplayMode::Auto,
242 exclude_imports: false,
243 max_words: None,
244 min_words: None,
245 max_characters: None,
246 min_characters: None,
247 }
248 }
249
250 #[test]
251 fn test_check_limits_no_limits() {
252 let args = make_test_cli();
253 let count = Count {
254 words: 100,
255 characters: 500,
256 };
257
258 assert!(check_limits(&args, &count).is_ok());
259 }
260
261 #[test]
262 fn test_check_limits_max_words_ok() {
263 let mut args = make_test_cli();
264 args.max_words = Some(200);
265 let count = Count {
266 words: 100,
267 characters: 500,
268 };
269
270 assert!(check_limits(&args, &count).is_ok());
271 }
272
273 #[test]
274 fn test_check_limits_max_words_exceeded() {
275 let mut args = make_test_cli();
276 args.max_words = Some(50);
277 let count = Count {
278 words: 100,
279 characters: 500,
280 };
281
282 let result = check_limits(&args, &count);
283 assert!(result.is_err());
284 let errors = result.unwrap_err();
285 assert_eq!(errors.len(), 1);
286 assert!(errors[0].contains("exceeds maximum"));
287 assert!(errors[0].contains("100 > 50"));
288 }
289
290 #[test]
291 fn test_check_limits_min_words_ok() {
292 let mut args = make_test_cli();
293 args.min_words = Some(50);
294 let count = Count {
295 words: 100,
296 characters: 500,
297 };
298
299 assert!(check_limits(&args, &count).is_ok());
300 }
301
302 #[test]
303 fn test_check_limits_min_words_below() {
304 let mut args = make_test_cli();
305 args.min_words = Some(200);
306 let count = Count {
307 words: 100,
308 characters: 500,
309 };
310
311 let result = check_limits(&args, &count);
312 assert!(result.is_err());
313 let errors = result.unwrap_err();
314 assert_eq!(errors.len(), 1);
315 assert!(errors[0].contains("below minimum"));
316 assert!(errors[0].contains("100 < 200"));
317 }
318
319 #[test]
320 fn test_check_limits_max_characters_ok() {
321 let mut args = make_test_cli();
322 args.max_characters = Some(1000);
323 let count = Count {
324 words: 100,
325 characters: 500,
326 };
327
328 assert!(check_limits(&args, &count).is_ok());
329 }
330
331 #[test]
332 fn test_check_limits_max_characters_exceeded() {
333 let mut args = make_test_cli();
334 args.max_characters = Some(300);
335 let count = Count {
336 words: 100,
337 characters: 500,
338 };
339
340 let result = check_limits(&args, &count);
341 assert!(result.is_err());
342 let errors = result.unwrap_err();
343 assert_eq!(errors.len(), 1);
344 assert!(errors[0].contains("exceeds maximum"));
345 assert!(errors[0].contains("500 > 300"));
346 }
347
348 #[test]
349 fn test_check_limits_min_characters_ok() {
350 let mut args = make_test_cli();
351 args.min_characters = Some(100);
352 let count = Count {
353 words: 100,
354 characters: 500,
355 };
356
357 assert!(check_limits(&args, &count).is_ok());
358 }
359
360 #[test]
361 fn test_check_limits_min_characters_below() {
362 let mut args = make_test_cli();
363 args.min_characters = Some(1000);
364 let count = Count {
365 words: 100,
366 characters: 500,
367 };
368
369 let result = check_limits(&args, &count);
370 assert!(result.is_err());
371 let errors = result.unwrap_err();
372 assert_eq!(errors.len(), 1);
373 assert!(errors[0].contains("below minimum"));
374 assert!(errors[0].contains("500 < 1000"));
375 }
376
377 #[test]
378 fn test_check_limits_multiple_violations() {
379 let mut args = make_test_cli();
380 args.max_words = Some(50);
381 args.min_words = Some(200);
382 args.max_characters = Some(300);
383 args.min_characters = Some(1000);
384 let count = Count {
385 words: 100,
386 characters: 500,
387 };
388
389 let result = check_limits(&args, &count);
390 assert!(result.is_err());
391 let errors = result.unwrap_err();
392 assert_eq!(errors.len(), 4);
395 }
396
397 #[test]
398 fn test_check_limits_boundary_values() {
399 let mut args = make_test_cli();
400 args.max_words = Some(100);
401 args.min_words = Some(100);
402 let count = Count {
403 words: 100,
404 characters: 500,
405 };
406
407 assert!(check_limits(&args, &count).is_ok());
409 }
410
411 #[test]
412 fn test_check_limits_mixed_ok_and_violations() {
413 let mut args = make_test_cli();
414 args.max_words = Some(200); args.min_words = Some(50); args.max_characters = Some(300); args.min_characters = Some(100); let count = Count {
419 words: 100,
420 characters: 500,
421 };
422
423 let result = check_limits(&args, &count);
424 assert!(result.is_err());
425 let errors = result.unwrap_err();
426 assert_eq!(errors.len(), 1);
427 assert!(errors[0].contains("Character count exceeds maximum"));
428 }
429}