mdbook_content_loader/
lib.rs1use anyhow::{bail, Context};
2use chrono::Utc;
3use mdbook_preprocessor::{
4 book::{Book, BookItem},
5 errors::Error,
6 Preprocessor, PreprocessorContext,
7};
8use serde_json::{json, Map, Value};
9use std::cmp::Reverse;
10use std::fs;
11use std::path::Path;
12
13pub struct ContentLoader;
14
15impl ContentLoader {
16 pub fn new() -> ContentLoader {
17 ContentLoader
18 }
19}
20
21impl Default for ContentLoader {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl Preprocessor for ContentLoader {
28 fn name(&self) -> &str {
29 "content-loader"
30 }
31
32 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
33 let src = ctx.config.book.src.to_str().unwrap_or("src");
35 let src_dir = ctx.root.join(src);
36 let index_path = src_dir.join("content-collections.json");
37
38 let payload: Value = match load_collections(&index_path) {
39 Ok(data) => data,
40 Err(e) => {
41 log::warn!("content-loader: {}", e);
42 return Ok(book);
43 }
44 };
45
46 let script = format!(
47 r#"<script>window.CONTENT_COLLECTIONS = {};</script>"#,
48 serde_json::to_string(&payload)?
49 );
50
51 book.for_each_mut(|item| {
52 if let BookItem::Chapter(chapter) = item {
53 chapter.content = format!("{}\n{}", script, chapter.content);
54 }
55 });
56
57 Ok(book)
58 }
59
60 fn supports_renderer(&self, renderer: &str) -> Result<bool, Error> {
61 Ok(renderer == "html")
62 }
63}
64
65fn load_collections(path: &Path) -> anyhow::Result<Value> {
66 if !path.exists() {
67 bail!("content-collections.json not found at {:?}", path);
68 }
69
70 let content = fs::read_to_string(path).context("Failed to read content-collections.json")?;
71 let json_val: Value = serde_json::from_str(&content).context("Failed to parse JSON")?;
72
73 let entries: Vec<Value> = json_val
74 .get("entries")
75 .and_then(|v| v.as_array())
76 .map(|a| a.to_vec())
77 .unwrap_or_default();
78
79 let published: Vec<_> = entries
80 .into_iter()
81 .filter(|e| !e.get("draft").and_then(|v| v.as_bool()).unwrap_or(false))
82 .collect();
83
84 let mut collections: Map<String, Value> = Map::new();
85 let mut default_collection = vec![];
86
87 for entry in &published {
88 let coll = entry
89 .get("collection")
90 .and_then(|v| v.as_str())
91 .unwrap_or("posts")
92 .to_string();
93 if coll == "posts" {
94 default_collection.push(entry.clone());
95 } else {
96 let entry_arr = collections
97 .entry(coll)
98 .or_insert_with(|| json!([]))
99 .as_array_mut()
100 .expect("Failed to convert to array");
101 entry_arr.push(entry.clone());
102 }
103 }
104
105 if !default_collection.is_empty() {
106 sort_by_date_desc(&mut default_collection);
107 collections.insert("posts".to_string(), json!(default_collection));
108 }
109
110 for coll in collections.values_mut() {
111 if let Value::Array(arr) = coll {
112 sort_by_date_desc(arr);
113 }
114 }
115
116 Ok(json!({
117 "entries": published,
118 "collections": collections,
119 "generated_at": Utc::now().to_rfc3339(),
120 }))
121}
122
123fn sort_by_date_desc(arr: &mut [Value]) {
124 arr.sort_by_key(|e| {
125 let date = e.get("date").and_then(|v| v.as_str()).unwrap_or("");
126 Reverse(date.to_string())
127 });
128}