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#[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}