use alloc::borrow::Cow;
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use crate::expression::{Expression, Markup};
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum PatternPart<'a> {
Text(Cow<'a, str>),
Expression(Expression<'a>),
Markup(Markup<'a>),
}
impl PatternPart<'_> {
pub fn into_owned(self) -> PatternPart<'static> {
match self {
PatternPart::Text(t) => PatternPart::Text(Cow::Owned(t.into_owned())),
PatternPart::Expression(e) => PatternPart::Expression(e.into_owned()),
PatternPart::Markup(m) => PatternPart::Markup(m.into_owned()),
}
}
}
#[derive(Clone, Default)]
pub struct Pattern<'a>(Repr<'a>);
#[derive(Clone, Default)]
enum Repr<'a> {
#[default]
Empty,
One(PatternPart<'a>),
Many(Vec<PatternPart<'a>>),
}
impl<'a> Pattern<'a> {
pub const fn new() -> Self {
Pattern(Repr::Empty)
}
pub fn with_capacity(capacity: usize) -> Self {
if capacity < 2 {
Pattern(Repr::Empty)
} else {
Pattern(Repr::Many(Vec::with_capacity(capacity)))
}
}
pub fn from_text(text: Cow<'a, str>) -> Self {
if text.is_empty() {
Pattern(Repr::Empty)
} else {
Pattern(Repr::One(PatternPart::Text(text)))
}
}
pub fn push(&mut self, part: PatternPart<'a>) {
self.push_with_hint(part, 0);
}
fn push_with_hint(&mut self, part: PatternPart<'a>, more: usize) {
let part = match part {
PatternPart::Text(text) => {
if text.is_empty() {
return;
}
if let Some(PatternPart::Text(last)) = self.last_mut() {
last.to_mut().push_str(&text);
return;
}
PatternPart::Text(text)
}
other => other,
};
self.0 = match core::mem::take(&mut self.0) {
Repr::Empty => Repr::One(part),
Repr::One(first) => {
let mut v = Vec::with_capacity(2 + more);
v.push(first);
v.push(part);
Repr::Many(v)
}
Repr::Many(mut v) => {
v.push(part);
Repr::Many(v)
}
};
}
fn last_mut(&mut self) -> Option<&mut PatternPart<'a>> {
match &mut self.0 {
Repr::Empty => None,
Repr::One(p) => Some(p),
Repr::Many(v) => v.last_mut(),
}
}
pub fn parts(&self) -> &[PatternPart<'a>] {
match &self.0 {
Repr::Empty => &[],
Repr::One(p) => core::slice::from_ref(p),
Repr::Many(v) => v,
}
}
pub fn iter(&self) -> core::slice::Iter<'_, PatternPart<'a>> {
self.parts().iter()
}
pub fn len(&self) -> usize {
self.parts().len()
}
pub fn is_empty(&self) -> bool {
self.parts().is_empty()
}
pub fn as_simple_text(&self) -> Option<&str> {
match self.parts() {
[] => Some(""),
[PatternPart::Text(t)] => Some(t),
_ => None,
}
}
pub fn into_parts(self) -> Vec<PatternPart<'a>> {
match self.0 {
Repr::Empty => Vec::new(),
Repr::One(p) => alloc::vec![p],
Repr::Many(v) => v,
}
}
pub fn into_owned(self) -> Pattern<'static> {
Pattern(match self.0 {
Repr::Empty => Repr::Empty,
Repr::One(p) => Repr::One(p.into_owned()),
Repr::Many(v) => Repr::Many(v.into_iter().map(PatternPart::into_owned).collect()),
})
}
}
impl PartialEq for Pattern<'_> {
fn eq(&self, other: &Self) -> bool {
self.parts() == other.parts()
}
}
impl Eq for Pattern<'_> {}
impl Hash for Pattern<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.parts().hash(state);
}
}
impl core::fmt::Debug for Pattern<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_list().entries(self.parts()).finish()
}
}
impl<'a> FromIterator<PatternPart<'a>> for Pattern<'a> {
fn from_iter<I: IntoIterator<Item = PatternPart<'a>>>(iter: I) -> Self {
let mut iter = iter.into_iter();
let mut pattern = Pattern::new();
while let Some(part) = iter.next() {
pattern.push_with_hint(part, iter.size_hint().0);
}
pattern
}
}
impl<'a> From<Vec<PatternPart<'a>>> for Pattern<'a> {
fn from(parts: Vec<PatternPart<'a>>) -> Self {
let mut previous_text = false;
let normal = parts.iter().all(|p| {
let (ok, is_text) = match p {
PatternPart::Text(t) => (!t.is_empty() && !previous_text, true),
_ => (true, false),
};
previous_text = is_text;
ok
});
if !normal {
return parts.into_iter().collect();
}
match parts.len() {
0 => Pattern::new(),
1 => parts.into_iter().collect(),
_ => Pattern(Repr::Many(parts)),
}
}
}
impl<'p, 'a> IntoIterator for &'p Pattern<'a> {
type Item = &'p PatternPart<'a>;
type IntoIter = core::slice::Iter<'p, PatternPart<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.parts().iter()
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use alloc::vec;
use super::{Pattern, PatternPart, Repr};
use crate::expression::{
Attributes, Expression, Markup, MarkupKind, Options, VariableExpression, VariableRef,
};
fn text(s: &str) -> PatternPart<'_> {
PatternPart::Text(Cow::Borrowed(s))
}
fn var(name: &str) -> PatternPart<'_> {
PatternPart::Expression(Expression::Variable(VariableExpression {
arg: VariableRef { name: name.into() },
function: None,
attributes: Attributes::new(),
}))
}
fn markup(name: &str) -> PatternPart<'_> {
PatternPart::Markup(Markup {
kind: MarkupKind::Open,
name: name.into(),
options: Options::new(),
attributes: Attributes::new(),
})
}
#[test]
fn empty_text_is_dropped() {
let mut p = Pattern::new();
p.push(text(""));
assert!(p.is_empty());
assert_eq!(p.as_simple_text(), Some(""));
assert!(Pattern::from_text(Cow::Borrowed("")).is_empty());
p.push(var("x"));
p.push(text(""));
assert_eq!(p.len(), 1);
}
#[test]
fn adjacent_text_merges() {
let mut p = Pattern::new();
p.push(text("a"));
p.push(text("b"));
assert_eq!(p.parts(), [text("ab")]);
assert!(matches!(&p.parts()[0], PatternPart::Text(Cow::Owned(_))));
p.push(var("x"));
p.push(text("c"));
p.push(text("d"));
assert_eq!(p.parts(), [text("ab"), var("x"), text("cd")]);
assert_eq!(p.as_simple_text(), None);
}
#[test]
fn one_part_is_stored_inline() {
let p = Pattern::from_text(Cow::Borrowed("Hello"));
assert!(matches!(p.0, Repr::One(_)));
assert_eq!(p.as_simple_text(), Some("Hello"));
let mut q = Pattern::new();
q.push(markup("b"));
assert!(matches!(q.0, Repr::One(_)));
q.push(text("x"));
assert!(matches!(q.0, Repr::Many(_)));
assert!(matches!(Pattern::with_capacity(1).0, Repr::Empty));
let mut r = Pattern::with_capacity(3);
r.push(text("t"));
assert_eq!(r.parts(), [text("t")]);
}
#[test]
fn equality_ignores_representation() {
let inline = Pattern::from_text(Cow::Borrowed("x"));
let mut many = Pattern::with_capacity(4);
many.push(text("x"));
assert!(matches!(many.0, Repr::Many(_)));
assert_eq!(inline, many);
assert_eq!(Pattern::new(), Pattern::with_capacity(8));
}
#[test]
#[allow(clippy::many_single_char_names)]
fn from_vec_and_iter_normalize() {
let p: Pattern<'_> = vec![text("a"), text(""), text("b"), var("x")].into();
assert_eq!(p.parts(), [text("ab"), var("x")]);
let q: Pattern<'_> = vec![text("a"), var("x"), text("b")].into();
assert_eq!(q.len(), 3);
let r: Pattern<'_> = vec![var("x")].into();
assert!(matches!(r.0, Repr::One(_)));
let s: Pattern<'_> = [text("a"), text("b")].into_iter().collect();
assert_eq!(s.parts(), [text("ab")]);
let t: Pattern<'_> = vec![].into();
assert!(t.is_empty());
let collected: alloc::vec::Vec<&PatternPart<'_>> = (&q).into_iter().collect();
assert_eq!(collected.len(), 3);
assert_eq!(q.clone().into_parts().len(), 3);
}
#[test]
fn into_owned_keeps_value() {
let mut p = Pattern::new();
p.push(text("a"));
p.push(var("x"));
let owned: Pattern<'static> = p.clone().into_owned();
assert_eq!(owned, p);
}
}