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 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 }
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(), };
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 }
438}