weaver_lib/renderers/
globals.rs

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