summaryrefslogtreecommitdiff
path: root/src/elements/line.rs
blob: d5c078e477a3a8dfdc30a6c5e16b9d7281c374b0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use crate::*;

macro_rules! opt {
    ($v:expr) => {|s| Some($v(s)) };
}

pub struct Line {
    pub elements: Vec<LineElement>,
}

impl Line {
    pub fn from_str(raw_string: &str) -> Self {
        fn unlabeled_extern_link(target: String) -> Option<LineElement> {
            target.contains("/").then( ||
                LineElement::ExternalLink(ExternalLink { target, label:String::new() })
            )
        }
        fn labelled_extern_link(s: String) -> Option<LineElement> {
            let (label, target) = match s.split_once("](") {
                Some((l, t)) => (l.to_string(), t.to_string()),
                None => return None };
            if label.contains("]") || target.contains("]") { return None }
            Some(LineElement::ExternalLink(ExternalLink { label, target })) }
        const DELIMITERS: [(fn(String)->Option<LineElement>, &str, &str, &str); 7] = [
            ( opt!(LineElement::Bold),          "**", "**", "*" ),
            ( opt!(LineElement::Italic),        "_",  "_",  "_" ),
            ( opt!(LineElement::Monospace),     "`",  "`",  "`" ),
            ( opt!(LineElement::Math),          "$",  "$",  "$" ),
            ( opt!(LineElement::InternalLink),  "[[", "]]", "[]" ),
            ( labelled_extern_link,             "[",  ")",  "[]()" ),
            ( unlabeled_extern_link,            "[",  "]",  "[]" ),
        ];
        let chars: Vec<char> = raw_string.chars().collect();
        let mut elements = Vec::new();
        let mut cached_chars = String::new();
        let mut i = 0;

        let starts_with = |i, p:&str| std::iter::zip(&chars[i..], p.chars()).all(|(a, b)| *a == b);

        'outer: while let Some(c) = chars.get(i) {
            // Only check for opening delimiters that directly follow a whitespace character.
            let follows_whitespace = match chars.get(i.wrapping_sub(1)) {
                Some(w) => is_whitespace(w),
                None => true,
            };
            if follows_whitespace {
                // Try to parse an opening delimiter.
                for (variant, start_delim, end_delim, delim_chars) in DELIMITERS {
                    // Try to match an opening delimiter with a terminating delimiter.
                    if starts_with(i, start_delim) {
                        let s_end = i + start_delim.chars().count();
                        let mut e_start = s_end;
                        let mut e_end = e_start + end_delim.chars().count();
                        while e_end <= chars.len() {
                            e_start += 1; e_end += 1;
                            let end_is_whitespace =
                                if let Some(end_char) = chars.get(e_end) {
                                    is_whitespace(end_char)
                                } else {
                                    e_end == chars.len()
                                };
                            // If the terminating delimiter is found, store the normal
                            // text and the styled text, and continue to the next character.
                            if end_is_whitespace && starts_with(e_start, end_delim) {
                                // Check that there is content within the styled string.
                                let styled_string: String = chars[s_end..e_start].iter().collect();
                                let non_content_chars: Vec<_> = delim_chars.chars().collect();
                                if !is_contentful(&styled_string, &non_content_chars) { continue }
                                if styled_string.len() != styled_string.trim().len() { continue }
                                let line_element = match variant(styled_string) {
                                    Some(e) => e,
                                    None => continue,
                                };
                                // Commit the normal and styled strings.
                                if !cached_chars.is_empty() {
                                    let normal_string = std::mem::take(&mut cached_chars);
                                    elements.push(LineElement::Normal(normal_string)); }
                                elements.push(line_element);
                                i = e_end;
                                continue 'outer;
                            }
                        }
                    }
                }
            }
            cached_chars.push(*c); i += 1;
        }
        if !cached_chars.is_empty() {
            let normal_string = std::mem::take(&mut cached_chars);
            elements.push(LineElement::Normal(normal_string)); }
        Self { elements }
    }

    /// Return only the character content, with none of the styling information.
    pub fn as_plain_text(&self) -> String {
        let mut string = String::new();
        for line_element in &self.elements {
            string.push_str(line_element.as_plain_text()) }
        return string;
    }
}

impl std::fmt::Display for Line {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        for line_element in &self.elements {
            write!(f, "{line_element}")?; }
        Ok(())
    }
}

impl std::fmt::Debug for Line {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        for line_element in &self.elements {
            write!(f, "{line_element:?}\n")?; }
        Ok(())
    }
}