use regex::Regex;
use super::{RegexCache, Segment, Shape, TypeRegistry, shape_of, template};
use crate::{Problem, Spanned, text};
#[derive(Debug, Clone)]
pub struct FormatVariant {
pub shape: Shape,
pub example: String,
}
#[derive(Debug, Clone)]
pub struct Format {
pub shape: Option<Shape>,
pub variants: Vec<(String, FormatVariant)>,
pub key: Option<String>,
pub prohibited: Vec<Prohibition>,
pub example: Option<String>,
pub raw: Spanned,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Captured {
pub key: String,
pub start: usize,
}
pub fn captured(shape: &Shape, name: &str, text: &str) -> Option<Captured> {
let caps = shape.captures(text)?;
Some(Captured {
key: caps.name(name)?.as_str().to_string(),
start: caps.get(0).map_or(0, |m| m.start()),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatMatch {
Ok(Option<Captured>),
Fail(String),
Refused(String),
}
impl Format {
pub fn examples(&self) -> Vec<&str> {
match &self.example {
Some(e) => vec![e.as_str()],
None => self
.variants
.iter()
.map(|(_, v)| v.example.as_str())
.collect(),
}
}
pub(crate) fn shapes(&self) -> impl Iterator<Item = (Option<&str>, &Shape)> {
self.shape.iter().map(|s| (None, s)).chain(
self.variants
.iter()
.map(|(name, v)| (Some(name.as_str()), &v.shape)),
)
}
pub fn reach(&self, text: &str) -> usize {
self.shapes().map(|(_, s)| s.reach(text)).max().unwrap_or(0)
}
pub(super) fn uses_any(&self, types: &[&str]) -> bool {
self.shapes().any(|(_, shape)| match shape {
Shape::Template(t) => t
.segments
.iter()
.any(|s| matches!(s, Segment::Hole { ty, .. } if types.contains(&ty.as_str()))),
_ => false,
})
}
pub fn first_example(&self) -> &str {
self.example
.as_deref()
.or_else(|| self.variants.first().map(|(_, v)| v.example.as_str()))
.unwrap_or("")
}
pub fn check(&self, text: &str) -> FormatMatch {
self.check_as(text, "line")
}
pub fn check_as(&self, text: &str, noun: &str) -> FormatMatch {
let (shape, example) = match self.matching_as(text, noun) {
Ok(m) => m,
Err(reason) => return FormatMatch::Fail(reason),
};
if let Some(reason) = refused_as(text, &self.prohibited, noun, Some(example)) {
return FormatMatch::Refused(reason);
}
FormatMatch::Ok(self.key.as_ref().and_then(|k| captured(shape, k, text)))
}
pub fn shape_for(&self, text: &str) -> Option<(&Shape, &str)> {
match &self.shape {
Some(shape) => shape
.matches(text, |_| None)
.then(|| (shape, self.example.as_deref().unwrap_or(""))),
None => self
.variants
.iter()
.find(|(_, v)| v.shape.matches(text, |_| None))
.map(|(_, v)| (&v.shape, v.example.as_str())),
}
}
pub fn conforms(&self, text: &str) -> bool {
self.shape_for(text).is_some() && !self.prohibited.iter().any(|b| b.pattern.is_match(text))
}
pub fn matching(&self, text: &str) -> std::result::Result<(&Shape, &str), String> {
self.matching_as(text, "line")
}
pub fn matching_as(
&self,
text: &str,
noun: &str,
) -> std::result::Result<(&Shape, &str), String> {
if let Some(found) = self.shape_for(text) {
return Ok(found);
}
if let Some(shape) = &self.shape {
let example = self.example.as_deref().unwrap_or("");
return Err(format!(
"{}; {} {noun} of this kind reads {example}",
shape.failure(text),
text::article(noun)
));
}
let reach: Vec<usize> = self
.variants
.iter()
.map(|(_, v)| v.shape.reach(text))
.collect();
let best = reach.iter().copied().max().unwrap_or(0);
let at: Vec<&(String, FormatVariant)> = self
.variants
.iter()
.zip(&reach)
.filter(|(_, r)| **r == best)
.map(|(v, _)| v)
.collect();
match at.as_slice() {
[] => Err("has no variants to match".into()),
[(name, v)] => Err(format!(
"does not match the `{name}` form: it {}; {} {noun} of this kind reads {}",
v.shape.failure(text),
text::article(noun),
v.example
)),
many => {
let at = if best > 0 {
format!(": each fails at column {}", template::column(text, best))
} else {
String::new()
};
Err(format!(
"matches none of the forms {}{at}; {} {noun} of this kind reads {}",
text::code_list(many.iter().map(|(n, _)| n), ", "),
text::article(noun),
many.iter()
.map(|(_, v)| v.example.as_str())
.collect::<Vec<_>>()
.join(" or ")
))
}
}
}
}
pub fn refused(text: &str, bans: &[Prohibition], example: Option<&str>) -> Option<String> {
refused_as(text, bans, "line", example)
}
pub fn refused_as(
text: &str,
bans: &[Prohibition],
noun: &str,
example: Option<&str>,
) -> Option<String> {
bans.iter().find_map(|ban| {
let span = ban.pattern.find(text)?.as_str();
let message = ban.message.trim_end();
Some(match example {
Some(e) => {
let mut article = text::article(noun).to_string();
article[..1].make_ascii_uppercase();
format!("`{span}` is refused: {message} {article} {noun} of this kind reads {e}")
}
None => format!("`{span}` is refused: {message}"),
})
})
}
#[derive(Debug, Clone)]
pub struct Prohibition {
pub pattern: Regex,
pub message: String,
}
pub fn prohibitions(node: &Spanned, regexes: &RegexCache) -> Vec<Prohibition> {
node.get("prohibited-patterns")
.and_then(|b| b.as_seq())
.unwrap_or(&[])
.iter()
.filter_map(|ban| {
let pattern = regexes.get(ban.get("pattern")?.as_str()?).ok()?;
let message = ban.get("message")?.as_str()?.to_string();
Some(Prohibition { pattern, message })
})
.collect()
}
fn format_shape(
node: &Spanned,
types: &TypeRegistry,
regexes: &RegexCache,
) -> std::result::Result<Shape, Problem> {
if node.get("text").is_some_and(|t| t.as_str().is_none()) {
return Err(Problem::new("TL_101", "a format's `text` is a string"));
}
shape_of(node, types, regexes)?.ok_or_else(|| {
Problem::new(
"TL_105",
"a shape is one of `text`, `template` or `pattern`",
)
})
}
pub(super) fn lower_format(
entry: &Spanned,
types: &TypeRegistry,
regexes: &RegexCache,
) -> std::result::Result<Format, Vec<(usize, Problem)>> {
let mut problems = Vec::new();
let mut variants = Vec::new();
if let Some(v) = entry.get("variants") {
for (name, variant) in v.as_map().unwrap_or(&[]) {
match format_shape(variant, types, regexes) {
Ok(shape) => variants.push((
name.value.clone(),
FormatVariant {
shape,
example: variant
.get("example")
.and_then(|d| d.as_str())
.unwrap_or_default()
.to_string(),
},
)),
Err(e) => problems.push((
variant.line,
e.prefixed(&format!("variant `{}`: ", name.value)),
)),
}
}
}
let shape = if entry.get("variants").is_some() {
None
} else {
match format_shape(entry, types, regexes) {
Ok(s) => Some(s),
Err(e) => {
problems.push((entry.line, e));
None
}
}
};
let example = entry
.get("example")
.and_then(|d| d.as_str())
.map(str::to_string);
match (&example, entry.get("variants")) {
(Some(_), Some(_)) => problems.push((
entry.line,
Problem::new(
"TL_110",
"a format with `variants` has no `example` of its own: each variant's example speaks for its shape",
),
)),
(None, None) => problems.push((
entry.line,
Problem::new("TL_40", "a format with one shape carries an `example`"),
)),
_ => {}
}
let format = Format {
shape,
variants,
key: entry
.get("key")
.and_then(|k| k.as_str())
.map(str::to_string),
prohibited: prohibitions(entry, regexes),
example,
raw: entry.clone(),
};
if let Some(k) = &format.key {
for (name, s) in format.shapes() {
if !s.has_group(k) {
let whose =
name.map_or_else(|| "the shape".to_string(), |n| format!("variant `{n}`"));
problems.push((
entry.line,
Problem::new(
"TL_31",
format!("`key` `{k}` names no hole or named group of {whose}"),
),
));
}
}
}
if !problems.is_empty() {
return Err(problems);
}
Ok(format)
}