use super::escape::escape_literal;
use super::error::LoadError;
enum LineClass {
Skip,
Literal(
String,
),
Regex {
body: String,
flags: String,
},
}
const BOM: char = '\u{FEFF}';
fn strip_bom(text: &str) -> &str {
return text.strip_prefix(BOM).unwrap_or(text);
}
fn classify_line(line: &str) -> LineClass {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
return LineClass::Skip;
}
let bytes = trimmed.as_bytes();
if bytes.len() >= 2
&& bytes[0] == b'/'
&& let Some(last) = trimmed.rfind('/')
&& last > 0
{
let body = &trimmed[1..last];
let flags = &trimmed[last + 1..];
if flags.chars().all(|c| return c.is_ascii_lowercase()) {
return LineClass::Regex {
body: body.to_string(),
flags: flags.to_string(),
};
}
}
return LineClass::Literal(trimmed.to_string());
}
fn first_bad_flag(flags: &str) -> Option<char> {
return flags.chars().find(|&c| return c != 'm' && c != 'x');
}
pub(super) fn parse_patterns(text: &str) -> Result<Vec<String>, LoadError> {
let mut patterns: Vec<String> = Vec::new();
for line in strip_bom(text).lines() {
match classify_line(line) {
LineClass::Skip => {}
LineClass::Literal(literal) => patterns.push(escape_literal(&literal)),
LineClass::Regex { body, flags } => {
if let Some(flag) = first_bad_flag(&flags) {
return Err(LoadError::UnsupportedFlag {
index: patterns.len(),
flag,
});
}
patterns.push(body);
}
}
}
if patterns.is_empty() {
return Err(LoadError::NoRules);
}
return Ok(patterns);
}
#[cfg(test)]
#[path = "format_tests.rs"]
mod format_tests;