Coverage Report

Created: 2026-06-30 07:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/deunicode-1.6.2/src/lib.rs
Line
Count
Source
1
//! The `deunicode` library transliterates Unicode strings such as "Æneid" into pure
2
//! ASCII ones such as "AEneid."
3
//!
4
//! Supports no-std. Stores Unicode data in a compact format.
5
//!
6
//! It started as a Rust port of [`Text::Unidecode`](http://search.cpan.org/~sburke/Text-Unidecode-1.30/lib/Text/Unidecode.pm) Perl module, and was extended to support emoji.
7
//!
8
//! See [README](https://github.com/kornelski/deunicode/blob/master/README.md) for more info.
9
//!
10
//! Examples
11
//! --------
12
#![cfg_attr(feature = "alloc", doc = "```rust")]
13
#![cfg_attr(not(feature = "alloc"), doc = "```rust,ignore")]
14
//! use deunicode::deunicode;
15
//!
16
//! assert_eq!(deunicode("Æneid"), "AEneid");
17
//! assert_eq!(deunicode("étude"), "etude");
18
//! assert_eq!(deunicode("北亰"), "Bei Jing");
19
//! assert_eq!(deunicode("ᔕᓇᓇ"), "shanana");
20
//! assert_eq!(deunicode("げんまい茶"), "genmaiCha");
21
//! assert_eq!(deunicode("🦄☣"), "unicorn biohazard");
22
//! assert_eq!(deunicode("…"), "...");
23
//!
24
//! // format without a temporary string
25
//! use deunicode::AsciiChars;
26
//! format!("what's up {}", "🐶".ascii_chars());
27
#![doc = "```"] // to mollify some syntax highlighters
28
29
#![no_std]
30
31
#[cfg(any(test, feature = "alloc"))]
32
extern crate alloc;
33
#[cfg(feature = "alloc")]
34
use alloc::borrow::Cow;
35
#[cfg(feature = "alloc")]
36
use alloc::string::String;
37
38
use core::iter::FusedIterator;
39
use core::str::Chars;
40
41
const MAPPING: &str = include_str!("mapping.txt");
42
43
#[repr(C)]
44
#[derive(Copy, Clone)]
45
struct Ptr {
46
    /// if len <= 2, it's the string itself,
47
    /// otherwise it's an u16 offset into MAPPING
48
    chr: [u8; 2],
49
    len: u8,
50
}
51
52
const POINTERS_BYTES: &[u8] = include_bytes!("pointers.bin");
53
/// POINTERS format is described by struct Ptr
54
const POINTERS: &[Ptr] = unsafe { core::slice::from_raw_parts(POINTERS_BYTES.as_ptr().cast(), POINTERS_BYTES.len() / core::mem::size_of::<Ptr>()) };
55
56
/// This function takes any Unicode string and returns an ASCII transliteration
57
/// of that string.
58
///
59
/// Guarantees and Warnings
60
/// -----------------------
61
/// Here are some guarantees you have when calling [`deunicode()`]:
62
///   * The `String` returned will be valid ASCII; the decimal representation of
63
///     every `char` in the string will be between 0 and 127, inclusive.
64
///   * Every ASCII character (0x0000 - 0x007F) is mapped to itself.
65
///   * All Unicode characters will translate to a string containing newlines
66
///     (`"\n"`) or ASCII characters in the range 0x0020 - 0x007E. So for example,
67
///     no Unicode character will translate to `\u{01}`. The exception is if the
68
///     ASCII character itself is passed in, in which case it will be mapped to
69
///     itself. (So `'\u{01}'` will be mapped to `"\u{01}"`.)
70
///
71
/// There are, however, some things you should keep in mind:
72
///   * As stated, some transliterations do produce `\n` characters.
73
///   * Some Unicode characters transliterate to an empty string on purpose.
74
///   * Some Unicode characters are unknown and transliterate to `"[?]"` (see [`deunicode_with_tofu()`])
75
///   * Many Unicode characters transliterate to multi-character strings. For
76
///     example, 北 is transliterated as "Bei ".
77
///   * Han characters are mapped to Mandarin, and will be mostly illegible to Japanese readers.
78
#[inline(always)]
79
#[cfg(feature = "alloc")]
80
#[must_use]
81
0
pub fn deunicode(s: &str) -> String {
82
0
    deunicode_with_tofu(s, "[?]")
83
0
}
84
85
/// Same as [`deunicode()`], but unknown characters can be replaced with a custom string.
86
///
87
/// You can use "\u{FFFD}" to use the usual Unicode Replacement Character.
88
///
89
/// "Tofu" is a nickname for a replacement character, which in Unicode fonts usually
90
/// looks like a block of tofu.
91
#[inline]
92
#[cfg(feature = "alloc")]
93
#[must_use]
94
0
pub fn deunicode_with_tofu(s: &str, custom_placeholder: &str) -> String {
95
0
    deunicode_with_tofu_cow(s, custom_placeholder).into_owned()
96
0
}
Unexecuted instantiation: deunicode::deunicode_with_tofu
Unexecuted instantiation: deunicode::deunicode_with_tofu
97
98
/// Same as [`deunicode_with_tofu()`], but avoids allocating a new `String` if not necessary.
99
///
100
/// You can use "\u{FFFD}" to use the usual Unicode Replacement Character.
101
///
102
/// "Tofu" is a nickname for a replacement character, which in Unicode fonts usually
103
/// looks like a block of tofu.
104
#[cfg(feature = "alloc")]
105
#[must_use]
106
0
pub fn deunicode_with_tofu_cow<'input>(s: &'input str, custom_placeholder: &str) -> Cow<'input, str> {
107
    // Fast path to skip over ASCII chars at the beginning of the string
