use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub const MAX_SYMBOL_BYTES: usize = 32;
pub const MAX_TITLE_CHARS: usize = 256;
pub fn is_control(c: char) -> bool {
c.is_control() || matches!(c, '\u{2028}' | '\u{2029}')
}
pub fn symbol(s: &str) -> Cow<'_, str> {
if s.is_empty() {
return Cow::Borrowed(s);
}
if let [b] = s.as_bytes()
&& (0x20..0x7f).contains(b)
{
return Cow::Borrowed(s);
}
let ok = s.len() <= MAX_SYMBOL_BYTES
&& !s.chars().any(is_control)
&& s.width() <= 2
&& s.graphemes(true).nth(1).is_none();
if ok {
Cow::Borrowed(s)
} else {
Cow::Borrowed("\u{FFFD}")
}
}
pub fn title(s: &str) -> String {
s.chars()
.filter(|c| !is_control(*c))
.take(MAX_TITLE_CHARS)
.collect()
}
pub fn text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' | '\t' => out.push(c),
c if is_control(c) => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbols() {
assert_eq!(symbol("a"), "a");
assert_eq!(symbol(""), "");
assert_eq!(symbol("é"), "é");
assert_eq!(symbol("日"), "日");
assert_eq!(symbol("👨👩👧"), "👨👩👧");
assert_eq!(symbol("\u{1b}"), "\u{FFFD}");
assert_eq!(symbol("\u{1b}[31m"), "\u{FFFD}");
assert_eq!(symbol("\u{7}"), "\u{FFFD}");
assert_eq!(symbol("\u{9b}"), "\u{FFFD}");
assert_eq!(symbol("ab"), "\u{FFFD}");
assert_eq!(symbol("\u{2028}"), "\u{FFFD}");
assert_eq!(symbol(&"x".repeat(MAX_SYMBOL_BYTES + 1)), "\u{FFFD}");
}
#[test]
fn titles_and_text() {
assert_eq!(title("hi\u{1b}]0;evil\u{7}there"), "hi]0;evilthere");
assert_eq!(title(&"t".repeat(1000)).chars().count(), MAX_TITLE_CHARS);
assert_eq!(text("ok\n\tfine"), "ok\n\tfine");
assert_eq!(text("\u{1b}[2J\u{7}"), "\\u{1b}[2J\\u{7}");
assert_eq!(text("c1 \u{9b}31m"), "c1 \\u{9b}31m");
}
}