Skip to main content

braid_core/fs/
mapping.rs

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    // Normalize URL string: handle Windows-style backslashes and missing protocols
8    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        // Use + instead of : for port separator on Windows (OS Error 123)
24        domain_dir.push_str(&format!("+{}", p));
25    }
26
27    let root = get_root_dir()?;
28    let mut path = root.join(domain_dir);
29
30    // Trim leading slash from path segments
31    for segment in url.path_segments().unwrap_or_else(|| "".split('/')) {
32        path.push(segment);
33    }
34
35    // If path ends in slash or is empty, it might be a directory in URL semantics
36    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    // Canonicalize both paths to ensure matching prefix format (e.g. \\?\ prefix and casing)
47    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    // First component is domain[:port]
61    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    // Construct URL
75    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            // Heuristic: Pick the script content that looks most like Markdown.
131            // Sort by length first (descending)
132            candidates.sort_by_key(|c| std::cmp::Reverse(c.len()));
133            for candidate in &candidates {
134                // If it has markdown-like features, it's likely the one we want
135                if candidate.contains("# ")
136                    || candidate.contains("\n- ")
137                    || candidate.contains("](")
138                    || candidate.contains("\n## ")
139                {
140                    return candidate.clone();
141                }
142            }
143            // Fallback to the longest candidate if no clear markdown indicators found
144            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        // Logic check
187    }
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}