use std::ops::Range;
use super::read::{Source, verbatim};
use crate::statement::Statement;
#[derive(Debug)]
pub(super) struct Split {
pub statement: Statement,
pub body: Option<String>,
pub rewritten: Option<&'static str>,
pub metadata: Vec<(String, String)>,
pub modal: bool,
pub joined: Option<usize>,
}
pub(super) fn split(source: &Source, text: Range<usize>, name: &str) -> Split {
let mut metadata = Vec::new();
let mut rest = Vec::new();
let mut modal = false;
let mut metadata_modal = false;
let mut placed = Vec::new();
for (index, (line, fenced)) in text.clone().zip(source.lines(text)) {
if let Some((label, value)) = (!fenced).then(|| metadata_entry(line)).flatten() {
metadata.push((label.to_owned(), value.to_owned()));
metadata_modal |= first_modal(line).is_some();
placed.push((index + 1, rest.len()));
continue;
}
modal |= !fenced && first_modal(line).is_some();
rest.push(line);
}
let first = rest.iter().position(|line| !line.trim().is_empty());
let (sentence, after) = first.map_or((None, &rest[..0]), |first| {
let length = rest[first..]
.iter()
.position(|line| line.trim().is_empty())
.unwrap_or(rest.len() - first);
let sentence = rest[first..first + length]
.iter()
.map(|line| line.trim())
.collect::<Vec<_>>()
.join(" ");
(Some(sentence), &rest[first + length..])
});
let joined = first.and_then(|first| {
let length = rest[first..]
.iter()
.position(|line| line.trim().is_empty())
.unwrap_or(rest.len() - first);
placed
.iter()
.find(|(_, before)| (first + 1..first + length).contains(before))
.map(|(line, _)| *line)
});
let sentence = sentence.unwrap_or_else(|| {
modal = metadata_modal || first_modal(name).is_some();
name.to_owned()
});
let (statement, rewritten) = statement(&sentence);
Split {
statement,
body: verbatim(after),
rewritten,
metadata,
modal,
joined,
}
}
pub(super) fn statement(sentence: &str) -> (Statement, Option<&'static str>) {
let verbatim = Statement::parse(sentence);
if matches!(verbatim, Statement::Form(_)) {
return (verbatim, None);
}
if let Some(range) = first_modal(sentence) {
let rewritten = format!(
"{}shall{}",
&sentence[..range.start],
&sentence[range.end..]
);
let parsed = Statement::parse(&rewritten);
if matches!(parsed, Statement::Form(_)) {
let word = if &sentence[range] == "MUST" {
"MUST"
} else {
"SHALL"
};
return (parsed, Some(word));
}
}
(Statement::Text(sentence.to_owned()), None)
}
pub(crate) fn first_modal(text: &str) -> Option<Range<usize>> {
["SHALL", "MUST"]
.into_iter()
.filter_map(|word| {
text.match_indices(word)
.map(|(start, _)| start..start + word.len())
.find(|range| stands_alone(text, range))
})
.min_by_key(|range| range.start)
}
fn stands_alone(text: &str, range: &Range<usize>) -> bool {
let word = |c: char| c.is_alphanumeric() || c == '_';
!text[..range.start].chars().next_back().is_some_and(word)
&& !text[range.end..].chars().next().is_some_and(word)
}
pub(crate) fn metadata_entry(line: &str) -> Option<(&str, &str)> {
let (label, after) = line.trim().strip_prefix("**")?.split_once("**")?;
let value = after.strip_prefix(':')?;
(!label.is_empty() && !label.contains('*')).then(|| (label, value.trim()))
}