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: "11",
subsection: None,
name: "vowel_ye_separator",
standard_ref: "2024 Korean Braille Standard, Ch.1 Sec.5 Art.11",
description: "Insert separator ⠤ between vowel-ending syllable and 예 (ㅇ+ㅖ)",
};
const SEPARATOR: u8 = 36;
#[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()
&& korean.cho == 'ㅇ'
&& korean.jung == 'ㅖ'
{
result.push(SEPARATOR);
}
Ok(())
}
pub struct Rule11;
impl BrailleRule for Rule11 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::InterCharacter
}
fn priority(&self) -> u16 {
100
}
fn matches(&self, ctx: &RuleContext) -> bool {
let Some(korean) = ctx.as_korean() else {
return false;
};
if korean.jong.is_some() {
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),
}
}
#[test]
fn inserts_separator_for_a_ye() {
let current = make_korean('아');
let mut result = Vec::new();
apply(¤t, '예', &mut result).unwrap();
assert_eq!(result, vec![SEPARATOR]);
}
#[test]
fn inserts_separator_for_do_ye() {
let current = make_korean('도');
let mut result = Vec::new();
apply(¤t, '예', &mut result).unwrap();
assert_eq!(result, vec![SEPARATOR]);
}
#[test]
fn inserts_separator_for_seo_ye() {
let current = make_korean('서');
let mut result = Vec::new();
apply(¤t, '예', &mut result).unwrap();
assert_eq!(result, vec![SEPARATOR]);
}
#[test]
fn skips_when_current_has_jongseong() {
let current = make_korean('본');
assert!(current.jong.is_some());
let mut result = Vec::new();
apply(¤t, '예', &mut result).unwrap();
assert!(result.is_empty());
}
#[test]
fn skips_when_next_is_not_ye() {
let current = make_korean('아');
let mut result = Vec::new();
apply(¤t, '이', &mut result).unwrap();
assert!(result.is_empty());
}
#[test]
fn skips_when_next_is_non_korean() {
let current = make_korean('아');
let mut result = Vec::new();
apply(¤t, 'A', &mut result).unwrap();
assert!(result.is_empty());
}
#[test]
fn golden_test_alignment() {
let cases = vec![
("아예", "⠣⠤⠌"),
("도예", "⠊⠥⠤⠌"),
("뭐예요", "⠑⠏⠤⠌⠬"),
("서예", "⠠⠎⠤⠌"),
];
for (input, expected_unicode) in cases {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(
result, expected_unicode,
"Rule 11 golden test failed for input: {}",
input
);
}
}
#[test]
fn meta_is_correct() {
assert_eq!(META.section, "11");
assert_eq!(META.name, "vowel_ye_separator");
}
use rstest::rstest;
fn ctx_for_pair(syllable_pair: &str) -> crate::test_helpers::CtxOwned {
crate::test_helpers::CtxOwned::for_text(syllable_pair, false)
}
#[rstest]
#[case("아예", true)] #[case("도예", true)] #[case("본예", false)] #[case("아이", false)] fn rule11_matches_vowel_ye_pattern(#[case] input: &str, #[case] expected: bool) {
let mut owned = ctx_for_pair(input);
let ctx = owned.ctx_at(0);
assert_eq!(Rule11.matches(&ctx), expected, "input={input}");
}
#[test]
fn rule11_apply_emits_separator() {
let mut owned = ctx_for_pair("아예");
let mut ctx = owned.ctx_at(0);
let outcome = Rule11.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Continue));
assert_eq!(owned.result, vec![SEPARATOR]);
}
#[test]
fn rule11_phase_and_priority() {
assert!(matches!(Rule11.phase(), Phase::InterCharacter));
assert_eq!(Rule11.priority(), 100);
}
#[test]
fn rule11_matches_false_for_non_korean_ctx() {
let mut owned = crate::test_helpers::CtxOwned::for_text("Ax", false);
let ctx = owned.ctx_at(0);
assert!(!Rule11.matches(&ctx));
}
#[test]
fn rule11_matches_false_at_end_of_word() {
let mut owned = crate::test_helpers::CtxOwned::for_text("아", false);
let ctx = owned.ctx_at(0);
assert!(!Rule11.matches(&ctx));
}
}