1pub mod globals;
2use async_trait::async_trait;
3use futures::StreamExt;
4use globals::LiquidGlobals;
5use liquid::partials::{EagerCompiler, InMemorySource};
6use markdown::CompileOptions;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use markdown::{ParseOptions, mdast::Node};
11use slug::slugify;
12use tokio::sync::Mutex;
13
14use crate::config::TemplateLang;
15use crate::document::Heading;
16use crate::filters::raw_html::RawHtml;
17use crate::partial::Partial;
18use crate::routes::route_from_path;
19use crate::template::Template;
20use crate::{BuildError, document::Document};
21
22#[derive(Debug, PartialEq)]
23pub struct WritableFile {
24 pub contents: String,
25 pub path: PathBuf,
26}
27
28#[async_trait]
29pub trait ContentRenderer {
30 async fn render(
31 &self,
32 data: &mut LiquidGlobals,
33 partials: Vec<Partial>,
34 ) -> Result<WritableFile, BuildError>;
35}
36
37fn out_path_for_document(document: &Document, weaver_config: &Arc<crate::WeaverConfig>) -> PathBuf {
38 let out_base = weaver_config.build_dir.clone();
39 let document_content_path = route_from_path(
40 weaver_config.content_dir.clone().into(),
41 document.at_path.clone().into(),
42 );
43
44 format!("{}{}index.html", out_base, document_content_path).into()
45}
46
47pub enum TemplateRenderer<'a> {
48 LiquidBuilder {
49 liquid_parser: liquid::Parser,
50 for_document: &'a Document,
51 weaver_template: Arc<Mutex<crate::Template>>,
52 weaver_config: Arc<crate::WeaverConfig>,
53 },
54}
55
56#[async_trait]
57impl<'a> ContentRenderer for TemplateRenderer<'a> {
58 async fn render(
59 &self,
60 data: &mut LiquidGlobals,
61 _partials: Vec<Partial>,
62 ) -> Result<WritableFile, BuildError> {
63 match self {
64 Self::LiquidBuilder {
65 liquid_parser,
66 weaver_template,
67 for_document,
68 weaver_config,
69 } => {
70 let wtemplate = weaver_template.lock().await;
71
72 match liquid_parser
73 .parse(&wtemplate.contents)
74 .unwrap()
75 .render(&data.to_liquid_data())
76 {
77 Ok(result) => Ok(WritableFile {
78 contents: result,
79 path: out_path_for_document(for_document, weaver_config),
80 }),
81 Err(err) => {
82 dbg!("Template rendering error {:#?}", &err);
83 Err(BuildError::Err(err.to_string()))
84 }
85 }
86 }
87 }
88 }
89}
90
91impl<'a> TemplateRenderer<'a> {
92 pub fn new(
93 template: Arc<Mutex<crate::Template>>,
94 for_document: &'a Document,
95 weaver_config: Arc<crate::WeaverConfig>,
96 partials: Vec<Partial>,
97 ) -> Self {
98 let mut registered_partials = EagerCompiler::<InMemorySource>::empty();
99
100 for partial in partials {
101 registered_partials.add(partial.name, partial.contents);
102 }
103
104 Self::LiquidBuilder {
105 liquid_parser: liquid::ParserBuilder::with_stdlib()
106 .filter(RawHtml)
107 .partials(registered_partials)
108 .build()
109 .unwrap(),
110 weaver_template: template.clone(),
111 for_document,
112 weaver_config,
113 }
114 }
115}
116
117pub struct MarkdownRenderer {
118 document: Arc<Mutex<Document>>,
119 templates: Arc<Vec<Arc<Mutex<crate::Template>>>>,
120 weaver_config: Arc<crate::WeaverConfig>,
121 partials: Vec<Partial>,
122}
123
124#[async_trait]
129impl ContentRenderer for MarkdownRenderer {
130 async fn render(
131 &self,
132 data: &mut LiquidGlobals,
133 partials: Vec<Partial>,
134 ) -> Result<WritableFile, BuildError> {
135 let mut doc_guard = self.document.lock().await;
136 let template = self
137 .find_template_by_string(doc_guard.metadata.template.clone())
138 .await
139 .unwrap();
140
141 doc_guard.toc = self.toc_from_document(&doc_guard);
142
143 let templated_md_html =
144 Template::new_from_string(doc_guard.markdown.clone(), TemplateLang::Liquid);
145
146 let body_template_renderer = TemplateRenderer::new(
147 Arc::new(Mutex::new(templated_md_html)),
148 &doc_guard,
149 self.weaver_config.clone(),
150 self.partials.clone(),
151 );
152 let body_html = body_template_renderer
153 .render(&mut data.to_owned(), partials.clone())
154 .await?;
155
156 let markdown_html = markdown::to_html_with_options(
157 body_html.contents.as_str(),
158 &markdown::Options {
159 compile: CompileOptions {
160 allow_dangerous_html: true,
161 ..CompileOptions::gfm()
162 },
163 ..markdown::Options::gfm()
164 },
165 )
166 .expect("failed to render markdown to html");
167
168 let template_renderer = TemplateRenderer::new(
169 template.clone(),
170 &doc_guard,
171 self.weaver_config.clone(),
172 partials.clone(),
173 );
174 data.page.body = markdown_html;
175
176 template_renderer
177 .render(&mut data.to_owned(), partials)
178 .await
179 }
180}
181
182impl MarkdownRenderer {
183 pub fn new(
184 document: Arc<Mutex<Document>>,
185 templates: Arc<Vec<Arc<Mutex<crate::Template>>>>,
186 weaver_config: Arc<crate::WeaverConfig>,
187 partials: Vec<Partial>,
188 ) -> Self {
189 Self {
190 document,
191 templates,
192 weaver_config,
193 partials,
194 }
195 }
196
197 fn extract_text_from_mdast_inline(node: &Node) -> String {
200 let mut text = String::new();
201 match &node {
202 Node::Text(text_node) => text.push_str(&text_node.value),
203 Node::Code(code_node) => text.push_str(&code_node.value),
204 Node::Emphasis(_) | Node::Strong(_) | Node::Link(_) => {
207 if let Some(children) = node.children() {
208 for child in children.iter() {
209 text.push_str(&Self::extract_text_from_mdast_inline(child)); }
211 }
212 }
213 _ => {
214 if let Some(children) = node.children() {
216 for child in children.iter() {
217 text.push_str(&Self::extract_text_from_mdast_inline(child));
218 }
219 }
220 }
221 }
222 text
223 }
224
225 fn collect_mdast_headings_to_map(node: &Node, headings_map: &mut Vec<Heading>) {
226 if let Node::Heading(heading) = &node {
228 let heading_text = if let Some(children) = node.children() {
229 let mut text = String::new();
230 for child in children.iter() {
231 text.push_str(&Self::extract_text_from_mdast_inline(child));
232 }
233 text
234 } else {
235 String::new()
236 };
237 let slug = slugify(&heading_text);
238 if !slug.is_empty() {
239 headings_map.push(Heading {
240 slug,
241 text: heading_text,
242 depth: heading.depth,
243 });
244 }
245 }
246
247 if let Some(children) = node.children() {
250 for child in children.iter() {
251 Self::collect_mdast_headings_to_map(child, headings_map);
252 }
253 }
254 }
255
256 fn toc_from_document(&self, document: &Document) -> Vec<Heading> {
257 let mut toc_map = vec![];
258 let ast = markdown::to_mdast(document.markdown.as_str(), &ParseOptions::gfm()).unwrap();
259 Self::collect_mdast_headings_to_map(&ast, &mut toc_map);
260 toc_map
261 }
262
263 async fn find_template_by_string(
264 &self,
265 template_name: String,
266 ) -> Option<&Arc<Mutex<crate::Template>>> {
267 futures::stream::iter(self.templates.iter())
268 .filter(|&t| {
269 let name = template_name.clone();
270 Box::pin(
271 async move { t.lock().await.at_path.ends_with(format!("{}.liquid", name)) },
272 )
273 })
274 .next()
275 .await
276 }
277}
278
279#[cfg(test)]
280mod test {
281 use std::collections::HashMap;
282
283 use crate::{config::WeaverConfig, normalize_line_endings, template::Template};
284
285 use super::*;
286
287 use pretty_assertions::assert_eq;
288
289 #[tokio::test]
290 async fn test_liquid() {
291 let base_path_wd = std::env::current_dir().unwrap().display().to_string();
292 let base_path = format!("{}/test_fixtures/example", base_path_wd);
293 let template = Template::new_from_path(
294 format!("{}/test_fixtures/liquid/template.liquid", base_path_wd).into(),
295 );
296 let doc_arc =
297 Document::new_from_path(format!("{}/content/with_headings.md", base_path).into());
298 let config = Arc::new(WeaverConfig::new_from_path(base_path.clone().into()));
299 let renderer = TemplateRenderer::new(
300 Arc::new(Mutex::new(template)),
301 &doc_arc,
302 config.clone(),
303 vec![],
304 );
305
306 let mut data = LiquidGlobals::new(
307 Arc::new(Mutex::new(Document::new_from_path(
308 format!("{}/content/with_headings.md", base_path).into(),
309 ))),
310 &Arc::new(HashMap::new()),
311 )
312 .await;
313
314 assert_eq!(
315 WritableFile {
316 contents: normalize_line_endings(
317 b"<!doctype html>
318<html>
319 <head>
320 <title>test</title>
321 </head>
322 <body></body>
323</html>
324"
325 )
326 .into(),
327 path: format!("{}/site/with_headings/index.html", base_path).into(),
328 },
329 renderer.render(&mut data, vec![]).await.unwrap()
330 );
331 }
332
333 #[test]
334 fn test_markdown_toc_generation() {
335 let base_path_wd = std::env::current_dir().unwrap().display().to_string();
336 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
337 let doc_arc = Arc::new(Mutex::new(Document::new_from_path(
338 format!("{}/with_headings.md", base_path).into(),
339 )));
340 let config_path = format!("{}/test_fixtures/config/custom_config", base_path_wd);
341 let config = Arc::new(WeaverConfig::new_from_path(config_path.clone().into()));
342 let renderer =
343 MarkdownRenderer::new(doc_arc.clone(), vec![].into(), config.clone(), vec![]);
344
345 assert_eq!(
346 vec![
347 Heading {
348 depth: 1,
349 text: "heading 1".into(),
350 slug: "heading-1".into(),
351 },
352 Heading {
353 depth: 2,
354 text: "heading 2".into(),
355 slug: "heading-2".into(),
356 },
357 Heading {
358 depth: 3,
359 text: "heading 3".into(),
360 slug: "heading-3".into(),
361 },
362 Heading {
363 depth: 4,
364 text: "heading 4".into(),
365 slug: "heading-4".into(),
366 },
367 Heading {
368 depth: 5,
369 text: "heading 5".into(),
370 slug: "heading-5".into(),
371 },
372 Heading {
373 depth: 6,
374 text: "heading 6".into(),
375 slug: "heading-6".into(),
376 },
377 ],
378 renderer.toc_from_document(&Document::new_from_path(
379 format!("{}/with_headings.md", base_path).into(),
380 ))
381 );
382 }
383
384 #[tokio::test]
385 async fn test_render() {
386 let base_path_wd = std::env::current_dir().unwrap().display().to_string();
387 let base_path = format!("{}/test_fixtures/example", base_path_wd);
388 let template =
389 Template::new_from_path(format!("{}/templates/default.liquid", base_path).into());
390 let doc_arc = Arc::new(Mutex::new(Document::new_from_path(
391 format!("{}/content/with_headings.md", base_path).into(),
392 )));
393 let config = Arc::new(WeaverConfig::new_from_path(base_path.clone().into()));
394 let renderer = MarkdownRenderer::new(
395 doc_arc.clone(),
396 vec![Arc::new(Mutex::new(template))].into(),
397 config.clone(),
398 vec![],
399 );
400
401 let mut data = LiquidGlobals::new(doc_arc, &Arc::new(HashMap::new())).await;
402 let result = renderer.render(&mut data, vec![]).await;
403
404 assert_eq!(
405 WritableFile {
406 contents: normalize_line_endings(
407 br#"<!doctype html>
408<html lang="en">
409 <head>
410 <meta charset="utf-8" />
411
412 <title>test</title>
413 <link rel="icon" href="/static/favicon.ico" />
414 <meta name="viewport" content="width=device-width, initial-scale=1" />
415
416 <meta name="description" content="test"/>
417 <meta name="keywords" content="test"/>
418 </head>
419 <body>
420 <main>
421 <h1>test</h1>
422 <article>
423 <h1>heading 1</h1>
424<p>I am a paragraph.</p>
425<h2>heading <span>2</span></h2>
426<p>I'm the second paragraph.</p>
427<h3>heading 3</h3>
428<h4>heading 4</h4>
429<h5>heading 5</h5>
430<h6>heading 6</h6>
431 </article>
432 </main>
433 </body>
434</html>
435
436"#
437 )
438 .into(),
439 path: format!("{}/site/with_headings/index.html", base_path).into()
440 },
441 result.unwrap()
442 );
443 }
444}