1use crate::error::{CompileError, CompileErrorKind};
4use crate::grapheme::{is_joining_type, KASHIDA};
5use crate::rasm::resolve_group_name;
6use icu_properties::props::{JoiningGroup, JoiningType};
7use icu_properties::CodePointMapData;
8
9#[derive(Clone, Debug)]
10pub(crate) enum Token {
11 Group(JoiningGroup), ExactGroup(JoiningGroup), GroupSet(Vec<Token>), NotGroupSet(Vec<Token>), Literal(u32), Any, }
18
19#[derive(Clone, Copy, Debug)]
22pub(crate) enum Weight {
23 Priority { base: u8, min: u8 },
24 Suppress,
25}
26
27#[derive(Clone, Copy, Debug)]
28pub(crate) enum LengthGuard {
29 Exact(usize),
30 Min(usize),
31 Range { lo: usize, hi: usize },
32}
33
34#[derive(Clone, Debug)]
35pub(crate) struct CompiledPattern {
36 pub(crate) guard: Option<LengthGuard>,
37 pub(crate) tokens: Vec<Token>,
38 pub(crate) weights: Vec<Option<Weight>>,
41 pub(crate) leading_boundary: bool,
42 pub(crate) trailing_boundary: bool,
43}
44
45#[derive(Clone, Debug)]
47pub struct PatternSet {
48 pub(crate) patterns: Vec<CompiledPattern>,
49}
50
51fn strip_comment(raw: &str) -> String {
52 let body = match raw.find('#') {
53 Some(hash) => &raw[..hash],
54 None => raw,
55 };
56 body.trim().to_string()
57}
58
59fn is_letter(ch: char) -> bool {
61 is_joining_type(CodePointMapData::<JoiningType>::new().get(ch))
62}
63
64fn resolve_reference(name: &str) -> Result<Token, CompileErrorKind> {
69 if name.strip_prefix(['@', '=']) == Some("Tatweel") {
70 return Ok(Token::Literal(KASHIDA as u32));
71 }
72 let group = resolve_group_name(name)?;
73 if name.starts_with('@') {
74 Ok(Token::Group(group))
75 } else {
76 Ok(Token::ExactGroup(group))
77 }
78}
79
80fn set_weight(
81 weights: &mut Vec<Option<Weight>>,
82 k: usize,
83 weight: Weight,
84) -> Result<(), CompileErrorKind> {
85 if k >= weights.len() {
86 weights.resize(k + 1, None);
87 }
88 if weights[k].is_some() {
90 return Err(CompileErrorKind::ConflictingWeights);
91 }
92 weights[k] = Some(weight);
93 Ok(())
94}
95
96struct Parser<'a> {
109 chars: &'a [char],
110 pos: usize,
111}
112
113impl Parser<'_> {
114 fn peek(&self) -> Option<char> {
115 self.chars.get(self.pos).copied()
116 }
117
118 fn eat(&mut self, expected: char) -> bool {
119 if self.peek() == Some(expected) {
120 self.pos += 1;
121 true
122 } else {
123 false
124 }
125 }
126
127 fn digit(&mut self) -> Option<u8> {
128 let digit = self.peek()?.to_digit(10)?;
129 self.pos += 1;
130 Some(digit as u8)
131 }
132
133 fn skip_whitespace(&mut self) {
134 while matches!(self.peek(), Some(' ' | '\t')) {
135 self.pos += 1;
136 }
137 }
138
139 fn pattern(&mut self) -> Result<CompiledPattern, CompileErrorKind> {
141 let guard = if self.peek() == Some('[') {
142 Some(self.guard()?)
143 } else {
144 None
145 };
146
147 let mut tokens: Vec<Token> = Vec::new();
148 let mut weights: Vec<Option<Weight>> = Vec::new();
149 let mut leading_boundary = false;
150 let mut trailing_boundary = false;
151
152 loop {
154 self.skip_whitespace();
155 let Some(ch) = self.peek() else { break };
156 if ch == '.' {
157 self.pos += 1;
158 if tokens.is_empty() {
159 leading_boundary = true;
160 } else {
161 trailing_boundary = true;
162 }
163 continue;
164 }
165 if trailing_boundary {
166 return Err(CompileErrorKind::TokenAfterTrailingBoundary);
167 }
168 if ch.is_ascii_digit() || ch == '!' || ch == '\\' {
169 set_weight(&mut weights, tokens.len(), self.weight()?)?;
170 continue;
171 }
172 tokens.push(self.token(ch)?);
173 }
174
175 if tokens.is_empty() {
176 return Err(CompileErrorKind::NoLetters);
177 }
178 weights.resize(tokens.len() + 1, None);
179 if (leading_boundary && weights[0].is_some())
182 || (trailing_boundary && weights[tokens.len()].is_some())
183 {
184 return Err(CompileErrorKind::WeightOutsideRun);
185 }
186 Ok(CompiledPattern {
187 guard,
188 tokens,
189 weights,
190 leading_boundary,
191 trailing_boundary,
192 })
193 }
194
195 fn guard(&mut self) -> Result<LengthGuard, CompileErrorKind> {
197 self.pos += 1; let start = self.pos;
199 while self.peek().is_some_and(|c| c != ']') {
200 self.pos += 1;
201 }
202 if !self.eat(']') {
203 return Err(CompileErrorKind::UnterminatedLengthGuard);
204 }
205 let body: String = self.chars[start..self.pos - 1].iter().collect();
206 let trimmed = body.trim();
207 let invalid = || CompileErrorKind::InvalidLengthGuard(body.clone());
208 let bound = |s: &str| -> Result<usize, CompileErrorKind> {
210 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
211 return Err(invalid());
212 }
213 s.parse::<usize>().map_err(|_| invalid())
214 };
215 let guard = if let Some(stripped) = trimmed.strip_suffix('+') {
216 LengthGuard::Min(bound(stripped)?)
217 } else if let Some(dash) = trimmed.find('-').filter(|&dash| dash > 0) {
218 LengthGuard::Range {
219 lo: bound(&trimmed[..dash])?,
220 hi: bound(&trimmed[dash + 1..])?,
221 }
222 } else {
223 LengthGuard::Exact(bound(trimmed)?)
224 };
225 let bounds_ok = match guard {
228 LengthGuard::Exact(n) | LengthGuard::Min(n) => n >= 2,
229 LengthGuard::Range { lo, hi } => lo >= 2 && lo <= hi,
230 };
231 if bounds_ok {
232 Ok(guard)
233 } else {
234 Err(invalid())
235 }
236 }
237
238 fn weight(&mut self) -> Result<Weight, CompileErrorKind> {
240 if self.eat('!') {
241 return Ok(Weight::Suppress);
242 }
243 let Some(base) = self.digit() else {
244 return Err(CompileErrorKind::BackslashWithoutDigit);
246 };
247 let mut min = base;
248 if self.eat('\\') {
249 match self.digit() {
252 Some(end) => {
253 min = end;
254 if min > base {
255 return Err(CompileErrorKind::IncreasingPriority { base, min });
256 }
257 }
258 None => return Err(CompileErrorKind::ExpectedDigitAfterBackslash),
259 }
260 }
261 Ok(Weight::Priority { base, min })
262 }
263
264 fn token(&mut self, ch: char) -> Result<Token, CompileErrorKind> {
266 match ch {
267 '*' => {
268 self.pos += 1;
269 Ok(Token::Any)
270 }
271 '{' => Ok(Token::GroupSet(self.set()?)),
272 '^' => {
273 self.pos += 1;
274 match self.peek() {
275 Some('{') => Ok(Token::NotGroupSet(self.set()?)),
276 Some('@' | '=') => Ok(Token::NotGroupSet(vec![self.reference()?])),
277 _ => Err(CompileErrorKind::CaretNotFollowed),
278 }
279 }
280 '@' | '=' => self.reference(),
281 _ => {
282 self.pos += 1;
283 if is_letter(ch) {
286 Ok(Token::Literal(ch as u32))
287 } else {
288 Err(CompileErrorKind::StrayCharacter(ch))
289 }
290 }
291 }
292 }
293
294 fn set(&mut self) -> Result<Vec<Token>, CompileErrorKind> {
297 self.pos += 1; let start = self.pos;
299 while self.peek().is_some_and(|c| c != '}') {
300 self.pos += 1;
301 }
302 if !self.eat('}') {
303 return Err(CompileErrorKind::UnterminatedGroupSet);
304 }
305 let body: String = self.chars[start..self.pos - 1].iter().collect();
306 if body.trim().is_empty() {
307 return Err(CompileErrorKind::EmptyGroupSet);
308 }
309 let mut members = Vec::new();
310 for part in body.split_whitespace() {
311 if part.starts_with('@') || part.starts_with('=') {
312 members.push(resolve_reference(part)?);
313 } else {
314 for ch in part.chars() {
315 if is_letter(ch) {
316 members.push(Token::Literal(ch as u32));
317 } else {
318 return Err(CompileErrorKind::StrayCharacter(ch));
319 }
320 }
321 }
322 }
323 Ok(members)
324 }
325
326 fn reference(&mut self) -> Result<Token, CompileErrorKind> {
329 let mut name = String::from(self.chars[self.pos]); self.pos += 1;
331 while let Some(c) = self.peek() {
332 if c.is_ascii_alphabetic() || c == '_' {
333 name.push(c);
334 self.pos += 1;
335 } else {
336 break;
337 }
338 }
339 if name.len() == 1 {
340 return Err(CompileErrorKind::EmptyGroupName);
341 }
342 resolve_reference(&name)
343 }
344}
345
346fn parse_line(raw: &str) -> Result<Option<CompiledPattern>, CompileErrorKind> {
348 let line = strip_comment(raw);
349 if line.is_empty() {
350 return Ok(None);
351 }
352 let chars: Vec<char> = line.chars().collect();
353 Parser {
354 chars: &chars,
355 pos: 0,
356 }
357 .pattern()
358 .map(Some)
359}
360
361fn parse_use(line: &str) -> Option<&str> {
364 let rest = line.strip_prefix("use")?;
365 if rest.starts_with([' ', '\t']) {
366 Some(rest.trim())
367 } else {
368 None
369 }
370}
371
372pub fn compile_pattern_text(text: &str) -> Result<PatternSet, CompileError> {
374 let mut patterns = Vec::new();
375 for (index, raw) in text.split('\n').enumerate() {
376 let raw = raw.strip_suffix('\r').unwrap_or(raw);
377 let context = |kind| CompileError {
378 kind,
379 line_number: index + 1,
380 };
381 if let Some(name) = parse_use(&strip_comment(raw)) {
382 let imported = crate::builtin::builtin_pattern_set(name)
383 .ok_or_else(|| context(CompileErrorKind::UnknownImport(name.to_string())))?;
384 patterns.extend(imported.patterns.iter().cloned());
385 continue;
386 }
387 if let Some(pattern) = parse_line(raw).map_err(context)? {
388 patterns.push(pattern);
389 }
390 }
391 Ok(PatternSet { patterns })
392}