use super::feature::PhonemeFeature;
use super::table::{PhonemeTab, PhonemeTabList};
use super::{PH_LIQUID, PH_PAUSE, PH_STRESS, PH_VOWEL};
pub fn parse_phoneme_dsl(source: &str) -> Vec<PhonemeTabList> {
let mut tables: Vec<PhonemeTabList> = Vec::new();
let mut cur: Option<PhonemeTab> = None; let mut next_code: u8 = 1;
for raw in source.lines() {
let line = match raw.find("//") {
Some(i) => &raw[..i],
None => raw,
};
let line = line.trim();
if line.is_empty() {
continue;
}
let mut tokens = line.split_whitespace();
let Some(head) = tokens.next() else { continue };
match head {
"phonemetable" => {
flush_phoneme(&mut cur, tables.last_mut());
let name = tokens.next().unwrap_or("").to_string();
let base = tokens.next();
let includes = base
.and_then(|b| tables.iter().position(|t| t.name == b))
.map(|i| (i + 1) as u8)
.unwrap_or(0);
tables.push(PhonemeTabList { name, phonemes: Vec::new(), n_phonemes: 0, includes });
next_code = 1;
}
"phoneme" => {
flush_phoneme(&mut cur, tables.last_mut());
if tables.is_empty() {
tables.push(PhonemeTabList { name: String::new(), phonemes: Vec::new(), n_phonemes: 0, includes: 0 });
next_code = 1;
}
let mnem = tokens.next().unwrap_or("");
let mut ph = PhonemeTab { mnemonic: PhonemeTab::pack_mnemonic(mnem), code: next_code, ..Default::default() };
next_code = next_code.wrapping_add(1);
apply_tokens(&mut ph, tokens);
cur = Some(ph);
}
"endphoneme" => {
flush_phoneme(&mut cur, tables.last_mut());
}
"length" => {
if let (Some(ph), Some(v)) = (cur.as_mut(), tokens.next().and_then(|t| t.parse::<u32>().ok())) {
ph.std_length = v.min(u8::MAX as u32) as u8;
}
}
"lengthmod" => {
if let (Some(ph), Some(v)) = (cur.as_mut(), tokens.next().and_then(|t| t.parse::<u32>().ok())) {
ph.length_mod = v.min(u8::MAX as u32) as u8;
}
}
_ => {
if let Some(ph) = cur.as_mut() {
apply_word(ph, head);
apply_tokens(ph, tokens);
}
}
}
}
flush_phoneme(&mut cur, tables.last_mut());
for t in &mut tables {
t.n_phonemes = t.phonemes.len();
}
tables
}
fn apply_tokens<'a>(ph: &mut PhonemeTab, tokens: impl Iterator<Item = &'a str>) {
for tok in tokens {
apply_word(ph, tok);
}
}
fn apply_word(ph: &mut PhonemeTab, word: &str) {
match word {
"pause" => ph.typ = PH_PAUSE,
"stress" => ph.typ = PH_STRESS,
"vowel" => ph.typ = PH_VOWEL, "liquid" => ph.typ = PH_LIQUID,
_ => {
if let Some(feat) = PhonemeFeature::from_str(word) {
let _ = ph.apply_feature(feat);
}
}
}
}
fn flush_phoneme(cur: &mut Option<PhonemeTab>, table: Option<&mut PhonemeTabList>) {
if let (Some(ph), Some(t)) = (cur.take(), table) {
t.phonemes.push(ph);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phoneme::table::{compile_phontab, PhonemeTab};
use crate::phoneme::{PH_STOP, PH_VOWEL, PH_VOICED, PH_VOICELESS, PHFLAG_ARTICULATION, PLACE_BILABIAL};
#[test]
fn parse_basic_phoneme_block() {
let src = "\
phonemetable base
phoneme _
\tpause
\tlength 20
endphoneme
phoneme p
\tvls blb stp
\tlengthmod 2
endphoneme
phoneme a
\tvwl
\tlength 180
endphoneme
";
let tables = parse_phoneme_dsl(src);
assert_eq!(tables.len(), 1);
let t = &tables[0];
assert_eq!(t.name, "base");
assert_eq!(t.phonemes.len(), 3);
assert_eq!(t.phonemes.iter().map(|p| p.code).collect::<Vec<_>>(), vec![1, 2, 3]);
assert_eq!(t.phonemes[0].mnemonic_str(), "_");
assert_eq!(t.phonemes[0].std_length, 20);
let p = &t.phonemes[1];
assert_eq!(p.mnemonic_str(), "p");
assert_eq!(p.typ, PH_STOP);
assert_eq!((p.phflags & PHFLAG_ARTICULATION) >> 16, PLACE_BILABIAL);
assert!(p.phflags & PH_VOICELESS != 0);
assert_eq!(p.length_mod, 2);
assert_eq!(t.phonemes[2].typ, PH_VOWEL);
assert_eq!(t.phonemes[2].std_length, 180);
}
#[test]
fn parse_then_compile_round_trips() {
let src = "\
phonemetable base
phoneme t
\tvls alv stp
endphoneme
phoneme d
\tvcd alv stp
endphoneme
phonemetable en base
phoneme a
\tvwl
\tlength 200
endphoneme
";
let tables = parse_phoneme_dsl(src);
assert_eq!(tables.len(), 2);
assert_eq!(tables[1].name, "en");
assert_eq!(tables[1].includes, 1);
let bytes = compile_phontab(&tables);
let reparsed = crate::phoneme::load::parse_phontab(&bytes).expect("reparse phontab");
assert_eq!(reparsed.len(), 2);
assert_eq!(reparsed[0].name, "base");
assert_eq!(reparsed[1].name, "en");
assert_eq!(reparsed[1].includes, 1);
let t = &reparsed[0].phonemes[0];
let d = &reparsed[0].phonemes[1];
assert_eq!(t.mnemonic_str(), "t");
assert_eq!(d.mnemonic_str(), "d");
assert_eq!(t.typ, PH_STOP);
assert_eq!(d.typ, PH_STOP);
assert!(t.phflags & PH_VOICELESS != 0);
assert!(d.phflags & PH_VOICED != 0);
let a = &reparsed[1].phonemes[0];
assert_eq!(a.typ, PH_VOWEL);
assert_eq!(a.std_length, 200);
assert_eq!(a, &PhonemeTab { mnemonic: PhonemeTab::pack_mnemonic("a"), code: a.code, typ: PH_VOWEL, std_length: 200, ..Default::default() });
}
}