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