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(&notefile, raw.as_bytes()).unwrap();
97    ///
98    /// // Start test
99    /// let mut context = Context::from(&notefile).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(&notefile, raw_error.as_bytes()).unwrap();
208    /// let mut context = Context::from(&notefile);
209    /// let e = NoteError::FrontMatterFieldMissing { field_name: "title".to_string() };
210    ///
211    /// // Start test
212    /// let mut context = Context::from(&notefile).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(&note_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(&notefile, raw.as_bytes()).unwrap();
277    ///
278    /// // Start test
279    /// let content = ContentString::open(&notefile).unwrap();
280    /// // You can plug in your own type (must impl. `Content`).
281    /// HtmlRenderer::save_exporter_page(
282    ///        &notefile, 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}