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