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