use crate::error::{Error, Result};
use crate::poetry::form::{FormsLibrary, PoemForm};
use crate::poetry::syllabify::{StressLevel, syllabify as syllabify_word};
use crate::prose::ProseLanguage;
pub fn syllabify(word: Option<&str>, line: Option<&str>, language: Option<&str>) -> Result<()> {
let lang = ProseLanguage::from_label(language.unwrap_or("en"));
let show = |w: &str| {
let clean = w.trim_matches(|c: char| !c.is_alphabetic());
if clean.is_empty() {
return;
}
let sylls = syllabify_word(clean, lang.clone());
let rendered: String = sylls
.iter()
.map(|s| {
let mark = if s.stress == StressLevel::Primary { "ˈ" } else { "" };
format!("{mark}{}", s.text)
})
.collect::<Vec<_>>()
.join("·");
println!(" {:<18} {rendered} ({} syl)", w, sylls.len());
};
if let Some(l) = line {
for w in l.split_whitespace() {
show(w);
}
Ok(())
} else if let Some(w) = word {
show(w);
Ok(())
} else {
Err(Error::Config("syllabify needs a <WORD> or --line \"…\"".into()))
}
}
pub fn metre(line: &str, form: Option<&str>, language: Option<&str>) -> Result<()> {
let lang = ProseLanguage::from_label(language.unwrap_or("en"));
let beats = crate::poetry::metre::line_to_beats(line, lang);
let pattern: String = beats.iter().map(|b| b.glyph().to_string()).collect::<Vec<_>>().join(" ");
println!(" {line}");
println!(" {pattern} ({} syllables)", beats.len());
match crate::poetry::metre::detect(&beats) {
Some(m) => println!(" → detected: {} (fit {:.2})", m.name, m.conformance),
None => println!(" → detected: irregular / free"),
}
if let Some(f) = form {
let lib = FormsLibrary::builtin();
let pf = lib
.localized(f, language.unwrap_or("en"))
.ok_or_else(|| Error::Config(format!("unknown form `{f}`")))?;
match crate::poetry::metre::Foot::parse(&pf.metre) {
Some(foot) if pf.feet > 0 => {
let scan = crate::poetry::metre::scan_line(&beats, foot, pf.feet as usize);
let extra = if scan.feminine_ending {
" · feminine ending"
} else if scan.catalectic {
" · catalectic (one short)"
} else {
""
};
println!(
" → declared {} ({} feet): {} of {} syllables, fit {:.2}{extra}",
pf.metre, pf.feet, scan.syllables, scan.expected_syllables, scan.conformance
);
}
_ => println!(" → form `{f}` declares no accentual-syllabic metre to check against"),
}
}
Ok(())
}
pub fn forms(form: Option<&str>, language: Option<&str>, new: bool, name: Option<&str>) -> Result<()> {
let lib = FormsLibrary::builtin();
if new {
let scaffold = PoemForm {
form: "custom".into(),
title: name.unwrap_or("my-form").to_string(),
metre: "iambic".into(),
feet: 5,
metre_tradition: "accentual_syllabic".into(),
rhyme_scheme: "ABAB".into(),
language: language.unwrap_or("en").to_string(),
..Default::default()
};
println!("// A custom-form scaffold — edit the fields, then paste this into a");
println!("// `poem:` sidecar, or into .inkhaven/custom-forms.hjson to reuse it.");
print!("{}", scaffold.to_poem_block());
return Ok(());
}
if let Some(f) = form {
let lang = language.unwrap_or("en");
let pf = lib.localized(f, lang).ok_or_else(|| {
Error::Config(format!(
"unknown form `{f}` — run `inkhaven poetry forms` to list the {} available",
lib.all().len()
))
})?;
print!("{}", pf.to_poem_block());
return Ok(());
}
println!(
"poetry forms — `--form <name> [--language en|ru|fr|de|es]` prints a poem: block:\n"
);
for pf in lib.all() {
println!(" {:<22} {}", pf.form, pf.desc);
}
Ok(())
}