switchbot_api/
markdown.rs

1use std::{fmt::Display, sync::LazyLock};
2
3use regex::Regex;
4
5/// Represents a simple Markdown.
6///
7/// # Examples
8/// ```
9/// # use switchbot_api::Markdown;
10/// assert_eq!(Markdown::new("a<br>b").to_string(), "a\nb");
11/// ```
12#[derive(Clone, Debug, Default)]
13pub struct Markdown {
14    markdown: String,
15}
16
17impl Markdown {
18    pub fn new(markdown: &str) -> Self {
19        Self {
20            markdown: markdown.to_string(),
21        }
22    }
23
24    /// The original Markdown.
25    pub fn markdown(&self) -> &str {
26        &self.markdown
27    }
28
29    pub(crate) fn em(text: &str) -> Option<&str> {
30        const RE_EM_PAT: &str = r"\*([^*]+)\*";
31        static RE_EM: LazyLock<Regex> = LazyLock::new(|| Regex::new(RE_EM_PAT).unwrap());
32        if let Some(captures) = RE_EM.captures(text) {
33            return captures.get(1).map(|m| m.as_str());
34        }
35        None
36    }
37
38    fn plain_text(&self) -> String {
39        const RE_BR_PAT: &str = r"(?i)<br\s*/?>";
40        static RE_BR: LazyLock<Regex> = LazyLock::new(|| Regex::new(RE_BR_PAT).unwrap());
41        RE_BR.replace_all(&self.markdown, "\n").into()
42    }
43
44    pub(crate) fn table_columns(line: &str) -> Option<Vec<&str>> {
45        if line.starts_with('|') && line.ends_with('|') {
46            let columns = line
47                .split_terminator('|')
48                .skip(1)
49                .map(|s| s.trim())
50                .collect::<Vec<&str>>();
51            return Some(columns);
52        }
53        None
54    }
55}
56
57impl Display for Markdown {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}", self.plain_text())
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn em() {
69        assert_eq!(Markdown::em("*a*"), Some("a"));
70        assert_eq!(Markdown::em("x*a*x"), Some("a"));
71
72        assert_eq!(Markdown::em("a"), None);
73        assert_eq!(Markdown::em("*a"), None);
74        assert_eq!(Markdown::em("a*"), None);
75
76        assert_eq!(
77            Markdown::em("device type. *Hub*, *Hub Plus*, *Hub Mini*, *Hub 2* or *Hub 3*."),
78            Some("Hub")
79        );
80    }
81
82    fn to_plain_text(markdown: &str) -> String {
83        Markdown::new(markdown).plain_text()
84    }
85
86    #[test]
87    fn plain_text() {
88        assert_eq!(to_plain_text(""), "");
89
90        assert_eq!(to_plain_text("<br>"), "\n");
91        assert_eq!(to_plain_text("<br/>"), "\n");
92        assert_eq!(to_plain_text("<br />"), "\n");
93        assert_eq!(to_plain_text("<BR>"), "\n");
94
95        assert_eq!(to_plain_text("a<br>b"), "a\nb");
96
97        assert_eq!(to_plain_text("a<br>b<br>c"), "a\nb\nc");
98    }
99
100    fn to_table_columns(line: &str) -> Option<Vec<&str>> {
101        Markdown::table_columns(line)
102    }
103
104    #[test]
105    fn table_columns() {
106        assert_eq!(to_table_columns("1|2|3"), None);
107        assert_eq!(to_table_columns("|1|2|3|"), Some(vec!["1", "2", "3"]));
108        assert_eq!(to_table_columns("| 1 | 2 | 3 |"), Some(vec!["1", "2", "3"]));
109    }
110}