1use regex::Regex;
5
6use super::{RegexCache, Segment, Shape, TypeRegistry, shape_of, template};
7use crate::{Problem, Spanned, text};
8
9#[derive(Debug, Clone)]
11pub struct FormatVariant {
12 pub shape: Shape,
14 pub example: String,
16}
17
18#[derive(Debug, Clone)]
21pub struct Format {
22 pub shape: Option<Shape>,
24 pub variants: Vec<(String, FormatVariant)>,
26 pub key: Option<String>,
28 pub prohibited: Vec<Prohibition>,
30 pub example: Option<String>,
32 pub raw: Spanned,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Captured {
40 pub key: String,
42 pub start: usize,
44}
45
46pub fn captured(shape: &Shape, name: &str, text: &str) -> Option<Captured> {
48 let caps = shape.captures(text)?;
49 Some(Captured {
50 key: caps.name(name)?.as_str().to_string(),
51 start: caps.get(0).map_or(0, |m| m.start()),
52 })
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum FormatMatch {
58 Ok(Option<Captured>),
60 Fail(String),
62 Refused(String),
64}
65
66impl Format {
67 pub fn examples(&self) -> Vec<&str> {
69 match &self.example {
70 Some(e) => vec![e.as_str()],
71 None => self
72 .variants
73 .iter()
74 .map(|(_, v)| v.example.as_str())
75 .collect(),
76 }
77 }
78
79 pub(crate) fn shapes(&self) -> impl Iterator<Item = (Option<&str>, &Shape)> {
82 self.shape.iter().map(|s| (None, s)).chain(
83 self.variants
84 .iter()
85 .map(|(name, v)| (Some(name.as_str()), &v.shape)),
86 )
87 }
88
89 pub fn reach(&self, text: &str) -> usize {
92 self.shapes().map(|(_, s)| s.reach(text)).max().unwrap_or(0)
93 }
94
95 pub(super) fn uses_any(&self, types: &[&str]) -> bool {
97 self.shapes().any(|(_, shape)| match shape {
98 Shape::Template(t) => t
99 .segments
100 .iter()
101 .any(|s| matches!(s, Segment::Hole { ty, .. } if types.contains(&ty.as_str()))),
102 _ => false,
103 })
104 }
105
106 pub fn first_example(&self) -> &str {
108 self.example
109 .as_deref()
110 .or_else(|| self.variants.first().map(|(_, v)| v.example.as_str()))
111 .unwrap_or("")
112 }
113
114 pub fn check(&self, text: &str) -> FormatMatch {
122 self.check_as(text, "line")
123 }
124
125 pub fn check_as(&self, text: &str, noun: &str) -> FormatMatch {
127 let (shape, example) = match self.matching_as(text, noun) {
128 Ok(m) => m,
129 Err(reason) => return FormatMatch::Fail(reason),
130 };
131 if let Some(reason) = refused_as(text, &self.prohibited, noun, Some(example)) {
132 return FormatMatch::Refused(reason);
133 }
134 FormatMatch::Ok(self.key.as_ref().and_then(|k| captured(shape, k, text)))
135 }
136
137 pub fn shape_for(&self, text: &str) -> Option<(&Shape, &str)> {
140 match &self.shape {
141 Some(shape) => shape
142 .matches(text, |_| None)
143 .then(|| (shape, self.example.as_deref().unwrap_or(""))),
144 None => self
145 .variants
146 .iter()
147 .find(|(_, v)| v.shape.matches(text, |_| None))
148 .map(|(_, v)| (&v.shape, v.example.as_str())),
149 }
150 }
151
152 pub fn conforms(&self, text: &str) -> bool {
154 self.shape_for(text).is_some() && !self.prohibited.iter().any(|b| b.pattern.is_match(text))
155 }
156
157 pub fn matching(&self, text: &str) -> std::result::Result<(&Shape, &str), String> {
162 self.matching_as(text, "line")
163 }
164
165 pub fn matching_as(
167 &self,
168 text: &str,
169 noun: &str,
170 ) -> std::result::Result<(&Shape, &str), String> {
171 if let Some(found) = self.shape_for(text) {
172 return Ok(found);
173 }
174 if let Some(shape) = &self.shape {
175 let example = self.example.as_deref().unwrap_or("");
176 return Err(format!(
177 "{}; {} {noun} of this kind reads {example}",
178 shape.failure(text),
179 text::article(noun)
180 ));
181 }
182 let reach: Vec<usize> = self
183 .variants
184 .iter()
185 .map(|(_, v)| v.shape.reach(text))
186 .collect();
187 let best = reach.iter().copied().max().unwrap_or(0);
188 let at: Vec<&(String, FormatVariant)> = self
189 .variants
190 .iter()
191 .zip(&reach)
192 .filter(|(_, r)| **r == best)
193 .map(|(v, _)| v)
194 .collect();
195 match at.as_slice() {
196 [] => Err("has no variants to match".into()),
197 [(name, v)] => Err(format!(
198 "does not match the `{name}` form: it {}; {} {noun} of this kind reads {}",
199 v.shape.failure(text),
200 text::article(noun),
201 v.example
202 )),
203 many => {
204 let at = if best > 0 {
206 format!(": each fails at column {}", template::column(text, best))
207 } else {
208 String::new()
209 };
210 Err(format!(
213 "matches none of the forms {}{at}; {} {noun} of this kind reads {}",
214 text::code_list(many.iter().map(|(n, _)| n), ", "),
215 text::article(noun),
216 many.iter()
217 .map(|(_, v)| v.example.as_str())
218 .collect::<Vec<_>>()
219 .join(" or ")
220 ))
221 }
222 }
223 }
224}
225
226pub fn refused(text: &str, bans: &[Prohibition], example: Option<&str>) -> Option<String> {
234 refused_as(text, bans, "line", example)
235}
236
237pub fn refused_as(
243 text: &str,
244 bans: &[Prohibition],
245 noun: &str,
246 example: Option<&str>,
247) -> Option<String> {
248 bans.iter().find_map(|ban| {
249 let span = ban.pattern.find(text)?.as_str();
250 let message = ban.message.trim_end();
251 Some(match example {
252 Some(e) => {
253 let mut article = text::article(noun).to_string();
254 article[..1].make_ascii_uppercase();
255 format!("`{span}` is refused: {message} {article} {noun} of this kind reads {e}")
256 }
257 None => format!("`{span}` is refused: {message}"),
258 })
259 })
260}
261
262#[derive(Debug, Clone)]
266pub struct Prohibition {
267 pub pattern: Regex,
269 pub message: String,
271}
272
273pub fn prohibitions(node: &Spanned, regexes: &RegexCache) -> Vec<Prohibition> {
277 node.get("prohibited-patterns")
278 .and_then(|b| b.as_seq())
279 .unwrap_or(&[])
280 .iter()
281 .filter_map(|ban| {
282 let pattern = regexes.get(ban.get("pattern")?.as_str()?).ok()?;
283 let message = ban.get("message")?.as_str()?.to_string();
284 Some(Prohibition { pattern, message })
285 })
286 .collect()
287}
288
289fn format_shape(
292 node: &Spanned,
293 types: &TypeRegistry,
294 regexes: &RegexCache,
295) -> std::result::Result<Shape, Problem> {
296 if node.get("text").is_some_and(|t| t.as_str().is_none()) {
297 return Err(Problem::new("TL_101", "a format's `text` is a string"));
298 }
299 shape_of(node, types, regexes)?.ok_or_else(|| {
301 Problem::new(
302 "TL_105",
303 "a shape is one of `text`, `template` or `pattern`",
304 )
305 })
306}
307
308pub(super) fn lower_format(
314 entry: &Spanned,
315 types: &TypeRegistry,
316 regexes: &RegexCache,
317) -> std::result::Result<Format, Vec<(usize, Problem)>> {
318 let mut problems = Vec::new();
319 let mut variants = Vec::new();
320 if let Some(v) = entry.get("variants") {
321 for (name, variant) in v.as_map().unwrap_or(&[]) {
322 match format_shape(variant, types, regexes) {
323 Ok(shape) => variants.push((
324 name.value.clone(),
325 FormatVariant {
326 shape,
327 example: variant
328 .get("example")
329 .and_then(|d| d.as_str())
330 .unwrap_or_default()
331 .to_string(),
332 },
333 )),
334 Err(e) => problems.push((
335 variant.line,
336 e.prefixed(&format!("variant `{}`: ", name.value)),
337 )),
338 }
339 }
340 }
341 let shape = if entry.get("variants").is_some() {
342 None
343 } else {
344 match format_shape(entry, types, regexes) {
345 Ok(s) => Some(s),
346 Err(e) => {
347 problems.push((entry.line, e));
348 None
349 }
350 }
351 };
352 let example = entry
353 .get("example")
354 .and_then(|d| d.as_str())
355 .map(str::to_string);
356 match (&example, entry.get("variants")) {
359 (Some(_), Some(_)) => problems.push((
360 entry.line,
361 Problem::new(
362 "TL_110",
363 "a format with `variants` has no `example` of its own: each variant's example speaks for its shape",
364 ),
365 )),
366 (None, None) => problems.push((
367 entry.line,
368 Problem::new("TL_40", "a format with one shape carries an `example`"),
369 )),
370 _ => {}
371 }
372 let format = Format {
373 shape,
374 variants,
375 key: entry
376 .get("key")
377 .and_then(|k| k.as_str())
378 .map(str::to_string),
379 prohibited: prohibitions(entry, regexes),
380 example,
381 raw: entry.clone(),
382 };
383 if let Some(k) = &format.key {
384 for (name, s) in format.shapes() {
385 if !s.has_group(k) {
386 let whose =
387 name.map_or_else(|| "the shape".to_string(), |n| format!("variant `{n}`"));
388 problems.push((
389 entry.line,
390 Problem::new(
391 "TL_31",
392 format!("`key` `{k}` names no hole or named group of {whose}"),
393 ),
394 ));
395 }
396 }
397 }
398 if !problems.is_empty() {
399 return Err(problems);
400 }
401 Ok(format)
402}