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