mdbook_reading_time/
lib.rs1use mdbook_preprocessor::{
2 book::{Book, BookItem, Chapter},
3 errors::Result,
4 Preprocessor, PreprocessorContext,
5};
6use unicode_segmentation::UnicodeSegmentation;
7
8#[derive(Default)]
9pub struct ReadingTime;
10
11impl ReadingTime {
12 pub fn new() -> ReadingTime {
13 ReadingTime
14 }
15}
16
17static WORDS_PER_MINUTE: usize = 200;
18
19impl Preprocessor for ReadingTime {
20 fn name(&self) -> &str {
21 "reading-time"
22 }
23
24 fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
25 let words_per_minute: usize = ctx
26 .config
27 .get::<usize>("preprocessor.reading-time.words-per-minute")
28 .ok()
29 .flatten()
30 .unwrap_or(WORDS_PER_MINUTE);
31
32 let mut book = book;
33 book.for_each_mut(|item: &mut BookItem| {
34 if let BookItem::Chapter(ref mut chapter) = *item {
35 let _ = handle_chapter(chapter, words_per_minute);
36 }
37 });
38
39 Ok(book)
40 }
41
42 fn supports_renderer(&self, renderer: &str) -> Result<bool> {
43 Ok(renderer != "not-supported")
44 }
45}
46
47fn handle_chapter(chapter: &mut Chapter, words_per_minute: usize) -> Result<()> {
48 let content = chapter.content.as_str();
49 let word_count = content.unicode_words().count();
50 let reading_time = word_count / words_per_minute;
51 let minutes = if reading_time == 1 {
52 "minute"
53 } else {
54 "minutes"
55 };
56
57 chapter.content = chapter
58 .content
59 .replace("{{ #word_count }}", word_count.to_string().as_str())
60 .replace(
61 "{{ #reading_time }}",
62 &format!("{} {minutes}", reading_time.to_string().as_str()),
63 );
64 Ok(())
65}
66
67#[cfg(test)]
68mod test {
69 use super::*;
70
71 #[test]
72 fn reading_preprocessor_run() {
73 let input_json = r##"[
74 {
75 "root": "/path/to/book",
76 "config": {
77 "book": {
78 "authors": ["AUTHOR"],
79 "language": "en",
80 "src": "src",
81 "title": "TITLE"
82 },
83 "preprocessor": {
84 "reading-time": {}
85 }
86 },
87 "renderer": "html",
88 "mdbook_version": "0.5.1"
89 },
90 {
91 "items": [
92 {
93 "Chapter": {
94 "name": "Chapter 1",
95 "content": "# Chapter 1\n {{ #word_count }}\n\n{{ #reading_time }}",
96 "number": [1],
97 "sub_items": [],
98 "path": "chapter_1.md",
99 "source_path": "chapter_1.md",
100 "parent_names": []
101 }
102 }
103 ],
104 "__non_exhaustive": null
105 }
106 ]"##;
107 let input_json = input_json.as_bytes();
108
109 let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
110 let result = ReadingTime::new().run(&ctx, book);
111 assert!(result.is_ok());
112
113 let actual_book = result.unwrap();
114 let chapter = actual_book.iter().next().unwrap();
115
116 match chapter {
117 BookItem::Chapter(chapter) => {
118 assert_eq!(chapter.content, "# Chapter 1\n 4\n\n0 minutes");
119 }
120 _ => panic!("Expected a chapter"),
121 };
122 }
123}