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() } else {
65 None
66 }
67 .and_then(|_| components.next()) .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 }
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(), };
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 }
434}