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(¬efile, raw.as_bytes()).unwrap();
95 ///
96 /// // Start test
97 /// let mut context = Context::from(¬efile).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(¬efile, raw_error.as_bytes()).unwrap();
206 /// let mut context = Context::from(¬efile);
207 /// let e = NoteError::FrontMatterFieldMissing { field_name: "title".to_string() };
208 ///
209 /// // Start test
210 /// let mut context = Context::from(¬efile).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(¬e_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(¬efile, raw.as_bytes()).unwrap();
273 ///
274 /// // Start test
275 /// let content = ContentString::open(¬efile).unwrap();
276 /// // You can plug in your own type (must impl. `Content`).
277 /// HtmlRenderer::save_exporter_page(
278 /// ¬efile, 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}