use pulldown_cmark::{CowStr, Event, Tag};
pub(crate) fn normalize_link_destinations(events: Vec<Event<'_>>) -> Vec<Event<'_>> {
events.into_iter().map(rewrite_event).collect()
}
fn rewrite_event(event: Event<'_>) -> Event<'_> {
match event {
Event::Start(Tag::Link {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Link {
link_type,
dest_url: normalize(&dest_url),
title,
id,
}),
Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Image {
link_type,
dest_url: normalize(&dest_url),
title,
id,
}),
other => other,
}
}
fn normalize(url: &str) -> CowStr<'static> {
if url.contains("://") || url.starts_with("//") {
return CowStr::from(url.to_string());
}
let forward_slashes = url.replace('\\', "/");
let collapsed = collapse_duplicate_slashes(&forward_slashes);
CowStr::from(collapsed)
}
fn collapse_duplicate_slashes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_was_slash = false;
for c in s.chars() {
if c == '/' {
if prev_was_slash {
continue;
}
prev_was_slash = true;
} else {
prev_was_slash = false;
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::refine::roundtrip::{parse, serialize};
fn refine_events(input: &str) -> String {
let events = normalize_link_destinations(parse(input));
serialize(events).unwrap()
}
#[test]
fn converts_backslashes_to_forward_slashes() {
let output = refine_events(r"[text](my\folder\file.png)");
assert_eq!(output, "[text](my/folder/file.png)");
}
#[test]
fn collapses_duplicate_slashes() {
let output = refine_events("[text](folder//sub///file.png)");
assert_eq!(output, "[text](folder/sub/file.png)");
}
#[test]
fn normalizes_image_destinations_too() {
let output = refine_events(r"");
assert_eq!(output, "");
}
#[test]
fn leaves_a_real_url_untouched() {
let input = "[text](https://example.com//path)";
assert_eq!(refine_events(input), input);
}
#[test]
fn leaves_a_protocol_relative_reference_untouched() {
let input = "[text](//example.com/path)";
assert_eq!(refine_events(input), input);
}
#[test]
fn leaves_an_already_clean_relative_path_untouched() {
let input = "[text](folder/file.png)";
assert_eq!(refine_events(input), input);
}
#[test]
fn leaves_non_link_text_untouched() {
let output = refine_events("just some text");
assert_eq!(output, "just some text");
}
}