use ignore::types::{Types, TypesBuilder};
use serde::{Deserialize, Serialize};
use std::path::Path;
use url::Url;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct FileExtensions(Vec<String>);
impl Default for FileExtensions {
fn default() -> Self {
FileType::default_extensions()
}
}
impl std::fmt::Display for FileExtensions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.join(","))
}
}
impl FileExtensions {
#[must_use]
pub const fn empty() -> Self {
Self(vec![])
}
pub fn extend<I: IntoIterator<Item = String>>(&mut self, iter: I) {
self.0.extend(iter);
}
pub fn contains<T: Into<String>>(&self, file_extension: T) -> bool {
self.0.contains(&file_extension.into())
}
pub fn build(self, skip_hidden: bool) -> super::Result<Types> {
let mut types_builder = TypesBuilder::new();
let prefix = if skip_hidden { "[!.]" } else { "" };
for ext in self.0 {
types_builder.add(&ext, &format!("{prefix}*.{ext}"))?;
}
Ok(types_builder.select("all").build()?)
}
}
impl From<FileExtensions> for Vec<String> {
fn from(value: FileExtensions) -> Self {
value.0
}
}
impl From<Vec<String>> for FileExtensions {
fn from(value: Vec<String>) -> Self {
Self(value)
}
}
impl From<FileType> for FileExtensions {
fn from(file_type: FileType) -> Self {
match file_type {
FileType::Html => FileType::html_extensions(),
FileType::Markdown => FileType::markdown_extensions(),
FileType::Css => FileType::css_extensions(),
FileType::Plaintext => FileType::plaintext_extensions(),
}
}
}
impl FromIterator<String> for FileExtensions {
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl Iterator for FileExtensions {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
self.0.pop()
}
}
impl std::str::FromStr for FileExtensions {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.split(',').map(String::from).collect()))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
pub enum FileType {
Html,
Markdown,
Css,
#[default]
Plaintext,
}
impl std::fmt::Display for FileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileType::Html => write!(f, "HTML"),
FileType::Markdown => write!(f, "Markdown"),
FileType::Css => write!(f, "CSS"),
FileType::Plaintext => write!(f, "plaintext"),
}
}
}
impl FileType {
const MARKDOWN_EXTENSIONS: &'static [&'static str] = &[
"markdown", "mkdown", "mkdn", "mdwn", "mdown", "mdx", "mkd", "md",
];
const HTML_EXTENSIONS: &'static [&'static str] = &["htm", "html"];
const CSS_EXTENSIONS: &'static [&'static str] = &["css"];
const PLAINTEXT_EXTENSIONS: &'static [&'static str] = &["txt"];
#[must_use]
pub fn default_extensions() -> FileExtensions {
let mut extensions = FileExtensions::empty();
extensions.extend(Self::markdown_extensions());
extensions.extend(Self::html_extensions());
extensions.extend(Self::css_extensions());
extensions.extend(Self::plaintext_extensions());
extensions
}
#[must_use]
pub fn markdown_extensions() -> FileExtensions {
Self::MARKDOWN_EXTENSIONS
.iter()
.map(|&s| s.to_string())
.collect()
}
#[must_use]
pub fn html_extensions() -> FileExtensions {
Self::HTML_EXTENSIONS
.iter()
.map(|&s| s.to_string())
.collect()
}
#[must_use]
pub fn css_extensions() -> FileExtensions {
Self::CSS_EXTENSIONS
.iter()
.map(|&s| s.to_string())
.collect()
}
#[must_use]
pub fn plaintext_extensions() -> FileExtensions {
Self::PLAINTEXT_EXTENSIONS
.iter()
.map(|&s| s.to_string())
.collect()
}
#[must_use]
pub fn from_extension(extension: &str) -> Option<Self> {
let ext = extension.to_lowercase();
if Self::MARKDOWN_EXTENSIONS.contains(&ext.as_str()) {
Some(Self::Markdown)
} else if Self::HTML_EXTENSIONS.contains(&ext.as_str()) {
Some(Self::Html)
} else if Self::CSS_EXTENSIONS.contains(&ext.as_str()) {
Some(Self::Css)
} else if Self::PLAINTEXT_EXTENSIONS.contains(&ext.as_str()) {
Some(Self::Plaintext)
} else {
None
}
}
}
impl<P: AsRef<Path>> From<P> for FileType {
fn from(p: P) -> FileType {
let path = p.as_ref();
match path
.extension()
.and_then(std::ffi::OsStr::to_str)
.map(str::to_lowercase)
.as_deref()
.and_then(FileType::from_extension)
{
Some(file_type) => file_type,
None if is_url(path) => FileType::Html,
_ => FileType::default(),
}
}
}
fn is_url(path: &Path) -> bool {
path.to_str()
.and_then(|s| Url::parse(s).ok())
.is_some_and(|url| url.scheme() == "http" || url.scheme() == "https")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extension() {
assert_eq!(FileType::from("foo.md"), FileType::Markdown);
assert_eq!(FileType::from("foo.MD"), FileType::Markdown);
assert_eq!(FileType::from("foo.mdx"), FileType::Markdown);
assert_eq!(FileType::from("README"), FileType::Plaintext);
assert_eq!(FileType::from("test"), FileType::Plaintext);
assert_eq!(FileType::from("test.unknown"), FileType::Plaintext);
assert_eq!(FileType::from("test.txt"), FileType::Plaintext);
assert_eq!(FileType::from("README.TXT"), FileType::Plaintext);
assert_eq!(FileType::from("test.htm"), FileType::Html);
assert_eq!(FileType::from("index.html"), FileType::Html);
assert_eq!(FileType::from("http://foo.com/index.html"), FileType::Html);
}
#[test]
fn test_default_extensions() {
let extensions = FileType::default_extensions();
assert!(extensions.contains("md"));
assert!(extensions.contains("html"));
assert!(extensions.contains("markdown"));
assert!(extensions.contains("htm"));
assert!(extensions.contains("css"));
let all_extensions: Vec<_> = extensions.into();
assert_eq!(
all_extensions.len(),
FileType::MARKDOWN_EXTENSIONS.len()
+ FileType::HTML_EXTENSIONS.len()
+ FileType::CSS_EXTENSIONS.len()
+ FileType::PLAINTEXT_EXTENSIONS.len()
);
}
#[test]
fn test_is_url() {
assert!(is_url(Path::new("http://foo.com")));
assert!(is_url(Path::new("https://foo.com")));
assert!(is_url(Path::new("http://www.foo.com")));
assert!(is_url(Path::new("https://www.foo.com")));
assert!(is_url(Path::new("http://foo.com/bar")));
assert!(is_url(Path::new("https://foo.com/bar")));
assert!(is_url(Path::new("http://foo.com:8080")));
assert!(is_url(Path::new("https://foo.com:8080")));
assert!(is_url(Path::new("http://foo.com/bar?q=hello")));
assert!(is_url(Path::new("https://foo.com/bar?q=hello")));
assert!(!is_url(Path::new("foo.com")));
assert!(!is_url(Path::new("www.foo.com")));
assert!(!is_url(Path::new("foo")));
assert!(!is_url(Path::new("foo/bar")));
assert!(!is_url(Path::new("foo/bar/baz")));
assert!(!is_url(Path::new("file:///foo/bar.txt")));
assert!(!is_url(Path::new("ftp://foo.com")));
}
#[test]
fn test_from_extension() {
assert_eq!(FileType::from_extension("html"), Some(FileType::Html));
assert_eq!(FileType::from_extension("HTML"), Some(FileType::Html));
assert_eq!(FileType::from_extension("htm"), Some(FileType::Html));
assert_eq!(
FileType::from_extension("markdown"),
Some(FileType::Markdown)
);
assert_eq!(FileType::from_extension("md"), Some(FileType::Markdown));
assert_eq!(FileType::from_extension("MD"), Some(FileType::Markdown));
assert_eq!(FileType::from_extension("txt"), Some(FileType::Plaintext));
assert_eq!(FileType::from_extension("TXT"), Some(FileType::Plaintext));
assert_eq!(FileType::from_extension("unknown"), None);
assert_eq!(FileType::from_extension("xyz"), None);
}
}