braillify 2.0.1

Rust 기반 크로스플랫폼 한국어 점역 라이브러리
Documentation
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: "26",
    subsection: None,
    name: "middle_korean_after_hanja",
    standard_ref: "2024 Korean Braille Standard, Ch.3 Art.26",
    description: "Middle Korean legacy syllables used after Hanja readings",
};

const MAPPINGS: &[(char, &str)] = &[
    ('', "⠘⠿"),
    ('', "⠚⠧"),
    ('', ""),
    ('', ""),
    ('', "⠐⠐⠼⠂"),
    ('', "⠐⠨⠝"),
    ('', "⠐⠼⠗⠐⠲"),
    ('', "⠐⠼"),
    ('', "⠊⠐⠼⠗"),
    ('', "⠊⠐⠼"),
    ('', "⠐⠐⠼"),
];

fn encode_unicode_cells(unicode: &str) -> Vec<u8> {
    unicode
        .chars()
        .map(crate::unicode::decode_unicode)
        .collect()
}

fn encode_legacy(c: char) -> Option<Vec<u8>> {
    MAPPINGS
        .iter()
        .find(|(candidate, _)| *candidate == c)
        .map(|(_, unicode)| encode_unicode_cells(unicode))
}

fn is_standalone_i_after_hanja(ctx: &RuleContext) -> bool {
    ctx.current_char() == '' && matches!(ctx.prev_char(), Some('' | ''))
}

pub struct Rule26;

impl BrailleRule for Rule26 {
    fn meta(&self) -> &'static RuleMeta {
        &META
    }

    fn phase(&self) -> Phase {
        Phase::CoreEncoding
    }

    fn priority(&self) -> u16 {
        60
    }

    fn matches(&self, ctx: &RuleContext) -> bool {
        matches!(ctx.char_type, CharType::Symbol(c) | CharType::KoreanPart(c) if encode_legacy(*c).is_some())
            || is_standalone_i_after_hanja(ctx)
    }

    fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
        let c = match ctx.char_type {
            CharType::Symbol(c) | CharType::KoreanPart(c) => *c,
            _ => return Ok(RuleResult::Skip),
        };

        if is_standalone_i_after_hanja(ctx) {
            ctx.emit_slice(&encode_unicode_cells("⠸⠕"));
            return Ok(RuleResult::Consumed);
        }

        // `matches()` requires `encode_legacy(c).is_some()` OR
        // `is_standalone_i_after_hanja`. The latter is already handled at line 72.
        // So reaching here implies `encode_legacy(c)` returns Some.
        let encoded = encode_legacy(c).expect(
            "matches() guarantees encode_legacy returns Some when standalone-i path didn't fire",
        );
        ctx.emit_slice(&encoded);
        Ok(RuleResult::Consumed)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn apply_skips_non_korean() {
        let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
        let mut ctx = owned.ctx_at(0);
        let outcome = Rule26.apply(&mut ctx).unwrap();
        assert!(matches!(outcome, RuleResult::Skip));
    }

    /// 제26항 — 한자(火) 뒤에 단독 모음 ㅣ가 나오면 `⠸⠕` 점역. Triggers the
    /// `is_standalone_i_after_hanja` branch (line 72-75).
    #[test]
    fn apply_standalone_i_after_hanja() {
        let mut owned = crate::test_helpers::CtxOwned::for_text("火ㅣ", false);
        let mut ctx = owned.ctx_at(1);
        let outcome = Rule26.apply(&mut ctx).unwrap();
        assert!(matches!(outcome, RuleResult::Consumed));
        assert!(!owned.result.is_empty());
    }

    /// 제26항 — Symbol entry in MAPPINGS (烽) emits the legacy mapping
    /// (line 77-82).
    #[test]
    fn apply_emits_for_legacy_symbol_in_mappings() {
        let mut owned = crate::test_helpers::CtxOwned::for_text("", false);
        let mut ctx = owned.ctx_at(0);
        let outcome = Rule26.apply(&mut ctx).unwrap();
        assert!(matches!(outcome, RuleResult::Consumed));
        assert!(!owned.result.is_empty());
    }
}