1use crate::fs::config::get_root_dir;
2use anyhow::{anyhow, Result};
3use std::path::{Path, PathBuf};
4use url::Url;
5
6pub fn url_to_path(url_str: &str) -> Result<PathBuf> {
7 let mut normalized = url_str.replace('\\', "/");
9 if !normalized.contains("://") {
10 if normalized.starts_with("braid.org") || normalized.starts_with("braidfs") {
11 normalized = format!("https://{}", normalized);
12 } else if normalized.starts_with("localhost") || normalized.starts_with("127.0.0.1") {
13 normalized = format!("http://{}", normalized);
14 }
15 }
16
17 let url = Url::parse(&normalized)?;
18 let host = url.host_str().ok_or_else(|| anyhow!("URL missing host"))?;
19 let port = url.port();
20
21 let mut domain_dir = host.to_string();
22 if let Some(p) = port {
23 domain_dir.push_str(&format!("+{}", p));
25 }
26
27 let root = get_root_dir()?;
28 let mut path = root.join(domain_dir);
29
30 for segment in url.path_segments().unwrap_or_else(|| "".split('/')) {
32 path.push(segment);
33 }
34
35 if url.path().ends_with('/') {
37 path.push("index");
38 }
39
40 Ok(path)
41}
42
43pub fn path_to_url(path: &Path) -> Result<String> {
44 let root = get_root_dir()?;
45
46 let root_abs = std::fs::canonicalize(&root).unwrap_or_else(|_| root.clone());
48 let path_abs = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
49
50 let relative = path_abs.strip_prefix(&root_abs).map_err(|_| {
51 anyhow!(
52 "Path {:?} is not within BraidFS root {:?}",
53 path_abs,
54 root_abs
55 )
56 })?;
57
58 let mut components = relative.components();
59
60 let domain_comp = components.next().ok_or_else(|| anyhow!("Path too short"))?;
62
63 let domain_str = domain_comp.as_os_str().to_string_lossy();
64 if domain_str.starts_with('.') {
65 return Err(anyhow!("Ignoring dotfile/directory"));
66 }
67
68 let (host, port) = if let Some((h, p)) = domain_str.rsplit_once('+') {
69 (h, Some(p.parse::<u16>()?))
70 } else {
71 (domain_str.as_ref(), None)
72 };
73
74 let scheme = if host == "localhost" || host == "127.0.0.1" {
76 "http"
77 } else {
78 "https"
79 };
80
81 let mut url = Url::parse(&format!("{}://{}", scheme, host))?;
82 if let Some(p) = port {
83 url.set_port(Some(p)).map_err(|_| anyhow!("Invalid port"))?;
84 }
85
86 let mut path_segments = Vec::new();
87 for comp in components {
88 path_segments.push(comp.as_os_str().to_string_lossy().to_string());
89 }
90
91 if let Some(last) = path_segments.last() {
92 if last == "index" {
93 path_segments.pop();
94 }
95 }
96
97 url.path_segments_mut()
98 .map_err(|_| anyhow!("Cannot be base"))?
99 .extend(path_segments);
100
101 Ok(url.to_string())
102}
103
104pub fn path_join(parent: &str, name: &str) -> String {
105 if parent == "/" {
106 format!("/{}", name)
107 } else {
108 format!("{}/{}", parent, name)
109 }
110}
111
112pub fn extract_markdown(content: &str) -> String {
113 let trimmed = content.trim();
114 if trimmed.starts_with("<!DOCTYPE") || trimmed.starts_with("<html") {
115 let mut candidates = Vec::new();
116 let mut current_pos = 0;
117
118 while let Some(start_idx) = trimmed[current_pos..].find("<script type=\"statebus\">") {
119 let actual_start = current_pos + start_idx + "<script type=\"statebus\">".len();
120 if let Some(end_idx) = trimmed[actual_start..].find("</script>") {
121 let script_content = trimmed[actual_start..actual_start + end_idx].trim();
122 candidates.push(script_content.to_string());
123 current_pos = actual_start + end_idx + "</script>".len();
124 } else {
125 break;
126 }
127 }
128
129 if !candidates.is_empty() {
130 candidates.sort_by_key(|c| std::cmp::Reverse(c.len()));
133 for candidate in &candidates {
134 if candidate.contains("# ")
136 || candidate.contains("\n- ")
137 || candidate.contains("](")
138 || candidate.contains("\n## ")
139 {
140 return candidate.clone();
141 }
142 }
143 return candidates[0].clone();
145 }
146 }
147 content.to_string()
148}
149
150pub fn wrap_markdown(original_content: &str, new_markdown: &str) -> String {
151 let trimmed = original_content.trim();
152 if trimmed.starts_with("<!DOCTYPE") || trimmed.starts_with("<html") {
153 if let Some(start_idx) = trimmed.find("<script type=\"statebus\">") {
154 let prefix = &trimmed[..start_idx + "<script type=\"statebus\">".len()];
155 let after_script = &trimmed[start_idx..];
156 if let Some(end_idx) = after_script.find("</script>") {
157 let suffix = &after_script[end_idx..];
158 return format!("{}\n{}\n{}", prefix, new_markdown, suffix);
159 }
160 }
161 }
162 new_markdown.to_string()
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_extract_markdown() {
171 let html = "<html><script type=\"statebus\">hello world</script></html>";
172 assert_eq!(extract_markdown(html), "hello world");
173 }
174
175 #[test]
176 fn test_wrap_markdown() {
177 let html = "<html><script type=\"statebus\">hello world</script></html>";
178 let wrapped = wrap_markdown(html, "new content");
179 assert!(wrapped.contains("<script type=\"statebus\">"));
180 assert!(wrapped.contains("new content"));
181 assert!(wrapped.contains("</script>"));
182 }
183
184 #[test]
185 fn test_url_to_path() {
186 }
188
189 #[test]
190 fn test_path_join() {
191 assert_eq!(path_join("/", "foo"), "/foo");
192 assert_eq!(path_join("/bar", "baz"), "/bar/baz");
193 }
194}