use regex::Regex;
use std::sync::LazyLock;
static TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[#([^\]]+)\]").unwrap());
static CONTEXT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[@([^\]]+)\]").unwrap());
static LINK_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\[([^\]]*)\]\(([^)]+)\)").unwrap());
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Tag {
pub name: String,
pub slug: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Context {
pub name: String,
pub slug: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InternalLink {
pub text: String,
pub url: String,
}
impl Tag {
pub fn new(name: &str) -> Self {
let slug = slug::slugify(name);
Self {
name: name.to_string(),
slug: slug.clone(),
url: format!("/tags/{}/", slug),
}
}
}
impl Context {
pub fn new(name: &str) -> Self {
let slug = slug::slugify(name);
Self {
name: name.to_string(),
slug: slug.clone(),
url: format!("/contexts/{}/", slug),
}
}
}
pub fn extract_tags(content: &str) -> Vec<Tag> {
TAG_REGEX
.captures_iter(content)
.map(|cap| Tag::new(&cap[1]))
.collect()
}
pub fn extract_contexts(content: &str) -> Vec<Context> {
CONTEXT_REGEX
.captures_iter(content)
.map(|cap| Context::new(&cap[1]))
.collect()
}
pub fn extract_internal_links(content: &str) -> Vec<InternalLink> {
LINK_REGEX
.captures_iter(content)
.filter_map(|cap| {
let url = &cap[2];
if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("mailto:")
{
None
} else {
Some(InternalLink {
text: cap[1].to_string(),
url: url.to_string(),
})
}
})
.collect()
}
pub fn transform_tags_to_links(content: &str) -> String {
TAG_REGEX
.replace_all(content, |caps: ®ex::Captures| {
let tag = Tag::new(&caps[1]);
format!("[#{}]({})", tag.name, tag.url)
})
.to_string()
}
pub fn transform_contexts_to_links(content: &str) -> String {
CONTEXT_REGEX
.replace_all(content, |caps: ®ex::Captures| {
let ctx = Context::new(&caps[1]);
format!("[@{}]({})", ctx.name, ctx.url)
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_tags() {
let content = "This is about [#rust systems programming] and [#web development].";
let tags = extract_tags(content);
assert_eq!(tags.len(), 2);
assert_eq!(tags[0].name, "rust systems programming");
assert_eq!(tags[0].slug, "rust-systems-programming");
assert_eq!(tags[0].url, "/tags/rust-systems-programming/");
}
#[test]
fn test_extract_contexts() {
let content = "This relates to [@linux networking] setup.";
let contexts = extract_contexts(content);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].name, "linux networking");
assert_eq!(contexts[0].url, "/contexts/linux-networking/");
}
#[test]
fn test_extract_internal_links() {
let content = "See [my post](/blog/my-post/) and [external](https://example.com).";
let links = extract_internal_links(content);
assert_eq!(links.len(), 1);
assert_eq!(links[0].url, "/blog/my-post/");
}
#[test]
fn test_transform_tags() {
let content = "Using [#rust].";
let transformed = transform_tags_to_links(content);
assert_eq!(transformed, "Using [#rust](/tags/rust/).");
}
}