use unicode_bidi::format_chars::{ALM, FSI, LRE, LRI, LRM, LRO, PDF, PDI, RLE, RLI, RLM, RLO};
pub const LINE_SEPARATOR: char = '\u{2028}';
pub const PARAGRAPH_SEPARATOR: char = '\u{2029}';
#[must_use]
pub fn is_unsafe(character: char) -> bool {
character.is_control()
|| matches!(
character,
ALM | LRM
| RLM
| LRE
| RLE
| PDF
| LRO
| RLO
| LRI
| RLI
| FSI
| PDI
| LINE_SEPARATOR
| PARAGRAPH_SEPARATOR
)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Keep {
Nothing,
NewlineAndTab,
}
#[must_use]
pub fn strip_unsafe(text: &str, keep: Keep) -> String {
text.chars()
.filter(|character| match keep {
Keep::Nothing => !is_unsafe(*character),
Keep::NewlineAndTab => matches!(character, '\n' | '\t') || !is_unsafe(*character),
})
.collect()
}
#[cfg(test)]
mod tests {
use unicode_bidi::format_chars::{ALM, FSI, LRE, LRI, LRM, LRO, PDF, PDI, RLE, RLI, RLM, RLO};
use super::{Keep, LINE_SEPARATOR, PARAGRAPH_SEPARATOR, is_unsafe, strip_unsafe};
const BIDI: [char; 12] = [ALM, LRM, RLM, LRE, RLE, PDF, LRO, RLO, LRI, RLI, FSI, PDI];
fn controls() -> impl Iterator<Item = char> {
('\u{0}'..='\u{1f}').chain('\u{7f}'..='\u{9f}')
}
#[test]
fn refuses_every_control() {
assert_eq!(controls().count(), 65);
for character in controls() {
assert!(is_unsafe(character), "{character:?} passed");
}
}
#[test]
fn refuses_every_bidi_formatting_character() {
for character in BIDI {
assert!(is_unsafe(character), "{character:?} passed");
}
}
#[test]
fn refuses_both_separators() {
assert!(is_unsafe(LINE_SEPARATOR));
assert!(is_unsafe(PARAGRAPH_SEPARATOR));
}
#[test]
fn keeps_the_neighbours_of_each_set() {
let neighbours = [
' ', '~', '\u{a0}', '\u{61b}', '\u{61d}', '\u{200d}', '\u{2010}', '\u{2027}',
'\u{202f}', '\u{2065}', '\u{206a}',
];
for character in neighbours {
assert!(!is_unsafe(character), "{character:?} refused");
}
}
#[test]
fn keeps_the_other_format_characters() {
let kept = [
'\u{ad}',
'\u{600}',
'\u{200b}',
'\u{200c}',
'\u{200d}',
'\u{2060}',
'\u{feff}',
'\u{e0001}',
'\u{e0067}',
'\u{e007f}',
];
for character in kept {
assert!(!is_unsafe(character), "{character:?} refused");
}
}
#[test]
fn refuses_nothing_outside_the_three_sets() {
let refused = (0..=u32::from(char::MAX))
.filter_map(char::from_u32)
.filter(|character| is_unsafe(*character))
.count();
assert_eq!(refused, 65 + BIDI.len() + 2);
}
#[test]
fn strips_every_unsafe_character_and_keeps_the_rest() {
let text = "a\u{1b}[2J b\u{202e}c\u{2028}d\u{7f}\tx\ny";
assert_eq!(strip_unsafe(text, Keep::Nothing), "a[2J bcdxy");
}
#[test]
fn keeps_newline_and_tab_when_asked() {
let text = "a\u{1b}b\tc\nd\u{2029}e";
assert_eq!(strip_unsafe(text, Keep::NewlineAndTab), "ab\tc\nde");
}
#[test]
fn strips_the_whole_unsafe_set_and_nothing_else() {
let everything: String = (0..=u32::from(char::MAX))
.filter_map(char::from_u32)
.collect();
let stripped = strip_unsafe(&everything, Keep::Nothing);
assert_eq!(
everything.chars().count() - stripped.chars().count(),
65 + BIDI.len() + 2
);
assert!(stripped.chars().all(|character| !is_unsafe(character)));
}
#[test]
fn unicode_bidi_still_targets_the_documented_version() {
assert_eq!(unicode_bidi::UNICODE_VERSION, (16, 0, 0));
}
}