summaryrefslogtreecommitdiff
path: root/src/highlighter.rs
blob: 40e618ee5a365a992fd1fc21135dfa0571aaad4f (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
118
119
120
121
122
123
124
125
126
use crate::*;

use std::collections::{HashMap, VecDeque};


pub struct Highlighter {
    pub templates: Vec<Template>,
}

impl Highlighter {
    pub fn from_str(template_set: &str) -> Self {
        let mut variables = HashMap::new();
        let mut templates = Vec::new();

        for line in template_set.lines().map(|l| l.trim()) {
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            let (left, right) = line.split_once('=').expect("Missing '=' character");
            let name = left.trim().to_string();
            let pattern = replace_variables(right.trim(), &variables);
            if name.starts_with('<') && name.ends_with('>') {
                variables.insert(name, pattern);
            } else {
                templates.push(Template::from_str(&pattern, name));
            }
        }
        Self { templates }
    }

    pub fn highlight(&self, text: &str) -> Vec<Span> {
        let mut remaining = &text[..];
        let mut accumulator = String::new();
        let mut spans = Vec::new();

        'outer: while !remaining.is_empty() {
            // Check all templates.
            for template in &self.templates {
                if let Some(captures) = template.expression.captures(remaining).unwrap() {
                    if captures[0].len() > 0 {
                        // Bank the accumulator as an untagged span.
                        if !accumulator.is_empty() {
                            let text = std::mem::take(&mut accumulator);
                            spans.push(Span { tag: String::new(), text });
                        }
                        // Tag each capture group.
                        let mut i = 0;
                        let mut groups = captures.iter().filter_map(|c| c);
                        groups.next();
                        for (tag_i, group) in groups.enumerate() {
                            // Tag the text before this capture group.
                            if group.start() > i {
                                let text = captures[0][i..group.start()].to_string();
                                spans.push(Span { tag: template.tag.clone(), text })
                            }
                            // Tag the text in this capture group.
                            let text = captures[0][group.start()..group.end()].to_string();
                            let tag = match template.subtags.get(tag_i) {
                                Some(tag) => tag.clone(),
                                None => template.tag.clone(),
                            };
                            spans.push(Span { tag, text });
                            i = group.end();
                        }
                        // Tag the remaining text.
                        if captures[0].len() > i {
                            let text = captures[0][i..].to_string();
                            spans.push(Span { tag: template.tag.clone(), text })
                        }
                        // Continue to the next match.
                        remaining = &remaining[captures[0].len()..];
                        continue 'outer;
                    }
                }
            }
            // Pop the first character into accumulator.
            if let Some(c) = remaining.chars().nth(0) {
                remaining = &remaining[c.len_utf8()..];
                accumulator.push(c);
            }
        }
        // Bank the accumulator as an untagged span.
        if !accumulator.is_empty() {
            let text = std::mem::take(&mut accumulator);
            spans.push(Span { tag: String::new(), text });
        }

        // Merge adjacent spans that have the same tag.
        let mut spans = VecDeque::from(spans);
        let mut merged_spans: Vec<Span> = Vec::new();
        while let Some(span) = spans.pop_front() {
            if let Some(last) = merged_spans.last_mut() {
                if span.tag == last.tag {
                    last.text.push_str(&span.text);
                    continue;
                }
            }
            merged_spans.push(span);
        }

        return merged_spans;
    }
}


fn replace_variables(pattern: &str, variables: &HashMap<String, String>) -> String {
    let mut output = String::new();
    let mut chars = pattern.chars();
    while let Some(c) = chars.next() {
        if c == '<' {
            let mut name = String::from('<');
            loop {
                match chars.next() {
                    Some('>') => { name.push('>'); break; }
                    Some(c)   => name.push(c),
                    None      => panic!("Missing '>' character"),
                }
            }
            let pattern = variables.get(&name).expect(&format!("Missing definition for {name:?}"));
            output.push_str(&format!("(?:{pattern})"));
        } else {
            output.push(c);
        }
    }
    return output;
}