| |
| |
| |
|
|
| use wasm_bindgen::prelude::*; |
| use unicode_normalization::UnicodeNormalization; |
| #[allow(unused_imports)] |
| use serde_json::{json, Value}; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| #[wasm_bindgen] |
| pub fn normalize(input: &str, form: &str) -> String { |
| match form.to_uppercase().as_str() { |
| "NFC" => input.nfc().collect::<String>(), |
| "NFKC" => input.nfkc().collect::<String>(), |
| _ => input.to_string(), |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[wasm_bindgen] |
| pub fn encode(input: &str) -> String { |
| let normalized = input.nfc().collect::<String>(); |
| let code_points: Vec<u32> = input.chars().map(|c| c as u32).collect(); |
| let utf8_bytes: Vec<u8> = input.as_bytes().to_vec(); |
| let length = input.chars().count(); |
| let byte_length = utf8_bytes.len(); |
|
|
| let result = json!({ |
| "normalized": normalized, |
| "codePoints": code_points, |
| "utf8Bytes": utf8_bytes, |
| "length": length, |
| "byteLength": byte_length |
| }); |
|
|
| result.to_string() |
| } |
|
|
| |
| |
| |
| #[wasm_bindgen] |
| pub fn has_astral_characters(input: &str) -> bool { |
| input.chars().any(|c| (c as u32) > 0xFFFF) |
| } |
|
|
| |
| |
| |
| #[wasm_bindgen] |
| pub fn has_combining_marks(input: &str) -> bool { |
| input.chars().any(|c| { |
| let code = c as u32; |
| |
| (code >= 0x0300 && code <= 0x036F) || |
| |
| (code >= 0x1AB0 && code <= 0x1AFF) || |
| |
| (code >= 0x1DC0 && code <= 0x1DFF) || |
| |
| (code >= 0xFE20 && code <= 0xFE2F) |
| }) |
| } |
|
|
| |
| |
| |
| #[wasm_bindgen] |
| pub fn detect_bidi_level(input: &str) -> String { |
| let mut has_rtl = false; |
| let mut has_ltr = false; |
|
|
| for c in input.chars() { |
| let code = c as u32; |
|
|
| |
| if (code >= 0x0590 && code <= 0x08FF) || |
| (code >= 0xFB1D && code <= 0xFB4F) || |
| (code >= 0xFB50 && code <= 0xFDFF) || |
| (code >= 0xFE70 && code <= 0xFEFF) { |
| has_rtl = true; |
| } |
|
|
| |
| if (code >= 0x0041 && code <= 0x005A) || |
| (code >= 0x0061 && code <= 0x007A) || |
| (code >= 0x0391 && code <= 0x03C9) || |
| (code >= 0x0410 && code <= 0x044F) { |
| has_ltr = true; |
| } |
| } |
|
|
| match (has_rtl, has_ltr) { |
| (true, false) => "RTL".to_string(), |
| (false, true) => "LTR".to_string(), |
| (true, true) => "MIXED".to_string(), |
| (false, false) => "NEUTRAL".to_string(), |
| } |
| } |
|
|
| |
| |
| |
| |
| #[wasm_bindgen] |
| pub fn verify_roundtrip(input: &str) -> bool { |
| |
| let encoded: String = input |
| .chars() |
| .filter_map(|c| char::from_u32(c as u32)) |
| .collect(); |
|
|
| if encoded != input { |
| return false; |
| } |
|
|
| |
| let nfc_form: String = input.nfc().collect(); |
| let nfc_again: String = nfc_form.nfc().collect(); |
|
|
| if nfc_form != nfc_again { |
| return false; |
| } |
|
|
| |
| let bytes = input.as_bytes(); |
| if let Ok(reconstructed) = std::str::from_utf8(bytes) { |
| reconstructed == input |
| } else { |
| false |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn test_normalize_nfc() { |
| |
| let decomposed = "e\u{0301}"; |
| let normalized = normalize(decomposed, "NFC"); |
| let expected = "รฉ"; |
| assert_eq!(normalized, expected); |
| } |
|
|
| #[test] |
| fn test_normalize_nfkc() { |
| let input = "๏ฌ"; |
| let normalized = normalize(input, "NFKC"); |
| assert_eq!(normalized, "fi"); |
| } |
|
|
| #[test] |
| fn test_encode_ascii() { |
| let input = "hello"; |
| let result = encode(input); |
| let parsed: Value = serde_json::from_str(&result).unwrap(); |
|
|
| assert_eq!(parsed["normalized"], "hello"); |
| assert_eq!(parsed["length"], 5); |
| assert_eq!(parsed["byteLength"], 5); |
| assert_eq!( |
| parsed["codePoints"].as_array().unwrap().len(), |
| 5 |
| ); |
| } |
|
|
| #[test] |
| fn test_encode_emoji() { |
| let input = "๐"; |
| let result = encode(input); |
| let parsed: Value = serde_json::from_str(&result).unwrap(); |
|
|
| assert_eq!(parsed["length"], 1); |
| assert_eq!(parsed["byteLength"], 4); |
| } |
|
|
| #[test] |
| fn test_has_astral_characters_true() { |
| assert!(has_astral_characters("๐")); |
| assert!(has_astral_characters("๐ณ๐๐๐๐")); |
| } |
|
|
| #[test] |
| fn test_has_astral_characters_false() { |
| assert!(!has_astral_characters("hello")); |
| assert!(!has_astral_characters("cafรฉ")); |
| assert!(!has_astral_characters("ๆฅๆฌ่ช")); |
| } |
|
|
| #[test] |
| fn test_has_combining_marks_true() { |
| assert!(has_combining_marks("e\u{0301}")); |
| assert!(has_combining_marks("a\u{0308}")); |
| } |
|
|
| #[test] |
| fn test_has_combining_marks_false() { |
| assert!(!has_combining_marks("hello")); |
| assert!(!has_combining_marks("รฉ")); |
| } |
|
|
| #[test] |
| fn test_detect_bidi_ltr() { |
| let level = detect_bidi_level("Hello World"); |
| assert_eq!(level, "LTR"); |
| } |
|
|
| #[test] |
| fn test_detect_bidi_rtl() { |
| let level = detect_bidi_level("ืฉืืื ืขืืื"); |
| assert_eq!(level, "RTL"); |
| } |
|
|
| #[test] |
| fn test_detect_bidi_mixed() { |
| let level = detect_bidi_level("Hello ืฉืืื"); |
| assert_eq!(level, "MIXED"); |
| } |
|
|
| #[test] |
| fn test_detect_bidi_neutral() { |
| let level = detect_bidi_level("123 !@#"); |
| assert_eq!(level, "NEUTRAL"); |
| } |
|
|
| #[test] |
| fn test_verify_roundtrip_ascii() { |
| assert!(verify_roundtrip("hello")); |
| assert!(verify_roundtrip("The quick brown fox jumps over the lazy dog")); |
| } |
|
|
| #[test] |
| fn test_verify_roundtrip_unicode() { |
| assert!(verify_roundtrip("cafรฉ")); |
| assert!(verify_roundtrip("ๆฅๆฌ่ช")); |
| assert!(verify_roundtrip("๐๐")); |
| } |
|
|
| #[test] |
| fn test_verify_roundtrip_empty() { |
| assert!(verify_roundtrip("")); |
| } |
|
|
| #[test] |
| fn test_verify_roundtrip_complex() { |
| |
| let complex = "Hello ู
ุฑุญุจุง ืฉืืื ๆฅๆฌ่ช ๐"; |
| assert!(verify_roundtrip(complex)); |
| } |
| } |
|
|