weaver_lib/renderers/
globals.rs

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