108
0
    let ascii_len = s.as_bytes().iter().take_while(|&&c| c < 0x7F).count();
109
0
    if ascii_len >= s.len() { // >= elides bounds check in split_at
110
0
        return Cow::Borrowed(s);
111
0
    }
112
113
0
    let (ascii, rest) = s.as_bytes().split_at(ascii_len);
114
    // safe, because it's been checked to be ASCII only
115
0
    debug_assert!(core::str::from_utf8(ascii).is_ok());
116
0
    let ascii = unsafe { core::str::from_utf8_unchecked(ascii) };
117
118
    // reserve a bit more space to avoid reallocations on longer transliterations
119
    // but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket for short strings
120
0
    let mut out = String::new();
121
    // this generates less code than with_capacity()
122
0
    out.try_reserve_exact(s.len() | 15).unwrap_or_else(|_| panic!());
123
124
    // this if optimizes out unused realloc code from push_str
125
0
    let needs_to_grow = ascii.as_bytes().len() > out.capacity().wrapping_sub(out.len());
126
0
    if !needs_to_grow {
127
0
        out.push_str(ascii);
128
0
    }
129
130
    // safe, because UTF-8 codepoint can't start with < 7F byte
131
0
    debug_assert!(core::str::from_utf8(rest).is_ok());
132
0
    let s = unsafe { core::str::from_utf8_unchecked(rest) };
133
134
0
    out.extend(s.ascii_chars().map(move |ch| ch.unwrap_or(custom_placeholder)));
135
0
    Cow::Owned(out)
136
0
}
137
138
/// This function takes a single Unicode character and returns an ASCII
139
/// transliteration.
140
///
141
/// The warnings and guarantees of [`deunicode()`] apply to this function as well.
142
///
143
/// Examples
144
/// --------
145
/// ```rust
146
/// # use deunicode::deunicode_char;
147
/// assert_eq!(deunicode_char('Æ'), Some("AE"));
148
/// assert_eq!(deunicode_char('北'), Some("Bei "));
149
/// ```
150
#[inline]
151
#[must_use]
152
0
pub fn deunicode_char(ch: char) -> Option<&'static str> {
153
0
    if let Some(p) = POINTERS.get(ch as usize) {
154
        // if length is 1 or 2, then the "pointer" data is used to store the char
155
0
        if p.len <= 2 {
156
0
            let chars = p.chr.get(..p.len as usize)?;
157
            // safe, because we're returning only ASCII
158
0
            debug_assert!(core::str::from_utf8(chars).is_ok());
159
            unsafe {
160
0
                Some(core::str::from_utf8_unchecked(chars))
161
            }
162
        } else {
163
0
            let map_pos = (u16::from(p.chr[0]) | u16::from(p.chr[1]) << 8) as usize;
164
            // unknown characters are intentionally mapped to out of range length
165
0
            MAPPING.get(map_pos..map_pos + p.len as usize)
166
        }
167
    } else {
168
0
        None
169
    }
