use std::fmt::Write as _;
use crate::printable::is_printable;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Str {
Utf8(Box<str>),
Wide(Box<[u32]>),
}
impl Str {
pub fn code_points(&self) -> impl Iterator<Item = u32> + '_ {
let (text, wide) = match self {
Str::Utf8(s) => (Some(s.chars()), None),
Str::Wide(w) => (None, Some(w.iter().copied())),
};
text.into_iter()
.flatten()
.map(u32::from)
.chain(wide.into_iter().flatten())
}
#[must_use]
pub fn repr(&self) -> String {
match self {
Str::Utf8(s) => str_repr(s),
Str::Wide(w) => repr_code_points(w.iter().copied(), w.len()),
}
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Str::Utf8(s) if s.is_ascii() => s.len(),
Str::Utf8(s) => s.chars().count(),
Str::Wide(w) => w.len(),
}
}
#[must_use]
pub fn code_point_at(&self, index: usize) -> Option<u32> {
match self {
Str::Utf8(s) if s.is_ascii() => s.as_bytes().get(index).copied().map(u32::from),
Str::Utf8(s) => s.chars().nth(index).map(u32::from),
Str::Wide(w) => w.get(index).copied(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Str::Utf8(s) => s.is_empty(),
Str::Wide(w) => w.is_empty(),
}
}
}
impl std::fmt::Display for Str {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Str::Utf8(s) => f.write_str(s),
Str::Wide(w) => w
.iter()
.map(|&cp| char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER))
.try_for_each(|c| f.write_char(c)),
}
}
}
impl From<&str> for Str {
fn from(s: &str) -> Self {
Str::Utf8(s.into())
}
}
impl From<String> for Str {
fn from(s: String) -> Self {
Str::Utf8(s.into_boxed_str())
}
}
#[derive(Debug, Default)]
pub struct StrBuf {
text: String,
wide: Option<Vec<u32>>,
}
impl StrBuf {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, c: char) {
match &mut self.wide {
Some(wide) => wide.push(u32::from(c)),
None => self.text.push(c),
}
}
pub fn push_str(&mut self, s: &str) {
match &mut self.wide {
Some(wide) => wide.extend(s.chars().map(u32::from)),
None => self.text.push_str(s),
}
}
pub fn push_code_point(&mut self, cp: u32) {
if let Some(c) = char::from_u32(cp) {
self.push(c);
return;
}
self.widen().push(cp);
}
pub fn push_string(&mut self, other: &Str) {
match other {
Str::Utf8(s) => self.push_str(s),
Str::Wide(w) => {
let wide = self.widen();
wide.extend(w.iter().copied());
}
}
}
fn widen(&mut self) -> &mut Vec<u32> {
self.wide.get_or_insert_with(|| {
let mut wide: Vec<u32> = Vec::with_capacity(self.text.len() + 1);
wide.extend(self.text.chars().map(u32::from));
self.text = String::new();
wide
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
match &self.wide {
Some(wide) => wide.is_empty(),
None => self.text.is_empty(),
}
}
pub fn clear(&mut self) {
self.text.clear();
self.wide = None;
}
#[must_use]
pub fn finish(self) -> Str {
match self.wide {
Some(wide) => Str::Wide(wide.into_boxed_slice()),
None => Str::Utf8(self.text.into_boxed_str()),
}
}
}
#[must_use]
pub fn str_repr(s: &str) -> String {
repr_code_points(s.chars().map(u32::from), s.len())
}
#[must_use]
pub fn repr_code_points(code_points: impl Iterator<Item = u32> + Clone, hint: usize) -> String {
let mut has_single = false;
let mut has_double = false;
for cp in code_points.clone() {
has_single |= cp == u32::from('\'');
has_double |= cp == u32::from('"');
}
let quote = if has_single && !has_double { '"' } else { '\'' };
let mut out = String::with_capacity(hint + 2);
out.push(quote);
for cp in code_points {
match char::from_u32(cp) {
Some('\\') => out.push_str("\\\\"),
Some('\t') => out.push_str("\\t"),
Some('\n') => out.push_str("\\n"),
Some('\r') => out.push_str("\\r"),
Some(c) if c == quote => {
out.push('\\');
out.push(c);
}
Some(c) if is_printable(c) => out.push(c),
_ => push_escape(&mut out, cp),
}
}
out.push(quote);
out
}
#[must_use]
pub fn bytes_repr(b: &[u8]) -> String {
let quote = if b.contains(&b'\'') && !b.contains(&b'"') {
b'"'
} else {
b'\''
};
let mut out = String::with_capacity(b.len() + 3);
out.push('b');
out.push(quote as char);
for &byte in b {
match byte {
b'\\' => out.push_str("\\\\"),
b'\t' => out.push_str("\\t"),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b if b == quote => {
out.push('\\');
out.push(b as char);
}
0x20..=0x7E => out.push(byte as char),
b => {
let _ = write!(out, "\\x{b:02x}");
}
}
}
out.push(quote as char);
out
}
fn push_escape(out: &mut String, cp: u32) {
let _ = if cp < 0x100 {
write!(out, "\\x{cp:02x}")
} else if cp < 0x1_0000 {
write!(out, "\\u{cp:04x}")
} else {
write!(out, "\\U{cp:08x}")
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_string_with_an_apostrophe_changes_quotes_rather_than_escaping() {
assert_eq!(str_repr("it's"), "\"it's\"");
assert_eq!(str_repr("it's \"so\""), "'it\\'s \"so\"'");
assert_eq!(str_repr("\"quoted\""), "'\"quoted\"'");
}
#[test]
fn control_characters_are_escaped_and_printable_ones_are_not() {
assert_eq!(str_repr("a\tb\nc\rd\\e"), "'a\\tb\\nc\\rd\\\\e'");
assert_eq!(str_repr("\x00\x1b\x7f"), "'\\x00\\x1b\\x7f'");
assert_eq!(str_repr("héllo"), "'héllo'");
assert_eq!(str_repr("\u{200b}"), "'\\u200b'");
assert_eq!(str_repr("\u{e0001}"), "'\\U000e0001'");
}
#[test]
fn bytes_print_everything_outside_printable_ascii_as_hex() {
assert_eq!(bytes_repr(b"abc"), "b'abc'");
assert_eq!(bytes_repr(&[0, 0x7f, 0xff]), "b'\\x00\\x7f\\xff'");
assert_eq!(bytes_repr(b"it's"), "b\"it's\"");
}
#[test]
fn a_lone_surrogate_prints_as_the_escape_that_made_it() {
let mut out = StrBuf::new();
out.push_code_point(0xD800);
assert_eq!(out.finish().repr(), "'\\ud800'");
}
#[test]
fn displaying_a_string_writes_the_text_and_not_the_quotes() {
assert_eq!(Str::from("it's").to_string(), "it's");
assert_eq!(Str::from("a\tb").to_string(), "a\tb");
let mut out = StrBuf::new();
out.push_str("a");
out.push_code_point(0xD800);
out.push_str("b");
assert_eq!(out.finish().to_string(), "a\u{fffd}b");
}
#[test]
fn what_looks_like_a_surrogate_pair_stays_two_code_points() {
let mut out = StrBuf::new();
out.push_code_point(0xD83D);
out.push_code_point(0xDE00);
let value = out.finish();
assert_eq!(value.code_points().count(), 2);
assert_eq!(value.repr(), "'\\ud83d\\ude00'");
}
#[test]
fn the_quote_choice_survives_widening() {
let mut out = StrBuf::new();
out.push_str("it's ");
out.push_code_point(0xD800);
assert_eq!(out.finish().repr(), "\"it's \\ud800\"");
}
#[test]
fn widening_keeps_what_was_already_in_the_buffer() {
let mut out = StrBuf::new();
out.push_str("héllo ");
out.push_code_point(0xDFFF);
out.push('!');
assert_eq!(out.finish().repr(), "'héllo \\udfff!'");
}
#[test]
fn the_common_case_never_leaves_the_narrow_arm() {
let mut out = StrBuf::new();
out.push_str("plain");
out.push_code_point(0x1F600);
assert!(matches!(out.finish(), Str::Utf8(_)));
}
#[test]
fn clearing_a_widened_buffer_goes_back_to_narrow() {
let mut out = StrBuf::new();
out.push_code_point(0xD800);
assert!(!out.is_empty());
out.clear();
assert!(out.is_empty());
out.push_str("after");
assert_eq!(out.finish(), Str::Utf8("after".into()));
}
#[test]
fn joining_two_strings_widens_only_when_one_of_them_is_wide() {
let mut wide = StrBuf::new();
wide.push_code_point(0xD800);
let wide = wide.finish();
let mut out = StrBuf::new();
out.push_string(&Str::from("a"));
out.push_string(&wide);
out.push_string(&Str::from("b"));
assert_eq!(out.finish().repr(), "'a\\ud800b'");
}
}