Skip to main content

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