#[cfg(test)]
mod tests;
use std::{collections::HashMap, str::Chars};
use fancy_regex::Regex;
use fancy_regex_macro::regex;
use super::{
replace::replace_classes, statements::split_statements, Classes, Draft, Message::*, Mode, Note,
RawRule, Rule, TestDraft,
};
use crate::{error::Error, REGEX_MATCH_FAIL};
impl Draft {
pub fn from(file: &str) -> Result<Draft, Error> {
let statements = split_statements(file);
let mut messages = Vec::new();
let mut mode_and_name: Option<(Mode, Option<String>)> = None;
let mut raw_rules = Vec::new();
let mut raw_classes: Classes = HashMap::new();
let mut last_note: Option<Note> = None;
for (statement, line) in statements {
let statement = statement.trim();
if statement.is_empty() {
continue;
}
let mut chars = statement.chars();
let Some(operator) = chars.next() else {
continue;
};
match operator {
'#' => unrecoverable_error!(
"A comment statement slipped past the initial statement parser!"
),
'~' => {
if mode_and_name.is_some() {
return parse_error!(line, ModeAlreadyDefined);
}
while chars.as_str().starts_with(' ') {
chars.next();
}
let (first, name, last) = chars_first_middle_last(&mut chars);
let name = if name.trim().is_empty() {
None
} else {
Some(name.trim().to_string())
};
mode_and_name = Some(match Mode::from_options(first, last) {
Some(value) => (value, name),
None => return parse_error!(line, InvalidModeSpecifier),
});
}
'$' => {
let mut split = chars.as_str().split('=');
let Some(name) = split.next() else {
return parse_error!(line, NoClassName);
};
let name = name.trim();
if !regex!(r"^\w+$").is_match(name).expect(REGEX_MATCH_FAIL) {
return parse_error!(line, InvalidClassName, name.to_string());
}
if raw_classes.get(name).is_some() {
return parse_error!(line, ClassAlreadyExists, name.to_string());
}
let Some(pattern) = split.next() else {
return parse_error!(line, NoClassPattern, name.to_string());
};
let pattern = pattern.replace(' ', "");
raw_classes.insert(
name.trim().to_string(),
(format!("(?:{})", pattern.trim()), line),
);
}
'+' | '!' => {
let intent = operator == '+';
let pattern = chars.as_str().replace(' ', "");
let note = last_note.clone();
raw_rules.push(RawRule {
intent,
pattern,
note,
line,
})
}
'?' => {
while chars.as_str().starts_with(' ') {
chars.next();
}
let intent = match chars.next() {
Some('+') => true,
Some('!') => false,
_ => {
return parse_error!(line, InvalidTestIntent);
}
};
for word in chars.as_str().split_whitespace() {
let word = word.trim().to_string();
if !word.is_empty() {
messages.push(Test(TestDraft { intent, word }));
}
}
}
'*' => {
let mut note = chars.as_str().trim();
if note.is_empty() {
return parse_error!(line, EmptyNote);
}
if note.starts_with(':') {
chars.next();
note = chars.as_str().trim();
} else {
messages.push(Info(Note(note.to_string())));
}
last_note = Some(Note(note.to_string()));
}
_ => {
return parse_error!(line, UnknownStatementOperator, operator);
}
}
}
let test_count = messages.iter().filter(|msg| msg.is_test()).count();
let (mode, name) = mode_and_name.unwrap_or_default();
Ok(Self {
rules: parse_rules(&raw_rules, &raw_classes)?,
raw_rules,
messages,
mode,
name,
test_count,
raw_classes,
})
}
}
fn parse_rules(rules: &[RawRule], classes: &Classes) -> Result<Vec<Rule>, Error> {
let mut new = Vec::new();
for RawRule {
pattern,
intent,
note,
line,
} in rules
{
new.push(Rule {
pattern: parse_regex(pattern, classes, *line)?,
intent: *intent,
note: note.clone(),
})
}
Ok(new)
}
fn parse_regex(pattern: &str, classes: &Classes, line: usize) -> Result<Regex, Error> {
let pattern = replace_classes(pattern, classes, line)?;
match Regex::new(&pattern) {
Ok(regex) => Ok(regex),
Err(err) => parse_error!(line, RegexParseFail, err),
}
}
fn chars_first_middle_last<'a>(chars: &'a mut Chars) -> (Option<char>, &'a str, Option<char>) {
let first = chars.next();
let last = chars.next_back();
(first, chars.as_str(), last)
}