Skip to main content

harper_core/expr/
sequence_expr.rs

1use paste::paste;
2
3use crate::{
4    CharStringExt, Lrc, Span, Token, TokenKind,
5    expr::{AsBoxedExpr, FirstMatchOf, FixedPhrase, LongestMatchOf},
6    patterns::{AnyPattern, IndefiniteArticle, RelativePronoun, WhitespacePattern, Word, WordSet},
7};
8
9use super::{Expr, Optional, OwnedExprExt, Repeating, Step, UnlessStep};
10
11#[derive(Default)]
12pub struct SequenceExpr {
13    exprs: Vec<Box<dyn Expr>>,
14}
15
16/// Generate a `then_*` method from an available `is_*` function on [`TokenKind`].
17macro_rules! gen_then_from_is {
18    ($quality:ident) => {
19        paste! {
20            #[doc = concat!("Adds a step matching a token where [`TokenKind::is_", stringify!($quality), "()`] returns true.")]
21            pub fn [< then_$quality >] (self) -> Self{
22                self.then_kind_where(|kind| {
23                    kind.[< is_$quality >]()
24                })
25            }
26
27            #[doc = concat!("Adds an optional step matching a token where [`TokenKind::is_", stringify!($quality), "()`] returns true.")]
28            pub fn [< then_optional_$quality >] (self) -> Self{
29                self.then_optional(|tok: &Token, _source: &[char]| {
30                    tok.kind.[< is_$quality >]()
31                })
32            }
33
34            #[doc = concat!("Adds a step matching one or more consecutive tokens where [`TokenKind::is_", stringify!($quality), "()`] returns true.")]
35            pub fn [< then_one_or_more_$quality s >] (self) -> Self{
36                self.then_one_or_more(Box::new(|tok: &Token, _source: &[char]| {
37                    tok.kind.[< is_$quality >]()
38                }))
39            }
40
41            #[doc = concat!("Adds a step matching a token where [`TokenKind::is_", stringify!($quality), "()`] returns false.")]
42            pub fn [< then_anything_but_$quality >] (self) -> Self{
43                self.then_kind_where(|kind| {
44                    !kind.[< is_$quality >]()
45                })
46            }
47        }
48    };
49}
50
51impl Expr for SequenceExpr {
52    /// Run the expression starting at an index, returning the total matched window.
53    ///
54    /// If any step returns `None`, the entire expression does as well.
55    fn run(&self, mut cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
56        let mut window = Span::empty(cursor);
57
58        for cur_expr in &self.exprs {
59            let out = cur_expr.run(cursor, tokens, source)?;
60
61            // Zero-width assertions (like AnchorEnd) validate position without consuming tokens
62            // They should not expand the window or advance the cursor
63            let is_zero_width = out.end == out.start;
64
65            if !is_zero_width {
66                // Only expand the window if the match actually covers some tokens
67                if out.end > out.start {
68                    window.expand_to_include(out.start);
69                    window.expand_to_include(out.end.checked_sub(1).unwrap_or(out.start));
70                }
71
72                // Only advance cursor if we actually matched something
73                if out.end > cursor {
74                    cursor = out.end;
75                } else if out.start < cursor {
76                    cursor = out.start;
77                }
78            }
79            // If zero-width, don't expand window or advance cursor - just validate position
80        }
81
82        Some(window)
83    }
84}
85
86impl SequenceExpr {
87    // Constructor methods
88
89    // Match an [expression](Expr).
90    pub fn with(expr: impl Expr + 'static) -> Self {
91        Self::default().then(expr)
92    }
93
94    // Single token methods
95
96    /// Construct a new sequence with an [`AnyPattern`] at the beginning of the operation list.
97    pub fn anything() -> Self {
98        Self::default().then_anything()
99    }
100
101    // Single word token methods
102
103    /// Construct a new sequence with a [`Word`] at the beginning of the operation list.
104    pub fn any_capitalization_of(word: &'static str) -> Self {
105        Self::default().then_any_capitalization_of(word)
106    }
107
108    /// Shorthand for [`Self::any_capitalization_of`].
109    pub fn aco(word: &'static str) -> Self {
110        Self::any_capitalization_of(word)
111    }
112
113    /// Match any word from the given set of words, case-insensitive.
114    pub fn word_set(words: &'static [&'static str]) -> Self {
115        Self::default().then_word_set(words)
116    }
117
118    /// Match any word.
119    pub fn any_word() -> Self {
120        Self::default().then_any_word()
121    }
122
123    /// Match any number.
124    pub fn number() -> Self {
125        Self::default().then_number()
126    }
127
128    // Expressions of more than one token
129
130    /// Optionally match an expression.
131    pub fn optional(expr: impl Expr + 'static) -> Self {
132        Self::default().then_optional(expr)
133    }
134
135    /// Match a series of words separated by whitespace.
136    pub fn word_seq(words: &'static [&'static str]) -> Self {
137        Self::default().then_word_seq(words)
138    }
139
140    /// Match a fixed phrase.
141    pub fn fixed_phrase(phrase: &'static str) -> Self {
142        Self::default().then_fixed_phrase(phrase)
143    }
144
145    // Multiple expressions
146
147    /// Match the first of multiple expressions.
148    pub fn any_of(exprs: impl IntoIterator<Item = impl AsBoxedExpr>) -> Self {
149        Self::default().then_any_of(exprs)
150    }
151
152    /// Match the longest of multiple expressions.
153    pub fn longest_of(exprs: impl IntoIterator<Item = impl AsBoxedExpr>) -> Self {
154        Self::default().then_longest_of(exprs)
155    }
156
157    pub fn whitespace() -> Self {
158        Self::default().then_whitespace()
159    }
160
161    /// Will be accepted unless the condition matches.
162    pub fn unless(condition: impl Expr + 'static) -> Self {
163        Self::default().then_unless(condition)
164    }
165
166    // Builder methods
167
168    /// Push an [expression](Expr) to the operation list.
169    pub fn then(mut self, expr: impl Expr + 'static) -> Self {
170        self.exprs.push(Box::new(expr));
171        self
172    }
173
174    /// Push an already-boxed [expression](Expr) to the operation list.
175    pub fn then_boxed(mut self, expr: Box<dyn Expr>) -> Self {
176        self.exprs.push(expr);
177        self
178    }
179
180    /// Pushes an expression that could move the cursor to the sequence, but does not require it.
181    pub fn then_optional(mut self, expr: impl Expr + 'static) -> Self {
182        self.exprs.push(Box::new(Optional::new(expr)));
183        self
184    }
185
186    /// Pushes an expression that will match any of the provided expressions.
187    ///
188    /// If more than one of the provided expressions match, this function provides no guarantee
189    /// as to which match will end up being used. If you need to get the longest of multiple
190    /// matches, use [`Self::then_longest_of()`] instead.
191    pub fn then_any_of(mut self, exprs: impl IntoIterator<Item = impl AsBoxedExpr>) -> Self {
192        self.exprs.push(Box::new(FirstMatchOf::new(exprs)));
193        self
194    }
195
196    /// Pushes an expression that will match the longest of the provided expressions.
197    ///
198    /// If you don't need the longest match, prefer using the short-circuiting
199    /// [`Self::then_any_of()`] instead.
200    pub fn then_longest_of(mut self, exprs: impl IntoIterator<Item = impl AsBoxedExpr>) -> Self {
201        self.exprs.push(Box::new(LongestMatchOf::new(exprs)));
202        self
203    }
204
205    /// Appends the steps in `other` onto the end of `self`.
206    /// This is more efficient than [`Self::then`] because it avoids pointer redirection.
207    pub fn then_seq(mut self, mut other: Self) -> Self {
208        self.exprs.append(&mut other.exprs);
209        self
210    }
211
212    /// Pushes an expression that will match any word from the given set of words, case-insensitive.
213    pub fn then_word_set(self, words: &'static [&'static str]) -> Self {
214        self.then(WordSet::new(words))
215    }
216
217    /// Shorthand for [`Self::then_word_set`].
218    pub fn t_set(self, words: &'static [&'static str]) -> Self {
219        self.then_word_set(words)
220    }
221
222    /// Match against one or more whitespace tokens.
223    pub fn then_whitespace(self) -> Self {
224        self.then(WhitespacePattern)
225    }
226
227    /// Shorthand for [`Self::then_whitespace`].
228    pub fn t_ws(self) -> Self {
229        self.then_whitespace()
230    }
231
232    /// Match against whitespace tokens or a hyphen.
233    pub fn then_whitespace_or_hyphen(self) -> Self {
234        self.then(WhitespacePattern.or(|tok: &Token, _: &[char]| tok.kind.is_hyphen()))
235    }
236
237    /// Shorthand for [`Self::then_whitespace_or_hyphen`].
238    pub fn t_ws_h(self) -> Self {
239        self.then_whitespace_or_hyphen()
240    }
241
242    /// Match against zero or more whitespace tokens.
243    pub fn then_optional_whitespace(self) -> Self {
244        self.then_optional(WhitespacePattern)
245    }
246
247    /// Shorthand for [`Self::then_optional_whitespace`].
248    pub fn t_ows(self) -> Self {
249        self.then_optional_whitespace()
250    }
251
252    /// Match against zero or more occurrences of the given expression. Like `*` in regex.
253    pub fn then_zero_or_more(self, expr: impl Expr + 'static) -> Self {
254        self.then(Repeating::new(Box::new(expr), 0))
255    }
256
257    /// Match against one or more occurrences of the given expression. Like `+` in regex.
258    pub fn then_one_or_more(self, expr: impl Expr + 'static) -> Self {
259        self.then(Repeating::new(Box::new(expr), 1))
260    }
261
262    /// Match against zero or more whitespace-separated occurrences of the given expression.
263    pub fn then_zero_or_more_spaced(self, expr: impl Expr + 'static) -> Self {
264        let expr = Lrc::new(expr);
265        self.then(SequenceExpr::with(expr.clone()).then(Repeating::new(
266            Box::new(SequenceExpr::default().t_ws().then(expr)),
267            0,
268        )))
269    }
270
271    /// Create a new condition that will step one token forward if met.
272    /// If the condition is _not_ met, the whole expression returns `None`.
273    ///
274    /// This can be used to build out exceptions to other rules.
275    ///
276    /// See [`UnlessStep`] for more info.
277    pub fn then_unless(self, condition: impl Expr + 'static) -> Self {
278        self.then(UnlessStep::new(condition, |_tok: &Token, _src: &[char]| {
279            true
280        }))
281    }
282
283    /// Match any single token.
284    ///
285    /// See [`AnyPattern`] for more info.
286    pub fn then_anything(self) -> Self {
287        self.then(AnyPattern)
288    }
289
290    /// Match any single token.
291    ///
292    /// Shorthand for [`Self::then_anything`].
293    pub fn t_any(self) -> Self {
294        self.then_anything()
295    }
296
297    // Word matching methods
298
299    /// Matches any word.
300    pub fn then_any_word(self) -> Self {
301        self.then_kind_where(|kind| kind.is_word())
302    }
303
304    /// Match examples of `word` that have any capitalization.
305    pub fn then_any_capitalization_of(self, word: &'static str) -> Self {
306        self.then(Word::new(word))
307    }
308
309    /// Shorthand for [`Self::then_any_capitalization_of`].
310    pub fn t_aco(self, word: &'static str) -> Self {
311        self.then_any_capitalization_of(word)
312    }
313
314    /// Match examples of `word` case-sensitively.
315    pub fn then_exact_word(self, word: &'static str) -> Self {
316        self.then(Word::new_exact(word))
317    }
318
319    /// Match a series of words separated by whitespace.
320    pub fn then_word_seq(self, words: &'static [&'static str]) -> Self {
321        if let Some((first, rest)) = words.split_first() {
322            let mut expr = self.t_aco(first);
323            for word in rest {
324                expr = expr.t_ws().t_aco(word);
325            }
326            expr
327        } else {
328            self
329        }
330    }
331
332    /// Match a fixed phrase.
333    pub fn then_fixed_phrase(self, phrase: &'static str) -> Self {
334        self.then(FixedPhrase::from_phrase(phrase))
335    }
336
337    /// Match any word except the ones in `words`.
338    pub fn then_word_except(self, words: &'static [&'static str]) -> Self {
339        self.then(move |tok: &Token, src: &[char]| {
340            !tok.kind.is_word() || !words.iter().any(|&word| tok.get_ch(src).eq_str(word))
341        })
342    }
343
344    // Token kind/predicate matching methods
345
346    // One kind
347
348    /// Matches any token whose `Kind` exactly matches.
349    pub fn then_kind(self, kind: TokenKind) -> Self {
350        self.then_kind_where(move |k| kind == *k)
351    }
352
353    /// Matches a token where the provided closure returns true for the token's kind.
354    pub fn then_kind_where<F>(mut self, predicate: F) -> Self
355    where
356        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
357    {
358        self.exprs
359            .push(Box::new(move |tok: &Token, _source: &[char]| {
360                predicate(&tok.kind)
361            }));
362        self
363    }
364
365    /// Match a token of a given kind which is not in the list of words.
366    pub fn then_kind_except<F>(self, pred_is: F, ex: &'static [&'static str]) -> Self
367    where
368        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
369    {
370        self.then(move |tok: &Token, src: &[char]| {
371            pred_is(&tok.kind) && !ex.iter().any(|&word| tok.get_ch(src).eq_str(word))
372        })
373    }
374
375    // Two kinds
376
377    /// Match a token where both token kind predicates return true.
378    /// For instance, a word that can be both noun and verb.
379    pub fn then_kind_both<F1, F2>(self, pred_is_1: F1, pred_is_2: F2) -> Self
380    where
381        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
382        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
383    {
384        self.then_kind_where(move |k| pred_is_1(k) && pred_is_2(k))
385    }
386
387    /// Match a token where either of the two token kind predicates returns true.
388    /// For instance, an adjective or an adverb.
389    pub fn then_kind_either<F1, F2>(self, pred_is_1: F1, pred_is_2: F2) -> Self
390    where
391        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
392        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
393    {
394        self.then_kind_where(move |k| pred_is_1(k) || pred_is_2(k))
395    }
396
397    /// Match a token where neither of the two token kind predicates returns true.
398    /// For instance, a word that can't be a verb or a noun.
399    pub fn then_kind_neither<F1, F2>(self, pred_isnt_1: F1, pred_isnt_2: F2) -> Self
400    where
401        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
402        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
403    {
404        self.then_kind_where(move |k| !pred_isnt_1(k) && !pred_isnt_2(k))
405    }
406
407    /// Match a token where the first token kind predicate returns true and the second returns false.
408    /// For instance, a word that can be a noun but cannot be a verb.
409    pub fn then_kind_is_but_is_not<F1, F2>(self, pred_is: F1, pred_not: F2) -> Self
410    where
411        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
412        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
413    {
414        self.then_kind_where(move |k| pred_is(k) && !pred_not(k))
415    }
416
417    /// Match a token where the first token kind predicate returns true and the second returns false,
418    /// and the token is not in the list of exceptions.
419    pub fn then_kind_is_but_is_not_except<F1, F2>(
420        self,
421        pred_is: F1,
422        pred_not: F2,
423        ex: &'static [&'static str],
424    ) -> Self
425    where
426        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
427        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
428    {
429        self.then(move |tok: &Token, src: &[char]| {
430            pred_is(&tok.kind)
431                && !pred_not(&tok.kind)
432                && !ex.iter().any(|&word| tok.get_ch(src).eq_str(word))
433        })
434    }
435
436    /// Match a token where the first token kind predicate returns true and all of the second return false.
437    /// For instance, a word that can be a verb but not a noun or an adjective.
438    pub fn then_kind_is_but_isnt_any_of<F1, F2>(
439        self,
440        pred_is: F1,
441        preds_isnt: &'static [F2],
442    ) -> Self
443    where
444        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
445        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
446    {
447        self.then_kind_where(move |k| pred_is(k) && !preds_isnt.iter().any(|pred| pred(k)))
448    }
449
450    /// Match a token where the first token kind predicate returns true and all of the second return false,
451    /// and the token is not in the list of exceptions.
452    /// For instance, an adjective that isn't also a verb or adverb or the word "likely".
453    pub fn then_kind_is_but_isnt_any_of_except<F1, F2>(
454        self,
455        pred_is: F1,
456        preds_isnt: &'static [F2],
457        ex: &'static [&'static str],
458    ) -> Self
459    where
460        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
461        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
462    {
463        self.then(move |tok: &Token, src: &[char]| {
464            pred_is(&tok.kind)
465                && !preds_isnt.iter().any(|pred| pred(&tok.kind))
466                && !ex.iter().any(|&word| tok.get_ch(src).eq_str(word))
467        })
468    }
469
470    // More than two kinds
471
472    /// Match a token where both of the first two token kind predicates return true,
473    /// and the third returns false.
474    /// For instance, a word that must be both noun and verb, but not adjective.
475    pub fn then_kind_both_but_not<F1, F2, F3>(
476        self,
477        (pred_is_1, pred_is_2): (F1, F2),
478        pred_not: F3,
479    ) -> Self
480    where
481        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
482        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
483        F3: Fn(&TokenKind) -> bool + Send + Sync + 'static,
484    {
485        self.then_kind_where(move |k| pred_is_1(k) && pred_is_2(k) && !pred_not(k))
486    }
487
488    /// Match a token where any of the token kind predicates returns true.
489    /// Like `then_kind_either` but for more than two predicates.
490    pub fn then_kind_any<F>(self, preds_is: &'static [F]) -> Self
491    where
492        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
493    {
494        self.then_kind_where(move |k| preds_is.iter().any(|pred| pred(k)))
495    }
496
497    /// Match a token where none of the token kind predicates returns true.
498    /// Like `then_kind_neither` but for more than two predicates.
499    pub fn then_kind_none_of<F>(self, preds_isnt: &'static [F]) -> Self
500    where
501        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
502    {
503        self.then_kind_where(move |k| preds_isnt.iter().all(|pred| !pred(k)))
504    }
505
506    /// Match a token where any of the token kind predicates returns true,
507    /// and the word is not in the list of exceptions.
508    pub fn then_kind_any_except<F>(
509        self,
510        preds_is: &'static [F],
511        ex: &'static [&'static str],
512    ) -> Self
513    where
514        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
515    {
516        self.then(move |tok: &Token, src: &[char]| {
517            preds_is.iter().any(|pred| pred(&tok.kind))
518                && !ex.iter().any(|&word| tok.get_ch(src).eq_str(word))
519        })
520    }
521
522    /// Match a token where any of the token kind predicates returns true,
523    /// or the token is in the list of words.
524    pub fn then_kind_any_or_words<F>(
525        self,
526        preds: &'static [F],
527        words: &'static [&'static str],
528    ) -> Self
529    where
530        F: Fn(&TokenKind) -> bool + Send + Sync + 'static,
531    {
532        self.then(move |tok: &Token, src: &[char]| {
533            preds.iter().any(|pred| pred(&tok.kind))
534                || words.iter().any(|&word| tok.get_ch(src).eq_str(word))
535        })
536    }
537
538    /// Match a token where any of the first token kind predicates returns true
539    /// and the second returns false.
540    pub fn then_kind_any_but_not<F1, F2>(self, preds_is: &'static [F1], pred_not: F2) -> Self
541    where
542        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
543        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
544    {
545        self.then(move |tok: &Token, _src: &[char]| {
546            preds_is.iter().any(|pred| pred(&tok.kind)) && !pred_not(&tok.kind)
547        })
548    }
549
550    /// Match a token where any of the first token kind predicates returns true,
551    /// the second returns false, and the token is not in the list of exceptions.    
552    pub fn then_kind_any_but_not_except<F1, F2>(
553        self,
554        preds_is: &'static [F1],
555        pred_not: F2,
556        ex: &'static [&'static str],
557    ) -> Self
558    where
559        F1: Fn(&TokenKind) -> bool + Send + Sync + 'static,
560        F2: Fn(&TokenKind) -> bool + Send + Sync + 'static,
561    {
562        self.then(move |tok: &Token, src: &[char]| {
563            preds_is.iter().any(|pred| pred(&tok.kind))
564                && !pred_not(&tok.kind)
565                && !ex.iter().any(|&word| tok.get_ch(src).eq_str(word))
566        })
567    }
568
569    // Word property matching methods
570
571    // Out-of-vocabulary word. (Words not in the dictionary)
572    gen_then_from_is!(oov);
573    gen_then_from_is!(swear);
574
575    // Part-of-speech matching methods
576
577    // Nominals (nouns and pronouns)
578
579    gen_then_from_is!(nominal);
580    gen_then_from_is!(plural_nominal);
581    gen_then_from_is!(non_plural_nominal);
582    gen_then_from_is!(possessive_nominal);
583
584    // Nouns
585
586    gen_then_from_is!(noun);
587    gen_then_from_is!(proper_noun);
588    gen_then_from_is!(plural_noun);
589    gen_then_from_is!(singular_noun);
590    gen_then_from_is!(mass_noun_only);
591
592    // Pronouns
593
594    gen_then_from_is!(pronoun);
595    gen_then_from_is!(personal_pronoun);
596    gen_then_from_is!(first_person_singular_pronoun);
597    gen_then_from_is!(first_person_plural_pronoun);
598    gen_then_from_is!(second_person_pronoun);
599    gen_then_from_is!(third_person_pronoun);
600    gen_then_from_is!(third_person_singular_pronoun);
601    gen_then_from_is!(third_person_plural_pronoun);
602    gen_then_from_is!(subject_pronoun);
603    gen_then_from_is!(object_pronoun);
604
605    pub fn then_relative_pronoun(self) -> Self {
606        self.then(RelativePronoun::default())
607    }
608
609    // Verbs
610
611    gen_then_from_is!(verb);
612    gen_then_from_is!(auxiliary_verb);
613    gen_then_from_is!(linking_verb);
614    gen_then_from_is!(verb_lemma);
615    gen_then_from_is!(verb_simple_past_form);
616    gen_then_from_is!(verb_past_participle_form);
617    gen_then_from_is!(verb_progressive_form);
618    gen_then_from_is!(verb_third_person_singular_present_form);
619
620    // Adjectives
621
622    gen_then_from_is!(adjective);
623    gen_then_from_is!(positive_adjective);
624    gen_then_from_is!(comparative_adjective);
625    gen_then_from_is!(superlative_adjective);
626
627    // Adverbs
628
629    gen_then_from_is!(adverb);
630    gen_then_from_is!(frequency_adverb);
631    gen_then_from_is!(degree_adverb);
632
633    // Determiners
634
635    gen_then_from_is!(determiner);
636    gen_then_from_is!(demonstrative_determiner);
637    gen_then_from_is!(possessive_determiner);
638    gen_then_from_is!(quantifier);
639    gen_then_from_is!(non_quantifier_determiner);
640    gen_then_from_is!(non_demonstrative_determiner);
641
642    /// Push an [`IndefiniteArticle`] to the end of the operation list.
643    pub fn then_indefinite_article(self) -> Self {
644        self.then(IndefiniteArticle::default())
645    }
646
647    // Other parts of speech
648
649    gen_then_from_is!(conjunction);
650    gen_then_from_is!(preposition);
651
652    // Numbers
653
654    gen_then_from_is!(number);
655    gen_then_from_is!(cardinal_number);
656    gen_then_from_is!(ordinal_number);
657
658    // Punctuation
659
660    gen_then_from_is!(punctuation);
661    gen_then_from_is!(apostrophe);
662    gen_then_from_is!(comma);
663    gen_then_from_is!(hyphen);
664    gen_then_from_is!(period);
665    gen_then_from_is!(semicolon);
666    gen_then_from_is!(acute);
667    gen_then_from_is!(quote);
668    gen_then_from_is!(backslash);
669    gen_then_from_is!(slash);
670    gen_then_from_is!(percent);
671    gen_then_from_is!(degree);
672    gen_then_from_is!(open_single);
673    gen_then_from_is!(single_prime);
674    gen_then_from_is!(double_prime);
675    gen_then_from_is!(backtick);
676    gen_then_from_is!(plus);
677
678    // Other
679
680    gen_then_from_is!(case_separator);
681    gen_then_from_is!(likely_homograph);
682    gen_then_from_is!(sentence_terminator);
683}
684
685impl<S> From<S> for SequenceExpr
686where
687    S: Step + 'static,
688{
689    fn from(step: S) -> Self {
690        Self {
691            exprs: vec![Box::new(step)],
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use crate::{
699        Document, TokenKind,
700        expr::{AnchorEnd, Expr, ExprExt, SequenceExpr},
701        linting::tests::SpanVecExt,
702    };
703
704    #[test]
705    fn test_kind_both() {
706        let noun_and_verb =
707            SequenceExpr::default().then_kind_both(TokenKind::is_noun, TokenKind::is_verb);
708        let doc = Document::new_plain_english_curated("Use a good example.");
709        let matches = noun_and_verb.iter_matches_in_doc(&doc).collect::<Vec<_>>();
710        assert_eq!(matches.to_strings(&doc), vec!["Use", "good", "example"]);
711    }
712
713    #[test]
714    fn test_adjective_or_determiner() {
715        let expr = SequenceExpr::default()
716            .then_kind_either(TokenKind::is_adjective, TokenKind::is_determiner);
717        let doc = Document::new_plain_english_curated("Use a good example.");
718        let matches = expr.iter_matches_in_doc(&doc).collect::<Vec<_>>();
719        assert_eq!(matches.to_strings(&doc), vec!["a", "good"]);
720    }
721
722    #[test]
723    fn test_noun_but_not_adjective() {
724        let expr = SequenceExpr::default()
725            .then_kind_is_but_is_not(TokenKind::is_noun, TokenKind::is_adjective);
726        let doc = Document::new_plain_english_curated("Use a good example.");
727        let matches = expr.iter_matches_in_doc(&doc).collect::<Vec<_>>();
728        assert_eq!(matches.to_strings(&doc), vec!["Use", "example"]);
729    }
730
731    #[test]
732    fn flag_foo_followed_by_bar_or_at_end_1() {
733        let expr = SequenceExpr::aco("foo").then_any_of([
734            Box::new(SequenceExpr::whitespace().t_aco("bar").then(AnchorEnd)) as Box<dyn Expr>,
735            Box::new(AnchorEnd),
736        ]);
737
738        let doc_with_bar = Document::new_plain_english_curated("foo bar");
739
740        let matches_with_bar = expr.iter_matches_in_doc(&doc_with_bar).collect::<Vec<_>>();
741
742        eprintln!("matches_with_bar: {:#?}", matches_with_bar);
743
744        // "foo bar" matches with span covering both tokens
745        assert_eq!(matches_with_bar.len(), 1);
746        assert_eq!(matches_with_bar[0].start, 0);
747        assert_eq!(matches_with_bar[0].end, 3);
748        assert_eq!(matches_with_bar.to_strings(&doc_with_bar), vec!["foo bar"]);
749    }
750
751    #[test]
752    fn flag_foo_followed_by_bar_or_at_end_2() {
753        let expr = SequenceExpr::aco("foo").then_any_of([
754            Box::new(SequenceExpr::whitespace().t_aco("bar").then(AnchorEnd)) as Box<dyn Expr>,
755            Box::new(AnchorEnd),
756        ]);
757
758        let doc_with_end = Document::new_plain_english_curated("foo");
759
760        let matches_with_end = expr.iter_matches_in_doc(&doc_with_end).collect::<Vec<_>>();
761
762        eprintln!("matches_with_end: {:#?}", matches_with_end);
763
764        // "foo" at end matches with span covering just "foo"
765        assert_eq!(matches_with_end.len(), 1);
766        assert_eq!(matches_with_end[0].start, 0);
767        assert_eq!(matches_with_end[0].end, 1);
768        assert_eq!(matches_with_end.to_strings(&doc_with_end), vec!["foo"]);
769    }
770
771    #[test]
772    fn flag_foo_followed_by_bar_or_at_end_3() {
773        let expr = SequenceExpr::aco("foo").then_any_of([
774            Box::new(SequenceExpr::whitespace().t_aco("bar").then(AnchorEnd)) as Box<dyn Expr>,
775            Box::new(AnchorEnd),
776        ]);
777
778        let doc_with_foo_bar_baz = Document::new_plain_english_curated("foo bar baz");
779
780        let matches_with_foo_bar_baz = expr
781            .iter_matches_in_doc(&doc_with_foo_bar_baz)
782            .collect::<Vec<_>>();
783
784        eprintln!("matches_with_foo_bar_baz: {:#?}", matches_with_foo_bar_baz);
785
786        // "foo bar baz" should NOT match because "bar" is not at the end
787        assert_eq!(matches_with_foo_bar_baz.len(), 0);
788        assert_eq!(
789            matches_with_foo_bar_baz.to_strings(&doc_with_foo_bar_baz),
790            Vec::<String>::new()
791        );
792    }
793}