Skip to main content

harper_core/patterns/
word_set.rs

1use super::SingleTokenPattern;
2use smallvec::SmallVec;
3
4use crate::{CharString, Token, char_ext::CharExt};
5
6/// A [`super::Pattern`] that matches against any of a set of provided words.
7/// For small sets of short words, it doesn't allocate.
8///
9/// Note that any capitalization of the contained words will result in a match.
10#[derive(Debug, Default, Clone)]
11pub struct WordSet {
12    words: SmallVec<[CharString; 4]>,
13}
14
15impl WordSet {
16    pub fn add(&mut self, word: &str) {
17        let chars = word.chars().collect();
18
19        if !self.words.contains(&chars) {
20            self.words.push(chars);
21        }
22    }
23
24    pub fn add_chars(&mut self, chars: &[char]) {
25        if !self.words.iter().any(|i| i.as_ref() == chars) {
26            self.words.push(chars.into());
27        }
28    }
29
30    pub fn contains(&self, word: &str) -> bool {
31        self.words.contains(&word.chars().collect())
32    }
33
34    /// Create a new word set that matches against any word in the provided list.
35    pub fn new<I, S>(words: I) -> Self
36    where
37        I: IntoIterator<Item = S>,
38        S: AsRef<str>,
39    {
40        let mut set = Self::default();
41
42        for str in words {
43            set.add(str.as_ref());
44        }
45
46        set
47    }
48}
49
50impl<S> FromIterator<S> for WordSet
51where
52    S: AsRef<str>,
53{
54    fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
55        let mut set = Self::default();
56        for str in iter {
57            set.add(str.as_ref());
58        }
59        set
60    }
61}
62
63impl SingleTokenPattern for WordSet {
64    fn matches_token(&self, token: &Token, source: &[char]) -> bool {
65        if !token.kind.is_word() {
66            return false;
67        }
68
69        let tok_chars = token.get_ch(source);
70
71        for word in &self.words {
72            if tok_chars.len() != word.len() {
73                continue;
74            }
75
76            let partial_match = tok_chars
77                .iter()
78                .map(CharExt::normalized)
79                .zip(word.iter().map(CharExt::normalized))
80                .all(|(a, b)| a.eq_ignore_ascii_case(&b));
81
82            if partial_match {
83                return true;
84            }
85        }
86
87        false
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use crate::{Document, Span, patterns::DocPattern};
94
95    use super::WordSet;
96
97    #[test]
98    fn fruit() {
99        let set = WordSet::new(&["banana", "apple", "orange"]);
100
101        let doc = Document::new_markdown_default_curated("I ate a banana and an apple today.");
102
103        let matches = set.find_all_matches_in_doc(&doc);
104
105        assert_eq!(matches, vec![Span::new(6, 7), Span::new(12, 13)]);
106    }
107
108    #[test]
109    fn fruit_whack_capitalization() {
110        let set = WordSet::new(&["banana", "apple", "orange"]);
111
112        let doc = Document::new_markdown_default_curated("I Ate A bAnaNa And aN apPlE today.");
113
114        let matches = set.find_all_matches_in_doc(&doc);
115
116        assert_eq!(matches, vec![Span::new(6, 7), Span::new(12, 13)]);
117    }
118
119    #[test]
120    fn supports_typographic_apostrophes() {
121        let set = WordSet::new(&["They're"]);
122
123        let doc = Document::new_markdown_default_curated("They’re");
124
125        let matches = set.find_all_matches_in_doc(&doc);
126
127        assert_eq!(matches, vec![Span::new(0, 1)]);
128    }
129}