use serde::{Deserialize, Serialize};
pub const FORMS_HJSON: &str = include_str!("../../assets/poetry/forms.hjson");
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PoemForm {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub title: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub form: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub desc: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub metre: String,
#[serde(default)]
pub feet: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub metre_tradition: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub rhyme_scheme: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub language: String,
#[serde(default)]
pub stanzas: u32,
#[serde(default)]
pub lines_per_stanza: u32,
#[serde(default)]
pub allow_pyrrhic: bool,
#[serde(default)]
pub require_final_stress: bool,
#[serde(default)]
pub elide_mute_e: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature_word: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suppress_while_drafting: Option<String>,
}
impl PoemForm {
#[allow(dead_code)]
pub fn from_hjson(src: &str) -> Option<PoemForm> {
#[derive(Deserialize)]
struct Wrap {
poem: PoemForm,
}
if let Ok(w) = serde_hjson::from_str::<Wrap>(src) {
if !w.poem.is_empty() {
return Some(w.poem);
}
}
match serde_hjson::from_str::<PoemForm>(src) {
Ok(f) if !f.is_empty() => Some(f),
_ => None,
}
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.form.is_empty() && self.metre.is_empty() && self.rhyme_scheme.is_empty()
}
pub fn to_poem_block(&self) -> String {
let mut s = String::from("poem: {\n");
if !self.desc.is_empty() {
s.push_str(&format!(" // {}\n", self.desc));
}
let str_field = |s: &mut String, k: &str, v: &str, quote: bool| {
if !v.is_empty() {
if quote {
s.push_str(&format!(" {k}: \"{v}\"\n"));
} else {
s.push_str(&format!(" {k}: {v}\n"));
}
}
};
str_field(&mut s, "title", &self.title, true);
str_field(&mut s, "form", &self.form, false);
str_field(&mut s, "metre", &self.metre, false);
if self.feet > 0 {
s.push_str(&format!(" feet: {}\n", self.feet));
}
str_field(&mut s, "metre_tradition", &self.metre_tradition, false);
str_field(&mut s, "rhyme_scheme", &self.rhyme_scheme, true);
str_field(&mut s, "language", &self.language, false);
if self.stanzas > 0 {
s.push_str(&format!(" stanzas: {}\n", self.stanzas));
}
if self.lines_per_stanza > 0 {
s.push_str(&format!(" lines_per_stanza: {}\n", self.lines_per_stanza));
}
if self.allow_pyrrhic {
s.push_str(" allow_pyrrhic: true\n");
}
if self.require_final_stress {
s.push_str(" require_final_stress: true\n");
}
if self.elide_mute_e {
s.push_str(" elide_mute_e: true\n");
}
if let Some(sw) = &self.signature_word {
s.push_str(&format!(" signature_word: \"{sw}\"\n"));
}
s.push_str("}\n");
s
}
}
pub fn localize(f: &mut PoemForm, lang: &str) {
f.language = lang.to_lowercase();
match f.language.as_str() {
"fr" => {
if f.metre_tradition == "accentual_syllabic" {
f.metre_tradition = "syllabic".into();
f.metre = "syllabic".into();
}
f.elide_mute_e = true;
}
"ru" => {
if f.metre_tradition == "accentual_syllabic" {
f.allow_pyrrhic = true;
f.require_final_stress = true;
}
}
_ => {}
}
}
pub struct FormsLibrary {
forms: Vec<PoemForm>,
}
impl FormsLibrary {
pub fn builtin() -> Self {
#[derive(Deserialize)]
struct FormsFile {
forms: Vec<PoemForm>,
}
let file: FormsFile =
serde_hjson::from_str(FORMS_HJSON).expect("bundled forms.hjson must parse");
FormsLibrary { forms: file.forms }
}
pub fn all(&self) -> &[PoemForm] {
&self.forms
}
pub fn get(&self, form: &str) -> Option<&PoemForm> {
self.forms.iter().find(|f| f.form.eq_ignore_ascii_case(form))
}
pub fn localized(&self, form: &str, lang: &str) -> Option<PoemForm> {
let mut f = self.get(form)?.clone();
localize(&mut f, lang);
Some(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_bundled_forms_parse() {
let lib = FormsLibrary::builtin();
assert_eq!(lib.all().len(), 18);
for f in lib.all() {
assert!(!f.form.is_empty(), "form missing name");
assert!(!f.desc.is_empty(), "form {} missing desc", f.form);
}
assert!(lib.get("sonnet").is_some());
assert!(lib.get("SONNET").is_some()); assert!(lib.get("nonesuch").is_none());
}
#[test]
fn russian_localization_adds_pyrrhic_and_final_stress() {
let lib = FormsLibrary::builtin();
let ru = lib.localized("sonnet", "ru").unwrap();
assert_eq!(ru.language, "ru");
assert!(ru.allow_pyrrhic);
assert!(ru.require_final_stress);
assert_eq!(ru.metre_tradition, "accentual_syllabic");
}
#[test]
fn french_localization_switches_to_syllabic() {
let lib = FormsLibrary::builtin();
let fr = lib.localized("sonnet", "fr").unwrap();
assert_eq!(fr.metre_tradition, "syllabic");
assert!(fr.elide_mute_e);
}
#[test]
fn poem_block_round_trips_through_from_hjson() {
let lib = FormsLibrary::builtin();
let ru = lib.localized("shakespearean_sonnet", "ru").unwrap();
let block = ru.to_poem_block();
let back = PoemForm::from_hjson(&block).expect("parse emitted block");
assert_eq!(back.form, "shakespearean_sonnet");
assert_eq!(back.language, "ru");
assert!(back.allow_pyrrhic);
assert_eq!(back.rhyme_scheme, ru.rhyme_scheme);
}
#[test]
fn from_hjson_reads_a_wrapper_and_rejects_junk() {
let f = PoemForm::from_hjson("{ poem: {\n form: haiku\n metre_tradition: syllabic\n} }")
.unwrap();
assert_eq!(f.form, "haiku");
assert!(PoemForm::from_hjson("{ unrelated: 3 }").is_none());
}
}