1use std::path::{Path, PathBuf};
2
3#[derive(Clone, Debug, PartialEq)]
4pub enum Location {
5 Frontmatter,
6 Inline(InlineLocation),
7}
8
9#[derive(Clone, Debug, PartialEq)]
14pub struct InlineLocation {
15 pub line: usize,
16 pub col_start: usize,
17 pub col_end: usize,
18}
19
20pub(crate) fn normalize_path(path: impl AsRef<Path>, root: Option<&Path>) -> PathBuf {
22 let path = if path.as_ref().is_absolute() {
23 path.as_ref().to_path_buf()
24 } else {
25 if let Some(r) = root {
26 r.to_path_buf().join(path)
27 } else {
28 std::path::absolute(&path).unwrap_or(path.as_ref().to_path_buf())
29 }
30 };
31
32 let mut realpath = PathBuf::new();
33 for component in path.components() {
34 realpath.push(component);
35 realpath = realpath.canonicalize().unwrap_or(realpath);
36 }
37 realpath
38}
39
40pub(crate) fn relative_path(from_dir: impl AsRef<Path>, to_file: impl AsRef<Path>) -> PathBuf {
43 let from: Vec<_> = from_dir.as_ref().components().collect();
44 let to: Vec<_> = to_file.as_ref().components().collect();
45 let common = from.iter().zip(to.iter()).take_while(|(a, b)| a == b).count();
46 let mut result = PathBuf::new();
47 for _ in 0..(from.len() - common) {
48 result.push("..");
49 }
50 for c in &to[common..] {
51 result.push(c);
52 }
53 result
54}
55
56pub(crate) fn rewrite_links(raw_content: &str, replacements: Vec<(crate::link::LocatedLink, String)>) -> String {
60 use std::collections::HashMap;
61
62 let mut by_line: HashMap<usize, Vec<usize>> = HashMap::new();
64 for (i, (ll, _)) in replacements.iter().enumerate() {
65 by_line.entry(ll.location.line).or_default().push(i);
66 }
67
68 let trailing_newline = raw_content.ends_with('\n');
69 let mut result_lines: Vec<String> = Vec::new();
70
71 for (line_idx, line) in raw_content.lines().enumerate() {
72 let line_num = line_idx + 1;
73 if let Some(indices) = by_line.get(&line_num) {
74 let mut sorted = indices.clone();
76 sorted.sort_by(|&a, &b| {
77 replacements[b]
78 .0
79 .location
80 .col_start
81 .cmp(&replacements[a].0.location.col_start)
82 });
83
84 let mut chars: Vec<char> = line.chars().collect();
85 for idx in sorted {
86 let (ll, new_text) = &replacements[idx];
87 let new_chars: Vec<char> = new_text.chars().collect();
88 chars.splice(ll.location.col_start..ll.location.col_end, new_chars);
89 }
90 result_lines.push(chars.into_iter().collect());
91 } else {
92 result_lines.push(line.to_string());
93 }
94 }
95
96 let mut result = result_lines.join("\n");
97 if trailing_newline {
98 result.push('\n');
99 }
100 result
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::env::current_dir;
107
108 #[test]
109 fn normalize_path_removes_dot() {
110 assert_eq!(
111 normalize_path(&PathBuf::from("/a/./b"), Some(¤t_dir().unwrap())),
112 PathBuf::from("/a/b")
113 );
114 }
115
116 #[test]
117 fn normalize_path_resolves_double_dot() {
118 let cwd = current_dir().unwrap();
119 assert_eq!(normalize_path(&cwd.join("../c"), None), cwd.parent().unwrap().join("c"));
120 }
121
122 #[test]
123 fn normalize_path_deep_traversal() {
124 let cwd = current_dir().unwrap();
125 assert_eq!(
126 normalize_path(&cwd.join("../../../d"), None),
127 cwd.parent().unwrap().parent().unwrap().parent().unwrap().join("d")
128 );
129 }
130
131 #[test]
132 fn normalize_path_traversal_beyond_root_stops_at_root() {
133 let cwd = current_dir().unwrap();
134 assert_eq!(
135 normalize_path(&cwd.join("../../../../../../b"), None),
136 PathBuf::from("/b")
137 );
138 }
139
140 #[test]
141 fn normalize_path_starting_with_single_dot() {
142 let cwd = current_dir().unwrap();
143 assert_eq!(normalize_path(&PathBuf::from("./b"), Some(&cwd.clone())), cwd.join("b"));
144 }
145
146 #[test]
147 fn normalize_path_starting_with_double_dot() {
148 let cwd = current_dir().unwrap();
149 assert_eq!(
150 normalize_path(&PathBuf::from("../b"), Some(&cwd.clone())),
151 cwd.parent().unwrap().join("b")
152 );
153 }
154}