summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: d8c9274cd55b390bf93f6ea54ba0f4e83c1ad80d (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
189
190
191
192
193
194
195
196
197
#![feature(path_add_extension)]

mod generate_html;
pub use generate_html::*;

use markdown::*;
use vagabond::*;


const NORMAL: &str = "\x1b[0m";
const BOLD:   &str = "\x1b[1m";
const WHITE:  &str = "\x1b[37m";
const RED:    &str = "\x1b[31m";
const BLUE:   &str = "\x1b[34m";

static mut VERBOSE: bool = false;
#[macro_export] macro_rules! verbose {
    ($($tokens:tt)*) => { if unsafe { VERBOSE } {
        eprint!("{BOLD}{BLUE}[INFO]{NORMAL}: "); eprint!($($tokens)*);
        eprintln!("{NORMAL}");
    } };
}
#[macro_export] macro_rules! error {
    ($($tokens:tt)*) => {{
        eprint!("{BOLD}{RED}[ERROR]{WHITE}: "); eprint!($($tokens)*);
        eprintln!("{NORMAL}"); std::process::exit(1);
    }};
}

fn main() {
    let args = Arguments::from_env_or_exit();
    if args.version {
        let version = env!("CARGO_PKG_VERSION");
        eprintln!("Markdown website generator, version {version}");
        std::process::exit(0);
    }
    if args.verbose {
        unsafe { VERBOSE = true; }
    }

    let mut website = Website {
        source_files: Vec::new(),
        static_files: Vec::new(),
        name: match Entry::from_path(&args.source) {
            Ok(entry) => entry.name,
            Err(err) => error!("Couldn't open {:?}: {:?}", args.source, err),
        },
        error: false,
    };

    // Collect all website files.
    match traverse_directory(&args.source) {
        Ok(entries) => for entry in entries {
            // Generate name, stripping any leading digit sequence.
            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();
                }
            }
            // Generate full URL with stripped name, no extension.
            let source_path = entry.original_path;
            let relative_path = source_path.strip_prefix(&args.source).unwrap_or_else(
                // Probably unreachable.
                |_| error!("Path doesn't start with {:?}: {:?}", args.source, source_path));
            let mut full_url = String::new();
            let mut components: Vec<_> = relative_path.components().collect();
            components.pop();  // Remove file segment, use the stripped name instead.
            for c in components {
                full_url.push_str(&make_url_safe(&c.as_os_str().to_string_lossy()));
                full_url.push('/')
            };
            full_url.push_str(&make_url_safe(&name));


            if extension == "md" {
                let mut file_url = make_url_safe(&name);
                if file_url == "+index" {
                    let components: Vec<_> = relative_path.components().collect();
                    if components.len() == 1 {
                        name = String::from("Home");
                        file_url = String::from("index");
                        full_url = String::from("index");
                    } else {
                        let parent = components[components.len()-2];
                        let parent_string = parent.as_os_str().to_string_lossy().to_string();
                        name = parent_string;
                        file_url = make_url_safe(&name);
                        full_url.clear();
                        for c in &components[..components.len()-2] {
                            full_url.push_str(&make_url_safe(&c.as_os_str().to_string_lossy()));
                            full_url.push('/')
                        };
                        full_url.push_str(&file_url);
                    }
                }
                website.source_files.push(SourceFile { name, file_url, full_url, source_path });
            } else {
                full_url.push('.'); full_url.push_str(&extension);
                website.static_files.push(StaticFile { full_url, source_path });
            }
        }
        Err(err) => error!("Could not read from source directory: {:?}", err),
    }

    let mut destination = args.destination.clone();
    destination.push(make_url_safe(&website.name));

    for source_file in &website.source_files {
        let markdown = std::fs::read_to_string(&source_file.source_path).unwrap();
        let document = MarkdownDocument::from_str(&markdown);
        let mut destination = destination.clone();
        destination.push(&source_file.full_url);
        // Convert document to different formats.
        if args.html {
            let html = generate_html(&document, source_file, &website);
            write_file(&html, &destination, "html");
        }
        // Copy original markdown file.
        write_file(&markdown, &destination, "md");
    }

    for static_file in &website.static_files {
        let mut destination = destination.clone();
        destination.push(&static_file.full_url);
        verbose!("Copying static file to {destination:?}");
        make_parent_directory(&destination).unwrap();
        copy(&static_file.source_path, &destination).unwrap();
    }
}



pub fn write_file(text: &str, destination: &PathBuf, ext: &str) {
    let mut destination = destination.clone();
    destination.add_extension(ext);
    verbose!("Generating {destination:?}");
    make_parent_directory(&destination).unwrap();
    write_to_file(destination, text).unwrap();
}

pub fn make_url_safe(text: &str) -> String {
    text.to_ascii_lowercase().chars().filter_map(|c|
        if c.is_alphanumeric() || "-_~.+/".contains(c) { Some(c) }
        else if c == ' ' { Some('-') }
        else { None } )
    .collect()
}


pub struct Website {
    pub name: String,
    pub source_files: Vec<SourceFile>,
    pub static_files: Vec<StaticFile>,
    pub error: bool,
}

impl Website {
    pub fn has_page(&self, path: &str) -> bool {
        for source_file in &self.source_files {
            if source_file.full_url == path {
                return true;
            }
        }
        return false;
    }
}

pub struct SourceFile {
    pub name: String,
    pub file_url: String,  // URL file segment, no extension
    pub full_url: String,  // URL full path, no extension
    pub source_path: PathBuf,
}

pub struct StaticFile {
    pub full_url: String,  // URL full path, with extension
    pub source_path: PathBuf,
}

xflags::xflags! {
    /// Generate a website from a structured directory of markdown files.
    cmd arguments {
        /// Source directory with markdown files
        required source: PathBuf
        /// Path to output directory
        required destination: PathBuf
        /// Generate HTML output
        optional --html
        /// Generate Gemtext output
        optional --gmi
        /// Print information as each file is parsed
        optional -v, --verbose
        /// Print the program version and exit
        optional --version
    }
}