concisemark/
utils.rs

1use std::{
2    fs::OpenOptions,
3    io::{Read, Write},
4    path::{Path, PathBuf},
5};
6
7pub fn escape_html_double_quote(text: &str) -> String {
8    text.chars()
9        .map(|x| {
10            if x == '"' {
11                """.to_string()
12            } else {
13                x.to_string()
14            }
15        })
16        .collect::<String>()
17}
18
19pub fn escape_to_html(text: &str) -> String {
20    let mut html = String::new();
21    for ch in text.chars() {
22        match ch {
23            '&' => {
24                html.push_str("&amp;");
25            }
26            '>' => {
27                html.push_str("&gt;");
28            }
29            '<' => {
30                html.push_str("&lt;");
31            }
32            _ => {
33                html.push(ch);
34            }
35        }
36    }
37    html
38}
39
40pub fn escape_to_tex(text: &str) -> String {
41    let mut content = String::new();
42    for ch in text.chars() {
43        match ch {
44            '$' => {
45                content.push_str(r#"\$"#);
46            }
47            '%' => {
48                content.push_str(r#"\%"#);
49            }
50            '`' => content.push_str(r#"\verb|`|"#),
51            '\\' => content.push_str(r#"\textbackslash"#),
52            _ => {
53                content.push(ch);
54            }
55        }
56    }
57    content
58}
59
60/// Download image from `url` and save it into directory `dir` with name `name`,
61/// the image suffix is guessed from its content type.
62pub fn download_image_fs<S1, S2, P>(
63    url: S1,
64    dir: P,
65    name: S2,
66) -> Option<PathBuf>
67where
68    S1: AsRef<str>,
69    S2: AsRef<str>,
70    P: AsRef<Path>,
71{
72    let url = url.as_ref();
73    let dir = dir.as_ref();
74    let name = name.as_ref();
75    if let Some((content_type, data)) = download_image(url) {
76        let suffix = match content_type.as_str() {
77            "image/png" => "png",
78            "image/jpeg" => "jpg",
79            "image/svg+xml" => "svg",
80            _ => ".unknwon",
81        };
82        // TODO: add more name safety checking
83        let name = name
84            .replace("%", "_")
85            .replace("/", "_")
86            .replace("\\", "_")
87            .replace(".", "_");
88        let output_path = dir.join(format!("{name}.{suffix}"));
89        let mut f = OpenOptions::new()
90            .truncate(true)
91            .write(true)
92            .create(true)
93            .open(&output_path)
94            .ok()?;
95        f.write_all(&data[..]).ok()?;
96        Some(output_path)
97    } else {
98        None
99    }
100}
101
102/// Download image from url and return its content type and data if success
103pub fn download_image<S: AsRef<str>>(url: S) -> Option<(String, Vec<u8>)> {
104    let url = url.as_ref();
105    let mut data: Vec<u8> = vec![];
106    match ureq::get(url).call() {
107        Ok(resp) => {
108            let content_type = resp.content_type().to_owned();
109            // max size is limited to 10MB
110            if let Err(e) =
111                resp.into_reader().take(10_000_000).read_to_end(&mut data)
112            {
113                // TODO: better error handling
114                log::error!("failed to read media data into buffer: {e:?}");
115            }
116            Some((content_type, data))
117        }
118        Err(e) => {
119            println!("error: {e:?} ==> {url}");
120            // TODO: better error handling
121            log::error!("failed to download media {} with error {e:?}", url);
122            None
123        }
124    }
125}
126
127/// split content into lines, find the common indent and remove them
128pub fn remove_indent<S: AsRef<str>>(content: S) -> String {
129    let content = content.as_ref();
130    if !content.contains("\n") {
131        return content.trim_start().to_string();
132    }
133
134    let mut indent = content.len();
135    for line in content
136        .split_inclusive("\n")
137        .filter(|line| !line.trim().is_empty())
138    {
139        let current_indent = line.len() - line.trim_start().len();
140        if current_indent < indent {
141            indent = current_indent;
142        }
143    }
144    let content = content
145        .split_inclusive("\n")
146        .map(|line| {
147            if !line.trim().is_empty() {
148                &line[indent..]
149            } else {
150                line
151            }
152        })
153        .collect::<Vec<&str>>();
154    content.join("").to_string()
155}