#![no_std]
#![forbid(unsafe_code)]
#![warn(clippy::pedantic, clippy::perf, missing_docs, clippy::panic, clippy::cargo)]
#![allow(clippy::type_complexity)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(any(feature = "std", docsrs))]
extern crate std;
#[cfg(any(feature = "std", docsrs))]
use std::error::Error as ErrorTrait;
#[cfg(all(feature = "core_error", not(feature = "std")))]
use core::error::Error as ErrorTrait;
extern crate alloc;
use alloc::{
borrow::Cow,
string::String,
str::CharIndices
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct InvalidEscape {
pub index: usize,
}
impl InvalidEscape {
#[must_use]
pub const fn new(index: usize) -> Self {
Self { index }
}
}
impl core::fmt::Display for InvalidEscape {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "invalid escape sequence at index {}", self.index)?;
Ok(())
}
}
#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "core_error"))))]
#[cfg(any(feature = "std", feature = "core_error", docsrs))]
impl ErrorTrait for InvalidEscape {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EscapeValue<'s> {
Remove,
Skip,
Character(char),
String(Cow<'s, str>)
}
impl From<char> for EscapeValue<'static> {
fn from(value: char) -> Self {
Self::Character(value)
}
}
impl<'string> From<&'string str> for EscapeValue<'string> {
fn from(value: &'string str) -> Self {
Self::String(Cow::Borrowed(value))
}
}
impl<'string> From<Cow<'string, str>> for EscapeValue<'string> {
fn from(value: Cow<'string, str>) -> Self {
Self::String(value)
}
}
impl<'string> From<String> for EscapeValue<'string> {
fn from(value: String) -> Self {
Self::String(Cow::Owned(value))
}
}
pub trait EscapeHandler {
#[allow(clippy::result_unit_err, clippy::missing_errors_doc)]
fn escape<'iter, 'source>(&mut self, idx: usize, chr: char, iter: &'iter mut CharIndices<'source>) -> Result<EscapeValue<'source>, ()>;
fn prefix(&self) -> char { '\\' }
}
impl<F> EscapeHandler for F
where F: for<'iter, 'source> FnMut(usize, char, &'iter mut CharIndices<'source>) -> Result<EscapeValue<'source>, ()>
{
fn escape<'iter, 'source>(&mut self, idx: usize, chr: char, iter: &'iter mut CharIndices<'source>) -> Result<EscapeValue<'source>, ()> {
self(idx, chr, iter)
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for str {}
}
pub trait UnescapeExt: sealed::Sealed {
fn to_unescaped(&self) -> Result<Cow<'_, str>, InvalidEscape>;
fn to_unescaped_with(
&self,
callback: impl EscapeHandler
) -> Result<Cow<'_, str>, InvalidEscape>;
}
impl UnescapeExt for str {
#[inline]
fn to_unescaped(&'_ self) -> Result<Cow<'_, str>, InvalidEscape> {
self.to_unescaped_with(DefaultEscapeHandler)
}
fn to_unescaped_with(
&'_ self,
mut callback: impl EscapeHandler
) -> Result<Cow<'_, str>, InvalidEscape> {
to_unescaped_with_mono(self, &mut callback)
}
}
fn to_unescaped_with_mono<'this, 'cb>(
this: &'this str,
callback: &'cb mut dyn EscapeHandler
) -> Result<Cow<'this, str>, InvalidEscape> {
let mut iter = this.char_indices();
let mut seen: &'this str = "";
let mut owned = None::<String>;
let prefix = callback.prefix();
while let Some((index, chr)) = iter.next() {
if chr != prefix {
if let Some(owned) = &mut owned {
owned.push(chr);
} else {
seen = &this[..index + chr.len_utf8()];
}
continue;
}
if let Some((i, chr)) = iter.next() {
let res = callback.escape(index, chr, &mut iter)
.map_err(|()| InvalidEscape { index })?;
if res == EscapeValue::Skip && owned.is_none() {
seen = &this[..i + chr.len_utf8()];
continue;
}
let owned = owned.get_or_insert_with(|| seen.into());
match res {
EscapeValue::Character(c) => { owned.push(c); continue; },
EscapeValue::String(str) => { owned.push_str(&*str); continue; },
EscapeValue::Remove => { continue; },
EscapeValue::Skip => { owned.push(prefix); owned.push(chr); continue; }
}
} else {
return Err(InvalidEscape::new(index));
}
}
match owned {
Some(string) => Ok(Cow::Owned(string)),
None => Ok(Cow::Borrowed(this)),
}
}
pub struct DefaultEscapeHandler;
impl EscapeHandler for DefaultEscapeHandler {
fn escape<'iter, 'source>(&mut self, _idx: usize, chr: char, iter: &'iter mut CharIndices<'source>) -> Result<EscapeValue<'source>, ()> {
Ok( match chr {
'a' => '\x07'.into(),
'b' => '\x08'.into(),
't' => '\x09'.into(),
'n' => '\x0A'.into(),
'v' => '\x0B'.into(),
'f' => '\x0C'.into(),
'r' => '\x0D'.into(),
'e' => '\x1B'.into(),
'`' => '`'.into(),
'\'' => '\''.into(),
'"' => '"'.into(),
'\\' => '\\'.into(),
'u' => {
let (chr, skip) = unescape_unicode(iter).ok_or(())?;
for _ in 0..skip { iter.next(); }
chr.into()
},
'x' => {
let res = unescape_hex(iter).ok_or(())?;
iter.next();
iter.next();
res.into()
},
c if c.is_digit(8) => {
let (chr, skip) = unescape_oct(c, iter).ok_or(())?;
for _ in 0..skip { iter.next(); }
chr.into()
},
_ => return Err(()),
} )
}
}
fn unescape_unicode(
iter: &mut CharIndices
) -> Option<(char, usize)> {
let string = iter.as_str();
let (_, next) = iter.next()?;
if next == '{' {
let end = string[1 ..].find('}')?;
let num = &string[1 ..= end];
let codepoint = u32::from_str_radix(num, 16).ok()?;
char::from_u32(codepoint).map(|v| (v, end + 1))
} else {
let next_four = string.get( ..4 )?;
let codepoint = u32::from_str_radix(next_four, 16).ok()?;
char::from_u32(codepoint).map(|v| (v, 3))
}
}
fn unescape_hex(
iter: &mut CharIndices
) -> Option<char> {
let codepoint = iter.as_str()
.get(..2)
.and_then(|num| u32::from_str_radix(num, 16).ok())?;
char::from_u32(codepoint)
}
#[allow(clippy::cast_possible_truncation)] fn unescape_oct(
chr: char,
iter: &mut CharIndices
) -> Option<(char, usize)> {
let str = iter.as_str();
let end = iter.clone() .take(2)
.take_while(|(_, c)| c.is_digit(8))
.enumerate()
.last()
.map_or(0, |(idx, _)| idx + 1);
let num = &str[ .. end];
let mut codepoint = if num.is_empty() { 0 } else { u32::from_str_radix(num, 8).ok()? };
codepoint += (chr as u32 - '0' as u32) * 8u32.pow(end as u32);
char::from_u32(codepoint).map(|chr| (chr, end))
}