use std::collections::BTreeMap;
use super::{Deriver, deriver::push_unique};
use crate::{
RepoPath,
check::Host,
grammar::{Only, Scaffold, Schema, States},
markdown::Dialect,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
pub line: usize,
pub criterion: &'static str,
pub what: String,
}
pub fn no_template(type_name: &str, schema: Option<&RepoPath>, refusals: &[Refusal]) -> String {
let mut out = format!("the schema of `{type_name}` derives no scaffold:");
for r in refusals {
match schema {
Some(path) => out.push_str(&format!("\n {path}:{}: {}", r.line, r.what)),
None => out.push_str(&format!("\n line {}: {}", r.line, r.what)),
}
}
out.push_str("\nwrite the `example` each rule asks for");
out
}
pub struct Derived {
pub scaffolds: Vec<Scaffold>,
pub omitted: Vec<Refusal>,
}
pub fn derive(schema: &Schema, host: &dyn Host) -> std::result::Result<Derived, Vec<Refusal>> {
let mut derived = Derived {
scaffolds: Vec::new(),
omitted: Vec::new(),
};
let mut refusals = Vec::new();
for states in combinations(schema) {
let (result, omitted) = one(schema, &states, host);
push_unique(&mut derived.omitted, omitted);
match result {
Ok(document) => derived.scaffolds.push(Scaffold {
document,
lifecycle: (!states.0.is_empty()).then(|| Only(states.0.clone())),
line: schema.core.line,
}),
Err(r) => push_unique(&mut refusals, r),
}
}
if refusals.is_empty() {
Ok(derived)
} else {
Err(refusals)
}
}
pub fn derive_one(
schema: &Schema,
states: &States,
host: &dyn Host,
) -> std::result::Result<String, Vec<Refusal>> {
one(schema, states, host).0
}
fn one(
schema: &Schema,
states: &States,
host: &dyn Host,
) -> (std::result::Result<String, Vec<Refusal>>, Vec<Refusal>) {
let dialect = host.dialect();
let mut d = Deriver {
schema,
host,
dialect,
states,
keys: BTreeMap::new(),
kinds: Vec::new(),
values: BTreeMap::new(),
section_rule: None,
refusals: Vec::new(),
omitted: Vec::new(),
in_optional: false,
scope: None,
};
let mut lines = d.frontmatter();
if !lines.is_empty() {
lines.push(String::new());
}
lines.extend(d.body());
if !d.refusals.is_empty() {
return (Err(d.refusals), d.omitted);
}
merge_optional(&mut lines, &dialect);
let mut text = lines.join("\n");
while text.ends_with("\n\n") {
text.pop();
}
if !text.ends_with('\n') {
text.push('\n');
}
(Ok(text), d.omitted)
}
fn merge_optional(lines: &mut Vec<String>, dialect: &Dialect) {
let mut i = 0;
while i < lines.len() {
if !dialect.is_optional_close(&lines[i]) {
i += 1;
continue;
}
let mut j = i + 1;
while j < lines.len() && lines[j].trim().is_empty() {
j += 1;
}
if j < lines.len()
&& dialect.is_optional_marker(&lines[j])
&& !opens_section(&lines[j + 1..], dialect)
{
lines.splice(i..=j, [String::new()]);
while i > 0 && lines[i - 1].trim().is_empty() {
lines.remove(i - 1);
i -= 1;
}
while i + 1 < lines.len() && lines[i + 1].trim().is_empty() {
lines.remove(i + 1);
}
continue;
}
i += 1;
}
}
fn opens_section(rest: &[String], dialect: &Dialect) -> bool {
rest.iter()
.flat_map(|chunk| chunk.lines())
.find(|l| !l.trim().is_empty() && !dialect.is_guidance_marker(l))
.is_some_and(|l| l.trim_start().starts_with('#'))
}
fn combinations(schema: &Schema) -> Vec<States> {
let mut out = vec![States::default()];
for dim in &schema.core.lifecycle {
let mut next = Vec::new();
for base in &out {
for state in &dim.states {
let mut one = base.clone();
one.0.insert(dim.key.clone(), vec![state.clone()]);
next.push(one);
}
}
out = next;
}
out
}
pub fn derived_title(slug: &str) -> String {
let words = slug.split('-').filter(|w| !w.is_empty());
let joined = words.collect::<Vec<_>>().join(" ");
let mut chars = joined.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}