use crate::rules::context::EncodingMode;
use crate::{encode, english, utils, with_encoder};
pub(crate) fn is_ipa_phonetic_symbol(c: char) -> bool {
matches!(c, 'θ' | 'ə' | 'æ' | 'ŋ' | 'ː')
}
pub(crate) fn detect_ipa_context(text: &str) -> bool {
let mut has_group_start = false;
let mut has_ipa_symbol = false;
for c in text.chars() {
has_group_start |= matches!(c, '[' | '/');
has_ipa_symbol |= is_ipa_phonetic_symbol(c);
if has_group_start && has_ipa_symbol {
break;
}
}
if !has_group_start || !has_ipa_symbol {
return false;
}
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'[' => {
if let Some(rel) = chars[i + 1..].iter().position(|&c| c == ']') {
let inner: &[char] = &chars[i + 1..i + 1 + rel];
if inner.iter().any(|c| is_ipa_phonetic_symbol(*c)) {
return true;
}
i += rel + 2;
continue;
}
}
'/' => {
if let Some(rel) = chars[i + 1..].iter().position(|&c| c == '/') {
let inner: &[char] = &chars[i + 1..i + 1 + rel];
if inner.iter().any(|c| is_ipa_phonetic_symbol(*c)) {
return true;
}
i += rel + 2;
continue;
}
}
_ => {}
}
i += 1;
}
false
}
pub(crate) fn encode_ipa(text: &str) -> Result<Vec<u8>, String> {
let mut out: Vec<u8> = Vec::new();
let mut bracket_open = false;
let mut slash_open = false;
let mut korean_buf = String::new();
let has_korean_anywhere = text.chars().any(utils::is_korean_char);
let flush_korean = |buf: &mut String, out: &mut Vec<u8>| -> Result<(), String> {
if !buf.is_empty() {
let enc = if has_korean_anywhere {
with_encoder(true, |encoder| {
let mut result = Vec::new();
encoder.set_default_mode(EncodingMode::Korean);
encoder.encode(buf.as_str(), &mut result)?;
Ok::<Vec<u8>, String>(result)
})?
} else {
encode(buf.as_str())?
};
out.extend(enc);
buf.clear();
}
Ok(())
};
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'[' => {
flush_korean(&mut korean_buf, &mut out)?;
let _ = remove_trailing_english_terminator(&mut out);
out.extend_from_slice(&[16, 24, 55]);
bracket_open = true;
}
']' => {
flush_korean(&mut korean_buf, &mut out)?;
out.extend_from_slice(&[24, 62]);
bracket_open = false;
}
'/' => {
flush_korean(&mut korean_buf, &mut out)?;
if slash_open {
out.extend_from_slice(&[24, 12]);
slash_open = false;
} else {
let _ = remove_trailing_english_terminator(&mut out);
out.extend_from_slice(&[16, 24, 12]);
slash_open = true;
}
}
' ' => {
if (bracket_open || slash_open) && matches!(chars.peek(), Some('(')) {
continue;
}
flush_korean(&mut korean_buf, &mut out)?;
out.push(0);
}
'(' if bracket_open || slash_open => {
flush_korean(&mut korean_buf, &mut out)?;
let mut text = String::from("(");
for next in chars.by_ref() {
text.push(next);
if next == ')' {
break;
}
}
out.extend_from_slice(&[48, 48]);
out.push(0);
out.extend(encode(text.as_str())?);
out.push(0);
out.extend_from_slice(&[16, 48, 6]);
if matches!(chars.peek(), Some(' ')) {
chars.next();
}
}
_ if bracket_open || slash_open => {
flush_korean(&mut korean_buf, &mut out)?;
let bytes =
encode_ipa_char(ch).ok_or_else(|| format!("Unknown IPA character: {ch:?}"))?;
out.extend(bytes);
}
_ => {
korean_buf.push(ch);
}
}
}
flush_korean(&mut korean_buf, &mut out)?;
Ok(out)
}
fn remove_trailing_english_terminator(out: &mut Vec<u8>) -> bool {
let mut i = out.len();
while i > 0 && out[i - 1] == 0 {
i -= 1;
}
if i > 0 && out[i - 1] == 50 {
out.remove(i - 1);
true
} else {
false
}
}
pub(crate) fn encode_ipa_char(ch: char) -> Option<Vec<u8>> {
match ch {
'ə' => Some(vec![34]), 'ː' => Some(vec![18]), 'θ' => Some(vec![40, 57]), 'ŋ' => Some(vec![43]), 'æ' => Some(vec![41]), 'ɔ' => Some(vec![35]), 'ʃ' => Some(vec![49]), 'ð' => Some(vec![59]), 'ɪ' => Some(vec![12]), 'ɹ' => Some(vec![60]), 'ɾ' => Some(vec![22, 23]), 'ˈ' => Some(vec![56, 3]), 'ˌ' => Some(vec![56, 6]), 'č' => Some(vec![3, 8, 38]), _ => {
if let Ok(code) = english::encode_english(ch) {
Some(vec![code])
} else {
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cells(s: &str) -> Vec<u8> {
s.chars().map(crate::unicode::decode_unicode).collect()
}
#[test]
fn ipa_input_without_korean_uses_plain_encode() {
let _ = encode_ipa("think [θɪŋk]");
let _ = encode_ipa("[θ]/æ/");
}
#[rstest::rstest]
#[case::edh('ð', vec![59])]
#[case::open_o('ɔ', vec![35])]
#[case::esh('ʃ', vec![49])]
#[case::turned_r('ɹ', vec![60])]
#[case::primary_stress('ˈ', vec![56, 3])]
#[case::secondary_stress('ˌ', vec![56, 6])]
#[case::fish_hook_r('ɾ', vec![22, 23])]
#[case::c_wedge('č', vec![3, 8, 38])]
fn encodes_ueb_14_4_ipa_table_chars(#[case] ch: char, #[case] expected: Vec<u8>) {
assert_eq!(encode_ipa_char(ch), Some(expected));
}
#[test]
fn bracket_open_flushes_prior_korean_and_strips_english_terminator() {
let bracket_first = encode_ipa("[θ]").expect("initial IPA bracket should encode");
assert!(bracket_first.starts_with(&[16, 24, 55]));
let korean_before = encode_ipa("한[θ]").expect("Korean before IPA bracket should encode");
assert!(korean_before.starts_with(&crate::encode("한").unwrap()));
assert!(korean_before.contains(&16));
let english_before =
encode_ipa("word[θ]").expect("English before IPA bracket should encode");
assert!(!english_before[..english_before.len() - 3].contains(&50));
}
#[test]
fn bracket_open_after_plain_english_flushes_buffer() {
let encoded = encode_ipa("word[θ]").expect("English before IPA bracket should encode");
let bracket_pos = encoded
.windows(3)
.position(|cells| cells == [16, 24, 55])
.expect("opening IPA bracket marker should be present");
assert!(bracket_pos > 0);
}
#[test]
fn bracket_open_after_buffer_flushes_then_opens_group() {
let encoded = encode_ipa("말[θ]").expect("Korean before IPA bracket should encode");
let prefix = crate::encode("말").expect("Korean prefix should encode");
assert!(encoded.starts_with(&prefix));
assert_eq!(&encoded[prefix.len()..prefix.len() + 3], &[16, 24, 55]);
}
#[test]
fn ipa_group_opening_removes_prior_english_terminator_after_korean_context() {
let bracketed = encode_ipa("말 word[θ]").expect("English before IPA bracket should encode");
let bracket_pos = bracketed
.windows(3)
.position(|cells| cells == [16, 24, 55])
.expect("opening IPA bracket marker should be present");
assert!(!bracketed[..bracket_pos].contains(&50));
let slashed = encode_ipa("말 word/θ/").expect("English before IPA slash should encode");
let slash_pos = slashed
.windows(3)
.position(|cells| cells == [16, 24, 12])
.expect("opening IPA slash marker should be present");
assert!(!slashed[..slash_pos].contains(&50));
}
#[test]
fn bracket_open_after_english_space_strips_terminator_before_group() {
let encoded =
encode_ipa("word [θ]").expect("English before spaced IPA bracket should encode");
let bracket_pos = encoded
.windows(3)
.position(|cells| cells == [16, 24, 55])
.expect("opening IPA bracket marker should be present");
assert!(!encoded[..bracket_pos].contains(&50));
}
#[test]
fn remove_trailing_english_terminator_skips_spaces_before_remove() {
let mut out = vec![1, 50, 0, 0];
assert!(remove_trailing_english_terminator(&mut out));
assert_eq!(out, vec![1, 0, 0]);
}
#[test]
fn bracketed_ipa_temporarily_switches_to_ueb_for_parentheses() {
let encoded = encode_ipa("[ðə (garbled section) dɪs]").expect("IPA should encode");
assert_eq!(encoded, cells("⠐⠘⠷⠻⠢⠰⠰⠀⠐⠣⠛⠜⠃⠇⠫⠀⠎⠑⠉⠰⠝⠐⠜⠀⠐⠰⠆⠙⠌⠎⠘⠾"));
}
#[test]
fn ipa_encode_char_none_for_unsupported() {
assert!(encode_ipa_char('\u{1F600}').is_none()); assert!(encode_ipa_char('한').is_none());
}
#[test]
fn plain_text_outside_ipa_group_buffers_until_end() {
let input = std::hint::black_box("plain");
let encoded = encode_ipa(input).expect("plain text should encode through normal encoder");
assert_eq!(
encoded,
crate::encode(input).expect("plain text should encode")
);
}
#[test]
fn runtime_plain_text_pushes_each_outside_group_char() {
let mut input = String::new();
input.push(std::hint::black_box('p'));
input.push(std::hint::black_box('q'));
let encoded = encode_ipa(&input).expect("plain runtime text should encode");
assert_eq!(
encoded,
crate::encode(&input).expect("plain text should encode")
);
}
#[test]
fn plain_text_after_ipa_group_buffers_until_final_flush() {
let encoded = encode_ipa("[θ]plain").expect("IPA then plain text should encode");
let suffix = crate::encode("plain").expect("plain suffix should encode");
assert!(encoded.ends_with(&suffix));
}
#[test]
fn plain_text_outside_slash_group_buffers_between_groups() {
let encoded = encode_ipa("/θ/plain").expect("IPA then plain text should encode");
let suffix = crate::encode("plain").expect("plain suffix should encode");
assert!(encoded.ends_with(&suffix));
}
#[test]
fn plain_text_between_ipa_groups_buffers_outside_group() {
let encoded =
encode_ipa("[θ]plain[θ]").expect("plain text between IPA groups should encode");
let plain = crate::encode("plain").expect("plain text should encode");
assert!(encoded.windows(plain.len()).any(|window| window == plain));
}
}