use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FileReference {
WikiLink {
target: String,
display: Option<String>,
},
RelativePath(String),
Filename(String),
}
impl FileReference {
fn strip_heading(s: &str) -> String {
if let Some(hash_pos) = s.find('#') {
s[..hash_pos].trim().to_string()
} else {
s.to_string()
}
}
#[must_use]
pub fn parse(s: &str) -> Self {
let trimmed = s.trim();
if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
let inner = &trimmed[2..trimmed.len() - 2];
if let Some(pipe_pos) = inner.find('|') {
let target = Self::strip_heading(inner[..pipe_pos].trim());
let display = inner[pipe_pos + 1..].trim().to_string();
return Self::WikiLink {
target,
display: Some(display),
};
}
return Self::WikiLink {
target: Self::strip_heading(inner.trim()),
display: None,
};
}
if trimmed.starts_with("./") || trimmed.starts_with("../") {
return Self::RelativePath(trimmed.to_string());
}
Self::Filename(trimmed.to_string())
}
#[must_use]
pub fn display_name(&self) -> &str {
match self {
Self::WikiLink { target, display } => display.as_deref().unwrap_or(target),
Self::RelativePath(path) => {
let filename = path.rsplit('/').next().unwrap_or(path);
filename.strip_suffix(".md").unwrap_or(filename)
}
Self::Filename(name) => name.strip_suffix(".md").unwrap_or(name),
}
}
#[must_use]
pub fn target(&self) -> &str {
match self {
Self::WikiLink { target, .. } => target,
Self::RelativePath(path) => path,
Self::Filename(name) => name,
}
}
#[must_use]
pub fn wiki_link(target: impl Into<String>) -> Self {
Self::WikiLink {
target: target.into(),
display: None,
}
}
#[must_use]
pub fn wiki_link_with_display(target: impl Into<String>, display: impl Into<String>) -> Self {
Self::WikiLink {
target: target.into(),
display: Some(display.into()),
}
}
#[must_use]
pub fn relative_path(path: impl Into<String>) -> Self {
Self::RelativePath(path.into())
}
#[must_use]
pub fn filename(name: impl Into<String>) -> Self {
Self::Filename(name.into())
}
}
impl fmt::Display for FileReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WikiLink { target, display } => {
if let Some(d) = display {
write!(f, "[[{target}|{d}]]")
} else {
write!(f, "[[{target}]]")
}
}
Self::RelativePath(path) => write!(f, "{path}"),
Self::Filename(name) => write!(f, "{name}"),
}
}
}
impl From<&str> for FileReference {
fn from(s: &str) -> Self {
Self::parse(s)
}
}
impl From<String> for FileReference {
fn from(s: String) -> Self {
Self::parse(&s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_wikilink_simple() {
let reference = FileReference::parse("[[My Project]]");
assert!(matches!(
reference,
FileReference::WikiLink { target, display: None } if target == "My Project"
));
}
#[test]
fn parse_wikilink_with_display() {
let reference = FileReference::parse("[[my-project|My Project]]");
assert!(matches!(
reference,
FileReference::WikiLink { target, display: Some(d) }
if target == "my-project" && d == "My Project"
));
}
#[test]
fn parse_wikilink_with_heading_strips_heading() {
let reference = FileReference::parse("[[Page Name#Heading]]");
assert!(matches!(
reference,
FileReference::WikiLink { target, display: None }
if target == "Page Name"
));
}
#[test]
fn parse_wikilink_with_heading_and_display() {
let reference = FileReference::parse("[[page#section|Display Text]]");
assert!(matches!(
reference,
FileReference::WikiLink { target, display: Some(d) }
if target == "page" && d == "Display Text"
));
}
#[test]
fn parse_relative_path() {
let reference = FileReference::parse("./projects/foo.md");
assert!(matches!(
reference,
FileReference::RelativePath(p) if p == "./projects/foo.md"
));
}
#[test]
fn parse_relative_path_parent() {
let reference = FileReference::parse("../areas/work.md");
assert!(matches!(
reference,
FileReference::RelativePath(p) if p == "../areas/work.md"
));
}
#[test]
fn parse_filename() {
let reference = FileReference::parse("foo.md");
assert!(matches!(
reference,
FileReference::Filename(n) if n == "foo.md"
));
}
#[test]
fn display_name_wikilink_without_display() {
let reference = FileReference::wiki_link("My Project");
assert_eq!(reference.display_name(), "My Project");
}
#[test]
fn display_name_wikilink_with_display() {
let reference = FileReference::wiki_link_with_display("my-project", "My Project");
assert_eq!(reference.display_name(), "My Project");
}
#[test]
fn display_name_relative_path() {
let reference = FileReference::relative_path("./projects/my-project.md");
assert_eq!(reference.display_name(), "my-project");
}
#[test]
fn display_name_filename() {
let reference = FileReference::filename("my-task.md");
assert_eq!(reference.display_name(), "my-task");
}
#[test]
fn to_string_preserves_format() {
let wiki = FileReference::parse("[[My Project]]");
assert_eq!(wiki.to_string(), "[[My Project]]");
let wiki_display = FileReference::parse("[[my-project|My Project]]");
assert_eq!(wiki_display.to_string(), "[[my-project|My Project]]");
let path = FileReference::parse("./projects/foo.md");
assert_eq!(path.to_string(), "./projects/foo.md");
let filename = FileReference::parse("foo.md");
assert_eq!(filename.to_string(), "foo.md");
}
#[test]
fn from_str_conversion() {
let reference: FileReference = "[[Test]]".into();
assert!(matches!(
reference,
FileReference::WikiLink { target, display: None } if target == "Test"
));
}
#[test]
fn whitespace_handling() {
let reference = FileReference::parse(" [[ Page Name ]] ");
assert!(matches!(
reference,
FileReference::WikiLink { target, display: None } if target == "Page Name"
));
}
}