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