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