This commit is contained in:
KitsuneCafe 2024-02-14 20:17:32 -05:00
parent 26f74906c1
commit d77e4d4b81
8 changed files with 279 additions and 91 deletions

16
Cargo.lock generated
View file

@ -827,6 +827,7 @@ dependencies = [
"roxy_syntect",
"roxy_tera_parser",
"serde",
"slugify",
"syntect",
"tera",
"toml",
@ -966,6 +967,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "slugify"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b8cf203d2088b831d7558f8e5151bfa420c57a34240b28cee29d0ae5f2ac8b"
dependencies = [
"unidecode",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -1194,6 +1204,12 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unidecode"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc"
[[package]]
name = "utf8parse"
version = "0.2.1"

View file

@ -15,4 +15,5 @@ clap = { version = "4.4.17", features = ["derive"] }
toml = "0.8.8"
tera = "1.19.1"
serde = "1.0.195"
slugify = "0.1.0"

View file

@ -1,17 +1,88 @@
use serde::Deserialize;
use std::str::FromStr;
use serde::Deserialize;
use crate::DEFAULT_THEME;
pub(crate) trait Merge {
fn merge(self, other: Self) -> Self;
}
#[derive(Debug, Default, Deserialize)]
pub(crate) struct Config {
pub syntect: Syntect,
fn merge_opts<M: Merge>(a: Option<M>, b: Option<M>) -> Option<M> {
match (a, b) {
(Some(a), Some(b)) => Some(a.merge(b)),
(None, Some(b)) => Some(b),
(Some(a), None) => Some(a),
_ => None,
}
}
impl FromStr for Config {
#[derive(Debug)]
pub(crate) struct Config {
pub roxy: RoxyConfig,
pub syntect: SyntectConfig
}
impl From<ConfigDeserializer> for Config {
fn from(value: ConfigDeserializer) -> Self {
Self {
roxy: value.roxy.map_or_else(Default::default, From::from),
syntect: value.syntect.map_or_else(Default::default, From::from)
}
}
}
#[derive(Debug)]
pub(crate) struct RoxyConfig {
pub slug_word_limit: usize
}
impl From<RoxyConfigDeserializer> for RoxyConfig {
fn from(value: RoxyConfigDeserializer) -> Self {
Self {
slug_word_limit: value.slug_word_limit.unwrap_or(24usize)
}
}
}
impl Default for RoxyConfig {
fn default() -> Self {
Self {
slug_word_limit: 24usize
}
}
}
#[derive(Debug)]
pub(crate) struct SyntectConfig {
pub theme: String,
pub theme_dir: Option<String>
}
impl From<SyntectConfigDeserializer> for SyntectConfig {
fn from(value: SyntectConfigDeserializer) -> Self {
Self {
theme: value.theme.unwrap_or(DEFAULT_THEME.into()),
theme_dir: value.theme_dir
}
}
}
impl Default for SyntectConfig {
fn default() -> Self {
Self {
theme: DEFAULT_THEME.into(),
theme_dir: None
}
}
}
#[derive(Debug, Default, Deserialize)]
pub(crate) struct ConfigDeserializer {
pub roxy: Option<RoxyConfigDeserializer>,
pub syntect: Option<SyntectConfigDeserializer>,
}
impl FromStr for ConfigDeserializer {
type Err = toml::de::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -19,26 +90,46 @@ impl FromStr for Config {
}
}
impl Merge for Config {
impl Merge for ConfigDeserializer {
fn merge(self, other: Self) -> Self {
Self {
syntect: self.syntect.merge(other.syntect),
roxy: merge_opts(self.roxy, other.roxy),
syntect: merge_opts(self.syntect, other.syntect),
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct RoxyConfigDeserializer {
pub slug_word_limit: Option<usize>,
}
impl Default for RoxyConfigDeserializer {
fn default() -> Self {
Self {
slug_word_limit: Some(8usize),
}
}
}
impl Merge for RoxyConfigDeserializer {
fn merge(self, other: Self) -> Self {
Self {
slug_word_limit: self.slug_word_limit.or(other.slug_word_limit),
}
}
}
#[derive(Debug, Default, Deserialize)]
pub(crate) struct Syntect {
pub(crate) struct SyntectConfigDeserializer {
pub theme: Option<String>,
pub theme_dir: Option<String>,
}
impl Merge for Syntect {
fn merge(self, other: Syntect) -> Self {
impl Merge for SyntectConfigDeserializer {
fn merge(self, other: SyntectConfigDeserializer) -> Self {
Self {
theme: self.theme.or(other.theme),
theme_dir: self.theme_dir.or(other.theme_dir)
theme_dir: self.theme_dir.or(other.theme_dir),
}
}
}

View file

@ -1,6 +1,7 @@
use std::{path::Path, borrow::BorrowMut};
use tera::{Map, Value};
use crate::iter_ext::Head;
fn merge(a: &mut Value, b: Value) {
match (a, b) {
@ -15,63 +16,6 @@ fn merge(a: &mut Value, b: Value) {
}
}
trait Head: Iterator {
fn head(self) -> Option<(Self::Item, Self)>
where
Self: Sized;
}
impl<I: Iterator> Head for I {
fn head(mut self) -> Option<(Self::Item, Self)> {
match self.next() {
Some(x) => Some((x, self)),
None => None,
}
}
}
#[derive(Debug)]
struct MapFold<I, F, V> {
iter: I,
f: F,
accum: V,
}
impl<I, F, V> MapFold<I, F, V> {
pub fn new(iter: I, init: V, f: F) -> MapFold<I, F, V> {
Self {
iter,
f,
accum: init,
}
}
}
impl<I: Iterator, F: FnMut(&V, &I::Item) -> V, V: Clone> Iterator for MapFold<I, F, V> {
type Item = V;
fn next(&mut self) -> Option<Self::Item> {
self.accum = (self.f)(&self.accum, &self.iter.next()?);
Some(self.accum.clone())
}
}
trait MapFoldExt {
type Item;
fn map_fold<B, F>(self, init: B, f: F) -> MapFold<Self, F, B>
where
Self: Sized,
F: FnMut(&B, &Self::Item) -> B,
{
MapFold::new(self, init, f)
}
}
impl<I: Iterator> MapFoldExt for I {
type Item = I::Item;
}
#[derive(Clone, Debug)]
pub(crate) struct Context {
inner: tera::Value,

View file

@ -1,11 +1,16 @@
use std::path::{PathBuf, Path, StripPrefixError};
use roxy_core::error::Error;
use slugify::slugify;
use std::{
ffi::OsStr,
path::{Path, PathBuf, StripPrefixError},
};
#[derive(Debug, Clone)]
pub(crate) struct FilePath<'a, P: AsRef<Path>> {
pub input: PathBuf,
pub root_dir: PathBuf,
pub output: &'a P,
pub slug_word_limit: usize,
}
impl<'a, P: AsRef<Path> + 'a> FilePath<'a, P> {
@ -14,6 +19,7 @@ impl<'a, P: AsRef<Path> + 'a> FilePath<'a, P> {
input: Self::make_recursive(input),
root_dir: Self::strip_wildcards(input),
output,
slug_word_limit: Default::default(),
}
}
@ -34,10 +40,37 @@ impl<'a, P: AsRef<Path> + 'a> FilePath<'a, P> {
.map_or_else(|| PathBuf::new(), PathBuf::from)
}
pub fn as_slug<P2: AsRef<Path> + ?Sized>(&self, path: &P2) -> Option<PathBuf> {
let path = path.as_ref();
let ext = path.extension();
let file_name: Option<PathBuf> = path
.with_extension("")
.file_name()
.or_else(|| Some(OsStr::new("")))
.and_then(OsStr::to_str)
.map(|name| slugify!(name, separator = "-"))
.map(|f| {
f.split_terminator('-')
.take(self.slug_word_limit)
.collect::<Vec<&str>>()
.join("-")
})
.map(PathBuf::from)
.map(|f| f.with_extension(ext.unwrap_or_default()));
match (path.parent(), file_name) {
(Some(parent), Some(name)) => Some(parent.join(name)),
(None, Some(name)) => Some(PathBuf::from(name)),
(Some(parent), None) => Some(parent.to_path_buf()),
_ => None,
}
}
pub fn to_output<P2: AsRef<Path>>(&self, value: &'a P2) -> Result<PathBuf, Error> {
value
.as_ref()
.strip_prefix(&self.root_dir)
.map(|p| self.as_slug(p).expect("could not slugify path"))
.map(|path| self.output.as_ref().join(path))
.map_err(Error::from)
}
@ -46,6 +79,3 @@ impl<'a, P: AsRef<Path> + 'a> FilePath<'a, P> {
value.as_ref().strip_prefix(&self.root_dir)
}
}

56
src/iter_ext.rs Normal file
View file

@ -0,0 +1,56 @@
pub(crate) trait Head: Iterator {
fn head(self) -> Option<(Self::Item, Self)>
where
Self: Sized;
}
impl<I: Iterator> Head for I {
fn head(mut self) -> Option<(Self::Item, Self)> {
match self.next() {
Some(x) => Some((x, self)),
None => None,
}
}
}
#[derive(Debug)]
pub(crate) struct MapFold<I, F, V> {
iter: I,
f: F,
accum: V,
}
impl<I, F, V> MapFold<I, F, V> {
pub fn new(iter: I, init: V, f: F) -> MapFold<I, F, V> {
Self {
iter,
f,
accum: init,
}
}
}
impl<I: Iterator, F: FnMut(&V, &I::Item) -> V, V: Clone> Iterator for MapFold<I, F, V> {
type Item = V;
fn next(&mut self) -> Option<Self::Item> {
self.accum = (self.f)(&self.accum, &self.iter.next()?);
Some(self.accum.clone())
}
}
pub(crate) trait MapFoldExt {
type Item;
fn map_fold<B, F>(self, init: B, f: F) -> MapFold<Self, F, B>
where
Self: Sized,
F: FnMut(&B, &Self::Item) -> B,
{
MapFold::new(self, init, f)
}
}
impl<I: Iterator> MapFoldExt for I {
type Item = I::Item;
}

View file

@ -2,8 +2,10 @@ pub mod config;
pub mod context;
mod file_path;
pub mod functions;
mod iter_ext;
mod result_ext;
use config::{Config, Merge};
use config::{Config, ConfigDeserializer, Merge};
use context::Context;
use file_path::FilePath;
use functions::register_functions;
@ -21,6 +23,7 @@ use std::{
io::{BufReader, Read},
path::{Path, PathBuf},
};
use tera::to_value;
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
use toml::Table;
@ -28,6 +31,9 @@ use toml::Table;
use glob::glob;
use roxy_core::roxy::{Parser, Roxy};
const DEFAULT_THEME: &'static str = "base16-ocean.dark";
const CONTENT_EXT: [&'static str; 4] = ["md", "tera", "html", "htm"];
fn handle_err<E: std::error::Error + 'static>(err: E) -> Error {
Error::new(err.to_string(), err)
}
@ -57,15 +63,40 @@ fn get_files<P: AsRef<Path> + std::fmt::Debug>(path: &P) -> Result<Vec<PathBuf>,
Ok(files)
}
fn try_find_file(path: &Path) -> Option<PathBuf> {
if let Some(file_name) = path.file_name() {
let mut path = path;
let mut result = None;
while let Some(parent) = path.parent() {
let file = parent.with_file_name(file_name);
if file.is_file() {
result = Some(file);
break;
}
path = parent;
}
result
} else {
None
}
}
fn load_config(path: &Path) -> Config {
fs::read_to_string(path).map_or_else(
|_| Config::default(),
|f| {
toml::from_str::<Config>(f.as_str())
.unwrap()
.merge(Config::default())
},
)
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 context_from_meta_files<'a, T: AsRef<Path>>(
@ -82,10 +113,17 @@ fn context_from_meta_files<'a, T: AsRef<Path>>(
let mut str = String::from_utf8(buf).map_err(handle_err)?;
let toml: Table = toml::from_str(&mut str).map_err(handle_err)?;
context.insert(
&file_path.strip_root(path)?,
&tera::to_value(toml).map_err(handle_err)?,
);
let path = file_path.strip_root(path)?;
context.insert(&path, &tera::to_value(toml).map_err(handle_err)?);
let path = path.with_file_name("");
if let Some(slug) = file_path.as_slug(&path) {
let slug = PathBuf::from("/").join(slug);
if let Ok(slug) = to_value(slug) {
context.insert(&path.join("path"), &slug);
}
}
}
Ok(context)
@ -104,17 +142,17 @@ fn copy_static<T: AsRef<Path>>(
Ok(())
}
const DEFAULT_THEME: &'static str = "base16-ocean.dark";
const CONTENT_EXT: [&'static str; 4] = ["md", "tera", "html", "htm"];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let opts = Options::parse();
let file_path = FilePath::new(&opts.input, &opts.output);
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");
@ -126,8 +164,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut context: Context = context_from_meta_files(&meta, &file_path)?;
let theme = config.syntect.theme.unwrap_or(DEFAULT_THEME.to_string());
let theme = config.syntect.theme;
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = if let Some(dir) = config.syntect.theme_dir {
ThemeSet::load_from_folder(dir)?
} else {
@ -164,6 +204,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
html.add_context(&ctx);
parser.push(&mut html);
//println!("{output_path:?}");
Roxy::process_file(&file, &output_path, &mut parser).unwrap();
}

9
src/result_ext.rs Normal file
View file

@ -0,0 +1,9 @@
pub(crate) trait ResultExt<T, E>: Sized {
fn then_err_into<U, E2: From<E>>(self, op: impl FnOnce(T) -> Result<U, E2>) -> Result<U, E2>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> {
fn then_err_into<U, E2: From<E>>(self, op: impl FnOnce(T) -> Result<U, E2>) -> Result<U, E2> {
op(self?)
}
}