use crate::poetry::form::PoemForm;
#[derive(Debug, Clone, PartialEq)]
pub struct FormStatus {
pub lines_written: usize,
pub expected_lines: Option<usize>,
pub complete: bool,
pub issues: Vec<String>,
}
pub fn check_form(text: &str, form: &PoemForm) -> FormStatus {
let lines: Vec<String> =
text.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect();
let written = lines.len();
let expected = expected_lines(form);
let complete = expected.map(|e| written >= e).unwrap_or(false);
let mut issues = Vec::new();
match form.form.as_str() {
"villanelle" => check_villanelle(&lines, &mut issues),
"pantoum" => check_pantoum(&lines, &mut issues),
"ghazal" => check_ghazal(&lines, form, &mut issues),
f if f.contains("sonnet") => check_sonnet(&lines, &mut issues),
_ => {}
}
FormStatus { lines_written: written, expected_lines: expected, complete, issues }
}
fn expected_lines(form: &PoemForm) -> Option<usize> {
let n = match form.form.as_str() {
"sonnet" | "petrarchan_sonnet" | "shakespearean_sonnet" => 14,
"haiku" | "senryuu" => 3,
"tanka" => 5,
"limerick" => 5,
"villanelle" => 19,
_ => {
return if form.stanzas > 0 && form.lines_per_stanza > 0 {
Some((form.stanzas * form.lines_per_stanza) as usize)
} else {
None
};
}
};
Some(n)
}
fn check_villanelle(lines: &[String], issues: &mut Vec<String>) {
if lines.len() < 6 {
return;
}
let a1 = lines[0].clone();
let a2 = lines.get(2).cloned().unwrap_or_default();
let refrain = |slots: &[usize], refr: &str, which: &str, issues: &mut Vec<String>| {
if refr.is_empty() {
return;
}
for &p in slots {
if let Some(line) = lines.get(p) {
if !line.eq_ignore_ascii_case(refr) {
issues.push(format!(
"Villanelle: line {} should repeat the {which} refrain “{refr}”.",
p + 1
));
}
}
}
};
refrain(&[5, 11, 17], &a1, "first", issues);
refrain(&[8, 14, 18], &a2, "third", issues);
}
fn check_pantoum(lines: &[String], issues: &mut Vec<String>) {
let stanzas = lines.len() / 4;
for s in 0..stanzas.saturating_sub(1) {
let (a, b) = (s * 4, (s + 1) * 4);
if lines[a + 1] != lines[b] {
issues.push(format!("Pantoum: line {} should repeat line {}.", b + 1, a + 2));
}
if lines[a + 3] != lines[b + 2] {
issues.push(format!("Pantoum: line {} should repeat line {}.", b + 3, a + 4));
}
}
}
fn check_ghazal(lines: &[String], form: &PoemForm, issues: &mut Vec<String>) {
let Some(sig) = form.signature_word.as_deref().filter(|s| !s.is_empty()) else {
return;
};
if let Some(last) = lines.last() {
if !last.to_lowercase().contains(&sig.to_lowercase()) {
issues.push(format!("Ghazal: the closing maqta should include the signature “{sig}”."));
}
}
}
fn check_sonnet(lines: &[String], issues: &mut Vec<String>) {
if lines.len() > 14 {
issues.push(format!("Sonnet: {} lines — a sonnet is fourteen.", lines.len()));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::poetry::form::PoemForm;
fn form(name: &str) -> PoemForm {
PoemForm { form: name.into(), ..Default::default() }
}
#[test]
fn completion_ratio_for_a_partial_sonnet() {
let text = "one\ntwo\nthree\nfour\nfive\nsix\nseven";
let st = check_form(text, &form("shakespearean_sonnet"));
assert_eq!(st.lines_written, 7);
assert_eq!(st.expected_lines, Some(14));
assert!(!st.complete);
}
#[test]
fn villanelle_flags_a_broken_refrain() {
let mut lines: Vec<String> = (1..=6).map(|i| format!("line {i}")).collect();
lines[0] = "the refrain returns".into();
let text = lines.join("\n");
let mut issues = Vec::new();
check_villanelle(&text.lines().map(str::to_string).collect::<Vec<_>>(), &mut issues);
assert!(issues.iter().any(|i| i.contains("line 6") && i.contains("refrain")));
}
#[test]
fn villanelle_clean_refrain_is_silent() {
let mut lines: Vec<String> = (1..=6).map(|i| format!("line {i}")).collect();
lines[0] = "the refrain".into();
lines[5] = "the refrain".into(); let mut issues = Vec::new();
check_villanelle(&lines, &mut issues);
assert!(issues.iter().all(|i| !i.contains("first refrain")));
}
#[test]
fn pantoum_flags_a_missing_carry_forward() {
let lines: Vec<String> = vec![
"a", "carried two", "b", "carried four", "different", "x", "y", "z",
]
.into_iter()
.map(String::from)
.collect();
let mut issues = Vec::new();
check_pantoum(&lines, &mut issues);
assert!(issues.iter().any(|i| i.contains("line 5")));
}
#[test]
fn ghazal_flags_a_missing_signature() {
let mut f = form("ghazal");
f.signature_word = Some("Ghalib".into());
let st = check_form("first couplet here\nsecond couplet ends", &f);
assert!(st.issues.iter().any(|i| i.contains("Ghalib")));
let ok = check_form("first couplet\nand Ghalib signs off", &f);
assert!(ok.issues.is_empty());
}
#[test]
fn open_form_has_no_expected_total() {
let st = check_form("a\nb\nc", &form("free_verse"));
assert_eq!(st.expected_lines, None);
assert!(!st.complete);
}
}