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(¬efile, raw.as_bytes()).unwrap();
98 ///
99 /// // Start test
100 /// let mut context = Context::from(¬efile).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(¬efile, raw_error.as_bytes()).unwrap();
209 /// let mut context = Context::from(¬efile);
210 /// let e = NoteError::FrontMatterFieldMissing { field_name: "title".to_string() };
211 ///
212 /// // Start test
213 /// let mut context = Context::from(¬efile).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(¬e_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(¬efile, raw.as_bytes()).unwrap();
277 ///
278 /// // Start test
279 /// let content = ContentString::open(¬efile).unwrap();
280 /// // You can plug in your own type (must impl. `Content`).
281 /// HtmlRenderer::save_exporter_page(
282 /// ¬efile, 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}