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 scan(text: &str, form_name: &str, language: Option<&str>) -> Result<()> {
use crate::inner_poet::fast::{Severity, scan_stanza};
let lang = language.unwrap_or("en");
let lib = FormsLibrary::builtin();
let form = lib
.localized(form_name, lang)
.ok_or_else(|| Error::Config(format!("unknown form `{form_name}` — run `inkhaven poetry forms`")))?;
let findings = scan_stanza(text, &form);
if findings.is_empty() {
println!("♪ no findings — the stanza matches its declared {} form.", form.form);
return Ok(());
}
for f in &findings {
let sev = match f.severity {
Severity::Praise => "Praise",
Severity::Note => "Note",
Severity::Concern => "Concern",
};
println!("♪ {sev:<8} [{}] {}", f.kind, f.message);
}
Ok(())
}
pub fn rhyme(w1: &str, w2: &str, language: Option<&str>) -> Result<()> {
use crate::poetry::rhyme::{RhymeQuality, RhymeType, analyse_rhyme};
let lang = ProseLanguage::from_label(language.unwrap_or("en"));
let r = analyse_rhyme(w1, w2, lang);
let quality = match r.quality {
RhymeQuality::Perfect => "perfect",
RhymeQuality::Near => "near",
RhymeQuality::Eye => "eye",
RhymeQuality::None => "no",
};
let rtype = match r.rhyme_type {
RhymeType::Masculine => "masculine",
RhymeType::Feminine => "feminine",
RhymeType::Dactylic => "dactylic",
};
if matches!(r.quality, RhymeQuality::None) {
println!(" {w1} / {w2}: no rhyme");
} else {
let shared = if r.shared.is_empty() { String::new() } else { format!(" on “-{}”", r.shared) };
println!(" {w1} / {w2}: {quality} {rtype} rhyme{shared}");
}
if let Some(note) = r.note {
println!(" ({note})");
}
Ok(())
}
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(())
}