use std::collections::BTreeSet;
const DEFAULT_EXTENSIONS: [&str; 3] = ["md", "mdc", "markdown"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtensionFilter(BTreeSet<String>);
impl Default for ExtensionFilter {
fn default() -> Self { DEFAULT_EXTENSIONS.into_iter().map(str::to_owned).collect() }
}
impl ExtensionFilter {
#[must_use]
pub fn matches(&self, candidate: &str) -> bool {
extension(candidate)
.is_some_and(|extension| self.0.contains(&extension.to_ascii_lowercase()))
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
let mut extensions: Vec<&str> = self.0.iter().map(String::as_str).collect();
extensions.sort_unstable_by_key(|extension| (extension.len(), *extension));
extensions.into_iter()
}
}
fn extension(path: &str) -> Option<&str> {
let name = path.rsplit('/').next()?;
let (stem, extension) = name.rsplit_once('.')?;
(!stem.is_empty()).then_some(extension)
}
impl FromIterator<String> for ExtensionFilter {
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
Self(
iter.into_iter()
.map(|value| value.to_ascii_lowercase())
.collect(),
)
}
}
impl std::fmt::Display for ExtensionFilter {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (index, extension) in self.iter().enumerate() {
if index > 0 {
formatter.write_str(", ")?;
}
formatter.write_str(extension)?;
}
Ok(())
}
}
pub fn parse_extension(value: &str) -> Result<String, ExtensionSpecError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ExtensionSpecError::Empty);
}
if trimmed == "." {
return Err(ExtensionSpecError::DotOnly {
value: value.to_owned(),
});
}
let without_dot = trimmed.strip_prefix('.').unwrap_or(trimmed);
if let Some(kind) = invalid_character(without_dot) {
return Err(ExtensionSpecError::InvalidCharacter {
value: value.to_owned(),
kind,
});
}
Ok(without_dot.to_ascii_lowercase())
}
fn invalid_character(value: &str) -> Option<InvalidCharacterKind> {
let separator = value
.find(['/', '\\'])
.map(|_| InvalidCharacterKind::PathSeparator);
let nul = value.find('\0').map(|_| InvalidCharacterKind::Nul);
let dot = value.find('.').map(|_| InvalidCharacterKind::Dot);
separator.or(nul).or(dot)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ExtensionSpecError {
#[error("extension is empty")]
Empty,
#[error("extension {value:?} is only a dot")]
DotOnly {
value: String,
},
#[error("extension {value:?} contains {kind}")]
InvalidCharacter {
value: String,
kind: InvalidCharacterKind,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidCharacterKind {
#[error("a path separator")]
PathSeparator,
#[error("a NUL byte")]
Nul,
#[error("a dot")]
Dot,
}
#[cfg(test)]
#[path = "extensions_tests.rs"]
mod tests;