1use crate::document::{BaseMetaData, Heading};
2use liquid::model::KString;
3use liquid::{self};
4use serde::{Deserialize, Serialize};
5use std::path::{Component, PathBuf};
6use std::{collections::HashMap, sync::Arc};
7
8#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
9pub struct LiquidGlobalsPage {
10 pub route: KString,
11 pub title: String,
12 pub body: String,
13 pub meta: BaseMetaData,
14 pub toc: Vec<Heading>,
15 pub excerpt: Option<String>,
16}
17
18impl LiquidGlobalsPage {
19 pub fn to_liquid_data(&self) -> liquid::model::Value {
20 liquid::model::to_value(self)
21 .expect("Failed to serialize LiquidGlobalsPage to liquid value")
22 }
23}
24
25impl From<&crate::Document> for LiquidGlobalsPage {
26 fn from(value: &crate::Document) -> Self {
27 let route_kstring = KString::from(value.at_path.clone());
28
29 println!("TOC? {:?}", &value.toc);
30 Self {
31 route: route_kstring,
32 excerpt: value.excerpt.clone(),
33 meta: value.metadata.clone(),
34 body: value.html.clone().unwrap_or("".into()),
35 toc: value.toc.clone(),
36 title: value.metadata.title.clone(),
37 }
38 }
39}
40
41#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
42pub struct LiquidGlobals {
43 pub page: LiquidGlobalsPage,
44 pub content: HashMap<KString, HashMap<KString, LiquidGlobalsPage>>,
45}
46
47impl LiquidGlobals {
48 pub async fn new(
49 page_arc_mutex: Arc<tokio::sync::Mutex<crate::Document>>,
50 all_documents_by_route: &Arc<HashMap<KString, LiquidGlobalsPage>>,
51 ) -> Self {
52 let page_guard = page_arc_mutex.lock().await;
53 let page_globals = LiquidGlobalsPage::from(&*page_guard);
54
55 let mut content_map: HashMap<KString, HashMap<KString, LiquidGlobalsPage>> = HashMap::new();
56 for (route, doc_arc_mutex) in all_documents_by_route.iter() {
57 let path = PathBuf::from(route);
58 let mut components = path.components().peekable();
59
60 if route == &page_globals.route {
61 continue;
62 }
63
64 let first_component = if let Some(Component::RootDir) = components.peek() {
65 components.next() } else {
67 None
68 }
69 .and_then(|_| components.next()) .map(|c| {
71 if let Component::Normal(os_str) = c {
72 KString::from(os_str.to_string_lossy().into_owned())
73 } else {
74 KString::from("root")
75 }
76 });
77
78 if first_component.is_none() {
79 content_map.insert(
80 route.clone(),
81 HashMap::from([(route.clone(), doc_arc_mutex.clone())]),
82 );
83 } else {
84 let f_path = first_component.unwrap();
85 match content_map.contains_key(&f_path) {
86 true => {
87 if route.clone() != format!("/{}/", f_path) {
89 let content_inner_map = content_map.get_mut(&f_path).unwrap();
90 content_inner_map.insert(route.clone(), doc_arc_mutex.clone());
91 }
92 }
93 false => {
94 content_map.insert(
95 f_path.clone(),
96 HashMap::from([(route.clone(), doc_arc_mutex.clone())]),
97 );
98 }
99 }
100 }
101 }
102
103 drop(page_guard);
104
105 Self {
106 page: page_globals,
107 content: content_map,
108 }
109 }
110
111 pub fn to_liquid_data(&self) -> liquid::Object {
112 liquid::object!({
113 "page": self.page.to_liquid_data(),
114 "content": liquid::model::to_value(&self.content)
115 .expect("Failed to serialize content HashMap to liquid value")
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use liquid::ValueView;
124 use liquid::model::KString;
125 use pretty_assertions::assert_eq;
126 use std::collections::HashMap;
127 use std::sync::Arc;
128 use tokio::sync::Mutex;
129
130 fn create_mock_document(
131 route: &str,
132 title: &str,
133 body: Option<&str>,
134 excerpt: Option<&str>,
135 ) -> crate::Document {
136 crate::Document {
137 at_path: route.to_string(),
138 excerpt: excerpt.map(|s| s.to_string()),
139 metadata: BaseMetaData {
140 title: title.to_string(),
141 ..Default::default()
142 },
143 emit: true,
144 html: body.map(|s| s.to_string()),
145 markdown: String::new(),
146 toc: vec![],
147 }
148 }
149
150 #[test]
151 fn test_liquid_globals_page_to_liquid_data() {
152 let liquid_page = LiquidGlobalsPage {
153 route: KString::from("/test"),
154 title: "Test Page".to_string(),
155 body: "<p>Test Body</p>".to_string(),
156 toc: vec![],
157 meta: BaseMetaData {
158 title: "Test Meta Title".to_string(),
159 ..Default::default()
160 },
161 excerpt: None,
162 };
163
164 let liquid_value = liquid_page.to_liquid_data();
165
166 assert!(liquid_value.is_object());
167 let liquid_object = liquid_value.as_object().unwrap();
168
169 assert_eq!(
170 liquid_object
171 .get(&KString::from("route"))
172 .unwrap()
173 .as_scalar()
174 .unwrap()
175 .to_kstr(),
176 "/test"
177 );
178 assert_eq!(
179 liquid_object
180 .get(&KString::from("title"))
181 .unwrap()
182 .as_scalar()
183 .unwrap()
184 .to_kstr(),
185 "Test Page"
186 );
187 assert_eq!(
188 liquid_object
189 .get(&KString::from("body"))
190 .unwrap()
191 .as_scalar()
192 .unwrap()
193 .to_kstr(),
194 "<p>Test Body</p>"
195 );
196
197 let meta_value = liquid_object.get(&KString::from("meta")).unwrap();
198 assert!(meta_value.is_object());
199 let meta_object = meta_value.as_object().unwrap();
200 assert_eq!(
201 meta_object
202 .get(&KString::from("title"))
203 .unwrap()
204 .as_scalar()
205 .unwrap()
206 .to_kstr(),
207 "Test Meta Title"
208 );
209 assert_eq!(
210 meta_object
211 .get(&KString::from("template"))
212 .unwrap()
213 .as_scalar()
214 .unwrap()
215 .to_kstr(),
216 "default"
217 );
218 assert_eq!(
219 liquid_object
220 .get(&KString::from("excerpt"))
221 .unwrap()
222 .is_nil(),
223 true
224 );
225
226 }
245
246 #[tokio::test]
247 async fn test_liquid_globals_new() {
248 let page_doc = create_mock_document("/page", "Page Title", Some("<p>page body</p>"), None);
249 let content_doc_1 = create_mock_document(
250 "/posts/post-1",
251 "Post One",
252 Some("<p>post 1 body</p>"),
253 Some("excerpt 1"),
254 );
255 let content_doc_2 = create_mock_document("/about", "About Us", None, None);
256
257 let page_arc_mutex = Arc::new(Mutex::new(page_doc.clone()));
258 let post1_arc_mutex = Arc::new(Mutex::new(content_doc_1.clone()));
259 let about_arc_mutex = Arc::new(Mutex::new(content_doc_2.clone()));
260
261 let mut all_documents_by_route = HashMap::new();
262 all_documents_by_route.insert(KString::from("/page"), LiquidGlobalsPage::from(&page_doc));
263 all_documents_by_route.insert(
264 KString::from("/posts/post-1"),
265 LiquidGlobalsPage::from(&content_doc_1),
266 );
267 all_documents_by_route.insert(
268 KString::from("/about"),
269 LiquidGlobalsPage::from(&content_doc_2),
270 );
271
272 let liquid_globals = LiquidGlobals::new(
273 Arc::clone(&page_arc_mutex),
274 &Arc::new(all_documents_by_route),
275 )
276 .await;
277
278 let page_doc_guard = page_arc_mutex.lock().await;
279 let expected_page_globals = LiquidGlobalsPage::from(&*page_doc_guard);
280 assert_eq!(liquid_globals.page, expected_page_globals);
281 drop(page_doc_guard);
282
283 assert_eq!(liquid_globals.content.len(), 2);
284
285 assert!(liquid_globals.content.contains_key(&KString::from("posts")));
286 assert!(
287 liquid_globals
288 .content
289 .get("posts")
290 .unwrap()
291 .contains_key(&KString::from("/posts/post-1"))
292 );
293 assert!(liquid_globals.content.contains_key(&KString::from("about")));
294 assert!(!liquid_globals.content.contains_key(&KString::from("page")));
295
296 let post1_doc_guard = post1_arc_mutex.lock().await;
297 let expected_post1_globals = LiquidGlobalsPage::from(&*post1_doc_guard);
298 assert_eq!(
299 liquid_globals
300 .content
301 .get(&KString::from("posts"))
302 .unwrap()
303 .get(&KString::from("/posts/post-1"))
304 .unwrap(),
305 &expected_post1_globals
306 );
307 drop(post1_doc_guard);
308
309 let about_doc_guard = about_arc_mutex.lock().await;
310 let expected_about_globals = LiquidGlobalsPage::from(&*about_doc_guard);
311 assert_eq!(
312 liquid_globals
313 .content
314 .get(&KString::from("about"))
315 .unwrap()
316 .get(&KString::from("/about"))
317 .unwrap(),
318 &expected_about_globals
319 );
320 drop(about_doc_guard);
321 }
322
323 #[tokio::test]
324 async fn test_liquid_globals_new_only_page_doc() {
325 let page_doc = create_mock_document("/index", "Home Page", Some("<p>home</p>"), None);
326 let page_arc_mutex = Arc::new(Mutex::new(page_doc.clone()));
327 let page_global = LiquidGlobalsPage::from(&page_doc);
328
329 let mut all_documents_by_route = HashMap::new();
330 all_documents_by_route.insert(KString::from("/index"), page_global);
331
332 let liquid_globals = LiquidGlobals::new(
333 Arc::clone(&page_arc_mutex),
334 &Arc::new(all_documents_by_route),
335 )
336 .await;
337
338 let page_doc_guard = page_arc_mutex.lock().await;
339 let expected_page_globals = LiquidGlobalsPage::from(&*page_doc_guard);
340 assert_eq!(liquid_globals.page, expected_page_globals);
341 drop(page_doc_guard);
342
343 assert_eq!(liquid_globals.content.len(), 0);
344 assert!(liquid_globals.content.is_empty());
345 }
346
347 #[test]
348 fn test_liquid_globals_to_liquid_data() {
349 let page_page = LiquidGlobalsPage {
350 route: KString::from("/page"),
351 title: "Page".to_string(),
352 body: "<p>page</p>".to_string(),
353 meta: BaseMetaData {
354 title: "Page Meta".to_string(),
355 ..Default::default()
356 },
357 toc: vec![],
358 excerpt: Some("page excerpt".to_string()),
359 };
360 let content_page_1 = LiquidGlobalsPage {
361 route: KString::from("/post-1"),
362 title: "Post 1".to_string(),
363 body: "<p>post1</p>".to_string(),
364 meta: BaseMetaData {
365 title: "Post 1 Meta".to_string(),
366 ..Default::default()
367 },
368 toc: vec![],
369 excerpt: Some("post1 excerpt".to_string()),
370 };
371 let content_page_2 = LiquidGlobalsPage {
372 route: KString::from("/about"),
373 title: "About".to_string(),
374 body: "".into(),
375 meta: BaseMetaData {
376 title: "About Meta".to_string(),
377 ..Default::default()
378 },
379 toc: vec![],
380 excerpt: None,
381 };
382
383 let mut content_map: HashMap<KString, HashMap<KString, LiquidGlobalsPage>> = HashMap::new();
384 content_map.insert(
385 KString::from("/post-1"),
386 HashMap::from([(KString::from("/post-1"), content_page_1.clone())]),
387 );
388 content_map.insert(
389 KString::from("/about"),
390 HashMap::from([(KString::from("/about"), content_page_2.clone())]),
391 );
392
393 let liquid_globals = LiquidGlobals {
394 page: page_page.clone(),
395 content: content_map.clone(), };
397
398 let liquid_object = liquid_globals.to_liquid_data();
399
400 assert!(liquid_object.is_object());
401 let liquid_map = liquid_object.as_object().unwrap();
402
403 assert!(liquid_map.contains_key(&KString::from("page")));
404 assert!(liquid_map.contains_key(&KString::from("content")));
405 assert_eq!(liquid_map.size(), 2);
406
407 }
444}