weaver_lib/renderers/
mod.rs

1pub mod globals;
2use async_trait::async_trait;
3use comrak::plugins::syntect::SyntectAdapterBuilder;
4use comrak::{ExtensionOptions, Options, Plugins, RenderOptions, markdown_to_html_with_plugins};
5use futures::StreamExt;
6use globals::LiquidGlobals;
7use liquid::partials::{EagerCompiler, InMemorySource};
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use tokio::sync::Mutex;
12
13use crate::config::TemplateLang;
14use crate::filters::has_key::HasKey;
15use crate::filters::json::JSON;
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    pub emit: bool,
27}
28
29#[async_trait]
30pub trait ContentRenderer {
31    async fn render(
32        &self,
33        data: &mut LiquidGlobals,
34        partials: Vec<Partial>,
35    ) -> Result<Option<WritableFile>, BuildError>;
36}
37
38fn out_path_for_document(document: &Document, weaver_config: &Arc<crate::WeaverConfig>) -> PathBuf {
39    let out_base = weaver_config.build_dir.clone();
40    let document_content_path = route_from_path(
41        weaver_config.content_dir.clone().into(),
42        document.at_path.clone().into(),
43    );
44
45    format!("{}{}index.html", out_base, document_content_path).into()
46}
47
48pub enum TemplateRenderer<'a> {
49    LiquidBuilder {
50        liquid_parser: liquid::Parser,
51        for_document: &'a Document,
52        weaver_template: Arc<Mutex<crate::Template>>,
53        weaver_config: Arc<crate::WeaverConfig>,
54    },
55}
56
57#[async_trait]
58impl<'a> ContentRenderer for TemplateRenderer<'a> {
59    async fn render(
60        &self,
61        data: &mut LiquidGlobals,
62        _partials: Vec<Partial>,
63    ) -> Result<Option<WritableFile>, BuildError> {
64        match self {
65            Self::LiquidBuilder {
66                liquid_parser,
67                weaver_template,
68                for_document,
69                weaver_config,
70            } => {
71                let wtemplate = weaver_template.lock().await;
72
73                match liquid_parser.parse(&wtemplate.contents) {
74                    Ok(parsed) => match parsed.render(&data.to_liquid_data()) {
75                        Ok(result) => Ok(Some(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                    Err(err) => {
89                        eprintln!(
90                            "Template rendering error '{}' {:#?}",
91                            &for_document.at_path, &err
92                        );
93                        Err(BuildError::Err(err.to_string()))
94                    }
95                }
96            }
97        }
98    }
99}
100
101impl<'a> TemplateRenderer<'a> {
102    pub fn new(
103        template: Arc<Mutex<crate::Template>>,
104        for_document: &'a Document,
105        weaver_config: Arc<crate::WeaverConfig>,
106        partials: Vec<Partial>,
107    ) -> Self {
108        let mut registered_partials = EagerCompiler::<InMemorySource>::empty();
109
110        for partial in partials {
111            registered_partials.add(partial.name, partial.contents);
112        }
113
114        Self::LiquidBuilder {
115            liquid_parser: liquid::ParserBuilder::with_stdlib()
116                .filter(RawHtml)
117                .filter(JSON)
118                .filter(HasKey)
119                .partials(registered_partials)
120                .build()
121                .unwrap(),
122            weaver_template: template.clone(),
123            for_document,
124            weaver_config,
125        }
126    }
127}
128
129pub struct MarkdownRenderer {
130    document: Arc<Mutex<Document>>,
131    templates: Arc<Vec<Arc<Mutex<crate::Template>>>>,
132    weaver_config: Arc<crate::WeaverConfig>,
133    partials: Vec<Partial>,
134}
135
136// This renderer is strange for several reasons, the way it works is as follows.
137// 1. Do a pass to gather the headings in the document
138// 2. Do a pass over the template.
139// 3. Do a pass over the markdown to get HTML from the
140#[async_trait]
141impl ContentRenderer for MarkdownRenderer {
142    async fn render(
143        &self,
144        data: &mut LiquidGlobals,
145        partials: Vec<Partial>,
146    ) -> Result<Option<WritableFile>, BuildError> {
147        let doc_guard = self.document.lock().await;
148        let template = self
149            .find_template_by_string(doc_guard.metadata.template.clone())
150            .await
151            .unwrap();
152
153        let templated_md_html =
154            Template::new_from_string(doc_guard.markdown.clone(), TemplateLang::Liquid);
155
156        let body_template_renderer = TemplateRenderer::new(
157            Arc::new(Mutex::new(templated_md_html)),
158            &doc_guard,
159            self.weaver_config.clone(),
160            self.partials.clone(),
161        );
162        let body_html = body_template_renderer
163            .render(&mut data.to_owned(), partials.clone())
164            .await?;
165
166        if body_html.is_none() {
167            return Ok(None);
168        }
169
170        let mut markdown_plugins = Plugins::default();
171        let markdown_syntax_hl_adapter = SyntectAdapterBuilder::new().css().build();
172        markdown_plugins.render.codefence_syntax_highlighter = Some(&markdown_syntax_hl_adapter);
173        let markdown_html = markdown_to_html_with_plugins(
174            body_html.unwrap().contents.as_str(),
175            &Options {
176                render: RenderOptions {
177                    unsafe_: true,
178                    figure_with_caption: true,
179                    gfm_quirks: true,
180                    ..Default::default()
181                },
182                extension: ExtensionOptions {
183                    strikethrough: true,
184                    tagfilter: true,
185                    table: true,
186                    autolink: true,
187                    header_ids: Some("".into()),
188                    alerts: true,
189                    ..Default::default()
190                },
191                ..Default::default()
192            },
193            &markdown_plugins,
194        );
195
196        let template_renderer = TemplateRenderer::new(
197            template.clone(),
198            &doc_guard,
199            self.weaver_config.clone(),
200            partials.clone(),
201        );
202        data.page.body = markdown_html;
203
204        template_renderer
205            .render(&mut data.to_owned(), partials)
206            .await
207    }
208}
209
210impl MarkdownRenderer {
211    pub fn new(
212        document: Arc<Mutex<Document>>,
213        templates: Arc<Vec<Arc<Mutex<crate::Template>>>>,
214        weaver_config: Arc<crate::WeaverConfig>,
215        partials: Vec<Partial>,
216    ) -> Self {
217        Self {
218            document,
219            templates,
220            weaver_config,
221            partials,
222        }
223    }
224
225    async fn find_template_by_string(
226        &self,
227        template_name: String,
228    ) -> Option<&Arc<Mutex<crate::Template>>> {
229        futures::stream::iter(self.templates.iter())
230            .filter(|&t| {
231                let name = template_name.clone();
232                Box::pin(
233                    async move { t.lock().await.at_path.ends_with(format!("{}.liquid", name)) },
234                )
235            })
236            .next()
237            .await
238    }
239}
240
241#[cfg(test)]
242mod test {
243    use std::collections::HashMap;
244
245    use crate::{config::WeaverConfig, normalize_line_endings, template::Template};
246
247    use super::*;
248
249    use pretty_assertions::assert_eq;
250
251    #[tokio::test]
252    async fn test_liquid() {
253        let base_path_wd = std::env::current_dir().unwrap().display().to_string();
254        let base_path = format!("{}/test_fixtures/example", base_path_wd);
255        let template = Template::new_from_path(
256            format!("{}/test_fixtures/liquid/template.liquid", base_path_wd).into(),
257        );
258        let doc_arc = Document::new_from_path(
259            base_path.clone().into(),
260            format!("{}/content/with_headings.md", base_path).into(),
261        );
262        let config = Arc::new(WeaverConfig::new(base_path.clone().into()));
263        let renderer = TemplateRenderer::new(
264            Arc::new(Mutex::new(template)),
265            &doc_arc,
266            config.clone(),
267            vec![],
268        );
269
270        let mut data = LiquidGlobals::new(
271            Arc::new(Mutex::new(Document::new_from_path(
272                base_path.clone().into(),
273                format!("{}/content/with_headings.md", base_path).into(),
274            ))),
275            &Arc::new(HashMap::new()),
276            Arc::new(WeaverConfig::default()),
277        )
278        .await;
279
280        assert_eq!(
281            WritableFile {
282                contents: normalize_line_endings(
283                    b"<!doctype html>
284<html>
285	<head>
286		<title>test</title>
287	</head>
288	<body></body>
289</html>
290"
291                ),
292                path: format!("{}/site/with_headings/index.html", base_path).into(),
293                emit: true,
294            },
295            renderer.render(&mut data, vec![]).await.unwrap().unwrap()
296        );
297    }
298
299    #[tokio::test]
300    async fn test_render() {
301        let base_path_wd = std::env::current_dir().unwrap().display().to_string();
302        let base_path = format!("{}/test_fixtures/example", base_path_wd);
303        let template =
304            Template::new_from_path(format!("{}/templates/default.liquid", base_path).into());
305        let doc_arc = Arc::new(Mutex::new(Document::new_from_path(
306            base_path.clone().into(),
307            format!("{}/content/with_headings.md", base_path).into(),
308        )));
309        let config = Arc::new(WeaverConfig::new(base_path.clone().into()));
310        let renderer = MarkdownRenderer::new(
311            doc_arc.clone(),
312            vec![Arc::new(Mutex::new(template))].into(),
313            config.clone(),
314            vec![],
315        );
316
317        let mut data = LiquidGlobals::new(
318            doc_arc,
319            &Arc::new(HashMap::new()),
320            Arc::new(WeaverConfig::default()),
321        )
322        .await;
323        let result = renderer.render(&mut data, vec![]).await;
324
325        assert_eq!(
326            WritableFile {
327                contents: normalize_line_endings(
328                    br##"<!doctype html>
329<html lang="en">
330	<head>
331		<meta charset="utf-8" />
332
333		<title>test</title>
334		<link rel="icon" href="/static/favicon.ico" />
335		<meta name="viewport" content="width=device-width, initial-scale=1" />
336
337		<meta name="description" content="test"/>
338		<meta name="keywords" content="test"/>
339	</head>
340	<body>
341		<main>
342			<h1>test</h1>
343			<article>
344				<h1><a href="#heading-1" aria-hidden="true" class="anchor" id="heading-1"></a>heading 1</h1>
345<p>I am a paragraph.</p>
346<h2><a href="#heading-2" aria-hidden="true" class="anchor" id="heading-2"></a>heading <span>2</span></h2>
347<p>I'm the second paragraph.</p>
348<h3><a href="#heading-3" aria-hidden="true" class="anchor" id="heading-3"></a>heading 3</h3>
349<h4><a href="#heading-4" aria-hidden="true" class="anchor" id="heading-4"></a>heading 4</h4>
350<h5><a href="#heading-5" aria-hidden="true" class="anchor" id="heading-5"></a>heading 5</h5>
351<h6><a href="#heading-6" aria-hidden="true" class="anchor" id="heading-6"></a>heading 6</h6>
352
353			</article>
354		</main>
355	</body>
356</html>
357"##
358                ),
359                path: format!("{}/site/with_headings/index.html", base_path).into(),
360                emit: true,
361            },
362            result.unwrap().unwrap()
363        );
364    }
365}