use crate::error::{CompileError, CompileErrorKind};
use crate::grapheme::{is_joining_type, KASHIDA};
use crate::rasm::resolve_group_name;
use icu_properties::props::{JoiningGroup, JoiningType};
use icu_properties::CodePointMapData;
#[derive(Clone, Debug)]
pub(crate) enum Token {
Group(JoiningGroup), ExactGroup(JoiningGroup), GroupSet(Vec<Token>), NotGroupSet(Vec<Token>), Literal(u32), Any, }
#[derive(Clone, Copy, Debug)]
pub(crate) enum Weight {
Priority { base: u8, min: u8 },
Suppress,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum LengthGuard {
Exact(usize),
Min(usize),
Range { lo: usize, hi: usize },
Open(usize),
}
#[derive(Clone, Debug)]
pub(crate) struct CompiledPattern {
pub(crate) guard: Option<LengthGuard>,
pub(crate) tokens: Vec<Token>,
pub(crate) weights: Vec<Option<Weight>>,
pub(crate) leading_boundary: bool,
pub(crate) trailing_boundary: bool,
}
#[derive(Clone, Debug)]
pub struct PatternSet {
pub(crate) patterns: Vec<CompiledPattern>,
}
fn strip_comment(raw: &str) -> String {
let body = match raw.find('#') {
Some(hash) => &raw[..hash],
None => raw,
};
body.trim().to_string()
}
fn is_letter(ch: char) -> bool {
is_joining_type(CodePointMapData::<JoiningType>::new().get(ch))
}
fn resolve_reference(name: &str) -> Result<Token, CompileErrorKind> {
if name.strip_prefix(['@', '=']) == Some("Tatweel") {
return Ok(Token::Literal(KASHIDA as u32));
}
let group = resolve_group_name(name)?;
if name.starts_with('@') {
Ok(Token::Group(group))
} else {
Ok(Token::ExactGroup(group))
}
}
fn set_weight(
weights: &mut Vec<Option<Weight>>,
k: usize,
weight: Weight,
) -> Result<(), CompileErrorKind> {
if k >= weights.len() {
weights.resize(k + 1, None);
}
if weights[k].is_some() {
return Err(CompileErrorKind::ConflictingWeights);
}
weights[k] = Some(weight);
Ok(())
}
struct Parser<'a> {
chars: &'a [char],
pos: usize,
}
impl Parser<'_> {
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn eat(&mut self, expected: char) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
fn digit(&mut self) -> Option<u8> {
let digit = self.peek()?.to_digit(10)?;
self.pos += 1;
Some(digit as u8)
}
fn skip_whitespace(&mut self) {
while matches!(self.peek(), Some(' ' | '\t')) {
self.pos += 1;
}
}
fn pattern(&mut self) -> Result<CompiledPattern, CompileErrorKind> {
let guard = if self.peek() == Some('[') {
Some(self.guard()?)
} else {
None
};
let mut tokens: Vec<Token> = Vec::new();
let mut weights: Vec<Option<Weight>> = Vec::new();
let mut leading_boundary = false;
let mut trailing_boundary = false;
loop {
self.skip_whitespace();
let Some(ch) = self.peek() else { break };
if ch == '.' {
self.pos += 1;
if tokens.is_empty() {
leading_boundary = true;
} else {
trailing_boundary = true;
}
continue;
}
if trailing_boundary {
return Err(CompileErrorKind::TokenAfterTrailingBoundary);
}
if ch.is_ascii_digit() || ch == '!' || ch == '\\' {
set_weight(&mut weights, tokens.len(), self.weight()?)?;
continue;
}
tokens.push(self.token(ch)?);
}
if tokens.is_empty() {
return Err(CompileErrorKind::NoLetters);
}
weights.resize(tokens.len() + 1, None);
if (leading_boundary && weights[0].is_some())
|| (trailing_boundary && weights[tokens.len()].is_some())
{
return Err(CompileErrorKind::WeightOutsideRun);
}
Ok(CompiledPattern {
guard,
tokens,
weights,
leading_boundary,
trailing_boundary,
})
}
fn guard(&mut self) -> Result<LengthGuard, CompileErrorKind> {
self.pos += 1; let start = self.pos;
while self.peek().is_some_and(|c| c != ']') {
self.pos += 1;
}
if !self.eat(']') {
return Err(CompileErrorKind::UnterminatedLengthGuard);
}
let body: String = self.chars[start..self.pos - 1].iter().collect();
let trimmed = body.trim();
let invalid = || CompileErrorKind::InvalidLengthGuard(body.clone());
let bound = |s: &str| -> Result<usize, CompileErrorKind> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(invalid());
}
s.parse::<usize>().map_err(|_| invalid())
};
let guard = if let Some(stripped) = trimmed
.strip_prefix(':')
.and_then(|rest| rest.strip_suffix(':'))
{
LengthGuard::Open(bound(stripped)?)
} else if let Some(stripped) = trimmed.strip_suffix(':') {
LengthGuard::Min(bound(stripped)?)
} else if let Some(colon) = trimmed.find(':') {
LengthGuard::Range {
lo: bound(&trimmed[..colon])?,
hi: bound(&trimmed[colon + 1..])?,
}
} else {
LengthGuard::Exact(bound(trimmed)?)
};
let bounds_ok = match guard {
LengthGuard::Exact(n) | LengthGuard::Min(n) | LengthGuard::Open(n) => n >= 2,
LengthGuard::Range { lo, hi } => lo >= 2 && lo <= hi,
};
if bounds_ok {
Ok(guard)
} else {
Err(invalid())
}
}
fn weight(&mut self) -> Result<Weight, CompileErrorKind> {
if self.eat('!') {
return Ok(Weight::Suppress);
}
let Some(base) = self.digit() else {
return Err(CompileErrorKind::BackslashWithoutDigit);
};
let mut min = base;
if self.eat('\\') {
match self.digit() {
Some(end) => {
min = end;
if min > base {
return Err(CompileErrorKind::IncreasingPriority { base, min });
}
}
None => return Err(CompileErrorKind::ExpectedDigitAfterBackslash),
}
}
Ok(Weight::Priority { base, min })
}
fn token(&mut self, ch: char) -> Result<Token, CompileErrorKind> {
match ch {
'*' => {
self.pos += 1;
Ok(Token::Any)
}
'{' => Ok(Token::GroupSet(self.set()?)),
'^' => {
self.pos += 1;
match self.peek() {
Some('{') => Ok(Token::NotGroupSet(self.set()?)),
Some('@' | '=') => Ok(Token::NotGroupSet(vec![self.reference()?])),
_ => Err(CompileErrorKind::CaretNotFollowed),
}
}
'@' | '=' => self.reference(),
_ => {
self.pos += 1;
if is_letter(ch) {
Ok(Token::Literal(ch as u32))
} else {
Err(CompileErrorKind::StrayCharacter(ch))
}
}
}
}
fn set(&mut self) -> Result<Vec<Token>, CompileErrorKind> {
self.pos += 1; let start = self.pos;
while self.peek().is_some_and(|c| c != '}') {
self.pos += 1;
}
if !self.eat('}') {
return Err(CompileErrorKind::UnterminatedGroupSet);
}
let body: String = self.chars[start..self.pos - 1].iter().collect();
if body.trim().is_empty() {
return Err(CompileErrorKind::EmptyGroupSet);
}
let mut members = Vec::new();
for part in body.split_whitespace() {
if part.starts_with('@') || part.starts_with('=') {
members.push(resolve_reference(part)?);
} else {
for ch in part.chars() {
if is_letter(ch) {
members.push(Token::Literal(ch as u32));
} else {
return Err(CompileErrorKind::StrayCharacter(ch));
}
}
}
}
Ok(members)
}
fn reference(&mut self) -> Result<Token, CompileErrorKind> {
let mut name = String::from(self.chars[self.pos]); self.pos += 1;
while let Some(c) = self.peek() {
if c.is_ascii_alphabetic() || c == '_' {
name.push(c);
self.pos += 1;
} else {
break;
}
}
if name.len() == 1 {
return Err(CompileErrorKind::EmptyGroupName);
}
resolve_reference(&name)
}
}
fn parse_line(raw: &str) -> Result<Option<CompiledPattern>, CompileErrorKind> {
let line = strip_comment(raw);
if line.is_empty() {
return Ok(None);
}
let chars: Vec<char> = line.chars().collect();
Parser {
chars: &chars,
pos: 0,
}
.pattern()
.map(Some)
}
fn parse_use(line: &str) -> Option<&str> {
let rest = line.strip_prefix("use")?;
if rest.starts_with([' ', '\t']) {
Some(rest.trim())
} else {
None
}
}
pub fn compile_pattern_text(text: &str) -> Result<PatternSet, CompileError> {
let mut patterns = Vec::new();
for (index, raw) in text.split('\n').enumerate() {
let raw = raw.strip_suffix('\r').unwrap_or(raw);
let context = |kind| CompileError {
kind,
line_number: index + 1,
};
if let Some(name) = parse_use(&strip_comment(raw)) {
let imported = crate::builtin::builtin_pattern_set(name)
.ok_or_else(|| context(CompileErrorKind::UnknownImport(name.to_string())))?;
patterns.extend(imported.patterns.iter().cloned());
continue;
}
if let Some(pattern) = parse_line(raw).map_err(context)? {
patterns.push(pattern);
}
}
Ok(PatternSet { patterns })
}