braillify 2.0.0

Rust 기반 크로스플랫폼 한국어 점역 라이브러리
Documentation
//! 제11항 — 모음자에 '예'가 붙어 나올 때에는 그 사이에 구분표 ⠤을 적어 나타낸다.
//!
//! When a vowel is followed by '예' (ㅇ+ㅖ), insert separator ⠤ (code 36) between them.
//! Condition: current syllable has no final consonant (jongseong).
//!
//! Reference: 2024 Korean Braille Standard, Chapter 1, Section 5, Article 11

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; //
/// Apply rule 11: insert ⠤ separator before 예 when preceded by a vowel-ending syllable.
///
/// # Arguments
/// * `current` - The current Korean syllable (already decomposed)
/// * `next` - The next raw character in the word
/// * `result` - The braille output buffer to append to
#[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(())
}

/// Plugin struct for the rule engine.
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() {
        // 아예: 아 (ㅇ+ㅏ, no jong) + 예 (ㅇ+ㅖ) → should insert 36
        let current = make_korean('');
        let mut result = Vec::new();
        apply(&current, '', &mut result).unwrap();
        assert_eq!(result, vec![SEPARATOR]);
    }

    #[test]
    fn inserts_separator_for_do_ye() {
        // 도예: 도 (ㄷ+ㅗ, no jong) + 예 (ㅇ+ㅖ)
        let current = make_korean('');
        let mut result = Vec::new();
        apply(&current, '', &mut result).unwrap();
        assert_eq!(result, vec![SEPARATOR]);
    }

    #[test]
    fn inserts_separator_for_seo_ye() {
        // 서예: 서 (ㅅ+ㅓ, no jong) + 예 (ㅇ+ㅖ)
        let current = make_korean('');
        let mut result = Vec::new();
        apply(&current, '', &mut result).unwrap();
        assert_eq!(result, vec![SEPARATOR]);
    }

    #[test]
    fn skips_when_current_has_jongseong() {
        // 본예: 본 (ㅂ+ㅗ+ㄴ) has jong → no separator
        let current = make_korean('');
        assert!(current.jong.is_some());
        let mut result = Vec::new();
        apply(&current, '', &mut result).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn skips_when_next_is_not_ye() {
        // 아이: next is 이, not 예
        let current = make_korean('');
        let mut result = Vec::new();
        apply(&current, '', &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(&current, 'A', &mut result).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn golden_test_alignment() {
        // From test_cases/rule_11.json
        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");
    }
}