weaver_lib/renderers/
mod.rs

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 tokio::sync::Mutex;
11
12use crate::config::TemplateLang;
13use crate::filters::raw_html::RawHtml;
14use crate::partial::Partial;
15use crate::routes::route_from_path;
16use crate::template::Template;
17use crate::{BuildError, document::Document};
18
19#[derive(Debug, PartialEq)]
20pub struct WritableFile {
21    pub contents: String,
22    pub path: PathBuf,
23    pub emit: bool,
24}
25
26#[async_trait]
27pub trait ContentRenderer {
28    async fn render(
29        &self,
30        data: &mut LiquidGlobals,
31        partials: Vec<Partial>,
32    ) -> Result<WritableFile, BuildError>;
33}
34
35fn out_path_for_document(document: &Document, weaver_config: &Arc<crate::WeaverConfig>) -> PathBuf {
36    let out_base = weaver_config.build_dir.clone();
37    let document_content_path = route_from_path(
38        weaver_config.content_dir.clone().into(),
39        document.at_path.clone().into(),
40    );
41
42    format!("{}{}index.html", out_base, document_content_path).into()
43}
44
45pub enum TemplateRenderer<'a> {
46    LiquidBuilder {
47        liquid_parser: liquid::Parser,
48        for_document: &'a Document,
49        weaver_template: Arc<Mutex<crate::Template>>,
50        weaver_config: Arc<crate::WeaverConfig>,
51    },
52}
53
54#[async_trait]
55impl<'a> ContentRenderer for TemplateRenderer<'a> {
56    async fn render(
57        &self,
58        data: &mut LiquidGlobals,
59        _partials: Vec<Partial>,
60    ) -> Result<WritableFile, BuildError> {
61        match self {
62            Self::LiquidBuilder {
63                liquid_parser,
64                weaver_template,
65                for_document,
66                weaver_config,
67            } => {
68                let wtemplate = weaver_template.lock().await;
69
70                match liquid_parser
71                    .parse(&wtemplate.contents)
72                    .unwrap()
73                    .render(&data.to_liquid_data())
74                {
75                    Ok(result) => Ok(WritableFile {
76                        contents: result,
77                        path: out_path_for_document(for_document, weaver_config),
78                        emit: for_document.emit,
79                    }),
80                    Err(err) => {
81                        eprintln!(
82                            "Template rendering error '{}' {:#?}",
83                            &for_document.at_path, &err
84                        );
85                        Err(BuildError::Err(err.to_string()))
86                    }
87                }
88            }
89        }
90    }
91}
92
93impl<'a> TemplateRenderer<'a> {
94    pub fn new(
95        template: Arc<Mutex<crate::Template>>,
96        for_document: &'a Document,
97        weaver_config: Arc<crate::WeaverConfig>,
98        partials: Vec<Partial>,
99    ) -> Self {
100        let mut registered_partials = EagerCompiler::<InMemorySource>::empty();
101
102        for partial in partials {
103            registered_partials.add(partial.name, partial.contents);
104        }
105
106        Self::LiquidBuilder {
107            liquid_parser: liquid::ParserBuilder::with_stdlib()
108                .filter(RawHtml)
109                .partials(registered_partials)
110                .build()
111                .unwrap(),
112            weaver_template: template.clone(),
113            for_document,
114            weaver_config,
115        }
116    }
117}
118
119pub struct MarkdownRenderer {
120    document: Arc<Mutex<Document>>,
121    templates: Arc<Vec<Arc<Mutex<crate::Template>>>>,
122    weaver_config: Arc<crate::WeaverConfig>,
123    partials: Vec<Partial>,
124}
125
126// This renderer is strange for several reasons, the way it works is as follows.
127// 1. do a pass to gather the headings in the document
128// 2. do a pass over the template.
129// 3. do a pass over the markdown to get html from the
130#[async_trait]
131impl ContentRenderer for MarkdownRenderer {
132    async fn render(
133        &self,
134        data: &mut LiquidGlobals,
135        partials: Vec<Partial>,
136    ) -> Result<WritableFile, BuildError> {
137        let doc_guard = self.document.lock().await;
138        let template = self
139            .find_template_by_string(doc_guard.metadata.template.clone())
140            .await
141            .unwrap();
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    async fn find_template_by_string(
198        &self,
199        template_name: String,
200    ) -> Option<&Arc<Mutex<crate::Template>>> {
201        futures::stream::iter(self.templates.iter())
202            .filter(|&t| {
203                let name = template_name.clone();
204                Box::pin(
205                    async move { t.lock().await.at_path.ends_with(format!("{}.liquid", name)) },
206                )
207            })
208            .next()
209            .await
210    }
211}
212
213#[cfg(test)]
214mod test {
215    use std::collections::HashMap;
216
217    use crate::{config::WeaverConfig, normalize_line_endings, template::Template};
218
219    use super::*;
220
221    use pretty_assertions::assert_eq;
222
223    #[tokio::test]
224    async fn test_liquid() {
225        let base_path_wd = std::env::current_dir().unwrap().display().to_string();
226        let base_path = format!("{}/test_fixtures/example", base_path_wd);
227        let template = Template::new_from_path(
228            format!("{}/test_fixtures/liquid/template.liquid", base_path_wd).into(),
229        );
230        let doc_arc =
231            Document::new_from_path(format!("{}/content/with_headings.md", base_path).into());
232        let config = Arc::new(WeaverConfig::new(base_path.clone().into()));
233        let renderer = TemplateRenderer::new(
234            Arc::new(Mutex::new(template)),
235            &doc_arc,
236            config.clone(),
237            vec![],
238        );
239
240        let mut data = LiquidGlobals::new(
241            Arc::new(Mutex::new(Document::new_from_path(
242                format!("{}/content/with_headings.md", base_path).into(),
243            ))),
244            &Arc::new(HashMap::new()),
245        )
246        .await;
247
248        assert_eq!(
249            WritableFile {
250                contents: normalize_line_endings(
251                    b"<!doctype html>
252<html>
253	<head>
254		<title>test</title>
255	</head>
256	<body></body>
257</html>
258"
259                )
260                .into(),
261                path: format!("{}/site/with_headings/index.html", base_path).into(),
262                emit: true,
263            },
264            renderer.render(&mut data, vec![]).await.unwrap()
265        );
266    }
267
268    #[tokio::test]
269    async fn test_render() {
270        let base_path_wd = std::env::current_dir().unwrap().display().to_string();
271        let base_path = format!("{}/test_fixtures/example", base_path_wd);
272        let template =
273            Template::new_from_path(format!("{}/templates/default.liquid", base_path).into());
274        let doc_arc = Arc::new(Mutex::new(Document::new_from_path(
275            format!("{}/content/with_headings.md", base_path).into(),
276        )));
277        let config = Arc::new(WeaverConfig::new(base_path.clone().into()));
278        let renderer = MarkdownRenderer::new(
279            doc_arc.clone(),
280            vec![Arc::new(Mutex::new(template))].into(),
281            config.clone(),
282            vec![],
283        );
284
285        let mut data = LiquidGlobals::new(doc_arc, &Arc::new(HashMap::new())).await;
286        let result = renderer.render(&mut data, vec![]).await;
287
288        assert_eq!(
289            WritableFile {
290                contents: normalize_line_endings(
291                    br#"<!doctype html>
292<html lang="en">
293	<head>
294		<meta charset="utf-8" />
295
296		<title>test</title>
297		<link rel="icon" href="/static/favicon.ico" />
298		<meta name="viewport" content="width=device-width, initial-scale=1" />
299
300		<meta name="description" content="test"/>
301		<meta name="keywords" content="test"/>
302	</head>
303	<body>
304		<main>
305			<h1>test</h1>
306			<article>
307				<h1>heading 1</h1>
308<p>I am a paragraph.</p>
309<h2>heading <span>2</span></h2>
310<p>I'm the second paragraph.</p>
311<h3>heading 3</h3>
312<h4>heading 4</h4>
313<h5>heading 5</h5>
314<h6>heading 6</h6>
315			</article>
316		</main>
317	</body>
318</html>
319"#
320                ),
321                path: format!("{}/site/with_headings/index.html", base_path).into(),
322                emit: true,
323            },
324            result.unwrap()
325        );
326    }
327}