tpnote_lib/html_renderer.rs
1//! Tp-Note's high level HTML rendering API.
2//!
3//! A set of functions that take a `Context` type and a `Content` type (or raw
4//! text) and return the HTML rendition of the content. The API is completely
5//! stateless. All functions read the `LIB_CFG` global variable to read the
6//! configuration stored in `LibCfg.tmpl_html`.
7
8use crate::config::LIB_CFG;
9use crate::config::LocalLinkKind;
10use crate::content::Content;
11use crate::context::Context;
12use crate::context::HasSettings;
13use crate::error::NoteError;
14#[cfg(feature = "viewer")]
15use crate::filter::TERA;
16use crate::html::HTML_EXT;
17use crate::html::rewrite_links;
18use crate::note::Note;
19#[cfg(feature = "viewer")]
20use crate::note::ONE_OFF_TEMPLATE_NAME;
21#[cfg(feature = "viewer")]
22use crate::note_error_tera_template;
23use crate::template::TemplateKind;
24use parking_lot::RwLock;
25use std::collections::HashSet;
26use std::fs::OpenOptions;
27use std::io;
28use std::io::Write;
29use std::path::Path;
30use std::path::PathBuf;
31use std::sync::Arc;
32#[cfg(feature = "viewer")]
33use tera::Tera;
34
35/// High level API to render a note providing its `content` and some `context`.
36pub struct HtmlRenderer;
37
38impl HtmlRenderer {
39 /// Returns the HTML rendition of a `ContentString`.
40 ///
41 /// The markup to HTML rendition engine is determined by the file extension
42 /// of the variable `context.path`. The resulting HTML and other HTML
43 /// template variables originating from `context` are inserted into the
44 /// `TMPL_HTML_VIEWER` template before being returned.
45 /// The string `viewer_doc_js` contains JavaScript live update code that
46 /// will be injected into the HTML page via the
47 /// `TMPL_HTML_VAR_DOC_VIEWER_JS` template variable.
48 /// This function is stateless.
49 ///
50 /// ```rust
51 /// use tpnote_lib::content::Content;
52 /// use tpnote_lib::content::ContentString;
53 /// use tpnote_lib::context::Context;
54 /// use tpnote_lib::html_renderer::HtmlRenderer;
55 /// use std::env::temp_dir;
56 /// use std::fs;
57 /// use std::path::Path;
58 ///
59 /// // Prepare test: create existing note file.
60 /// let content = ContentString::from_string(String::from(r#"---
61 /// title: My day
62 /// subtitle: Note
63 /// ---
64 /// Body text
65 /// "#), "doc".to_string());
66 ///
67 /// // Start test
68 /// let mut context = Context::from(Path::new("/path/to/note.md")).unwrap();
69 /// // We do not inject any JavaScript.
70 /// // Render.
71 /// let html = HtmlRenderer::viewer_page::<ContentString>(context, content, "")
72 /// .unwrap();
73 /// // Check the HTML rendition.
74 /// assert!(html.starts_with("<!DOCTYPE html>\n<html"))
75 /// ```
76 ///
77 /// A more elaborated example that reads from disk:
78 ///
79 /// ```rust
80 /// use tpnote_lib::config::LIB_CFG;
81 /// use tpnote_lib::content::Content;
82 /// use tpnote_lib::content::ContentString;
83 /// use tpnote_lib::context::Context;
84 /// use tpnote_lib::html_renderer::HtmlRenderer;
85 /// use std::env::temp_dir;
86 /// use std::fs;
87 ///
88 /// // Prepare test: create existing note file.
89 /// let raw = r#"---
90 /// title: My day2
91 /// subtitle: Note
92 /// ---
93 /// Body text
94 /// "#;
95 /// let notefile = temp_dir().join("20221030-My day2--Note.md");
96 /// fs::write(¬efile, raw.as_bytes()).unwrap();
97 ///
98 /// // Start test
99 /// let mut context = Context::from(¬efile).unwrap();
100 /// // We do not inject any JavaScript.
101 /// // Render.
102 /// let content = ContentString::open(context.get_path()).unwrap();
103 /// // You can plug in your own type (must impl. `Content`).
104 /// let html = HtmlRenderer::viewer_page(context, content, "").unwrap();
105 /// // Check the HTML rendition.
106 /// assert!(html.starts_with("<!DOCTYPE html>\n<html"))
107 /// ```
108 pub fn viewer_page<T: Content>(
109 context: Context<HasSettings>,
110 content: T,
111 // Java Script live updater inject code. Will be inserted into
112 // `tmpl_html.viewer`.
113 viewer_doc_js: &str,
114 ) -> Result<String, NoteError> {
115 let tmpl_html = &LIB_CFG.read_recursive().tmpl_html.viewer;
116 HtmlRenderer::render(context, content, viewer_doc_js, tmpl_html)
117 }
118
119 /// Returns the HTML rendition of a `ContentString`.
120 /// The markup to HTML rendition engine is determined by the file extension
121 /// of the variable `context.path`. The resulting HTML and other HTML
122 /// template variables originating from `context` are inserted into the
123 /// `TMPL_HTML_EXPORTER` template before being returned.
124 /// `context` is expected to have at least all `HasSettings` keys
125 /// and the additional key `TMPL_HTML_VAR_VIEWER_DOC_JS` set and valid.
126 /// All other keys are ignored.
127 /// This function is stateless.
128 ///
129 /// ```rust
130 /// use tpnote_lib::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
131 /// use tpnote_lib::content::Content;
132 /// use tpnote_lib::content::ContentString;
133 /// use tpnote_lib::context::Context;
134 /// use tpnote_lib::html_renderer::HtmlRenderer;
135 /// use std::env::temp_dir;
136 /// use std::fs;
137 /// use std::path::Path;
138 ///
139 /// // Prepare test: create existing note file.
140 /// let content= ContentString::from_string(String::from(r#"---
141 /// title: "My day"
142 /// subtitle: "Note"
143 /// ---
144 /// Body text
145 /// "#), "doc".to_string());
146 ///
147 /// // Start test
148 /// let mut context = Context::from(Path::new("/path/to/note.md")).unwrap();
149 /// // Render.
150 /// let html = HtmlRenderer::exporter_page::<ContentString>(context, content)
151 /// .unwrap();
152 /// // Check the HTML rendition.
153 /// assert!(html.starts_with("<!DOCTYPE html>\n<html"))
154 /// ```
155 pub fn exporter_page<T: Content>(
156 context: Context<HasSettings>,
157 content: T,
158 ) -> Result<String, NoteError> {
159 let tmpl_html = &LIB_CFG.read_recursive().tmpl_html.exporter;
160 HtmlRenderer::render(context, content, "", tmpl_html)
161 }
162
163 /// Helper function.
164 fn render<T: Content>(
165 context: Context<HasSettings>,
166 content: T,
167 viewer_doc_js: &str,
168 tmpl_html: &str,
169 ) -> Result<String, NoteError> {
170 let note = Note::from_existing_content(context, content, TemplateKind::None)?;
171
172 note.render_content_to_html(tmpl_html, viewer_doc_js)
173 }
174
175 /// When the header cannot be deserialized, the file located in
176 /// `context.path` is rendered as "Error HTML page".
177 ///
178 /// The erroneous content is rendered to html with
179 /// `parse_hyperlinks::renderer::text_rawlinks2html` and inserted in
180 /// the `TMPL_HTML_VIEWER_ERROR` template (which can be configured at
181 /// runtime).
182 /// The string `viewer_doc_js` contains JavaScript live update code that
183 /// will be injected into the HTML page via the
184 /// `TMPL_HTML_VAR_DOC_VIEWER_JS` template variable.
185 /// This function is stateless.
186 ///
187 /// ```rust
188 /// use tpnote_lib::config::LIB_CFG;
189 /// use tpnote_lib::config::TMPL_HTML_VAR_DOC_ERROR;
190 /// use tpnote_lib::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
191 /// use tpnote_lib::content::Content;
192 /// use tpnote_lib::content::ContentString;
193 /// use tpnote_lib::context::Context;
194 /// use tpnote_lib::error::NoteError;
195 /// use tpnote_lib::html_renderer::HtmlRenderer;
196 /// use std::env::temp_dir;
197 /// use std::fs;
198 ///
199 /// // Prepare test: create existing erroneous note file.
200 /// let raw_error = r#"---
201 /// title: "My day3"
202 /// subtitle: "Note"
203 /// --
204 /// Body text
205 /// "#;
206 /// let notefile = temp_dir().join("20221030-My day3--Note.md");
207 /// fs::write(¬efile, raw_error.as_bytes()).unwrap();
208 /// let mut context = Context::from(¬efile);
209 /// let e = NoteError::FrontMatterFieldMissing { field_name: "title".to_string() };
210 ///
211 /// // Start test
212 /// let mut context = Context::from(¬efile).unwrap();
213 /// // We do not inject any JavaScript.
214 /// // Render.
215 /// // Read from file.
216 /// // You can plug in your own type (must impl. `Content`).
217 /// let content = ContentString::open(context.get_path()).unwrap();
218 /// let html = HtmlRenderer::error_page(
219 /// context, content, &e.to_string(), "").unwrap();
220 /// // Check the HTML rendition.
221 /// assert!(html.starts_with("<!DOCTYPE html>\n<html"))
222 /// ```
223 #[cfg(feature = "viewer")]
224 pub fn error_page<T: Content>(
225 context: Context<HasSettings>,
226 note_erroneous_content: T,
227 error_message: &str,
228 // Java Script live updater inject code. Will be inserted into
229 // `tmpl_html.viewer`.
230 viewer_doc_js: &str,
231 ) -> Result<String, NoteError> {
232 //
233 let context =
234 context.insert_error_content(¬e_erroneous_content, error_message, viewer_doc_js);
235
236 let tmpl_html = &LIB_CFG.read_recursive().tmpl_html.viewer_error;
237
238 // Apply template.
239 let mut tera = Tera::default();
240 // Switch `autoescape_on()` only for HTML templates.
241 tera.autoescape_on(vec![ONE_OFF_TEMPLATE_NAME]);
242 tera.extend(&TERA)?;
243 let html = tera
244 .render_str(tmpl_html, &context)
245 .map_err(|e| note_error_tera_template!(e, "[html_tmpl] viewer_error".to_string()))?;
246 Ok(html)
247 }
248
249 /// Renders `doc_path` with `content` into HTML and saves the result in
250 /// `export_dir` in case `export_dir` is an absolute directory. Otherwise
251 /// the parent directory of `doc_path` is concatenated with `export_dir`
252 /// and the result is stored there.
253 /// `-` dumps the rendition to STDOUT. The filename of the HTML rendition is
254 /// the same as in `doc_path` but with `.html` appended.
255 ///
256 /// ```rust
257 /// use tpnote_lib::config::LIB_CFG;
258 /// use tpnote_lib::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
259 /// use tpnote_lib::config::LocalLinkKind;
260 /// use tpnote_lib::content::Content;
261 /// use tpnote_lib::content::ContentString;
262 /// use tpnote_lib::context::Context;
263 /// use tpnote_lib::html_renderer::HtmlRenderer;
264 /// use std::env::temp_dir;
265 /// use std::fs;
266 /// use std::path::Path;
267 ///
268 /// // Prepare test: create existing note file.
269 /// let raw = r#"---
270 /// title: "My day3"
271 /// subtitle: "Note"
272 /// ---
273 /// Body text
274 /// "#;
275 /// let notefile = temp_dir().join("20221030-My day3--Note.md");
276 /// fs::write(¬efile, raw.as_bytes()).unwrap();
277 ///
278 /// // Start test
279 /// let content = ContentString::open(¬efile).unwrap();
280 /// // You can plug in your own type (must impl. `Content`).
281 /// HtmlRenderer::save_exporter_page(
282 /// ¬efile, content, Path::new("."), LocalLinkKind::Long).unwrap();
283 /// // Check the HTML rendition.
284 /// let expected_file = temp_dir().join("20221030-My day3--Note.md.html");
285 /// let html = fs::read_to_string(expected_file).unwrap();
286 /// assert!(html.starts_with("<!DOCTYPE html>\n<html"))
287 /// ```
288 pub fn save_exporter_page<T: Content>(
289 doc_path: &Path,
290 content: T,
291 export_dir: &Path,
292 local_link_kind: LocalLinkKind,
293 ) -> Result<(), NoteError> {
294 let context = Context::from(doc_path)?;
295
296 let doc_path = context.get_path();
297 let doc_dir = context.get_dir_path().to_owned();
298
299 // Determine filename of html-file.
300 let html_path = match export_dir {
301 p if p == Path::new("-") => PathBuf::new(),
302 p => {
303 let mut html_filename = doc_path
304 .file_name()
305 .unwrap_or_default()
306 .to_str()
307 .unwrap_or_default()
308 .to_string();
309 html_filename.push_str(HTML_EXT);
310 let mut q = doc_path.parent().unwrap_or(Path::new("")).to_path_buf();
311 q.push(p);
312 q.push(PathBuf::from(html_filename));
313 q
314 }
315 };
316
317 if html_path == Path::new("") {
318 log::debug!("Rendering HTML to STDOUT (`{:?}`)", export_dir);
319 } else {
320 log::debug!("Rendering HTML into: {:?}", html_path);
321 };
322
323 // These must live longer than `writeable`, and thus are declared first:
324 let (mut stdout_write, mut file_write);
325 // We need to ascribe the type to get dynamic dispatch.
326 let writeable: &mut dyn Write = if html_path == Path::new("") {
327 stdout_write = io::stdout();
328 &mut stdout_write
329 } else {
330 file_write = OpenOptions::new()
331 .write(true)
332 .create(true)
333 .truncate(true)
334 .open(&html_path)?;
335 &mut file_write
336 };
337
338 // Render HTML.
339 let root_path = context.get_root_path().to_owned();
340 let html = Self::exporter_page(context, content)?;
341 let html = rewrite_links(
342 html,
343 &root_path,
344 &doc_dir,
345 local_link_kind,
346 // Do append `.html` to `.md` in links.
347 true,
348 Arc::new(RwLock::new(HashSet::new())),
349 );
350
351 // Write HTML rendition.
352 writeable.write_all(html.as_bytes())?;
353 Ok(())
354 }
355}