use pulldown_cmark::{Event, LinkType, Options, Parser, Tag, TagEnd};
use crate::content::{Site, Slug};
#[derive(Debug, PartialEq)]
pub struct WikiLinkRef {
pub target: String,
pub display: Option<String>,
}
enum WikiEvent<'a> {
Other(Event<'a>),
Link {
target: String,
display: Option<String>,
},
}
fn walk_with_wiki_links<'a>(
events: impl IntoIterator<Item = Event<'a>>,
mut handle: impl FnMut(WikiEvent<'a>),
) {
let mut state: Option<(String, String)> = None;
for event in events {
match (state.take(), event) {
(
None,
Event::Start(Tag::Link {
link_type: LinkType::WikiLink { .. },
dest_url,
..
}),
) => {
state = Some((dest_url.to_string(), String::new()));
}
(None, other) => handle(WikiEvent::Other(other)),
(Some(mut s), Event::Text(text)) => {
s.1.push_str(&text);
state = Some(s);
}
(Some(s), Event::End(TagEnd::Link)) => {
let (target, label) = s;
let display = if label == target { None } else { Some(label) };
handle(WikiEvent::Link { target, display });
}
(Some(s), other) => {
state = Some(s);
handle(WikiEvent::Other(other));
}
}
}
}
pub fn extract_wiki_links(input: &str) -> Vec<WikiLinkRef> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_WIKILINKS);
let mut refs = Vec::new();
walk_with_wiki_links(Parser::new_ext(input, opts), |event| {
if let WikiEvent::Link { target, display } = event {
refs.push(WikiLinkRef { target, display });
}
});
refs
}
pub fn rewrite_wiki_links<'a>(
events: Vec<Event<'a>>,
site: &Site,
) -> (Vec<Event<'a>>, Vec<String>) {
let mut out = Vec::with_capacity(events.len());
let mut broken = Vec::new();
walk_with_wiki_links(events, |event| match event {
WikiEvent::Other(e) => out.push(e),
WikiEvent::Link { target, display } => {
let slug: Slug = target.clone().into();
let html = match site.get(&slug) {
Some(page) => {
let url = page.url_path();
let text = display.unwrap_or_else(|| page.title().into_owned());
format!(
"<a href=\"{}\" class=\"wikilink\">{}</a>",
url,
html_escape(&text)
)
}
None => {
let text = display.unwrap_or_else(|| target.clone());
broken.push(target);
format!(
"<span class=\"wikilink broken\">{}</span>",
html_escape(&text)
)
}
};
out.push(Event::InlineHtml(html.into()));
}
});
(out, broken)
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_link() {
let refs = extract_wiki_links("See [[glossary]] for details.");
assert_eq!(
refs,
vec![WikiLinkRef {
target: "glossary".into(),
display: None,
}]
);
}
#[test]
fn pipe_aliased_link() {
let refs = extract_wiki_links("See [[glossary|the glossary]] for details.");
assert_eq!(
refs,
vec![WikiLinkRef {
target: "glossary".into(),
display: Some("the glossary".into()),
}]
);
}
#[test]
fn no_links() {
let refs = extract_wiki_links("No wiki links here.");
assert!(refs.is_empty());
}
#[test]
fn multiple_links() {
let refs = extract_wiki_links("[[foo]] and [[bar|Bar Page]] and [[baz]].");
assert_eq!(refs.len(), 3);
assert_eq!(
refs[0],
WikiLinkRef {
target: "foo".into(),
display: None
}
);
assert_eq!(
refs[1],
WikiLinkRef {
target: "bar".into(),
display: Some("Bar Page".into()),
}
);
assert_eq!(
refs[2],
WikiLinkRef {
target: "baz".into(),
display: None
}
);
}
#[test]
fn link_inside_code_block_ignored() {
let refs = extract_wiki_links("```\n[[not-a-link]]\n```");
assert!(refs.is_empty());
}
}