pub mod config;
pub mod context;
mod file_path;
pub mod functions;
mod iter_ext;
mod result_ext;
use config::{Config, ConfigDeserializer, Merge};
use context::Context;
use file_path::FilePath;
use functions::register_functions;
use roxy_markdown_parser::{MarkdownParser, Options as MPOptions};
use roxy_markdown_tera_rewriter::{MarkdownTeraPreformatter, MarkdownTeraRewriter};
use roxy_syntect::SyntectParser;
use roxy_tera_parser::{TeraParser, TeraParserOptions};
use clap::Parser as Clap;
use std::{
ffi, fs,
path::{Path, PathBuf},
};
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
use glob::glob;
use roxy_core::result::Result;
use roxy_core::roxy::{Parser, Roxy};
use crate::iter_ext::{Head, MapFoldExt};
const DEFAULT_THEME: &'static str = "base16-ocean.dark";
const CONTENT_EXT: [&'static str; 4] = ["md", "tera", "html", "htm"];
#[derive(Clap)]
#[command(name = "Roxy")]
#[command(author = "KitsuneCafe")]
#[command(version = "2.0")]
#[command(about = "A very small static site generator", long_about = None)]
pub struct Options {
pub input: String,
pub output: String,
}
fn get_files<P: AsRef<Path> + std::fmt::Debug>(path: &P) -> Result<Vec<PathBuf>> {
let path = path
.as_ref()
.to_str()
.ok_or_else(|| anyhow::Error::msg(format!("{path:?} is not a valid path.")))?;
let files: Vec<PathBuf> = glob(path)
.map_err(anyhow::Error::from)?
.filter_map(|x| x.ok())
.filter(|f| Path::is_file(f))
.collect();
Ok(files)
}
fn try_find_file(path: &Path) -> Option<PathBuf> {
let file_name = path.file_name()?;
let paths = path
.with_file_name("")
.components()
.map_fold(PathBuf::new(), |acc, path| acc.join(path))
.collect::<Vec<PathBuf>>();
paths
.iter()
.rev()
.skip(1)
.filter_map(|p| p.with_file_name(file_name).canonicalize().ok())
.take(1)
.head()
.map(|x| x.0)
}
fn load_config(path: &Path) -> Config {
try_find_file(path)
.and_then(|p| fs::read_to_string(p).ok())
.map_or_else(
|| ConfigDeserializer::default(),
|f| {
toml::from_str::<ConfigDeserializer>(f.as_str())
.unwrap()
.merge(ConfigDeserializer::default())
},
)
.into()
}
fn copy_static<T: AsRef<Path>>(files: &Vec<&PathBuf>, file_path: &FilePath<T>) -> Result<()> {
for file in files {
let output = file_path.to_output(file).unwrap();
fs::create_dir_all(output.parent().unwrap())?;
fs::copy(file, output)?;
}
Ok(())
}
fn main() -> Result<()> {
let opts = Options::parse();
let mut file_path = FilePath::new(&opts.input, &opts.output);
let config_path = file_path.input.with_file_name("config.toml");
let config = load_config(&config_path);
file_path.slug_word_limit = config.roxy.slug_word_limit;
let file_path = file_path;
let files = get_files(&file_path.input)?;
let (meta, files): (Vec<&PathBuf>, Vec<&PathBuf>) =
files.iter().partition(|f| f.extension().unwrap() == "toml");
let (content, files): (Vec<&PathBuf>, Vec<&PathBuf>) = files.iter().partition(|f| {
let ext = f.extension().and_then(ffi::OsStr::to_str).unwrap();
CONTENT_EXT.contains(&ext)
});
let mut context = Context::from_files(meta, &file_path)?;
context.build_paths(&content, &file_path)?;
let md_options = MPOptions::all()
& !MPOptions::ENABLE_SMART_PUNCTUATION
& !MPOptions::ENABLE_YAML_STYLE_METADATA_BLOCKS
& !MPOptions::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS;
let theme = config.syntect.theme;
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = if let Some(dir) = config.syntect.theme_dir {
let path = file_path.input.join(dir);
try_find_file(path.as_path())
.and_then(|p| ThemeSet::load_from_folder(p).ok())
.unwrap_or_else(|| ThemeSet::load_defaults())
} else {
ThemeSet::load_defaults()
};
for file in content {
let file_name = file.with_extension("html");
let output_path = file_path.to_output(&file_name).unwrap();
let mut parser = Parser::new();
let mut preformatter = MarkdownTeraPreformatter::new();
parser.push(&mut preformatter);
let mut syntect = SyntectParser::new(&syntax_set, &theme_set, theme.as_str());
parser.push(&mut syntect);
let mut md = MarkdownParser::with_options(md_options);
parser.push(&mut md);
let mut rewriter = MarkdownTeraRewriter::new();
parser.push(&mut rewriter);
if let Ok(path) = &file_path.strip_root(&file) {
if let Some(current_context) = context.get(path) {
context.set(&"this", ¤t_context.clone());
}
}
let mut tera = tera::Tera::default();
register_functions(&mut tera);
let mut html = TeraParser::new(&mut tera, TeraParserOptions::default());
let ctx: tera::Context = context.clone().try_into().unwrap();
html.add_context(&ctx);
parser.push(&mut html);
Roxy::process_file(&file, &output_path, &mut parser).unwrap();
}
copy_static(&files, &file_path)?;
Ok(())
}