harper_core/patterns/
word_set.rs1use super::SingleTokenPattern;
2use smallvec::SmallVec;
3
4use crate::{CharString, Token, char_ext::CharExt};
5
6#[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 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}