use serde::Serialize;
use crate::conlang::morphology::gloss;
use crate::conlang::types::Phonology;
use crate::conlang::types::morphology::Morphology;
use crate::language_entry::DictionaryEntry;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct IgtWord {
pub surface: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gloss: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Default)]
pub struct Igt {
pub text: String,
pub words: Vec<IgtWord>,
pub translation: String,
pub recognised: usize,
}
fn lexical_meaning(gloss: &str) -> &str {
gloss.split_once('-').map(|(base, _)| base).unwrap_or(gloss)
}
pub fn build(phon: &Phonology, morph: &Morphology, entries: &[DictionaryEntry], text: &str) -> Igt {
let index = gloss::build_index(phon, morph, entries);
let mut words = Vec::new();
let mut literal = Vec::new();
let mut recognised = 0;
for item in index.gloss_text(text) {
match &item.gloss {
Some(g) => {
recognised += 1;
literal.push(lexical_meaning(g).to_string());
}
None => literal.push(item.surface.clone()),
}
words.push(IgtWord { surface: item.surface, root: item.root, gloss: item.gloss });
}
Igt { text: text.to_string(), words, translation: literal.join(" "), recognised }
}
impl Igt {
pub fn render(&self) -> String {
let cells: Vec<(&str, &str)> = self
.words
.iter()
.map(|w| (w.surface.as_str(), w.gloss.as_deref().unwrap_or(w.surface.as_str())))
.collect();
let mut surface_line = String::new();
let mut gloss_line = String::new();
for (surface, gloss) in &cells {
let width = surface.chars().count().max(gloss.chars().count()) + 2;
surface_line.push_str(&pad(surface, width));
gloss_line.push_str(&pad(gloss, width));
}
format!(
"{}\n{}\n'{}'",
surface_line.trim_end(),
gloss_line.trim_end(),
self.translation
)
}
}
fn pad(s: &str, width: usize) -> String {
let n = s.chars().count();
let mut out = s.to_string();
out.extend(std::iter::repeat_n(' ', width.saturating_sub(n)));
out
}
#[cfg(test)]
mod tests {
use super::*;
fn phon() -> Phonology {
let body = r#"{
phonemes: [
{ ipa: "k", kind: "consonant" }, { ipa: "t", kind: "consonant" },
{ ipa: "n", kind: "consonant" }, { ipa: "l", kind: "consonant" },
{ ipa: "a", kind: "vowel" }, { ipa: "i", kind: "vowel" }, { ipa: "o", kind: "vowel" }
]
}"#;
Phonology::from_hjson(body).unwrap().unwrap()
}
fn morph() -> Morphology {
let body = r#"{
morphemes: [ { id: "pl", gloss: "PL", form: "i", position: "suffix" } ]
paradigms: [ { name: "noun", cells: [
{ features: {}, morphemes: [] }
{ features: {}, morphemes: ["pl"] }
] } ]
}"#;
Morphology::from_hjson(body).unwrap().unwrap()
}
fn entry(word: &str, gloss: &str, paradigm: Option<&str>) -> DictionaryEntry {
DictionaryEntry {
word: word.into(),
translation: gloss.into(),
paradigm: paradigm.map(String::from),
..Default::default()
}
}
fn lex() -> Vec<DictionaryEntry> {
vec![entry("kata", "stone", Some("noun")), entry("nilo", "friend", None)]
}
#[test]
fn glosses_a_recognised_sentence() {
let igt = build(&phon(), &morph(), &lex(), "katai nilo");
assert_eq!(igt.words.len(), 2);
assert_eq!(igt.words[0].gloss.as_deref(), Some("stone-PL"));
assert_eq!(igt.words[1].gloss.as_deref(), Some("friend"));
assert_eq!(igt.recognised, 2);
}
#[test]
fn the_literal_translation_drops_grammatical_tags() {
let igt = build(&phon(), &morph(), &lex(), "katai nilo");
assert_eq!(igt.translation, "stone friend");
}
#[test]
fn an_unrecognised_word_passes_through() {
let igt = build(&phon(), &morph(), &lex(), "xyz nilo");
assert!(igt.words[0].gloss.is_none());
assert_eq!(igt.recognised, 1);
assert!(igt.translation.starts_with("xyz"));
}
#[test]
fn render_aligns_the_surface_and_gloss_columns() {
let igt = build(&phon(), &morph(), &lex(), "katai nilo");
let block = igt.render();
let lines: Vec<&str> = block.lines().collect();
assert_eq!(lines.len(), 3);
let s_col = lines[0].find("nilo").unwrap();
let g_col = lines[1].find("friend").unwrap();
assert_eq!(s_col, g_col, "columns misaligned:\n{block}");
assert_eq!(lines[2], "'stone friend'");
}
}