unreal-doc 1.0.9

Tool for generating documentation from Unreal C++ sources.
#[macro_use]
extern crate pest_derive;

mod ast;
mod backends;
mod config;
mod document;

use crate::{
    ast::unreal_cpp_header::parse_unreal_cpp_header,
    backends::{json::bake_json, mdbook::bake_mdbook},
    config::*,
    document::Document,
};
use clap::{Arg, Command};
use std::{
    fs::{create_dir_all, read_to_string},
    io::Result,
    path::{Path, PathBuf},
};

const BOM: char = '\u{FEFF}';

fn main() {
    let matches = Command::new(env!("CARGO_BIN_NAME"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .version(env!("CARGO_PKG_VERSION"))
        .about(env!("CARGO_PKG_DESCRIPTION"))
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .takes_value(true)
                .value_name("FILE")
                .default_value("./UnrealDoc.toml")
                .help("UnrealDoc.toml config file"),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .takes_value(true)
                .value_name("DIR")
                .required(false)
                .help("Force documentation output to specified directory"),
        )
        .get_matches();

    let input = matches
        .value_of("input")
        .expect("No `input` argument provided!");
    let input = PathBuf::from(input);
    let output = matches.value_of("output").map(PathBuf::from);
    let output = output.as_deref();
    let (mut config, dir) = load_config(&input, output);

    let mut document = Document::default();
    for path in &config.input_dirs {
        document_path(path, path, &mut document, &config.settings);
    }
    document.resolve_injects();
    document.resolve_self_names_in_docs();
    document.sort_items_by_name();

    match config.backend {
        Backend::Json => bake_json(&document, &config),
        Backend::MdBook => {
            if let Ok(site_url) = std::env::var("UNREAL_DOC_MDBOOK_SITE_URL") {
                if let Some(config) = config.backend_mdbook.as_mut() {
                    config.site_url = Some(site_url.to_owned());
                }
            }
            bake_mdbook(&document, &config, &dir)
        }
    }
}

fn load_config(input: &Path, output: Option<&Path>) -> (Config, PathBuf) {
    let content =
        read_file(input).unwrap_or_else(|_| panic!("Input config file not found: {:?}", input));
    let mut config = toml::from_str::<Config>(&content)
        .unwrap_or_else(|_| panic!("Could not parse config file:\n{}", content));
    let mut dir = PathBuf::from(input);
    if dir.is_file() {
        dir.pop();
    }
    for path in &mut config.dependencies {
        if path.is_relative() {
            *path = dir.join(&path);
        }
    }
    for path in &mut config.input_dirs {
        if path.is_relative() {
            *path = dir.join(&path);
        }
    }
    if let Some(output) = output {
        config.output_dir = output.into();
    }
    if config.output_dir.is_relative() {
        config.output_dir = dir.join(&config.output_dir);
    }
    for path in &config.dependencies {
        let inputs = load_config(path, None).0.input_dirs;
        config.input_dirs.extend(inputs);
    }
    (config, dir)
}

fn document_path(path: &Path, root: &Path, document: &mut Document, settings: &Settings) {
    if path.is_file() {
        if let Some(ext) = path.extension() {
            if ext == "h" {
                let path = path.canonicalize().unwrap_or_else(|_| path.to_owned());
                let content =
                    read_file(&path).unwrap_or_else(|_| panic!("Could not read file: {:?}", &path));
                document_header(&path, &content, document, settings);
            } else if ext == "md" {
                let content =
                    read_file(path).unwrap_or_else(|_| panic!("Could not read file: {:?}", path));
                let root = root.to_string_lossy().into_owned();
                let path = path.to_string_lossy().into_owned();
                let pat: &[_] = &['/', '\\'];
                let relative = path
                    .trim_start_matches(&root)
                    .trim_start_matches(pat)
                    .replace('\\', "/")
                    .to_owned();
                document.book.insert(relative, content);
            } else if let Some(file_name) = path.file_name() {
                if file_name == "index.txt" {
                    let content = read_file(path)
                        .unwrap_or_else(|_| panic!("Could not read file: {:?}", path));
                    let root = root.to_string_lossy().into_owned();
                    let path = path.to_string_lossy().into_owned();
                    let pat: &[_] = &['/', '\\'];
                    let relative = path
                        .trim_start_matches(&root)
                        .trim_start_matches(pat)
                        .replace('\\', "/")
                        .to_owned();
                    document.book.insert(relative, content);
                }
            }
        }
    } else if path.is_dir() {
        for entry in path
            .read_dir()
            .unwrap_or_else(|_| panic!("Could not read directory: {:?}", path))
        {
            let path = entry.expect("Could not read directory entry!").path();
            document_path(&path, root, document, settings);
        }
    }
}

fn document_header(path: &Path, content: &str, document: &mut Document, settings: &Settings) {
    parse_unreal_cpp_header(content, document, settings).unwrap_or_else(|error| {
        panic!(
            "Could not parse Unreal C++ header file content!\nFile: {:?}\nError:\n{}",
            path, error
        )
    });
}

fn ensure_dir(path: &Path) {
    if path.is_dir() {
        let _ = create_dir_all(path);
    } else {
        let mut path = path.to_path_buf();
        path.pop();
        let _ = create_dir_all(&path);
    }
}

fn read_file(path: impl AsRef<Path>) -> Result<String> {
    let content = read_to_string(path)?;
    if content.starts_with(BOM) {
        Ok(content.chars().skip(1).collect())
    } else {
        Ok(content)
    }
}