diff --git a/src/normalize.rs b/src/normalize.rs index 15a0a4d..bfe5ed6 100644 --- a/src/normalize.rs +++ b/src/normalize.rs @@ -42,7 +42,7 @@ fn decompose(c: char, decompose_char: D, mut emit_char: F) } // Perform decomposition for Hangul - if is_hangul(c) { + if is_hangul_syllable(c) { decompose_hangul(c, emit_char); return; } @@ -77,7 +77,12 @@ const T_COUNT: u32 = 28; const N_COUNT: u32 = (V_COUNT * T_COUNT); const S_COUNT: u32 = (L_COUNT * N_COUNT); -pub(crate) fn is_hangul(c: char) -> bool { +const S_END: u32 = S_BASE + S_COUNT - 1; +const L_END: u32 = L_BASE + L_COUNT - 1; +const V_END: u32 = V_BASE + V_COUNT - 1; +const T_END: u32 = T_BASE + T_COUNT - 1; + +pub(crate) fn is_hangul_syllable(c: char) -> bool { (c as u32) >= S_BASE && (c as u32) < (S_BASE + S_COUNT) } @@ -85,18 +90,17 @@ pub(crate) fn is_hangul(c: char) -> bool { #[allow(unsafe_code)] #[inline(always)] fn decompose_hangul(s: char, mut emit_char: F) where F: FnMut(char) { - let si = s as u32 - S_BASE; - - let li = si / N_COUNT; + let s_index = s as u32 - S_BASE; + let l_index = s_index / N_COUNT; unsafe { - emit_char(char::from_u32_unchecked(L_BASE + li)); + emit_char(char::from_u32_unchecked(L_BASE + l_index)); - let vi = (si % N_COUNT) / T_COUNT; - emit_char(char::from_u32_unchecked(V_BASE + vi)); + let v_index = (s_index % N_COUNT) / T_COUNT; + emit_char(char::from_u32_unchecked(V_BASE + v_index)); - let ti = si % T_COUNT; - if ti > 0 { - emit_char(char::from_u32_unchecked(T_BASE + ti)); + let t_index = s_index % T_COUNT; + if t_index > 0 { + emit_char(char::from_u32_unchecked(T_BASE + t_index)); } } } @@ -112,20 +116,33 @@ pub(crate) fn hangul_decomposition_length(s: char) -> usize { #[allow(unsafe_code)] #[inline(always)] fn compose_hangul(a: char, b: char) -> Option { - let l = a as u32; - let v = b as u32; - // Compose an LPart and a VPart - if L_BASE <= l && l < (L_BASE + L_COUNT) // l should be an L choseong jamo - && V_BASE <= v && v < (V_BASE + V_COUNT) { // v should be a V jungseong jamo - let r = S_BASE + (l - L_BASE) * N_COUNT + (v - V_BASE) * T_COUNT; - return unsafe { Some(char::from_u32_unchecked(r)) }; + let (a, b) = (a as u32, b as u32); + match (a, b) { + // Compose a leading consonant and a vowel together into an LV_Syllable + (L_BASE ... L_END, V_BASE ... V_END) => { + let l_index = a - L_BASE; + let v_index = b - V_BASE; + let lv_index = l_index * N_COUNT + v_index * T_COUNT; + let s = S_BASE + lv_index; + Some(unsafe {char::from_u32_unchecked(s)}) + }, + // Compose an LV_Syllable and a trailing consonant into an LVT_Syllable + (S_BASE ... S_END, T_BASE ... T_END) if (a - S_BASE) % T_COUNT == 0 && (b - T_BASE) > 0 => { + Some(unsafe {char::from_u32_unchecked(a + (b - T_BASE))}) + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::compose_hangul; + + // Regression test from a bugfix where we were composing an LV_Syllable with + // T_BASE directly. (We should only compose an LV_Syllable with a character + // in the range `T_BASE + 1 ... T_END`.) + #[test] + fn test_hangul_composition() { + assert_eq!(compose_hangul('\u{c8e0}', '\u{11a7}'), None); } - // Compose an LVPart and a TPart - if S_BASE <= l && l <= (S_BASE+S_COUNT-T_COUNT) // l should be a syllable block - && T_BASE <= v && v < (T_BASE+T_COUNT) // v should be a T jongseong jamo - && (l - S_BASE) % T_COUNT == 0 { // l should be an LV syllable block (not LVT) - let r = l + (v - T_BASE); - return unsafe { Some(char::from_u32_unchecked(r)) }; - } - None } diff --git a/src/stream_safe.rs b/src/stream_safe.rs index 9ff0596..ee88c83 100644 --- a/src/stream_safe.rs +++ b/src/stream_safe.rs @@ -1,6 +1,6 @@ use normalize::{ hangul_decomposition_length, - is_hangul, + is_hangul_syllable, }; use tables; @@ -71,7 +71,7 @@ fn classify_nonstarters(c: char) -> Decomposition { } } // Next, special case Hangul, since it's not handled by our tables. - if is_hangul(c) { + if is_hangul_syllable(c) { return Decomposition { leading_nonstarters: 0, trailing_nonstarters: 0,