specfold_core/authoring/
refs.rs1use regex::Regex;
4use serde_json::{Map, Value};
5
6pub fn rewrite_body(text: &str, old: &str, new: &str) -> String {
8 let re = Regex::new(&format!(r"\[\[{}(#[^\]|]*)?(\|[^\]]*)?\]\]", regex::escape(old)))
9 .expect("an escaped id always forms a valid regex");
10 re.replace_all(text, |c: ®ex::Captures| {
11 format!("[[{new}{}{}]]", c.get(1).map_or("", |m| m.as_str()), c.get(2).map_or("", |m| m.as_str()))
12 })
13 .into_owned()
14}
15
16pub fn same_path(cited: &str, path: &str) -> bool {
18 let mut t = cited.trim_end_matches('/');
19 while let Some(rest) = t.strip_prefix("../").or_else(|| t.strip_prefix("./")) {
20 t = rest;
21 }
22 t == path
23}
24
25pub fn rewrite_rel(map: &mut Map<String, Value>, old: &str, new: &str) -> bool {
27 let Some(Value::Object(rel)) = map.get_mut("rel") else { return false };
28 let mut changed = false;
29 for v in rel.values_mut() {
30 let Value::Array(items) = v else { continue };
31 for item in items.iter_mut() {
32 let Some(s) = item.as_str() else { continue };
33 let (target, anchor) = s.split_once('#').map(|(t, a)| (t, Some(a))).unwrap_or((s, None));
34 if target == old {
35 *item = Value::String(match anchor {
36 Some(a) => format!("{new}#{a}"),
37 None => new.to_string(),
38 });
39 changed = true;
40 }
41 }
42 }
43 changed
44}
45
46fn normalize(path: &str) -> Option<String> {
48 let mut out: Vec<&str> = vec![];
49 for part in path.split('/') {
50 match part {
51 "" | "." => {}
52 ".." => {
53 out.pop()?;
54 }
55 p => out.push(p),
56 }
57 }
58 Some(out.join("/"))
59}
60
61fn relative(from: &str, to: &str) -> String {
63 let base: Vec<&str> =
64 from.rsplit_once('/').map(|x| x.0).unwrap_or("").split('/').filter(|p| !p.is_empty()).collect();
65 let target: Vec<&str> = to.split('/').collect();
66 let common = base.iter().zip(&target).take_while(|(a, b)| a == b).count();
67 let mut parts: Vec<&str> = vec![".."; base.len() - common];
68 parts.extend(&target[common..]);
69 parts.join("/")
70}
71
72pub fn rewrite_relative_links(body: &str, file: &str, old: &str, new: &str) -> String {
75 let dir = file.rsplit_once('/').map(|x| x.0).unwrap_or("");
76 crate::markdown::link_dest::rewrite(body, |target| {
77 let local = !target.is_empty() && !target.contains("://") && !target.starts_with('/');
78 let joined = if dir.is_empty() { target.to_string() } else { format!("{dir}/{target}") };
79 (local && normalize(&joined).as_deref() == Some(old)).then(|| relative(file, new))
80 })
81 .0
82}
83
84pub fn rewrite_bundle_links(body: &str, file: &str, old_dir: &str, new_dir: &str) -> String {
87 let dir = file.rsplit_once('/').map(|x| x.0).unwrap_or("");
88 let prefix = format!("{old_dir}/");
89 crate::markdown::link_dest::rewrite(body, |target| {
90 let local = !target.is_empty() && !target.contains("://") && !target.starts_with('/');
91 let joined = if dir.is_empty() { target.to_string() } else { format!("{dir}/{target}") };
92 let rest = normalize(&joined).filter(|_| local)?.strip_prefix(&prefix)?.to_string();
93 Some(relative(file, &format!("{new_dir}/{rest}")))
94 })
95 .0
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn rewrites() {
104 let t = "a [[adr:x/1]] b [[adr:x/1#K-1|see]] c [[adr:x/10]]";
105 assert_eq!(rewrite_body(t, "adr:x/1", "adr:y/1"), "a [[adr:y/1]] b [[adr:y/1#K-1|see]] c [[adr:x/10]]");
106 let mut m: Map<String, Value> =
107 serde_json::from_str(r#"{"rel":{"implements":["adr:x/1#A","sot:z/q"]}}"#).unwrap();
108 assert!(rewrite_rel(&mut m, "adr:x/1", "adr:y/1"));
109 assert_eq!(m["rel"]["implements"][0], "adr:y/1#A");
110 }
111}