1use crate::config::EmbeddedContentErrorPolicy;
3use crate::config::LIB_CFG;
4use crate::error::NoteError;
5#[cfg(feature = "renderer")]
6use crate::highlight::SyntaxPreprocessor;
7#[cfg(feature = "renderer")]
8use crate::html2md::convert_html_to_md;
9use crate::settings::SETTINGS;
10use parse_hyperlinks::renderer::text_links2html;
11use parse_hyperlinks::renderer::text_rawlinks2html;
12#[cfg(feature = "renderer")]
13use pulldown_cmark::{Options, Parser, html};
14#[cfg(feature = "renderer")]
15use rst_parser;
16#[cfg(feature = "renderer")]
17use rst_renderer;
18use serde::{Deserialize, Serialize};
19use std::path::Path;
20#[cfg(feature = "renderer")]
21use std::str::from_utf8;
22
23#[cfg(test)] #[cfg(feature = "renderer")]
27const FILTERED_TAGS: &[&str; 4] = &["<span", "</span>", "<div", "</div>"];
28
29#[non_exhaustive]
32#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
33pub enum InputConverter {
34 ToMarkdown,
36 #[default]
38 Disabled,
39 PassThrough,
41}
42
43impl InputConverter {
44 #[inline]
49 pub(crate) fn build(extension: &str) -> fn(String) -> Result<String, NoteError> {
50 let settings = SETTINGS.read_recursive();
51 let scheme = &LIB_CFG.read_recursive().scheme[settings.current_scheme];
52
53 let mut input_converter = InputConverter::default();
54 for e in &scheme.filename.extensions {
55 if e.0 == *extension {
56 input_converter = e.1;
57 break;
58 }
59 }
60
61 match input_converter {
62 #[cfg(feature = "renderer")]
63 InputConverter::ToMarkdown => |s| convert_html_to_md(&s),
64
65 InputConverter::Disabled => {
66 |_: String| -> Result<String, NoteError> { Err(NoteError::HtmlToMarkupDisabled) }
67 }
68
69 _ => Ok,
70 }
71 }
72
73 #[cfg(test)] #[cfg(feature = "renderer")]
79 fn filter_tags(text: String) -> String {
80 let mut res = String::new();
81 let mut i = 0;
82 while let Some(mut start) = text[i..].find('<') {
83 if let Some(mut end) = text[i + start..].find('>') {
84 end += 1;
85 if let Some(new_start) = text[i + start + 1..i + start + end].rfind('<') {
87 start += new_start + 1;
88 end -= new_start + 1;
89 }
90
91 let filter_tag = FILTERED_TAGS
93 .iter()
94 .any(|&pat| text[i + start..i + start + end].starts_with(pat));
95
96 if filter_tag {
97 res.push_str(&text[i..i + start]);
98 } else {
99 res.push_str(&text[i..i + start + end]);
100 };
101 i = i + start + end;
102 } else {
103 res.push_str(&text[i..i + start + 1]);
104 i = i + start + 1;
105 }
106 }
107 if i > 0 {
108 res.push_str(&text[i..]);
109 if res != text {
110 log::trace!("`html_to_markup` filter: removed tags in \"{}\"", text);
111 }
112 res
113 } else {
114 text
115 }
116 }
117}
118
119#[non_exhaustive]
121#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
122pub enum MarkupLanguage {
123 Markdown,
124 ReStructuredText,
125 Html,
126 PlainText,
127 RendererDisabled,
129 Unkown,
132 #[default]
134 None,
135}
136
137impl MarkupLanguage {
138 pub fn or(self, rhs: Self) -> Self {
140 match self {
141 MarkupLanguage::None => rhs,
142 _ => self,
143 }
144 }
145
146 pub fn mine_type(&self) -> Option<&'static str> {
149 match self {
150 Self::Markdown => Some("text/markodwn"),
151 Self::ReStructuredText => Some("x-rst"),
152 Self::Html => Some("text/html"),
153 Self::PlainText => Some("text/plain"),
154 Self::RendererDisabled => Some("text/plain"),
155 Self::Unkown => Some("text/plain"),
156 _ => None,
157 }
158 }
159
160 pub fn is_some(&self) -> bool {
164 !matches!(self, Self::None)
165 }
166
167 pub fn is_none(&self) -> bool {
171 matches!(self, Self::None)
172 }
173
174 #[cfg_attr(not(feature = "renderer"), allow(unused_variables))]
192 pub fn render(
193 &self,
194 input: &str,
195 error_policy: EmbeddedContentErrorPolicy,
196 ) -> Result<String, NoteError> {
197 match self {
198 #[cfg(feature = "renderer")]
199 Self::Markdown => {
200 let options = Options::all();
204 let parser = Parser::new_ext(input, options);
205 let parser = SyntaxPreprocessor::new(parser, error_policy);
206
207 #[cfg(any(feature = "mermaid", feature = "latex"))]
211 let err_sink = parser.error_sink();
212
213 let mut html_output: String = String::with_capacity(input.len() * 3 / 2);
215 html::push_html(&mut html_output, parser);
216
217 #[cfg(any(feature = "mermaid", feature = "latex"))]
219 if let Some(e) = err_sink.borrow_mut().take() {
220 return Err(e);
221 }
222
223 Ok(html_output)
224 }
225
226 #[cfg(feature = "renderer")]
227 Self::ReStructuredText => {
228 let rest_input = input.trim();
231 let mut html_output: Vec<u8> = Vec::with_capacity(rest_input.len() * 3 / 2);
233 const STANDALONE: bool = false; let doc = rst_parser::parse(rest_input.trim_start())
235 .map_err(|e| NoteError::RstParse { msg: e.to_string() })?;
236 rst_renderer::render_html(&doc, &mut html_output, STANDALONE)
237 .map_err(|e| NoteError::RstParse { msg: e.to_string() })?;
238 Ok(from_utf8(&html_output).unwrap_or_default().to_string())
239 }
240
241 Self::Html => Ok(input.to_string()),
242
243 Self::PlainText | Self::RendererDisabled => Ok(text_links2html(input)),
244
245 Self::Unkown => Ok(text_rawlinks2html(input)),
246
247 _ => Ok(String::new()),
248 }
249 }
250}
251
252impl From<&Path> for MarkupLanguage {
253 #[inline]
257 fn from(path: &Path) -> Self {
258 let file_extension = path
259 .extension()
260 .unwrap_or_default()
261 .to_str()
262 .unwrap_or_default();
263
264 Self::from(file_extension)
265 }
266}
267
268impl From<&str> for MarkupLanguage {
269 #[inline]
271 fn from(file_extension: &str) -> Self {
272 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
273
274 for e in &scheme.filename.extensions {
275 if e.0 == file_extension {
276 return e.2;
277 }
278 }
279
280 MarkupLanguage::None
282 }
283}
284
285#[cfg(test)]
286mod tests {
287
288 use super::InputConverter;
289 use super::MarkupLanguage;
290 #[cfg(any(feature = "mermaid", feature = "latex"))]
291 use crate::config::EmbeddedContentErrorPolicy;
292 use std::path::Path;
293
294 #[test]
295 fn test_markuplanguage_from() {
296 let path = Path::new("/dir/file.md");
298 assert_eq!(MarkupLanguage::from(path), MarkupLanguage::Markdown);
299
300 let path = Path::new("md");
302 assert_eq!(MarkupLanguage::from(path), MarkupLanguage::None);
303 let ext = "/dir/file.md";
305 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::None);
306
307 let ext = "md";
309 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::Markdown);
310
311 let ext = "rst";
313 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::ReStructuredText);
314 }
315
316 #[test]
317 fn test_markuplanguage_render() {
318 let input = "[Link text](https://domain.invalid/)";
320 let expected: &str = "<p><a href=\"https://domain.invalid/\">Link text</a></p>\n";
321
322 let result = MarkupLanguage::Markdown
323 .render(input, Default::default())
324 .unwrap();
325 assert_eq!(result, expected);
326
327 let input = "`Link text <https://domain.invalid/>`_";
329 let expected: &str = "<p><a href=\"https://domain.invalid/\">Link text</a></p>";
330
331 let result = MarkupLanguage::ReStructuredText
332 .render(input, Default::default())
333 .unwrap();
334 assert_eq!(result, expected);
335 }
336
337 #[cfg(feature = "mermaid")]
338 #[test]
339 fn test_mermaid_error_policy() {
340 let input = "```mermaid\nthis is not a valid mermaid diagram !!!\n```";
341
342 let result = MarkupLanguage::Markdown.render(input, EmbeddedContentErrorPolicy::HardError);
344 assert!(result.is_err());
345
346 let result = MarkupLanguage::Markdown
348 .render(input, EmbeddedContentErrorPolicy::Inline)
349 .unwrap();
350 assert!(result.contains("mermaid-error"));
351 }
352
353 #[cfg(feature = "latex")]
354 #[test]
355 fn test_latex_error_policy() {
356 let input = "```math\n\\left( x\n```";
358
359 let result = MarkupLanguage::Markdown.render(input, EmbeddedContentErrorPolicy::HardError);
361 assert!(result.is_err());
362
363 let result = MarkupLanguage::Markdown
365 .render(input, EmbeddedContentErrorPolicy::Inline)
366 .unwrap();
367 assert!(result.contains("math-error"));
368
369 let inline = MarkupLanguage::Markdown
372 .render("text $\\left( x$ more", EmbeddedContentErrorPolicy::Inline)
373 .unwrap();
374 assert!(inline.contains("math-error-inline"));
375 }
376
377 #[cfg(feature = "latex")]
378 #[test]
379 fn test_latex_embedded_marker() {
380 let input = "```math\n\\frac{1}\n```";
383
384 let result = MarkupLanguage::Markdown.render(input, EmbeddedContentErrorPolicy::HardError);
386 assert!(result.is_err());
387
388 let result = MarkupLanguage::Markdown
391 .render(input, EmbeddedContentErrorPolicy::Inline)
392 .unwrap();
393 assert!(result.contains("[PARSE ERROR"));
394 assert!(result.contains("<math"));
395 assert!(result.contains("<mtext class=\"math-parse-error\">"));
396 }
397
398 #[test]
399 fn test_input_converter_md() {
400 let ic = InputConverter::build("md");
401 let input: &str =
402 "<div id=\"videopodcast\">outside <span id=\"pills\">inside</span>\n</div>";
403 let expected: &str = "outside inside";
404
405 let result = ic(input.to_string());
406 assert_eq!(result.unwrap(), expected);
407
408 let input: &str = r#"<p><a href="/my_uri">link</a></p>"#;
410 let expected: &str = "[link](/my_uri)";
411
412 let result = ic(input.to_string());
413 assert_eq!(result.unwrap(), expected);
414
415 let input: &str = r#"<p><a href="/my uri">link</a></p>"#;
418 let expected: &str = "[link](</my uri>)";
419
420 let result = ic(input.to_string());
421 assert_eq!(result.unwrap(), expected);
422
423 let input: &str = r#"<p><a href="/my%20uri">link</a></p>"#;
426 let expected: &str = "[link](</my uri>)";
427
428 let result = ic(input.to_string());
429 assert_eq!(result.unwrap(), expected);
430
431 let input: &str = r#"<p><h1>Title</h1></p>"#;
434 let expected: &str = "# Title";
435
436 let result = ic(input.to_string());
437 assert_eq!(result.unwrap(), expected);
438 }
439
440 #[test]
441 fn test_filter_tags() {
442 let input: &str =
443 "A<div id=\"videopodcast\">out<p>side <span id=\"pills\">inside</span>\n</div>B";
444 let expected: &str = "Aout<p>side inside\nB";
445
446 let result = InputConverter::filter_tags(input.to_string());
447 assert_eq!(result, expected);
448
449 let input: &str = "A<B<C <div>D<E<p>F<>G";
450 let expected: &str = "A<B<C D<E<p>F<>G";
451
452 let result = InputConverter::filter_tags(input.to_string());
453 assert_eq!(result, expected);
454 }
455}
456