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() } else {
69 None
70 }
71 .and_then(|_| components.next()) .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 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 }
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 }
437}