use crate::char_struct::CharType;
use crate::korean_part;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META_8: RuleMeta = RuleMeta {
section: "8",
subsection: None,
name: "standalone_jamo",
standard_ref: "2024 Korean Braille Standard, Ch.1 Sec.4 Art.8",
description: "Standalone jamo: prefix with 온표 ⠿ (63), consonants as jongseong",
};
pub const ONTAB: u8 = 63;
pub const WORD_ATTACHED_PREFIX: u8 = 56;
pub fn determine_prefix<F>(
word_len: usize,
char_index: usize,
word_chars: &[char],
has_korean_char: bool,
is_symbol: F,
) -> u8
where
F: Fn(char) -> bool,
{
match word_len {
1 => ONTAB, 2 => ONTAB, _ => {
let is_first_with_ja = char_index == 0 && word_len > 1 && word_chars[1] == '자';
let is_bordered_by_symbols = {
let prev_is_symbol_or_start =
char_index == 0 || (char_index > 0 && is_symbol(word_chars[char_index - 1]));
let next_is_symbol_or_end = word_len - 1 == char_index
|| (char_index < word_len - 1 && is_symbol(word_chars[char_index + 1]));
prev_is_symbol_or_start && next_is_symbol_or_end
};
if (is_first_with_ja || is_bordered_by_symbols) || !has_korean_char {
ONTAB } else {
WORD_ATTACHED_PREFIX }
}
}
}
pub struct Rule8;
impl BrailleRule for Rule8 {
fn meta(&self) -> &'static RuleMeta {
&META_8
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn matches(&self, ctx: &RuleContext) -> bool {
matches!(ctx.char_type, CharType::KoreanPart(_))
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let CharType::KoreanPart(c) = ctx.char_type else {
return Ok(RuleResult::Skip);
};
let is_symbol_fn = |ch: char| matches!(CharType::new(ch), Ok(CharType::Symbol(_)));
if is_jamo_numbering(ctx.index, ctx.word_chars) {
ctx.emit(ONTAB);
ctx.emit_slice(crate::jauem::jongseong::encode_jongseong(*c)?);
return Ok(RuleResult::Consumed);
}
let prefix = determine_prefix(
ctx.word_len(),
ctx.index,
ctx.word_chars,
ctx.has_korean_char,
is_symbol_fn,
);
ctx.emit(prefix);
ctx.emit_slice(korean_part::encode_korean_part(*c)?);
Ok(RuleResult::Consumed)
}
}
pub fn is_jamo_numbering(char_index: usize, word_chars: &[char]) -> bool {
word_chars.len() == 2 && char_index == 0 && word_chars[1] == '.'
}
#[cfg(test)]
mod tests {
use super::*;
fn not_symbol(_: char) -> bool {
false
}
fn is_sym(c: char) -> bool {
matches!(c, '.' | ',' | '(' | ')' | '[' | ']')
}
#[test]
fn standalone_single_char() {
assert_eq!(determine_prefix(1, 0, &['ㄱ'], false, not_symbol), ONTAB);
}
#[test]
fn jamo_numbering_format() {
let chars = ['ㄱ', '.'];
assert!(is_jamo_numbering(0, &chars));
assert_eq!(determine_prefix(2, 0, &chars, false, not_symbol), ONTAB);
}
#[test]
fn non_numbering_two_char() {
let chars = ['ㄱ', 'ㄴ'];
assert!(!is_jamo_numbering(0, &chars));
}
#[test]
fn attached_to_korean_word() {
let chars = ['가', 'ㄱ', '나'];
assert_eq!(
determine_prefix(3, 1, &chars, true, not_symbol),
WORD_ATTACHED_PREFIX
);
}
#[test]
fn bordered_by_symbols_uses_ontab() {
let chars = ['(', 'ㄱ', ')'];
assert_eq!(determine_prefix(3, 1, &chars, true, is_sym), ONTAB);
}
#[test]
fn first_with_ja_uses_ontab() {
let chars = ['ㄱ', '자', '도'];
assert_eq!(determine_prefix(3, 0, &chars, true, not_symbol), ONTAB);
}
#[test]
fn golden_test_alignment() {
let cases = vec![("ㄱ", "⠿⠁"), ("ㅏ", "⠿⠣")];
for (input, expected) in cases {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(result, expected, "Rule 8 golden test failed for: {}", input);
}
}
}