170
0
}
Unexecuted instantiation: deunicode::deunicode_char
Unexecuted instantiation: deunicode::deunicode_char
171
172
/// Convenience functions for deunicode. `use deunicode::AsciiChars`
173
pub trait AsciiChars {
174
    /// Iterate over Unicode characters converted to ASCII sequences.
175
    ///
176
    /// Items of this iterator may be `None` for some characters.
177
    /// Use `.map(|ch| ch.unwrap_or("?"))` to replace invalid characters.
178
    ///
179
    /// Alternatively, this iterator can be used in formatters:
180
    #[cfg_attr(feature = "alloc", doc = "```rust")]
181
    #[cfg_attr(not(feature = "alloc"), doc = "```rust,ignore")]
182
    /// use deunicode::AsciiChars;
183
    /// format!("what's up {}", "🐶".ascii_chars());
184
    #[doc = "```"]
185
    fn ascii_chars(&self) -> AsciiCharsIter<'_>;
186
187
    /// Convert any Unicode string to ASCII-only string.
188
    ///
189
    /// Characters are converted to closest ASCII equivalent.
190
    /// Characters that can't be converted are replaced with `"[?]"`.
191
    #[cfg(feature = "alloc")]
192
    fn to_ascii_lossy(&self) -> String;
193
}
194
195
#[cfg(feature = "alloc")]
196
impl AsciiChars for String {
197
    #[inline(always)]
198
0
    fn ascii_chars(&self) -> AsciiCharsIter<'_> {
199
0
        AsciiCharsIter::new(self)
200
0
    }
201
    #[inline(always)]
202
0
    fn to_ascii_lossy(&self) -> String {
203
0
        deunicode(self)
204
0
    }
205
}
206
207
impl AsciiChars for str {
208
    #[inline(always)]
209
0
    fn ascii_chars(&self) -> AsciiCharsIter<'_> {
210
0
        AsciiCharsIter::new(self)
211
0
    }
212
    #[inline(always)]
213
    #[cfg(feature = "alloc")]
214
0
    fn to_ascii_lossy(&self) -> String {
215
0
        deunicode(self)
216
0
    }
217
}
218
219
/// Iterator that translates Unicode characters to ASCII strings.
220
///
221
/// See [`AsciiChars`] trait's `str.ascii_chars()` method.
222
///
223
/// Additionally, it implements `Display` for formatting strings without allocations.
224
///
225
#[cfg_attr(feature = "alloc", doc = "```rust")]
226
#[cfg_attr(not(feature = "alloc"), doc = "```rust,ignore")]
227
/// use deunicode::AsciiChars;
228
/// format!("what's up {}", "🐶".ascii_chars());
229
#[doc = "```"]
230
#[derive(Clone)]
231
pub struct AsciiCharsIter<'a> {
232
    next_char: Option<Option<&'static str>>,
233
    chars: Chars<'a>,
234
}
235
236
/// Use `.map(|ch| ch.unwrap_or("?"))` to replace invalid characters.
237
impl<'a> AsciiCharsIter<'a> {
238
    #[inline]
239
0
    pub fn new(unicode_string: &'a str) -> Self {
240
0
        let mut chars = unicode_string.chars();
241
0
        Self {
242
0
            next_char: chars.next().map(deunicode_char),
243
0
            chars,
244
0
        }
245
0
    }
246
}
247
248
impl<'a> FusedIterator for AsciiCharsIter<'a> {}
249
250
impl<'a> Iterator for AsciiCharsIter<'a> {
251
    type Item = Option<&'static str>;
252
253
    #[inline]
254
0
    fn next(&mut self) -> Option<Self::Item> {
255
0
        let dch = self.next_char?;
256
0
        self.next_char = self.chars.next().map(deunicode_char);
257
0
        let dch = match dch {
258
0
            None => return Some(None),
259
0
            Some(dch) => dch,
260
        };
261
        // ends with space
262
0
        let trim_last_char = dch.as_bytes().len() > 1 && dch.as_bytes().last().copied() == Some(b' ') &&
263
0
            self.next_char.map_or(true, |ch| { // true if end
264
0
            ch.map_or(false, |ch| ch.as_bytes().first().copied() == Some(b' ')) // space next (assume placeholder is not space)
265
0
        });
266
0
        Some(if !trim_last_char {
267
0
            Some(dch)
268
        } else {
269
0
            dch.get(..dch.len()-1)
270
        })
271
0
    }
272
273
    #[inline]
274
0
    fn count(self) -> usize {
275
0
        self.chars.count() + if self.next_char.is_some() {1} else {0}
276
0
    }
277
278
    #[inline]
279
0
    fn size_hint(&self) -> (usize, Option<usize>) {
280
0
        (self.chars.size_hint().0 + if self.next_char.is_some() {1} else {0}, None)
281
0
    }
282
}
283
284
/// Format without a temporary string
285
///
286
#[cfg_attr(feature = "alloc", doc = "```rust")]
287
#[cfg_attr(not(feature = "alloc"), doc = "```rust,ignore")]
288
/// use deunicode::AsciiChars;
289
/// format!("what's up {}", "🐶".ascii_chars());
290
#[doc = "```"]
291
impl core::fmt::Display for AsciiCharsIter<'_> {
292
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
293
0
        self.clone().try_for_each(|ch| f.write_str(ch.unwrap_or("\u{FFFD}")))
