use std::fmt::Debug;
use ahtml::myfrom::MyFrom;
use anyhow::{bail, Result};
use chj_util::{
myasstr::MyAsStr,
slice::{first, rest},
};
use crate::path::path_segments;
#[derive(Clone, Debug, PartialEq)]
pub struct PPath<Segment: Clone + Debug> {
is_absolute: bool,
ends_with_slash: bool,
segments: Vec<Segment>, }
impl<'s, T> PPath<T>
where
T: MyFrom<&'s str> + Clone + Debug + 's,
{
pub fn from_str(s: &'s str) -> Self {
let is_absolute = s.chars().next() == Some('/');
let ends_with_slash = s.chars().last() == Some('/');
PPath {
is_absolute,
ends_with_slash,
segments: path_segments(s).map(|v| T::myfrom(v)).collect(),
}
}
}
fn repeated_dotdot<'t, T>(n: usize) -> Vec<T>
where
T: From<&'t str> + Clone,
{
std::iter::repeat(T::from("..")).take(n).collect()
}
impl<'s, T> PPath<T>
where
T: From<&'s str> + MyAsStr + Clone + Debug,
{
pub fn to_string(&self) -> String {
let mut s = String::new();
if self.is_absolute {
s.push('/');
}
if self.segments.is_empty() {
if !self.is_absolute {
s.push('.'); if self.ends_with_slash {
s.push('/');
}
}
} else {
let mut seen = false;
for p in &self.segments {
if seen {
s.push('/');
}
s.push_str(p.my_as_str());
seen = true;
}
if self.ends_with_slash {
s.push('/');
}
}
s
}
pub fn sub(&self, base: &Self) -> Result<Self> {
if self.is_absolute == base.is_absolute {
let mut ss = self.segments.iter();
let base_segments = base.segments();
let mut bs = if base.ends_with_slash || base_segments.is_empty() {
base_segments
} else {
&base_segments[0..base_segments.len() - 1]
}
.iter();
loop {
let s = ss.next();
let b = bs.next();
match (s, b) {
(Some(s), Some(b)) => {
let s_str = s.my_as_str();
let b_str = b.my_as_str();
if s_str != b_str {
let mut v = repeated_dotdot(bs.count() + 1);
v.push(s.clone());
v.extend(ss.cloned());
return Ok(PPath {
is_absolute: false,
ends_with_slash: self.ends_with_slash,
segments: v,
});
}
}
(Some(s), None) => {
let mut v = vec![s.clone()];
v.extend(ss.cloned());
return Ok(PPath {
is_absolute: false,
ends_with_slash: self.ends_with_slash,
segments: v,
});
}
(None, Some(_b)) => {
let v = repeated_dotdot(bs.count() + 1);
return Ok(PPath {
is_absolute: false,
ends_with_slash: self.ends_with_slash,
segments: v,
});
}
(None, None) => {
return Ok(PPath {
is_absolute: false,
ends_with_slash: self.ends_with_slash,
segments: vec![],
});
}
}
}
} else {
bail!("minus_base: the paths are not both absolute or relative")
}
}
pub fn contains_dot_or_dotdot(&self) -> bool {
self.segments.iter().any(|s| match s.my_as_str() {
"." => true,
".." => true,
_ => false,
})
}
pub fn is_canonical(&self) -> bool {
!self.contains_dot_or_dotdot()
}
pub fn same_document_as_path_str(&self, other: &str) -> bool {
itertools::equal(
self.segments.iter().map(|v| v.my_as_str()),
path_segments(other),
)
}
}
impl<P: Clone + Debug> PPath<P> {
pub fn new(is_absolute: bool, ends_with_slash: bool, segments: Vec<P>) -> Self {
PPath {
is_absolute,
ends_with_slash,
segments,
}
}
pub fn is_absolute(&self) -> bool {
self.is_absolute
}
pub fn ends_with_slash(&self) -> bool {
self.ends_with_slash
}
pub fn segments(&self) -> &[P] {
&self.segments
}
pub fn as_dir(&self) -> Self {
PPath {
is_absolute: self.is_absolute,
ends_with_slash: true,
segments: self.segments.clone(),
}
}
pub fn add_segments(&self, segments: &[P], ends_with_slash: bool) -> Self {
let mut newsegments = if self.ends_with_slash {
self.segments.clone()
} else {
self.segments[0..self.segments.len() - 1]
.iter()
.map(|c| (*c).clone())
.collect()
};
newsegments.extend_from_slice(segments);
PPath {
is_absolute: self.is_absolute,
ends_with_slash,
segments: newsegments,
}
}
pub fn add(&self, other: &Self) -> Self {
if other.is_absolute {
(*other).clone()
} else {
self.add_segments(&other.segments, other.ends_with_slash)
}
}
pub fn into_add(self, other: Self) -> Self {
if other.is_absolute {
other
} else {
self.add(&other)
}
}
pub fn first(&self) -> Option<P> {
first(&self.segments).cloned()
}
pub fn rest(&self) -> Option<Self> {
Some(PPath {
is_absolute: false,
ends_with_slash: self.ends_with_slash,
segments: rest(&self.segments)?.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_add() {
let paths = [
PPath::from_str(""), PPath::from_str("/"), PPath::from_str("/hello"), PPath::from_str("/world/"), PPath::from_str("foo"), PPath::from_str("bar/baz/"), PPath::from_str("foo/hum"), PPath::from_str("foo/hum/"), ];
let t = |p0: usize, p1: usize| {
let p: PPath<&str> = paths[p0].add(&paths[p1]);
let s = p.to_string();
(p, s)
};
assert_eq!(
t(1, 2),
(
PPath {
is_absolute: true,
ends_with_slash: false,
segments: vec!["hello"]
},
String::from("/hello")
)
);
assert_eq!(
t(2, 4),
(
PPath {
is_absolute: true,
ends_with_slash: false,
segments: vec!["foo"]
},
String::from("/foo")
)
);
assert_eq!(
t(3, 4),
(
PPath {
is_absolute: true,
ends_with_slash: false,
segments: vec!["world", "foo"]
},
String::from("/world/foo")
)
);
assert_eq!(
t(2, 5),
(
PPath {
is_absolute: true,
ends_with_slash: true,
segments: vec!["bar", "baz"]
},
String::from("/bar/baz/")
)
);
assert_eq!(
t(4, 5),
(
PPath {
is_absolute: false,
ends_with_slash: true,
segments: vec!["bar", "baz"]
},
String::from("bar/baz/")
)
);
assert_eq!(
t(6, 5),
(
PPath {
is_absolute: false,
ends_with_slash: true,
segments: vec!["foo", "bar", "baz"]
},
String::from("foo/bar/baz/")
)
);
assert_eq!(
t(7, 5),
(
PPath {
is_absolute: false,
ends_with_slash: true,
segments: vec!["foo", "hum", "bar", "baz"]
},
String::from("foo/hum/bar/baz/")
)
);
}
#[test]
fn t_minus_base() {
let minus = |a, b| -> String {
match PPath::<&str>::from_str(a).sub(&PPath::from_str(b)) {
Ok(p) => p.to_string(),
Err(e) => String::from("ERR: ") + &e.to_string(),
}
};
assert_eq!(minus("/a", "/"), "a");
assert_eq!(
minus("/a", ""),
"ERR: minus_base: the paths are not both absolute or relative"
);
assert_eq!(
minus("a", ""),
"a" );
assert_eq!(
minus("a/b/", ""),
"a/b/" );
assert_eq!(minus("a/b/", "a"), "a/b/");
assert_eq!(minus("a/b/", "a/"), "b/");
assert_eq!(minus("a/b/", "c"), "a/b/");
assert_eq!(minus("a/b/", "c/d"), "../a/b/");
assert_eq!(minus("a/b/", "c/d/"), "../../a/b/");
assert_eq!(minus("c/b/", "c/d/"), "../b/");
assert_eq!(minus("/a/b/", "/c/d"), "../a/b/");
assert_eq!(minus("/a/b/", "/c/d/"), "../../a/b/");
assert_eq!(minus("/c/b/", "/c/d/"), "../b/");
assert_eq!(minus("a/", "a/"), "./");
assert_eq!(minus("a", "a/"), ".");
assert_eq!(minus("a/", "a"), "a/");
assert_eq!(minus("a/", "a/b/c"), "../");
assert_eq!(minus("a", "a/b/c"), "..");
assert_eq!(minus("a", "a/b/c/"), "../..");
assert_eq!(minus("/blog", "/blog/2023/10/22/foo.html"), "../../..");
}
#[test]
fn t_canonical() {
let canon = |s| -> bool { PPath::<&str>::from_str(s).is_canonical() };
assert!(canon("a///b/c.html"));
assert!(canon("c.html"));
assert!(canon("")); assert!(!canon("."));
assert!(!canon("./a"));
assert!(!canon("a//./b/c.html"));
assert!(!canon("a//../c.html"));
}
}