(input: &str)
| 62 | // ── Tokenizer ──────────────────────────────────────────────────────────────── |
| 63 | |
| 64 | fn tokenize(input: &str) -> Vec<String> { |
| 65 | let mut tokens = Vec::new(); |
| 66 | let mut chars = input.char_indices().peekable(); |
| 67 | while let Some((i, c)) = chars.next() { |
| 68 | match c { |
| 69 | ' ' | '\t' | '\n' | '\r' => continue, |
| 70 | '(' | ')' | '&' | '|' | '!' => tokens.push(c.to_string()), |
| 71 | ':' => { |
| 72 | // ':*' suffix on the preceding term — already handled in term |
| 73 | // parsing; if we see a bare ':' here it's part of the next |
| 74 | // term text. Push ':' as a token so we can detect ':*'. |
| 75 | if chars.peek().map(|(_, nc)| *nc) == Some('*') { |
| 76 | chars.next(); // consume '*' |
| 77 | tokens.push(":*".into()); |
| 78 | } else { |
| 79 | tokens.push(":".into()); |
| 80 | } |
| 81 | } |
| 82 | _ => { |
| 83 | // Collect a term: alphanumeric + hyphen + underscore. |
| 84 | let start = i; |
| 85 | let mut end = i + c.len_utf8(); |
| 86 | while let Some(&(j, nc)) = chars.peek() { |
| 87 | if nc.is_alphanumeric() || nc == '-' || nc == '_' { |
| 88 | end = j + nc.len_utf8(); |
| 89 | chars.next(); |
| 90 | } else { |
| 91 | break; |
| 92 | } |
| 93 | } |
| 94 | tokens.push(input[start..end].to_ascii_lowercase()); |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | tokens |
| 99 | } |
| 100 | |
| 101 | // ── Recursive-descent parser ────────────────────────────────────────────────── |
| 102 |
no test coverage detected