use std::{borrow::Cow, ops::Deref};
#[derive(Debug, PartialEq, Eq)]
pub enum Text<'s> {
Regular(Cow<'s, str>),
Quoted(QuotedText<'s>),
}
impl<'s> Deref for Text<'s> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl<'s> Text<'s> {
pub fn as_str(&self) -> &str {
match self {
Text::Regular(t) => t,
Text::Quoted(t) => t,
}
}
}
impl<'s> std::fmt::Display for Text<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
crate::fmt::Display::fmt(self, f)
}
}
impl<'s> crate::fmt::Display for Text<'s> {
fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
match self {
Text::Regular(s) => {
f.write_char('\'')?;
let mut last = None::<&str>;
let mut parts = s.split_inclusive('\'');
if let Some(first) = parts.next() {
f.write_str(first)?;
last = Some(first);
}
for part in parts {
f.write_char('\'')?;
f.write_str(part)?;
last = Some(part);
}
if let Some(last) = last
&& last.ends_with('\'')
{
f.write_char('\'')?;
}
f.write_char('\'')
}
Text::Quoted(t) => {
f.write_str("Q'")?;
f.write_str(t.as_str_with_delimiters())?;
f.write_char('\'')
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct QuotedText<'s>(&'s str);
impl<'s> QuotedText<'s> {
pub(crate) fn new_unchecked(s: &'s str) -> Self {
if cfg!(debug_assertions) {
Self::try_from(s).unwrap();
}
Self(s)
}
pub fn quote_delimiters(&self) -> (char, char) {
let mut chars = self.0.chars();
(
chars.next().expect("empty string"),
chars.last().expect("empty string"),
)
}
pub(crate) fn as_str_with_delimiters(&self) -> &str {
self.0
}
pub fn as_str(&self) -> &str {
let mut chars = self.0.char_indices();
let i = chars.next().expect("empty string").1.len_utf8();
let j = chars.last().expect("empoty string").0;
&self.0[i..j]
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum QuotedTextFromError {
TooShort,
InvalidDelimiters,
InvalidContent,
}
impl std::fmt::Display for QuotedTextFromError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
crate::fmt::Display::fmt(self, f)
}
}
impl crate::fmt::Display for QuotedTextFromError {
fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
let msg = match self {
QuotedTextFromError::TooShort => "text too short; must be at least two characters long",
QuotedTextFromError::InvalidDelimiters => {
"invalid delimiters; text must be enclosed with the same character or `{...}`, `[...]`, `<...>`, `(...)`"
}
QuotedTextFromError::InvalidContent => {
"invalid text; must not contain end-quote-delimiter followed by single quote"
}
};
f.write_str(msg)
}
}
impl<'s> TryFrom<&'s str> for QuotedText<'s> {
type Error = QuotedTextFromError;
fn try_from(s: &'s str) -> Result<Self, Self::Error> {
let mut chars = s.chars();
let end_delim = match (chars.next(), chars.last()) {
(Some(start_delim), Some(end_delim)) => {
match (start_delim, end_delim) {
('{', '}') | ('[', ']') | ('<', '>') | ('(', ')') => {}
(q1, q2) if q1 == q2 => {}
_ => {
return Err(QuotedTextFromError::InvalidDelimiters);
}
}
end_delim
}
_ => return Err(QuotedTextFromError::TooShort),
};
let mut buf = [0u8; 5];
let n = end_delim.encode_utf8(&mut buf).len();
buf[n] = b'\'';
let end_delim_quote = unsafe { str::from_utf8_unchecked(&buf[..n + 1]) };
if s.contains(end_delim_quote) {
Err(QuotedTextFromError::InvalidContent)
} else {
Ok(Self(s))
}
}
}
impl<'s> Deref for QuotedText<'s> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl<'s> std::fmt::Display for QuotedText<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
crate::fmt::Display::fmt(self, f)
}
}
impl<'s> crate::fmt::Display for QuotedText<'s> {
fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use crate::ast::Text;
use super::{QuotedText, QuotedTextFromError};
#[test]
fn test_quoted_text() {
let qt = QuotedText::try_from("{as'df}").unwrap();
assert_eq!(&format!("{qt}"), "as'df");
assert_eq!(qt.as_str(), "as'df");
assert_eq!(&*qt, "as'df");
assert_eq!(qt.quote_delimiters(), ('{', '}'));
}
#[rustfmt::skip]
#[test]
fn test_invalid_quoted_text() {
assert_eq!(Err(QuotedTextFromError::TooShort), QuotedText::try_from(""));
assert_eq!(Err(QuotedTextFromError::TooShort), QuotedText::try_from("a"));
assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from(".asdf,"));
assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("{adf>"));
assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("{adf>"));
assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("oasdfx"));
assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from(",a,'df,"));
assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from("{a}'df}"));
assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from("<a>'df>"));
}
#[rustfmt::skip]
#[test]
fn test_text_regular_display() {
assert_eq!(
&format!("{}", Text::Regular("hello world".into())),
r#"'hello world'"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular("how's life?".into())),
r#">>'how''s life?'<<"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular(".'_'".into())),
r#">>'.''_'''<<"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular("".into())),
r#">>''<<"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular("'".into())),
r#">>''''<<"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular("''".into())),
r#">>''''''<<"#
);
assert_eq!(
&format!(">>{}<<", Text::Regular("''".into())),
r#">>''''''<<"#
);
}
#[test]
fn test_text_quoted_display() {
assert_eq!(
&format!(
"{}",
Text::Quoted(QuotedText::try_from("|hello world|").unwrap())
),
r#"Q'|hello world|'"#
);
}
}