tpnote_lib/
markup_language.rs1use crate::config::LIB_CFG;
3use crate::error::NoteError;
4#[cfg(feature = "renderer")]
5use crate::highlight::SyntaxPreprocessor;
6#[cfg(feature = "renderer")]
7use crate::html2md::convert_html_to_md;
8use crate::settings::SETTINGS;
9use parse_hyperlinks::renderer::text_links2html;
10use parse_hyperlinks::renderer::text_rawlinks2html;
11#[cfg(feature = "renderer")]
12use pulldown_cmark::{Options, Parser, html};
13#[cfg(feature = "renderer")]
14use rst_parser;
15#[cfg(feature = "renderer")]
16use rst_renderer;
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19#[cfg(feature = "renderer")]
20use std::str::from_utf8;
21
22#[cfg(test)] #[cfg(feature = "renderer")]
26const FILTERED_TAGS: &[&str; 4] = &["<span", "</span>", "<div", "</div>"];
27
28#[non_exhaustive]
31#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
32pub enum InputConverter {
33 ToMarkdown,
35 #[default]
37 Disabled,
38 PassThrough,
40}
41
42impl InputConverter {
43 #[inline]
48 pub(crate) fn build(extension: &str) -> fn(String) -> Result<String, NoteError> {
49 let settings = SETTINGS.read_recursive();
50 let scheme = &LIB_CFG.read_recursive().scheme[settings.current_scheme];
51
52 let mut input_converter = InputConverter::default();
53 for e in &scheme.filename.extensions {
54 if e.0 == *extension {
55 input_converter = e.1;
56 break;
57 }
58 }
59
60 match input_converter {
61 #[cfg(feature = "renderer")]
62 InputConverter::ToMarkdown => |s| convert_html_to_md(&s),
63
64 InputConverter::Disabled => {
65 |_: String| -> Result<String, NoteError> { Err(NoteError::HtmlToMarkupDisabled) }
66 }
67
68 _ => Ok,
69 }
70 }
71
72 #[cfg(test)] #[cfg(feature = "renderer")]
78 fn filter_tags(text: String) -> String {
79 let mut res = String::new();
80 let mut i = 0;
81 while let Some(mut start) = text[i..].find('<') {
82 if let Some(mut end) = text[i + start..].find('>') {
83 end += 1;
84 if let Some(new_start) = text[i + start + 1..i + start + end].rfind('<') {
86 start += new_start + 1;
87 end -= new_start + 1;
88 }
89
90 let filter_tag = FILTERED_TAGS
92 .iter()
93 .any(|&pat| text[i + start..i + start + end].starts_with(pat));
94
95 if filter_tag {
96 res.push_str(&text[i..i + start]);
97 } else {
98 res.push_str(&text[i..i + start + end]);
99 };
100 i = i + start + end;
101 } else {
102 res.push_str(&text[i..i + start + 1]);
103 i = i + start + 1;
104 }
105 }
106 if i > 0 {
107 res.push_str(&text[i..]);
108 if res != text {
109 log::trace!("`html_to_markup` filter: removed tags in \"{}\"", text);
110 }
111 res
112 } else {
113 text
114 }
115 }
116}
117
118#[non_exhaustive]
120#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
121pub enum MarkupLanguage {
122 Markdown,
123 ReStructuredText,
124 Html,
125 PlainText,
126 RendererDisabled,
128 Unkown,
131 #[default]
133 None,
134}
135
136impl MarkupLanguage {
137 pub fn or(self, rhs: Self) -> Self {
139 match self {
140 MarkupLanguage::None => rhs,
141 _ => self,
142 }
143 }
144
145 pub fn mine_type(&self) -> Option<&'static str> {
148 match self {
149 Self::Markdown => Some("text/markodwn"),
150 Self::ReStructuredText => Some("x-rst"),
151 Self::Html => Some("text/html"),
152 Self::PlainText => Some("text/plain"),
153 Self::RendererDisabled => Some("text/plain"),
154 Self::Unkown => Some("text/plain"),
155 _ => None,
156 }
157 }
158
159 pub fn is_some(&self) -> bool {
163 !matches!(self, Self::None)
164 }
165
166 pub fn is_none(&self) -> bool {
170 matches!(self, Self::None)
171 }
172
173 pub fn render(&self, input: &str) -> Result<String, NoteError> {
187 match self {
188 #[cfg(feature = "renderer")]
189 Self::Markdown => {
190 let options = Options::all();
194 let parser = Parser::new_ext(input, options);
195 let parser = SyntaxPreprocessor::new(parser);
196
197 let mut html_output: String = String::with_capacity(input.len() * 3 / 2);
199 html::push_html(&mut html_output, parser);
200 Ok(html_output)
201 }
202
203 #[cfg(feature = "renderer")]
204 Self::ReStructuredText => {
205 let rest_input = input.trim();
208 let mut html_output: Vec<u8> = Vec::with_capacity(rest_input.len() * 3 / 2);
210 const STANDALONE: bool = false; let doc = rst_parser::parse(rest_input.trim_start())
212 .map_err(|e| NoteError::RstParse { msg: e.to_string() })?;
213 rst_renderer::render_html(&doc, &mut html_output, STANDALONE)
214 .map_err(|e| NoteError::RstParse { msg: e.to_string() })?;
215 Ok(from_utf8(&html_output).unwrap_or_default().to_string())
216 }
217
218 Self::Html => Ok(input.to_string()),
219
220 Self::PlainText | Self::RendererDisabled => Ok(text_links2html(input)),
221
222 Self::Unkown => Ok(text_rawlinks2html(input)),
223
224 _ => Ok(String::new()),
225 }
226 }
227}
228
229impl From<&Path> for MarkupLanguage {
230 #[inline]
234 fn from(path: &Path) -> Self {
235 let file_extension = path
236 .extension()
237 .unwrap_or_default()
238 .to_str()
239 .unwrap_or_default();
240
241 Self::from(file_extension)
242 }
243}
244
245impl From<&str> for MarkupLanguage {
246 #[inline]
248 fn from(file_extension: &str) -> Self {
249 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
250
251 for e in &scheme.filename.extensions {
252 if e.0 == file_extension {
253 return e.2;
254 }
255 }
256
257 MarkupLanguage::None
259 }
260}
261
262#[cfg(test)]
263mod tests {
264
265 use super::InputConverter;
266 use super::MarkupLanguage;
267 use std::path::Path;
268
269 #[test]
270 fn test_markuplanguage_from() {
271 let path = Path::new("/dir/file.md");
273 assert_eq!(MarkupLanguage::from(path), MarkupLanguage::Markdown);
274
275 let path = Path::new("md");
277 assert_eq!(MarkupLanguage::from(path), MarkupLanguage::None);
278 let ext = "/dir/file.md";
280 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::None);
281
282 let ext = "md";
284 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::Markdown);
285
286 let ext = "rst";
288 assert_eq!(MarkupLanguage::from(ext), MarkupLanguage::ReStructuredText);
289 }
290
291 #[test]
292 fn test_markuplanguage_render() {
293 let input = "[Link text](https://domain.invalid/)";
295 let expected: &str = "<p><a href=\"https://domain.invalid/\">Link text</a></p>\n";
296
297 let result = MarkupLanguage::Markdown.render(input).unwrap();
298 assert_eq!(result, expected);
299
300 let input = "`Link text <https://domain.invalid/>`_";
302 let expected: &str = "<p><a href=\"https://domain.invalid/\">Link text</a></p>";
303
304 let result = MarkupLanguage::ReStructuredText.render(input).unwrap();
305 assert_eq!(result, expected);
306 }
307
308 #[test]
309 fn test_input_converter_md() {
310 let ic = InputConverter::build("md");
311 let input: &str =
312 "<div id=\"videopodcast\">outside <span id=\"pills\">inside</span>\n</div>";
313 let expected: &str = "outside inside";
314
315 let result = ic(input.to_string());
316 assert_eq!(result.unwrap(), expected);
317
318 let input: &str = r#"<p><a href="/my_uri">link</a></p>"#;
320 let expected: &str = "[link](/my_uri)";
321
322 let result = ic(input.to_string());
323 assert_eq!(result.unwrap(), expected);
324
325 let input: &str = r#"<p><a href="/my uri">link</a></p>"#;
328 let expected: &str = "[link](</my uri>)";
329
330 let result = ic(input.to_string());
331 assert_eq!(result.unwrap(), expected);
332
333 let input: &str = r#"<p><a href="/my%20uri">link</a></p>"#;
336 let expected: &str = "[link](</my uri>)";
337
338 let result = ic(input.to_string());
339 assert_eq!(result.unwrap(), expected);
340
341 let input: &str = r#"<p><h1>Title</h1></p>"#;
344 let expected: &str = "# Title";
345
346 let result = ic(input.to_string());
347 assert_eq!(result.unwrap(), expected);
348 }
349
350 #[test]
351 fn test_filter_tags() {
352 let input: &str =
353 "A<div id=\"videopodcast\">out<p>side <span id=\"pills\">inside</span>\n</div>B";
354 let expected: &str = "Aout<p>side inside\nB";
355
356 let result = InputConverter::filter_tags(input.to_string());
357 assert_eq!(result, expected);
358
359 let input: &str = "A<B<C <div>D<E<p>F<>G";
360 let expected: &str = "A<B<C D<E<p>F<>G";
361
362 let result = InputConverter::filter_tags(input.to_string());
363 assert_eq!(result, expected);
364 }
365}
366