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