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}