294
0
    }
295
}
296
297
#[test]
298
fn iter_test() {
299
    use alloc::vec::Vec;
300
    let chars: Vec<_> = AsciiCharsIter::new("🄏中国").flatten().collect();
301
    assert_eq!(&chars, &["NonCommercial", "Zhong ", "Guo"]);
302
    let chars: Vec<_> = "中国x🅶".ascii_chars().flatten().collect();
303
    assert_eq!(&chars, &["Zhong ", "Guo ", "x", "G"]);
304
    let chars: Vec<_> = "☃中 国".ascii_chars().flatten().collect();
305
    assert_eq!(&chars, &["snowman ", "Zhong", " ", "Guo"]);
306
}
307
308
#[test]
309
fn zalgo() {
310
    assert_eq!(deunicode_with_tofu("h̵̡̢̛̻̬͔̦͓̥̞̳͇̭̣̪̰̞̲̩̭̤͚͖͓̰̭̝̬̖̭͇͇̰͇͓̠͑͆͐͛̏͒͆̊́̊̂̉̉̈́̿̆̾̌̀͒͌́͗͋͜͝͝͝ͅĕ̷̡̧̡̧̜̮͙̗͙͕͖̩͈͙̞̞̭͙̯͖̰͖̙̹͖͚̦̬̄̀̓̈́͗̆̓̽͛̀͛̄͂̉͒̓̐̃̑́͊̀͋͊͗́̈́͑͗̐̔̈͊͋̓͊̓́̏̍̍̓͘̕͝͝͠ͅl̶̠̮̺̦̩͓̣̪͚͌̊̈́̀̄̈́̉͗̀̏͋̆̈̈́̉̋̊̉̉̌̈́̚̕͠͠l̴̨̡͍͇̝̟̩̙̤̰̬̬͖͙̺̟̯͓̥̯͔̤̠̻̤̮̘̋͑̑̿͗͂̃̓̓̉͒̑͜͠ͅo̸̢̧̨̜͉̜͓͙̰̳̙̖̰͇̺͈̝̬̩̫͛̅̍͌̎̅̿̂̚̕͜ ̵̛̗͍̊̈͋̀̊͒̄̔̔͋͋̆͋̅̀͂͂̍́̀̈́̈́͂̂̂̆̅͗̄̈́̀̈́̅̒̈̋͊̍̈́͂̑̓̽̂̂̓̚̕̚̕̚͠͝w̷̨͍͖̗͔͖͎̩̠̜͖̞͍̘̤͕̮̥̭͛̆̎̋̄͒̓̈́͆̀̆̚ǫ̷̢̢̧̧̨̧̧̨̢̼̮̺̬͇͓̪̯͖̥͙̠͍̭̩̰͎̘̺̝̲̖̮̞̝̠̠͎̻̠͙̫͙̞̫̭͖̱͉̱̮̌͑̈̅̈́̊̓͌̇͌̏̾̆͗̉͊̐̈́̾́̔̆͐́͘͜͜͝ͅŗ̵̡̛̛̟̭͉̰̮̺̜̼̰̟̲͖͔͕̰͕͇̪̲̫̬͚̱̮͎̭̩̩̉̇̉̀̉͑̔͋͆͌͜͠ļ̴̢̨̢̛͙̳̮̠͔͇͈̟͇̦̯͖̖͚̺̤͈̻͔̤̤̪̫͔͕̻̟̥̤̩͚̟̳͔̘̤͈͍͍̯̻̙̺̪̄̈́́͊̋̊́̅͛̉̊̉̅̋̆̔͑̈́͋̑͂̍̌̓̾̆̕̕͝ͅḏ̶̡̨̢̡̛̙͕̘̜͚̺̬̭̜͖͎͚̹̖͈̖̤͎̙̫͎̜̩̰̬̪̣̎͛̓̏̃͊̈́̽̆̒̈́̎̄̍́͘̚̚͝͠͠ͅ!̶̨̨̨̛̛̟̳̼̘͎͔̜͎͚̖̮̰͕̞̦̩̗̫̠͔͕͎͎͎̦̬̫̩̰̲̈́͋̽̀̒͆̄̑̐̀̐̋͆̈́̊̽̊̅̊̀͆͆͑̈͋̌͆͑̂̊͑̚͝͝ͅͅͅ", ""), "hello world!");
311
}