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: "22",
subsection: None,
name: "middle_korean_fortis_series",
standard_ref: "2024 Korean Braille Standard, Ch.3 Art.22",
description: "Middle Korean fortis/cluster legacy syllable glyphs",
};
const OLD_CONSONANT_BODIES_RULE22: &[(char, &str)] = &[
('ㅲ', "⠐⠘⠈"), ('ㅳ', "⠐⠘⠊"), ('ᄡ', "⠐⠘⠠"), ('ㅶ', "⠐⠘⠨"), ('ㅷ', "⠐⠘⠓"), ('ㅴ', "⠐⠘⠠⠈"), ('ㅵ', "⠐⠘⠠⠊"), ('ㅺ', "⠐⠠⠈"), ('ㅻ', "⠐⠠⠉"), ('ㅼ', "⠐⠠⠊"), ('ㅽ', "⠐⠠⠘"), ('ㅾ', "⠐⠠⠨"), ];
fn old_consonant_body_rule22(c: char) -> Option<&'static [u8]> {
static CACHE: std::sync::OnceLock<Vec<(char, Vec<u8>)>> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| {
OLD_CONSONANT_BODIES_RULE22
.iter()
.map(|(c, s)| (*c, encode_unicode_cells(s)))
.collect()
});
cache
.iter()
.find(|(candidate, _)| *candidate == c)
.map(|(_, bytes)| bytes.as_slice())
}
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))
}
pub struct Rule22;
impl BrailleRule for Rule22 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
58
}
fn matches(&self, ctx: &RuleContext) -> bool {
matches!(ctx.char_type, CharType::KoreanPart(c) | CharType::Symbol(c)
if old_consonant_body_rule22(*c).is_some())
|| matches!(ctx.char_type, CharType::Symbol(c) if encode_legacy(*c).is_some())
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
if let CharType::KoreanPart(c) | CharType::Symbol(c) = ctx.char_type
&& let Some(body) = old_consonant_body_rule22(*c)
{
let is_symbol_fn = |ch: char| matches!(CharType::new(ch), Ok(CharType::Symbol(_)));
let prefix = crate::rules::korean::rule_8::determine_prefix(
ctx.word_len(),
ctx.index,
ctx.word_chars,
ctx.has_korean_char,
is_symbol_fn,
);
ctx.emit(prefix);
ctx.emit_slice(body);
return Ok(RuleResult::Consumed);
}
if ctx.current_char() == '禽' && ctx.next_char() == Some('은') {
ctx.emit_slice(&encode_unicode_cells("⠈⠪⠢⠵"));
*ctx.skip_count = 1;
return Ok(RuleResult::Consumed);
}
let CharType::Symbol(c) = ctx.char_type else {
return Ok(RuleResult::Skip);
};
let Some(encoded) = encode_legacy(*c) else {
return Ok(RuleResult::Skip);
};
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 = Rule22.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Skip));
}
#[test]
fn apply_returns_skip_for_unknown_symbol() {
let mut owned = crate::test_helpers::CtxOwned::for_text(".", false);
let mut ctx = owned.ctx_at(0);
let outcome = Rule22.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Skip));
}
#[test]
fn apply_emits_for_old_consonant_body() {
let mut owned = crate::test_helpers::CtxOwned::for_text("ㅺ", false);
let mut ctx = owned.ctx_at(0);
let outcome = Rule22.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Consumed));
assert!(!owned.result.is_empty());
}
}