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