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> {
107 chars: &'a [char],
108 pos: usize,
109}
110
111impl Parser<'_> {
112 fn peek(&self) -> Option<char> {
113 self.chars.get(self.pos).copied()
114 }
115
116 fn eat(&mut self, expected: char) -> bool {
117 if self.peek() == Some(expected) {
118 self.pos += 1;
119 true
120 } else {
121 false
122 }
123 }
124
125 fn digit(&mut self) -> Option<u8> {
126 let digit = self.peek()?.to_digit(10)?;
127 self.pos += 1;
128 Some(digit as u8)
129 }
130
131 fn skip_whitespace(&mut self) {
132 while matches!(self.peek(), Some(' ' | '\t')) {
133 self.pos += 1;
134 }
135 }
136
137 fn pattern(&mut self) -> Result<CompiledPattern, CompileErrorKind> {
139 let guard = if self.peek() == Some('[') {
140 Some(self.guard()?)
141 } else {
142 None
143 };
144
145 let mut tokens: Vec<Token> = Vec::new();
146 let mut weights: Vec<Option<Weight>> = Vec::new();
147 let mut leading_boundary = false;
148 let mut trailing_boundary = false;
149
150 loop {
152 self.skip_whitespace();
153 let Some(ch) = self.peek() else { break };
154 if ch == '.' {
155 self.pos += 1;
156 if tokens.is_empty() {
157 leading_boundary = true;
158 } else {
159 trailing_boundary = true;
160 }
161 continue;
162 }
163 if trailing_boundary {
164 return Err(CompileErrorKind::TokenAfterTrailingBoundary);
165 }
166 if ch.is_ascii_digit() || ch == '!' || ch == '\\' {
167 set_weight(&mut weights, tokens.len(), self.weight()?)?;
168 continue;
169 }
170 tokens.push(self.token(ch)?);
171 }
172
173 if tokens.is_empty() {
174 return Err(CompileErrorKind::NoLetters);
175 }
176 weights.resize(tokens.len() + 1, None);
177 if (leading_boundary && weights[0].is_some())
180 || (trailing_boundary && weights[tokens.len()].is_some())
181 {
182 return Err(CompileErrorKind::WeightOutsideRun);
183 }
184 Ok(CompiledPattern {
185 guard,
186 tokens,
187 weights,
188 leading_boundary,
189 trailing_boundary,
190 })
191 }
192
193 fn guard(&mut self) -> Result<LengthGuard, CompileErrorKind> {
195 self.pos += 1; let start = self.pos;
197 while self.peek().is_some_and(|c| c != ']') {
198 self.pos += 1;
199 }
200 if !self.eat(']') {
201 return Err(CompileErrorKind::UnterminatedLengthGuard);
202 }
203 let body: String = self.chars[start..self.pos - 1].iter().collect();
204 let trimmed = body.trim();
205 let invalid = || CompileErrorKind::InvalidLengthGuard(body.clone());
206 let bound = |s: &str| -> Result<usize, CompileErrorKind> {
208 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
209 return Err(invalid());
210 }
211 s.parse::<usize>().map_err(|_| invalid())
212 };
213 let guard = if let Some(stripped) = trimmed.strip_suffix('+') {
214 LengthGuard::Min(bound(stripped)?)
215 } else if let Some(dash) = trimmed.find('-').filter(|&dash| dash > 0) {
216 LengthGuard::Range {
217 lo: bound(&trimmed[..dash])?,
218 hi: bound(&trimmed[dash + 1..])?,
219 }
220 } else {
221 LengthGuard::Exact(bound(trimmed)?)
222 };
223 let bounds_ok = match guard {
226 LengthGuard::Exact(n) | LengthGuard::Min(n) => n >= 2,
227 LengthGuard::Range { lo, hi } => lo >= 2 && lo <= hi,
228 };
229 if bounds_ok {
230 Ok(guard)
231 } else {
232 Err(invalid())
233 }
234 }
235
236 fn weight(&mut self) -> Result<Weight, CompileErrorKind> {
238 if self.eat('!') {
239 return Ok(Weight::Suppress);
240 }
241 let Some(base) = self.digit() else {
242 return Err(CompileErrorKind::BackslashWithoutDigit);
244 };
245 let mut min = base;
246 if self.eat('\\') {
247 match self.digit() {
250 Some(end) => {
251 min = end;
252 if min > base {
253 return Err(CompileErrorKind::IncreasingPriority { base, min });
254 }
255 }
256 None => return Err(CompileErrorKind::ExpectedDigitAfterBackslash),
257 }
258 }
259 Ok(Weight::Priority { base, min })
260 }
261
262 fn token(&mut self, ch: char) -> Result<Token, CompileErrorKind> {
264 match ch {
265 '*' => {
266 self.pos += 1;
267 Ok(Token::Any)
268 }
269 '{' => Ok(Token::GroupSet(self.set()?)),
270 '^' => {
271 self.pos += 1;
272 match self.peek() {
273 Some('{') => Ok(Token::NotGroupSet(self.set()?)),
274 Some('@' | '=') => Ok(Token::NotGroupSet(vec![self.reference()?])),
275 _ => Err(CompileErrorKind::CaretNotFollowed),
276 }
277 }
278 '@' | '=' => self.reference(),
279 _ => {
280 self.pos += 1;
281 if is_letter(ch) {
284 Ok(Token::Literal(ch as u32))
285 } else {
286 Err(CompileErrorKind::StrayCharacter(ch))
287 }
288 }
289 }
290 }
291
292 fn set(&mut self) -> Result<Vec<Token>, CompileErrorKind> {
295 self.pos += 1; let start = self.pos;
297 while self.peek().is_some_and(|c| c != '}') {
298 self.pos += 1;
299 }
300 if !self.eat('}') {
301 return Err(CompileErrorKind::UnterminatedGroupSet);
302 }
303 let body: String = self.chars[start..self.pos - 1].iter().collect();
304 if body.trim().is_empty() {
305 return Err(CompileErrorKind::EmptyGroupSet);
306 }
307 let mut members = Vec::new();
308 for part in body.split_whitespace() {
309 if part.starts_with('@') || part.starts_with('=') {
310 members.push(resolve_reference(part)?);
311 } else {
312 for ch in part.chars() {
313 if is_letter(ch) {
314 members.push(Token::Literal(ch as u32));
315 } else {
316 return Err(CompileErrorKind::StrayCharacter(ch));
317 }
318 }
319 }
320 }
321 Ok(members)
322 }
323
324 fn reference(&mut self) -> Result<Token, CompileErrorKind> {
327 let mut name = String::from(self.chars[self.pos]); self.pos += 1;
329 while let Some(c) = self.peek() {
330 if c.is_ascii_alphabetic() || c == '_' {
331 name.push(c);
332 self.pos += 1;
333 } else {
334 break;
335 }
336 }
337 if name.len() == 1 {
338 return Err(CompileErrorKind::EmptyGroupName);
339 }
340 resolve_reference(&name)
341 }
342}
343
344fn parse_line(raw: &str) -> Result<Option<CompiledPattern>, CompileErrorKind> {
346 let line = strip_comment(raw);
347 if line.is_empty() {
348 return Ok(None);
349 }
350 let chars: Vec<char> = line.chars().collect();
351 Parser {
352 chars: &chars,
353 pos: 0,
354 }
355 .pattern()
356 .map(Some)
357}
358
359pub fn compile_pattern_text(text: &str) -> Result<PatternSet, CompileError> {
361 let mut patterns = Vec::new();
362 for (index, raw) in text.split('\n').enumerate() {
363 let raw = raw.strip_suffix('\r').unwrap_or(raw);
364 let context = |kind| CompileError {
365 kind,
366 line_number: index + 1,
367 };
368 if let Some(pattern) = parse_line(raw).map_err(context)? {
369 patterns.push(pattern);
370 }
371 }
372 Ok(PatternSet { patterns })
373}