use std::collections::BTreeMap;
use regex::Regex;
use super::{Deriver, deriver::push_unique};
use crate::{
check,
grammar::{Block, DerivationSource, KeyDerivation, Shape, Text, Unit, squash},
text,
};
#[derive(Debug, Clone)]
pub(super) struct KeyReader {
pub regex: Regex,
pub name: String,
}
impl KeyReader {
pub(super) fn of(shape: &Shape, name: &str) -> Option<KeyReader> {
Some(KeyReader {
regex: shape.regex()?.clone(),
name: name.to_string(),
})
}
}
impl Deriver<'_> {
pub(super) fn unit_texts(&mut self, block: &Block, rule: Option<&Unit>) -> (Vec<String>, bool) {
let line = rule.map_or(block.common.line, |r| r.common.line);
if let Some(r) = rule {
match &r.shape {
Some(Shape::Text(Text::Literal(t))) => return (vec![t.clone()], false),
Some(Shape::Text(Text::From(key))) => {
let value = self.values.get(key).cloned().unwrap_or_else(|| {
self.refuse_on(
line,
"TLMD_155",
format!("a unit rule takes its text from `{key}`, which no frontmatter writes"),
);
String::new()
});
return (vec![value], false);
}
_ => {}
}
if !r.examples.is_empty() {
return (r.examples.clone(), r.example_list);
}
if let Some(name) = &r.format {
if let Some(format) = self.schema.core.formats.get(name) {
return (vec![format.first_example().to_string()], false);
}
self.refuse_on(
line,
"TL_97",
format!("a unit rule binds `{name}`, which resolves to no format"),
);
return (vec![String::new()], false);
}
}
if !block.examples.is_empty() {
return (block.examples.clone(), block.example_list);
}
let constrained = rule.is_some_and(|r| r.has_shape() || r.key.is_some());
let what = if constrained {
"a unit rule declares a shape and no `example`; give the rule an `example`".to_string()
} else {
format!(
"{} `{}` block has no `example`, and the scaffold writes no text of its own; give the block an `example`",
text::article(block.kind.name_in(&self.dialect)),
block.kind.name_in(&self.dialect)
)
};
self.refuse(line, what);
(vec![String::new()], false)
}
pub(super) fn unit_text_at(&mut self, block: &Block, rule: Option<&Unit>, i: usize) -> String {
let (mut texts, _) = self.unit_texts(block, rule);
let at = i.min(texts.len() - 1);
texts.swap_remove(at)
}
pub(super) fn key_reader(&self, rule: &Unit, text: &str) -> Option<KeyReader> {
if let Some(shape) = &rule.shape {
return KeyReader::of(shape, rule.key.as_deref()?);
}
let format = self.schema.core.formats.get(rule.format.as_ref()?)?;
let name = rule.key.as_ref().or(format.key.as_ref())?;
let (shape, _) = format.shape_for(&squash(text))?;
KeyReader::of(shape, name)
}
pub(super) fn captured(&self, rule: &Unit, text: &str) -> Option<String> {
capture_of(&self.key_reader(rule, text)?, text)
}
pub(super) fn record_capture(&mut self, block: &Block, rule: &Unit, text: &str) {
let key = self.captured(rule, text);
self.record(rule.rule_id.as_ref().or(block.rule_id.as_ref()), key);
}
pub(super) fn derived_key(&mut self, rule: &Unit, text: String) -> String {
let Some(kd) = &rule.key_derives_from else {
return text;
};
let Some(reader) = self.key_reader(rule, &text) else {
return text;
};
self.derive_with(kd, &reader, text, rule.common.line)
}
pub(super) fn derive_with(
&mut self,
kd: &KeyDerivation,
reader: &KeyReader,
text: String,
line: usize,
) -> String {
let Some(key) = capture_of(reader, &text) else {
return text;
};
let split = key.split_once(&kd.separator);
let parents = match &kd.source {
DerivationSource::Rule(r) => self.keys_of(r),
DerivationSource::From(k) => self.values.get(k).cloned().into_iter().collect(),
};
if split.is_some_and(|(p, _)| parents.iter().any(|q| q == p)) {
return text;
}
let Some(parent) = parents.first() else {
return text;
};
let tail = split.map_or("1", |(_, t)| t);
let want = format!("{parent}{}{tail}", kd.separator);
match respell(reader, &text, &want) {
Some(spliced) => spliced,
None => {
self.refuse_on(
line,
"TLMD_154",
format!(
"a key deriving from {} cannot be written: `{key}` does not read as `{want}` where `{}` takes it",
kd.source_label(),
reader.name
),
);
text
}
}
}
pub(super) fn record(&mut self, rule_id: Option<&String>, key: Option<String>) {
let id = rule_id.or(self.section_rule.as_ref());
if let (Some(id), Some(key)) = (id, key) {
self.keys.entry(id.clone()).or_default().push(key);
}
}
pub(super) fn keys_of(&self, rule_id: &str) -> Vec<String> {
self.keys.get(rule_id).cloned().unwrap_or_default()
}
pub(super) fn keys_in_scope(&self, rule_ids: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for rule_id in rule_ids {
let from = self
.scope
.as_ref()
.and_then(|s| s.get(rule_id).copied())
.unwrap_or(0);
let keys = self.keys.get(rule_id).into_iter().flatten().skip(from);
push_unique(&mut out, keys.cloned());
}
out
}
pub(super) fn key_counts(&self) -> BTreeMap<String, usize> {
self.keys
.iter()
.map(|(k, v)| (k.clone(), v.len()))
.collect()
}
pub(super) fn paired_units(&mut self, block: &Block, rule: &Unit) -> Option<Vec<String>> {
let with = rule.for_each_key_of.as_deref()?;
let example = self.unit_text_at(block, Some(rule), 0);
let reader = self.key_reader(rule, &example)?;
let mut out = Vec::new();
for key in self.keys_in_scope(with) {
match respell(&reader, &example, &key) {
Some(text) => out.push(text),
None => self.refuse_on(
rule.common.line,
"TLMD_154",
format!(
"a rule demands one unit per key of {}, and its example `{example}` does not carry `{key}` where `{}` reads it",
check::rules_named(with),
reader.name
),
),
}
}
Some(out)
}
}
pub(super) fn capture_of(reader: &KeyReader, text: &str) -> Option<String> {
reader
.regex
.captures(text.trim_start())?
.name(&reader.name)
.map(|m| m.as_str().to_string())
}
pub(super) fn respell(reader: &KeyReader, text: &str, want: &str) -> Option<String> {
let body = text.trim_start();
let indent = &text[..text.len() - body.len()];
let at = reader.regex.captures(body)?.name(&reader.name)?;
let mut out = String::with_capacity(text.len());
out.push_str(indent);
out.push_str(&body[..at.start()]);
out.push_str(want);
out.push_str(&body[at.end()..]);
(capture_of(reader, &out).as_deref() == Some(want)).then_some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_splice_keeps_the_shape_or_gives_up() {
let p = KeyReader {
regex: Regex::new(r"^(?P<id>[A-Z]+-\d+): ").unwrap(),
name: "id".into(),
};
assert_eq!(
respell(&p, "EX-1: a thing", "EX-2").as_deref(),
Some("EX-2: a thing")
);
assert_eq!(respell(&p, "EX-1: a thing", "nope"), None);
assert_eq!(respell(&p, "no key here", "EX-2"), None);
}
}