1use regex::Regex;
10use std::sync::OnceLock;
11
12const WS_CLASS: &str = concat!(
19 "\\t\\n\\x0B\\x0C\\r",
20 " ",
21 "\\u{00A0}\\u{1680}\\u{2000}-\\u{200A}",
22 "\\u{2028}\\u{2029}\\u{202F}\\u{205F}\\u{3000}\\u{FEFF}",
23);
24
25fn js_ws_regex(template: &str) -> Regex {
28 let positive = format!("[{WS_CLASS}]");
29 let negative = format!("[^{WS_CLASS}]");
30 let pattern = template.replace(r"\s", &positive).replace(r"\S", &negative);
31 Regex::new(&pattern).expect("lexer regex compiles")
32}
33
34struct Regexes {
36 urls: Regex,
37 ids: Regex,
38 time: Regex,
39 number: Regex,
40 space: Regex,
41 email: Regex,
42 punctuation: Regex,
43 unblank: Regex,
44}
45
46fn regexes() -> &'static Regexes {
47 static REGEXES: OnceLock<Regexes> = OnceLock::new();
48 REGEXES.get_or_init(|| {
49 Regexes {
53 urls: Regex::new(
54 r"(?i)(?:https?://)(?:[0-9a-z.-]+)\.(?:[a-z.]{2,6})(?:[/0-9A-Za-z_.\-?#=&%]*)*/?",
55 )
56 .expect("urls regex compiles"),
57 ids: js_ws_regex(r"(?i)(?:^|\s)([a-z0-9-]{8,45})"),
58 time: js_ws_regex(r"(?i)(?:[0-9]|0[0-9]|1[0-9]|2[0-3]):(?:[0-5][0-9])\s?(?:[ap][m])"),
59 number: Regex::new(r"(?i)[0-9]*\.[0-9]+|[0-9]+").expect("number regex compiles"),
60 space: js_ws_regex(r"\s+"),
61 email: Regex::new(
62 r#"(?i)[-!#$%&'*+/0-9=?A-Z^_a-z{|}~](?:\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](?:-?\.?[a-zA-Z0-9])*(?:\.[a-zA-Z](?:-?[a-zA-Z0-9])*)+"#,
63 )
64 .expect("email regex compiles"),
65 punctuation: Regex::new(r#"[/.,?!"':;$()#’`]"#).expect("punctuation regex compiles"),
66 unblank: js_ws_regex(r"\S"),
67 }
68 })
69}
70
71#[derive(Debug, Default, Clone, Copy)]
82pub struct Lexer;
83
84impl Lexer {
85 pub fn new() -> Self {
87 Lexer
88 }
89
90 pub fn lex(&self, string: &str) -> Vec<String> {
96 let re = regexes();
97 let order = [
98 SplitRule::new(&re.urls),
99 SplitRule::id(&re.ids),
100 SplitRule::new(&re.time),
101 SplitRule::new(&re.email),
102 SplitRule::new(&re.number),
103 SplitRule::new(&re.space),
104 SplitRule::new(&re.punctuation),
105 ];
106 let root = LexerNode::new(string, order[0], &order[1..]);
107 let mut out = Vec::new();
108 root.fill_array(&mut out, &re.unblank);
109 out
110 }
111}
112
113#[derive(Clone, Copy)]
114struct SplitRule<'a> {
115 regex: &'a Regex,
116 id_capture: bool,
117}
118
119impl<'a> SplitRule<'a> {
120 fn new(regex: &'a Regex) -> Self {
121 Self {
122 regex,
123 id_capture: false,
124 }
125 }
126
127 fn id(regex: &'a Regex) -> Self {
128 Self {
129 regex,
130 id_capture: true,
131 }
132 }
133}
134
135enum LexerNode {
140 Branch {
142 matches: Vec<String>,
143 children: Vec<LexerNode>,
144 },
145 Leaf {
147 matches: Vec<String>,
148 gaps: Vec<String>,
149 },
150}
151
152impl LexerNode {
153 fn new(string: &str, rule: SplitRule<'_>, rest: &[SplitRule<'_>]) -> LexerNode {
156 let string = js_trim(string);
157
158 let (matches, gaps) = if string.is_empty() {
159 (Vec::new(), vec![string])
160 } else {
161 let (matches, gaps) = split_with_rule(rule, &string);
162 if matches.is_empty() {
163 (Vec::new(), vec![string])
164 } else {
165 (matches, gaps)
166 }
167 };
168
169 if rest.is_empty() {
170 LexerNode::Leaf { matches, gaps }
171 } else {
172 let next = rest[0];
173 let next_rest = &rest[1..];
174 let children = gaps
175 .iter()
176 .map(|gap| LexerNode::new(gap, next, next_rest))
177 .collect();
178 LexerNode::Branch { matches, children }
179 }
180 }
181
182 fn fill_array(&self, out: &mut Vec<String>, unblank: &Regex) {
188 match self {
189 LexerNode::Branch { matches, children } => {
190 for (i, child) in children.iter().enumerate() {
191 child.fill_array(out, unblank);
192 if i < matches.len() && unblank.is_match(&matches[i]) {
193 out.push(js_trim(&matches[i]));
194 }
195 }
196 }
197 LexerNode::Leaf { matches, gaps } => {
198 for (i, gap) in gaps.iter().enumerate() {
199 if unblank.is_match(gap) {
200 out.push(js_trim(gap));
201 }
202 if i < matches.len() && unblank.is_match(&matches[i]) {
203 out.push(js_trim(&matches[i]));
204 }
205 }
206 }
207 }
208 }
209}
210
211fn split_with_rule(rule: SplitRule<'_>, s: &str) -> (Vec<String>, Vec<String>) {
217 if rule.id_capture {
218 split_id_matches(rule.regex, s)
219 } else {
220 let matches: Vec<String> = rule
221 .regex
222 .find_iter(s)
223 .map(|m| m.as_str().to_string())
224 .collect();
225 let gaps = rule.regex.split(s).map(|piece| piece.to_string()).collect();
226 (matches, gaps)
227 }
228}
229
230fn split_id_matches(regex: &Regex, s: &str) -> (Vec<String>, Vec<String>) {
231 let mut matches = Vec::new();
232 let mut gaps = Vec::new();
233 let mut last_end = 0;
234
235 for capture in regex.captures_iter(s) {
236 let Some(body) = capture.get(1) else {
237 continue;
238 };
239
240 if !is_id_boundary(s, body.end()) {
241 continue;
242 }
243
244 gaps.push(s[last_end..body.start()].to_string());
245 matches.push(body.as_str().to_string());
246 last_end = body.end();
247 }
248
249 if matches.is_empty() {
250 (matches, Vec::new())
251 } else {
252 gaps.push(s[last_end..].to_string());
253 (matches, gaps)
254 }
255}
256
257fn is_id_boundary(s: &str, end: usize) -> bool {
258 let mut chars = s[end..].chars();
259 match chars.next() {
260 None => true,
261 Some(c) if is_js_whitespace(c) => true,
262 Some('.') => !matches!(chars.next(), Some(next) if next.is_ascii_digit()),
263 Some(c) => is_id_punctuation(c),
264 }
265}
266
267fn is_id_punctuation(c: char) -> bool {
268 matches!(
269 c,
270 '/' | ',' | '?' | '!' | '"' | '\'' | ':' | ';' | '$' | '(' | ')' | '#' | '’' | '`'
271 )
272}
273
274fn js_trim(s: &str) -> String {
280 s.trim_matches(is_js_whitespace).to_string()
281}
282
283fn is_js_whitespace(c: char) -> bool {
285 matches!(
286 c,
287 '\u{0009}'
288 | '\u{000A}'
289 | '\u{000B}'
290 | '\u{000C}'
291 | '\u{000D}'
292 | '\u{0020}'
293 | '\u{00A0}'
294 | '\u{1680}'
295 | '\u{2000}'
296 ..='\u{200A}'
297 | '\u{2028}'
298 | '\u{2029}'
299 | '\u{202F}'
300 | '\u{205F}'
301 | '\u{3000}'
302 | '\u{FEFF}'
303 )
304}