use crate::char_struct::CharType;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META: RuleMeta = RuleMeta {
section: "12",
subsection: None,
name: "vowel_ae_separator",
standard_ref: "2024 Korean Braille Standard, Ch.1 Sec.5 Art.12",
description: "Insert separator ⠤ between ㅑ/ㅘ/ㅜ/ㅝ and 애 (ㅇ+ㅐ)",
};
const SEPARATOR: u8 = 36; const TRIGGERING_VOWELS: [char; 4] = ['ㅑ', 'ㅘ', 'ㅜ', 'ㅝ'];
#[cfg(test)]
fn apply(
current: &crate::char_struct::KoreanChar,
next: char,
result: &mut Vec<u8>,
) -> Result<(), String> {
if let CharType::Korean(korean) = CharType::new(next)?
&& current.jong.is_none()
&& TRIGGERING_VOWELS.contains(¤t.jung)
&& korean.cho == 'ㅇ'
&& korean.jung == 'ㅐ'
{
result.push(SEPARATOR);
}
Ok(())
}
pub struct Rule12;
impl BrailleRule for Rule12 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::InterCharacter
}
fn priority(&self) -> u16 {
110 }
fn matches(&self, ctx: &RuleContext) -> bool {
let Some(korean) = ctx.as_korean() else {
return false;
};
if korean.jong.is_some() {
return false;
}
if !TRIGGERING_VOWELS.contains(&korean.jung) {
return false;
}
let Some(next) = ctx.next_char() else {
return false;
};
let Ok(CharType::Korean(next_k)) = CharType::new(next) else {
return false;
};
next_k.cho == 'ㅇ' && next_k.jung == 'ㅐ'
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
ctx.emit(SEPARATOR);
Ok(RuleResult::Continue)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::char_struct::KoreanChar;
fn make_korean(ch: char) -> KoreanChar {
match CharType::new(ch).unwrap() {
CharType::Korean(k) => k,
_ => panic!("Expected Korean character: {}", ch),
}
}
#[rstest::rstest]
#[case::ya_followed_by_ae('야', '애', vec![SEPARATOR])]
#[case::hwa_followed_by_aek('화', '액', vec![SEPARATOR])]
#[case::su_followed_by_aek('수', '액', vec![SEPARATOR])]
#[case::weo_followed_by_aem('워', '앰', vec![SEPARATOR])]
#[case::a_non_triggering('가', '애', vec![])]
#[case::eo_non_triggering('서', '애', vec![])]
#[case::current_has_jong_skipped('숙', '애', vec![])]
#[case::next_not_ae_skipped('야', '이', vec![])]
fn rule12_apply_separator_paths(
#[case] current_syllable: char,
#[case] next_char: char,
#[case] expected: Vec<u8>,
) {
let current = make_korean(current_syllable);
let mut result = Vec::new();
apply(¤t, next_char, &mut result).unwrap();
assert_eq!(result, expected);
}
#[rstest::rstest]
#[case::ya_ae("야애", "⠜⠤⠗")]
#[case::sohwa_aek("소화액", "⠠⠥⠚⠧⠤⠗⠁")]
#[case::su_aek("수액", "⠠⠍⠤⠗⠁")]
fn golden_test_alignment(#[case] input: &str, #[case] expected: &str) {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(
result, expected,
"Rule 12 golden test failed for input: {input}"
);
}
#[test]
fn meta_is_correct() {
assert_eq!(META.section, "12");
assert_eq!(META.name, "vowel_ae_separator");
}
use rstest::rstest;
#[rstest]
#[case("야애", true)] #[case("화애", true)] #[case("아애", false)] #[case("어애", false)] #[case("관애", false)] #[case("야이", false)] #[case("A", false)] #[case("야", false)] fn rule12_matches_triggering_vowel_ae(#[case] input: &str, #[case] expected: bool) {
let mut owned = crate::test_helpers::CtxOwned::for_text(input, false);
let ctx = owned.ctx_at(0);
assert_eq!(Rule12.matches(&ctx), expected, "input={input}");
}
}