specfold_core/markdown/
link_dest.rs1use crate::export::links::{md_link_re, md_ref_def_re, outside_code};
7
8pub fn spans(body: &str) -> Vec<(usize, usize)> {
10 let mut out = vec![];
11 for (re, groups) in [(md_link_re(), [3, 4]), (md_ref_def_re(), [2, 3])] {
12 outside_code(body, re, |start, m| {
13 if let Some(g) = groups.iter().find_map(|&i| m.get(i)) {
14 out.push((start + g.start(), start + g.end()));
15 }
16 });
17 }
18 out.sort_unstable();
19 out
20}
21
22pub fn decode(p: &str) -> String {
24 let b = p.as_bytes();
25 let mut out = Vec::with_capacity(b.len());
26 let mut i = 0;
27 while i < b.len() {
28 let hex = |c: u8| (c as char).to_digit(16);
29 match (b[i], b.get(i + 1).copied().and_then(hex), b.get(i + 2).copied().and_then(hex)) {
30 (b'%', Some(h), Some(l)) => {
31 out.push((h * 16 + l) as u8);
32 i += 3;
33 }
34 (c, _, _) => {
35 out.push(c);
36 i += 1;
37 }
38 }
39 }
40 String::from_utf8(out).unwrap_or_else(|_| p.to_string())
41}
42
43pub fn encode(p: &str) -> String {
45 p.bytes()
46 .map(|c| match c {
47 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => (c as char).to_string(),
48 _ => format!("%{c:02X}"),
49 })
50 .collect()
51}
52
53pub fn rewrite(body: &str, mut new_path: impl FnMut(&str) -> Option<String>) -> (String, usize) {
57 let mut out = String::with_capacity(body.len());
58 let (mut at, mut n) = (0, 0);
59 for (a, b) in spans(body) {
60 let dest = &body[a..b];
61 let cut = dest.find(['#', '?']).unwrap_or(dest.len());
62 let (path, tail) = dest.split_at(cut);
63 if path.is_empty() {
64 continue;
65 }
66 let Some(new) = new_path(&decode(path)) else { continue };
67 let new = if path.contains('%') { encode(&new) } else { new };
68 out.push_str(&body[at..a]);
69 out.push_str(&new);
70 out.push_str(tail);
71 at = b;
72 n += 1;
73 }
74 out.push_str(&body[at..]);
75 (out, n)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn every_link_form_is_rewritten_outside_code() {
84 let body = "[a](old.md#x) [b](<old.md>) [c](%EC%98%9B.md)\n\n[r]: old.md \"t\"\n`[d](old.md)`\n```\n[e](old.md)\n```\n";
85 let (out, n) = rewrite(body, |p| matches!(p, "old.md" | "옛.md").then(|| "new dir/새.md".to_string()));
86 assert_eq!(n, 4);
87 assert_eq!(
88 out,
89 "[a](new dir/새.md#x) [b](<new dir/새.md>) [c](new%20dir/%EC%83%88.md)\n\n[r]: new dir/새.md \"t\"\n`[d](old.md)`\n```\n[e](old.md)\n```\n"
90 );
91 assert_eq!(decode("%ZZ%41"), "%ZZA");
92 }
93}