summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs197
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
+ }
+}