fallow_output/
json_paths.rs1pub fn strip_root_prefix(value: &mut serde_json::Value, prefix: &str) {
9 match value {
10 serde_json::Value::String(s) => strip_root_prefix_from_string(s, prefix),
11 serde_json::Value::Array(items) => {
12 for item in items {
13 strip_root_prefix(item, prefix);
14 }
15 }
16 serde_json::Value::Object(map) => {
17 for value in map.values_mut() {
18 strip_root_prefix(value, prefix);
19 }
20 }
21 _ => {}
22 }
23}
24
25fn strip_root_prefix_from_string(value: &mut String, prefix: &str) {
26 if let Some(rest) = value.strip_prefix(prefix) {
27 *value = rest.to_string();
28 return;
29 }
30
31 let normalized = normalize_output_path(value);
32 let normalized_prefix = normalize_output_path(prefix);
33 if let Some(rest) = normalized.strip_prefix(&normalized_prefix) {
34 *value = rest.to_string();
35 } else if let Some(stripped) = strip_embedded_root_prefixes(&normalized, &normalized_prefix) {
36 *value = stripped;
37 }
38}
39
40fn normalize_output_path(path: &str) -> String {
41 normalize_uri(path)
42}
43
44#[must_use]
51pub fn normalize_uri(path: &str) -> String {
52 path.replace('\\', "/")
53 .replace('[', "%5B")
54 .replace(']', "%5D")
55}
56
57fn strip_embedded_root_prefixes(value: &str, prefix: &str) -> Option<String> {
58 let mut output = String::with_capacity(value.len());
59 let mut changed = false;
60 let mut last = 0;
61 let mut search_from = 0;
62
63 while let Some(offset) = value[search_from..].find(prefix) {
64 let index = search_from + offset;
65 let can_strip = index > 0
66 && value[..index]
67 .chars()
68 .next_back()
69 .is_some_and(is_embedded_path_boundary);
70
71 if can_strip {
72 output.push_str(&value[last..index]);
73 last = index + prefix.len();
74 changed = true;
75 }
76
77 search_from = index + prefix.len();
78 }
79
80 if changed {
81 output.push_str(&value[last..]);
82 Some(output)
83 } else {
84 None
85 }
86}
87
88fn is_embedded_path_boundary(c: char) -> bool {
89 c.is_whitespace() || matches!(c, '"' | '\'' | '`' | '(' | '[' | '{' | ':' | '=')
90}
91
92#[cfg(test)]
93mod tests {
94 use serde_json::json;
95
96 use super::*;
97
98 #[test]
99 fn strips_root_from_nested_strings() {
100 let mut value = json!({
101 "path": "/project/src/index.ts",
102 "items": ["/project/src/a.ts", { "path": "/project/src/b.ts" }]
103 });
104
105 strip_root_prefix(&mut value, "/project/");
106
107 assert_eq!(value["path"], "src/index.ts");
108 assert_eq!(value["items"][0], "src/a.ts");
109 assert_eq!(value["items"][1]["path"], "src/b.ts");
110 }
111
112 #[test]
113 fn normalizes_windows_separators_before_stripping() {
114 let mut value = json!("C:\\repo\\src\\index.ts");
115
116 strip_root_prefix(&mut value, "C:/repo/");
117
118 assert_eq!(value, json!("src/index.ts"));
119 }
120
121 #[test]
122 fn rewrites_embedded_path_strings() {
123 let mut value = json!("See /project/src/a.ts and /project/src/b.ts");
124
125 strip_root_prefix(&mut value, "/project/");
126
127 assert_eq!(value, json!("See src/a.ts and src/b.ts"));
128 }
129
130 #[test]
131 fn leaves_non_matching_strings_unchanged() {
132 let mut value = json!("src/index.ts");
133
134 strip_root_prefix(&mut value, "/project/");
135
136 assert_eq!(value, json!("src/index.ts"));
137 }
138
139 #[test]
140 fn normalize_uri_rewrites_backslashes_and_brackets() {
141 assert_eq!(
142 normalize_uri("app\\[lang]\\posts\\[id].tsx"),
143 "app/%5Blang%5D/posts/%5Bid%5D.tsx"
144 );
145 }
146}