use turbovault_core::LinkType;
pub fn classify_url(url: &str) -> LinkType {
if url.starts_with("http://") || url.starts_with("https://") || url.starts_with("mailto:") {
return LinkType::ExternalLink;
}
if let Some(hash_pos) = url.find('#') {
if url[hash_pos..].starts_with("#^") {
return LinkType::BlockRef;
}
if hash_pos == 0 {
LinkType::Anchor
} else {
LinkType::HeadingRef
}
} else {
LinkType::MarkdownLink
}
}
pub fn classify_wikilink(target: &str) -> LinkType {
if let Some(hash_pos) = target.find('#') {
if target[hash_pos..].starts_with("#^") {
return LinkType::BlockRef;
}
if hash_pos == 0 {
LinkType::Anchor
} else {
LinkType::HeadingRef
}
} else {
LinkType::WikiLink
}
}
pub fn split_wikilink(inner: &str) -> (String, Option<String>) {
match inner.find('|') {
Some(pipe) => {
let target = &inner[..pipe];
let target = target.strip_suffix('\\').unwrap_or(target);
(target.to_string(), Some(inner[pipe + 1..].to_string()))
}
None => (inner.to_string(), None),
}
}
const LINK_PATH_ESCAPES: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'%')
.add(b'(')
.add(b')')
.add(b'<')
.add(b'>')
.add(b'#');
pub fn markdown_link_target(destination: &str) -> String {
if classify_url(destination) == LinkType::ExternalLink {
return destination.to_string();
}
percent_encoding::percent_decode_str(destination)
.decode_utf8()
.map_or_else(|_| destination.to_string(), |decoded| decoded.into_owned())
}
pub fn markdown_link_path(path: &str) -> String {
percent_encoding::utf8_percent_encode(path, LINK_PATH_ESCAPES).to_string()
}
pub fn link_destination_end(text: &str, start: usize) -> Option<usize> {
let bytes = text.as_bytes();
let mut end = start;
if bytes.get(start) == Some(&b'<') {
while *bytes.get(end)? != b'>' {
end += if bytes[end] == b'\\' { 2 } else { 1 };
}
return Some(end + 1);
}
let mut parens = 0usize;
while let Some(&byte) = bytes.get(end) {
match byte {
b'\\' => end += 1,
b'(' => parens += 1,
b')' if parens == 0 => break,
b')' => parens -= 1,
b' ' | b'\t' | b'\r' | b'\n' => break,
_ => {}
}
end += 1;
}
let end = end.min(bytes.len());
(end > start).then_some(end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_url_external() {
assert_eq!(classify_url("https://example.com"), LinkType::ExternalLink);
assert_eq!(
classify_url("http://example.com/path"),
LinkType::ExternalLink
);
assert_eq!(
classify_url("mailto:user@example.com"),
LinkType::ExternalLink
);
}
#[test]
fn test_classify_url_anchor() {
assert_eq!(classify_url("#section"), LinkType::Anchor);
assert_eq!(classify_url("#heading-with-dashes"), LinkType::Anchor);
}
#[test]
fn test_classify_url_heading_ref() {
assert_eq!(classify_url("file.md#section"), LinkType::HeadingRef);
assert_eq!(classify_url("./path/file.md#heading"), LinkType::HeadingRef);
assert_eq!(classify_url("note#Heading"), LinkType::HeadingRef);
}
#[test]
fn test_classify_url_block_ref() {
assert_eq!(classify_url("#^blockid"), LinkType::BlockRef);
assert_eq!(classify_url("file.md#^blockid"), LinkType::BlockRef);
assert_eq!(classify_url("note#^abc123"), LinkType::BlockRef);
}
#[test]
fn test_classify_url_markdown_link() {
assert_eq!(classify_url("./docs/api.md"), LinkType::MarkdownLink);
assert_eq!(classify_url("relative/path.md"), LinkType::MarkdownLink);
assert_eq!(classify_url("../parent/file.txt"), LinkType::MarkdownLink);
}
#[test]
fn test_classify_wikilink_simple() {
assert_eq!(classify_wikilink("Note"), LinkType::WikiLink);
assert_eq!(classify_wikilink("My Note"), LinkType::WikiLink);
assert_eq!(classify_wikilink("folder/Note"), LinkType::WikiLink);
}
#[test]
fn test_classify_wikilink_heading() {
assert_eq!(classify_wikilink("Note#Heading"), LinkType::HeadingRef);
assert_eq!(
classify_wikilink("Note#Heading with spaces"),
LinkType::HeadingRef
);
}
#[test]
fn test_classify_wikilink_block() {
assert_eq!(classify_wikilink("Note#^blockid"), LinkType::BlockRef);
assert_eq!(classify_wikilink("#^blockid"), LinkType::BlockRef);
}
#[test]
fn test_classify_wikilink_same_doc_anchor() {
assert_eq!(classify_wikilink("#Heading"), LinkType::Anchor);
assert_eq!(classify_wikilink("#Section Title"), LinkType::Anchor);
}
}