use crate::error::{Error, Result};
use super::*;
pub(crate) fn tone_sandhi(project: &Path, language: &str, tones: &str) -> Result<()> {
let (_store, phonology) = open_phonology(project, language)?;
let system = phonology.tone.as_ref().ok_or_else(|| {
Error::Config(format!(
"language `{language}` declares no `tone` system in its Phonology block"
))
})?;
let input: Vec<String> = tones.split_whitespace().map(String::from).collect();
let surface = crate::conlang::phonology::tone_eval::apply_sandhi(system, &input);
println!("{}", surface.join(" "));
Ok(())
}
pub(crate) fn romanize_text(
project: &Path,
language: &str,
text: &str,
scheme: Option<&str>,
reverse: bool,
) -> Result<()> {
use crate::conlang::phonology::romanize;
let (_store, phonology) = open_phonology(project, language)?;
let scheme_ref = phonology.scheme(scheme).ok_or_else(|| {
Error::Config(match scheme {
Some(s) => format!("language `{language}` has no romanization scheme `{s}`"),
None => format!(
"language `{language}` declares no romanization schemes — add a `romanizations` \
block to its Phonology, or rely on the per-phoneme `romanize` field"
),
})
})?;
if reverse {
let seq = romanize::deromanize(scheme_ref, &phonology, text);
println!("/{}/", seq.join(""));
} else {
let seq: Vec<String> = text.split_whitespace().map(String::from).collect();
println!("{}", romanize::romanize(scheme_ref, &phonology, &seq));
}
Ok(())
}
pub(crate) fn stress_word(project: &Path, language: &str, word: &str) -> Result<()> {
use crate::conlang::phonology::{stress_eval, syllable};
let (_store, phonology) = open_phonology(project, language)?;
let rule = phonology.stress.clone().ok_or_else(|| {
Error::Config(format!(
"language `{language}` declares no `stress` rule in its Phonology block \
(e.g. `stress: \"penultimate\"`)"
))
})?;
let seq = phonology.segment(word);
let sylls = syllable::syllabify(&phonology, &seq);
let stressed = stress_eval::primary_stress(&rule, &sylls);
let g = |ipa: &String| {
phonology
.phoneme(ipa)
.map(|p| p.grapheme().to_string())
.unwrap_or_else(|| ipa.clone())
};
let out = sylls
.iter()
.enumerate()
.map(|(i, s)| {
let body: String = s.onset.iter().chain(&s.nucleus).chain(&s.coda).map(&g).collect();
if Some(i) == stressed {
format!("ˈ{body}")
} else {
body
}
})
.collect::<Vec<_>>()
.join(".");
println!("{out}");
Ok(())
}
pub(crate) fn scan_verse(project: &Path, language: &str, text: &str) -> Result<()> {
use crate::conlang::scansion;
let (store, hierarchy, lang_book) = open_lang_book(project, language)?;
let phonology = load_phonology(&store, &hierarchy, &lang_book)?.ok_or_else(|| {
Error::Config(format!(
"language `{language}` has no phoneme block yet — add a `phonemes` block \
under its `Phonology` chapter before scanning verse"
))
})?;
let entries = load_dictionary(&store, &hierarchy, &lang_book)?;
let lines = scansion::scan_text(&phonology, &entries, text);
if lines.is_empty() {
return Err(Error::Config("no verse to scan (the text is empty)".into()));
}
println!("scan · {language} (/ stressed × unstressed · flexible)\n");
for line in &lines {
let mut top: Vec<String> = Vec::new();
let mut bot: Vec<String> = Vec::new();
for (wi, w) in line.words.iter().enumerate() {
if wi > 0 {
top.push("|".into());
bot.push("|".into());
}
for s in &w.syllables {
let width = s.text.chars().count().max(1);
top.push(format!("{:<width$}", s.text, width = width));
bot.push(format!("{:^width$}", s.beat.glyph(), width = width));
}
}
println!(" {}", top.join(" "));
println!(" {}", bot.join(" "));
let n = line.beats().len();
match &line.meter {
Some(m) => println!(" → {} · {n} syllable(s) · fit {:.2}\n", m.name, m.conformance),
None => println!(" → irregular / free · {n} syllable(s)\n"),
}
}
if lines.len() > 1 {
match scansion::dominant_meter(&lines) {
Some(m) => println!("dominant metre · {} (fit {:.2})", m.name, m.conformance),
None => println!("dominant metre · irregular"),
}
}
Ok(())
}
pub(crate) fn ipa_surface(project: &Path, language: &str, word: &str) -> Result<()> {
let (_store, phonology) = open_phonology(project, language)?;
let underlying = phonology.segment(word);
let surface = crate::conlang::phonology::allophony_eval::surface_form(&phonology, &underlying);
let render_ipa = |seq: &[String]| seq.join("");
let render_roman = |seq: &[String]| -> String {
seq.iter()
.map(|ipa| {
phonology
.phoneme(ipa)
.map(|p| p.grapheme().to_string())
.unwrap_or_else(|| ipa.clone())
})
.collect()
};
println!("underlying /{}/", render_ipa(&underlying));
println!("surface [{}]", render_ipa(&surface));
println!("romanized {}", render_roman(&surface));
Ok(())
}
pub(crate) fn syllabify_word(project: &Path, language: &str, word: &str) -> Result<()> {
let (_store, phonology) = open_phonology(project, language)?;
let seq = phonology.segment(word);
let sylls = crate::conlang::phonology::syllable::syllabify(&phonology, &seq);
println!("{}", crate::conlang::phonology::syllable::render(&phonology, &sylls));
eprintln!(
"{} → {} syllable(s), {} phoneme(s)",
word,
sylls.len(),
seq.len()
);
Ok(())
}
pub(crate) fn generate_word(project: &Path, language: &str, role: &str, count: usize) -> Result<()> {
let role = crate::conlang::TemplateRole::parse(role).ok_or_else(|| {
Error::Config(format!(
"unknown role `{role}` — use root | prefix | suffix | infix | circumfix | compound"
))
})?;
let (_store, phonology) = open_phonology(project, language)?;
if phonology.templates_for(role).is_empty() {
return Err(Error::Config(format!(
"language `{language}` declares no `{}` templates in its Phonology block",
role.as_str()
)));
}
let words = crate::conlang::generate::word::generate_words(&phonology, role, count);
if words.is_empty() {
eprintln!(
"no words satisfied the constraints in {} attempts — loosen the phonotactic constraints",
count
);
return Ok(());
}
for w in &words {
println!("{w}");
}
eprintln!(
"generated {} / {} requested `{}` word(s) for {}",
words.len(),
count,
role.as_str(),
language
);
Ok(())
}