summaryrefslogtreecommitdiff
path: root/src/collect_files.rs
blob: 88d065ff117a757fea44af3264b150f34653e735 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
use crate::*;

use vagabond::*;


pub struct Website {
    pub name: String,
    pub pages: Vec<Page>,
    pub static_files: Vec<StaticItem>,
    pub static_dirs: Vec<StaticItem>,
}

pub struct Page {
    pub name: String,                 // Display name
    pub parent_url: String,           // URL base for relative links
    pub file_url: String,             // Safe file name, no extension
    pub full_url: String,             // Safe full URL, no extension
    pub source_path: PathBuf,         // Absolute path to source file
    pub document: MarkdownDocument,   // File content parsed as markdown
    pub headings: Vec<String>,        // Safe name of each document heading
}

pub struct StaticItem {
    pub full_url: String,            // Safe full URL, with extension
    pub source_path: PathBuf,        // Absolute path to source file
}



impl Page {
    pub fn back_string(&self) -> String {
        let mut back = String::new();
        for c in self.full_url.chars() {
            if c == '/' {
                back.push_str("../");
            }
        }
        return back;
    }
}



impl Website {
    pub fn from_path(path: &Path) -> Self {
        let mut new = Self {
            pages: Vec::new(),
            static_files: Vec::new(),
            static_dirs: Vec::new(),
            name: match Entry::from_path(path) {
                Ok(entry) => entry.name,
                Err(err) => error!("Couldn't open {:?}: {:?}", &path, err),
            },
        };
        new.collect_entry(path, path);
        return new;
    }

    fn collect_entry(&mut self, path: &Path, prefix: &Path) {
        let entry = Entry::from_path(path).unwrap();
        // Ignore dotted entries.
        if entry.name.starts_with('.') {
            return;
        }
        // Get name and extension.
        let (mut name, extension) = entry.split_name();
        if let Some((prefix, suffix)) = name.split_once(' ') {
            if prefix.chars().all(|c| "0123456789-".contains(c)) {
                name = suffix.to_string();
            }
        }
        let file_url = make_url_safe(&name);
        // Generate parent URL, used only for files.
        let source_path = entry.original_path.clone();
        let relative_path = source_path.strip_prefix(prefix).unwrap_or_else(
            |_| error!("Path doesn't start with {:?}: {:?}", prefix, source_path));
        let mut parent_url = String::new();
        let mut components: Vec<_> = relative_path.components().collect();
        components.pop();  // Remove file segment.
        for c in &components {
            let segment = &make_url_safe(&c.as_os_str().to_string_lossy());
            parent_url.push_str(segment); parent_url.push('/')
        };

        // Process each entry.
        if entry.is_directory() {
            if let Some(stripped) = entry.name.strip_prefix("!") {
                let full_url = make_url_safe(stripped);
                self.static_dirs.push(StaticItem { full_url, source_path });
            } else {
                for child in list_directory(entry.original_path).unwrap() {
                    self.collect_entry(&child.original_path, prefix);
                }
            }
        } else {
            match extension.as_str() {
                "md" => {
                    let markdown = std::fs::read_to_string(&source_path).unwrap();
                    let document = MarkdownDocument::from_str(&markdown);
                    let headings = document.blocks.iter()
                        .filter_map(|block| if let Block::Heading { line, .. } = block {
                            Some(make_url_safe(&line.to_string()))
                        } else {
                            None
                        }).collect();
                    // Change name and path if this is an index file.
                    let mut name = name;
                    let mut file_url = file_url;
                    let mut full_url = format!("{parent_url}{file_url}");
                    if file_url == "+index" {
                        if components.is_empty() {
                            // This is the index file for the whole site.
                            name = String::from("Home");
                            file_url = String::from("index");
                            full_url = String::from("index");
                        } else {
                            // This is an index file for a directory.
                            name = components[components.len()-1]
                                .as_os_str().to_string_lossy().to_string();
                            file_url = make_url_safe(&name);
                            full_url = parent_url.strip_suffix('/').unwrap_or(&parent_url).to_string();
                        }
                    }
                    self.pages.push(
                        Page { name, parent_url, file_url, full_url, source_path, document, headings });
                },
                _ => {
                    let full_url = format!("{parent_url}{file_url}.{extension}");
                    self.static_files.push(StaticItem { full_url, source_path });
                },
            }
        }
    }

    // Ext is extension without a dot.
    // Checks if a relative link to an internal page name can be reached from
    // the current page, and returns a resolved absolute link to the page with extension.
    pub fn has_page(&self, from: &Page, path: &str, ext: &str) -> Option<String> {
        // Remove heading fragment and file extension.
        let (path, heading) = match path.rsplit_once('#') {
            Some((path, heading)) => match heading.is_empty() {
                true => (path, None),
                false => (path, Some(heading)),
            }
            None => (path, None),
        };
        let path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path);

        // Attach parent of current page to given path.
        let directory = match from.parent_url.rsplit_once('/') {
            Some((parent, _)) => parent,
            None => &from.parent_url,
        };
        let full_path = match path.starts_with("/") {
            true => path.to_string(),
            false => format!("{directory}/{path}"),
        };

        // Remove relative portions of path.
        let segments: Vec<&str> = full_path.split("/")
            .filter(|seg| !seg.is_empty() && *seg != ".")
            .collect();
        let mut reduced_segments: Vec<&str> = segments.windows(2)
            .filter(|w| w[1] != "..")
            .map(|w| w[1])
            .collect();
        // The first segment is always skipped by the previous step.
        if !segments.is_empty() && segments.get(1) != Some(&"..") {
            if segments[0] != ".." {
                reduced_segments.insert(0, segments[0]);
            }
        }
        let path = reduced_segments.join("/");

        for page in &self.pages {
            if page.full_url == path {
                if let Some(heading) = heading {
                    if !page.headings.contains(&make_url_safe(heading)) {
                        warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name);
                    }
                }
                return Some(format!("{path}.{ext}"));
            }
        }
        return None;
    }
}