diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d8c9274 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,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 + } +} |