use super::error::LoadError;
use super::format::{is_significant, significant_line_pattern};
use std::collections::HashMap;
const HEADER_PREFIX: &str = "==> ";
const HEADER_SUFFIX: &str = " <==";
const NEAR_ARROW: &str = "==>";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ParsedRule {
pub(super) name: Option<String>,
pub(super) pattern: String,
}
enum HeaderKind {
Strict(
String,
),
Near,
NotHeader,
}
struct OpenSection<'a> {
name: String,
name_line: usize,
body: Vec<&'a str>,
}
fn is_name_start(ch: char) -> bool {
return ch.is_ascii_lowercase() || ch.is_ascii_digit();
}
fn is_name_char(ch: char) -> bool {
return is_name_start(ch) || ch == '.' || ch == '-';
}
fn strict_name(line: &str) -> Option<String> {
let middle = line.strip_prefix(HEADER_PREFIX)?.strip_suffix(HEADER_SUFFIX)?;
let first = middle.chars().next()?;
if !is_name_start(first) || !middle.chars().all(|ch| return is_name_char(ch)) {
return None;
}
return Some(middle.to_string());
}
fn classify_header(line: &str) -> HeaderKind {
if !line.trim_start().starts_with(NEAR_ARROW) {
return HeaderKind::NotHeader;
}
return match strict_name(line) {
Some(name) => HeaderKind::Strict(name),
None => HeaderKind::Near,
};
}
pub(super) fn detect_tail_format(text: &str) -> bool {
return text
.lines()
.find(|&line| return is_significant(line))
.is_some_and(|line| return strict_name(line).is_some());
}
fn finish_section(section: OpenSection<'_>, index: usize) -> Result<ParsedRule, LoadError> {
let end = section
.body
.iter()
.rposition(|line| return !line.trim().is_empty())
.map_or(0, |last| return last + 1);
let body = §ion.body[..end];
let significant: Vec<&str> =
body.iter().copied().filter(|&line| return is_significant(line)).collect();
if significant.is_empty() {
return Err(LoadError::EmptySection { line: section.name_line });
}
let pattern = if significant.len() == 1 {
significant_line_pattern(significant[0], index)?
} else {
body.join("\n")
};
return Ok(ParsedRule { name: Some(section.name), pattern });
}
fn close_section(
section: OpenSection<'_>,
rules: &mut Vec<ParsedRule>,
name_lines: &mut Vec<usize>,
) -> Result<(), LoadError> {
let name_line = section.name_line;
let rule = finish_section(section, rules.len())?;
rules.push(rule);
name_lines.push(name_line);
return Ok(());
}
pub(super) fn parse_sections(text: &str) -> Result<Vec<ParsedRule>, LoadError> {
let mut rules: Vec<ParsedRule> = Vec::new();
let mut name_lines: Vec<usize> = Vec::new();
let mut current: Option<OpenSection<'_>> = None;
for (offset, line) in text.lines().enumerate() {
let line_number = offset + 1;
match classify_header(line) {
HeaderKind::Strict(name) => {
if let Some(section) = current.take() {
close_section(section, &mut rules, &mut name_lines)?;
}
current = Some(OpenSection { name, name_line: line_number, body: Vec::new() });
}
HeaderKind::Near => {
return Err(LoadError::NearHeader { line: line_number });
}
HeaderKind::NotHeader => match current.as_mut() {
Some(section) => section.body.push(line),
None => {
if is_significant(line) {
return Err(LoadError::PreHeaderContent { line: line_number });
}
}
},
}
}
if let Some(section) = current.take() {
close_section(section, &mut rules, &mut name_lines)?;
}
if rules.is_empty() {
return Err(LoadError::NoRules);
}
let mut seen: HashMap<&str, usize> = HashMap::new();
for (rule, &name_line) in rules.iter().zip(&name_lines) {
let Some(name) = rule.name.as_deref() else {
continue;
};
if let Some(&first_line) = seen.get(name) {
return Err(LoadError::DuplicateName { first_line, line: name_line });
}
seen.insert(name, name_line);
}
return Ok(rules);
}
#[cfg(test)]
#[path = "sections_tests.rs"]
mod sections_tests;