/* pull the std into scope and inline it so that we get documentation for it, even when running offline */ #[doc(inline)] pub use std; use std::path::{Path, PathBuf}; use std::io::{self, Read, Write, BufRead, BufReader}; use std::fs::{self, File}; use std::time; use std::collections::HashMap; use structopt::StructOpt; use regex::Regex; #[derive(Debug, StructOpt)] #[structopt(name = "vokobe", about = "A static site generator")] struct Opt { /// Input path #[structopt(parse(from_os_str))] input_path: PathBuf, /// Output path #[structopt(parse(from_os_str))] output_path: PathBuf, /// Site name (e.g. emile.space) site_name: String, /// Activate sending analytics to stats.emile.space // -a and --analytics will be generated // analytics are sent to stats.emile.space #[structopt(short, long)] analytics: bool, } fn main() -> std::io::Result<()> { let mut internal_links: HashMap> = HashMap::new(); let opt = Opt::from_args(); let in_path = opt.input_path; let output_path = opt.output_path; // read the style let style_path = Path::new(&in_path).join("style.css"); let mut style_file = File::open(style_path) .expect("could not open style file"); let mut style = String::new(); style_file.read_to_string(&mut style) .expect("could not read style file to string"); // read all dirs in the input path let pathes = recursive_read_dir(&in_path, false)?; // pass 1: store the backlinks for path in &pathes { if path.ends_with("README.md") { // open the file and read it as a string let mut readme_file = File::open(path)?; let mut readme = String::new(); readme_file.read_to_string(&mut readme)?; let internal_links_in_file = parse_internal_links(readme.as_str()); for link in internal_links_in_file { internal_links.entry(link).or_insert_with(Vec::new).push(path.to_string_lossy().into_owned()) } } } // for each markdown_file in markdown_files { // let internal_links_in_file = parse_internal_links(markdown_file); // internal_links.insert(markdown_file, internal_links_in_file); // } // pass 2: create the html println!("Got {} files", pathes.len()); let mut readme_counter = 0; for path in pathes { let stripped_path = path.strip_prefix(&in_path) .expect(format!( "could not strip the in_path prefix: {:?}", in_path).as_str()); // copy images and other files to the output folder if path.is_file() { // define the source and destination let src = Path::new(&in_path).join(stripped_path); let dst = Path::new(&output_path).join(stripped_path); // define the destination folder (the dst path without the file) and create it let mut dst_folder = dst.clone(); dst_folder.pop(); // remove the file itself from the path fs::create_dir_all(dst_folder)?; // copy the file to the destination std::fs::copy(src, dst.as_path())?; } if stripped_path.ends_with("README.md") { readme_counter += 1; // define the "raw" path (no infile prefix, no file) let mut ancestors = stripped_path.ancestors(); ancestors.next(); let raw_path = ancestors.next() .expect("could not extract next ancestor"); // out + rawpath let index_path = output_path.join(raw_path); // (out + rawpath) + "index.html" let index_file = index_path.join("index.html"); // - create the dir for the index.html as well as the index.html // itself fs::create_dir_all(index_path)?; let mut file = File::create(&index_file)?; // this is the main block calling all other smaller functions. The // whole output is compsed here write_header(&mut file, &opt.site_name, &style)?; write_body_start(&mut file, &opt.site_name)?; write_nav(&mut file, in_path.as_path(), raw_path, opt.analytics)?; write_same_level(&mut file, in_path.as_path(), raw_path)?; write_readme_content(&mut file, in_path.as_path(), raw_path)?; write_footer(&mut file, raw_path, &internal_links)?; file.write_all("".as_bytes())?; } } println!("Got {readme_counter} README.md files"); Ok(()) } fn parse_internal_links(markdown_file: &str) -> Vec { // Define a regular expression to match markdown-style links let link_regex = Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap(); // Initialize a vector to store internal links found in the markdown file let mut internal_links = Vec::new(); // Iterate over each match of the regular expression in the markdown content for capture in link_regex.captures_iter(&markdown_file) { // Extract the link text and URL from the capture groups // let link_text = &capture[1]; let mut link_url = &capture[2]; // Check if the link is an internal link (e.g., relative URL) // You can customize this condition based on your site's URL structure if link_url.starts_with('/') || link_url.starts_with("../") { if link_url.ends_with('/') { link_url = link_url.trim_end_matches('/'); } internal_links.push(link_url.to_string()); } } internal_links } /// Write the html header including the style file /// TODO: Don't add the style file into each compiled html output, as the /// style can be included allowing the user to cache the style file in their /// browser. fn write_header(file: &mut File, site_name: &String, style: &String) -> std::io::Result<()>{ // write the header including the style file file.write_all(format!(r#" {} "#, site_name, style).as_bytes())?; Ok(()) } /// write the start of the html body tag and the header linking back to the /// site itself. fn write_body_start(file: &mut File, site_name: &String) -> std::io::Result<()>{ file.write_all(format!(r#"
{}
"#, site_name).as_bytes())?; Ok(()) } /// Write the navigation section to the given file fn write_nav(file: &mut File, in_path: &Path, raw_path: &Path, analytics: bool) -> std::io::Result<()> { if analytics == true { /* file.write_all(format!(r#"