#![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,
pub static_files: Vec,
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
}
}