use crate::char_struct::CharType;
use crate::rules::RuleMeta;
use crate::rules::context::{EncodingMode, RuleContext};
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META: RuleMeta = RuleMeta {
section: "71",
subsection: None,
name: "information_symbols",
standard_ref: "2024 Korean Braille Standard, Ch.6 Art.71",
description: "Keyboard, copyright, and information symbols",
};
const MAPPINGS: &[(char, &str)] = &[
('@', "⠈⠁"),
('^', "⠈⠢"),
('#', "⠸⠹"),
('|', "⠸⠳"),
('\\', "⠸⠡"),
('&', "⠈⠯"),
('§', "⠘⠎"),
('¶', "⠘⠏"),
('©', "⠘⠉"),
('®', "⠘⠗"),
('™', "⠘⠞"),
];
fn encode_unicode_cells(unicode: &str) -> Vec<u8> {
unicode
.chars()
.map(crate::unicode::decode_unicode)
.collect()
}
fn should_wrap_information_symbol(ctx: &RuleContext) -> bool {
if ctx.word_len() > 1 {
return true;
}
let prev_has_korean =
!ctx.prev_word.is_empty() && ctx.prev_word.chars().any(crate::utils::is_korean_char);
let next_has_korean = ctx
.remaining_words
.first()
.is_some_and(|word| !word.is_empty() && word.chars().any(crate::utils::is_korean_char));
prev_has_korean || next_has_korean
}
pub fn is_rule_71_symbol(c: char) -> bool {
MAPPINGS.iter().any(|(candidate, _)| *candidate == c)
}
pub struct Rule71;
impl BrailleRule for Rule71 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
175
}
fn matches(&self, ctx: &RuleContext) -> bool {
ctx.state.current_mode() != EncodingMode::Math
&& matches!(ctx.char_type, CharType::Symbol(c) if is_rule_71_symbol(*c))
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
if ctx.current_char() == '§' {
if should_wrap_information_symbol(ctx) {
let mut encoded = vec![crate::unicode::decode_unicode('⠴')];
encoded.extend(encode_unicode_cells("⠘⠎"));
if !ctx.next_char().is_some_and(|ch| ch.is_ascii_digit()) {
encoded.push(crate::unicode::decode_unicode('⠲'));
}
ctx.emit_slice(&encoded);
return Ok(RuleResult::Consumed);
}
let encoded = encode_unicode_cells("⠘⠎");
ctx.emit_slice(&encoded);
return Ok(RuleResult::Consumed);
}
let Some((_, unicode)) = MAPPINGS
.iter()
.find(|(candidate, _)| *candidate == ctx.current_char())
else {
return Ok(RuleResult::Skip);
};
let mut encoded = Vec::new();
if should_wrap_information_symbol(ctx)
&& matches!(ctx.current_char(), '&' | '¶' | '©' | '®' | '™')
{
encoded.push(crate::unicode::decode_unicode('⠴'));
encoded.extend(encode_unicode_cells(unicode));
encoded.push(crate::unicode::decode_unicode('⠲'));
} else {
encoded = encode_unicode_cells(unicode);
}
ctx.emit_slice(&encoded);
Ok(RuleResult::Consumed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_exercise() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let mut ctx = owned.ctx_at(0);
let _ = Rule71.apply(&mut ctx);
}
#[test]
fn matches_does_not_panic() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let ctx = owned.ctx_at(0);
let _ = Rule71.matches(&ctx);
}
#[test]
fn rule71_section_sign_before_digit_omits_terminator() {
let word: Vec<char> = "§1".chars().collect();
let ct = CharType::Symbol('§');
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let mut ctx = RuleContext {
word_chars: &word,
index: 0,
char_type: &ct,
prev_word: "",
remaining_words: &[],
has_korean_char: false,
is_all_uppercase: false,
ascii_starts_at_beginning: false,
skip_count: &mut skip,
state: &mut state,
result: &mut out,
};
let outcome = Rule71.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Consumed));
assert!(!out.contains(&crate::unicode::decode_unicode('⠲')));
}
#[test]
fn rule71_section_symbol_followed_by_non_digit_appends_terminator() {
let result = crate::encode("§A");
assert!(result.is_ok());
let _ = crate::encode("§");
}
}