use std::fmt::Formatter;
use email_address_parser::EmailAddress;
use miette::Diagnostic;
use thiserror::Error;
use url::Url;
#[derive(Debug)]
pub enum LinkDest {
External(Url),
Local(String),
Email(String),
}
impl LinkDest {
pub fn parse(s: &str) -> Result<Self, LinkDestError> {
if let Ok(url) = Url::parse(s) {
Ok(Self::External(url))
} else if EmailAddress::parse(s, None).is_some() {
Ok(Self::Email(s.to_string()))
} else {
Ok(Self::Local(s.to_string()))
}
}
pub fn is_external(&self) -> bool {
matches!(self, Self::External(_))
}
pub fn is_local(&self) -> bool {
match self {
Self::External(_) | Self::Email(_) => false,
Self::Local(_) => true,
}
}
pub fn is_relative(&self) -> bool {
match self {
Self::External(_) | Self::Email(_) => false,
Self::Local(s) => !s.starts_with('/'),
}
}
pub fn is_absolute(&self) -> bool {
!self.is_relative()
}
pub fn fragment(&self) -> Option<&str> {
match self {
Self::External(url) => url.fragment(),
Self::Local(s) => s.rsplit_once('#').map(|(_, f)| f),
Self::Email(_) => None,
}
}
pub fn path(&self) -> &str {
match self {
Self::External(url) => url.path(),
Self::Local(s) => {
let path = s.split_once('#').map_or(s.as_str(), |(p, _)| p);
if path.starts_with("./") {
&path[2..]
} else {
path
}
}
Self::Email(source) => source,
}
}
pub fn is_possible_source_link(&self) -> bool {
if !self.is_local() {
return false;
}
if self.is_absolute() {
return false;
}
let path = self.path();
if path.is_empty() {
return false;
}
if path.ends_with('/') {
return false;
}
if path.ends_with(".md") {
return true;
}
true
}
}
impl std::fmt::Display for LinkDest {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::External(url) => write!(f, "{}", url),
Self::Local(s) => write!(f, "{}", s),
Self::Email(source) => write!(f, "{}", source),
}
}
}
#[derive(Diagnostic, Debug, Error)]
pub enum LinkDestError {}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn external_link() -> miette::Result<()> {
let dest = LinkDest::parse("https://example.com")?;
assert!(matches!(dest, LinkDest::External(_)));
assert!(!dest.is_relative());
Ok(())
}
#[test]
fn local_link() -> miette::Result<()> {
let dest = LinkDest::parse("/foo/bar")?;
assert!(matches!(dest, LinkDest::Local(_)));
assert!(!dest.is_relative());
let dest = LinkDest::parse("foo/bar")?;
assert!(matches!(dest, LinkDest::Local(_)));
assert!(dest.is_relative());
let dest = LinkDest::parse("../foo/bar")?;
assert!(matches!(dest, LinkDest::Local(_)));
assert!(dest.is_relative());
let dest = LinkDest::parse("./testimonials.md")?;
assert!(matches!(dest, LinkDest::Local(_)));
assert!(dest.is_relative());
Ok(())
}
#[test]
fn fragment() -> miette::Result<()> {
let dest = LinkDest::parse("https://example.com#foo")?;
assert_eq!(dest.fragment(), Some("foo"));
let dest = LinkDest::parse("/foo/bar#foo")?;
assert_eq!(dest.fragment(), Some("foo"));
let dest = LinkDest::parse("foo/bar#foo")?;
assert_eq!(dest.fragment(), Some("foo"));
let dest = LinkDest::parse("../foo/bar#foo")?;
assert_eq!(dest.fragment(), Some("foo"));
Ok(())
}
#[test]
fn path() -> miette::Result<()> {
let dest = LinkDest::parse("https://example.com")?;
assert_eq!(dest.path(), "/");
let dest = LinkDest::parse("/foo/bar")?;
assert_eq!(dest.path(), "/foo/bar");
let dest = LinkDest::parse("foo/bar")?;
assert_eq!(dest.path(), "foo/bar");
let dest = LinkDest::parse("../foo/bar")?;
assert_eq!(dest.path(), "../foo/bar");
let dest = LinkDest::parse("./testimonials.md")?;
assert_eq!(dest.path(), "testimonials.md");
Ok(())
}
#[test]
fn is_possible_source_link() -> miette::Result<()> {
let patterns = [
("https://example.com", false),
("./testimonials.md", true),
("#gat-desugaring", false),
(
"/blog/2013/09/10/how-to-write-a-simple-scheme-debugger/",
false,
),
("/papers/dissertation.pdf", false),
("eric@theincredibleholk.org", false),
("/images/whereabouts-clock-drawing.pdf", false),
];
for (pattern, expected) in patterns {
let dest = LinkDest::parse(pattern)?;
assert_eq!(dest.is_possible_source_link(), expected, "{}", pattern);
}
Ok(())
}
}