From d77e4d4b81ed04d84cc7d67a99aa64aff96648c4 Mon Sep 17 00:00:00 2001 From: KitsuneCafe <10284516+kitsunecafe@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:17:32 -0500 Subject: [PATCH] finally --- Cargo.lock | 16 +++++++ Cargo.toml | 1 + src/config.rs | 115 +++++++++++++++++++++++++++++++++++++++++----- src/context.rs | 58 +---------------------- src/file_path.rs | 38 +++++++++++++-- src/iter_ext.rs | 56 ++++++++++++++++++++++ src/main.rs | 77 +++++++++++++++++++++++-------- src/result_ext.rs | 9 ++++ 8 files changed, 279 insertions(+), 91 deletions(-) create mode 100644 src/iter_ext.rs create mode 100644 src/result_ext.rs diff --git a/Cargo.lock b/Cargo.lock index d4b90ef..0f6ce27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 63c03fe..9c12266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/config.rs b/src/config.rs index 6825ebc..753ca21 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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(a: Option, b: Option) -> Option { + 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 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 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 +} + +impl From 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, + pub syntect: Option, +} + +impl FromStr for ConfigDeserializer { type Err = toml::de::Error; fn from_str(s: &str) -> Result { @@ -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, +} + +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, pub theme_dir: Option, } -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), } } } - diff --git a/src/context.rs b/src/context.rs index db881ab..66b0528 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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 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 { - iter: I, - f: F, - accum: V, -} - -impl MapFold { - pub fn new(iter: I, init: V, f: F) -> MapFold { - Self { - iter, - f, - accum: init, - } - } -} - -impl V, V: Clone> Iterator for MapFold { - type Item = V; - - fn next(&mut self) -> Option { - self.accum = (self.f)(&self.accum, &self.iter.next()?); - Some(self.accum.clone()) - } -} - -trait MapFoldExt { - type Item; - - fn map_fold(self, init: B, f: F) -> MapFold - where - Self: Sized, - F: FnMut(&B, &Self::Item) -> B, - { - MapFold::new(self, init, f) - } -} - -impl MapFoldExt for I { - type Item = I::Item; -} - #[derive(Clone, Debug)] pub(crate) struct Context { inner: tera::Value, diff --git a/src/file_path.rs b/src/file_path.rs index 15eb821..42accad 100644 --- a/src/file_path.rs +++ b/src/file_path.rs @@ -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> { pub input: PathBuf, pub root_dir: PathBuf, pub output: &'a P, + pub slug_word_limit: usize, } impl<'a, P: AsRef + 'a> FilePath<'a, P> { @@ -14,6 +19,7 @@ impl<'a, P: AsRef + '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 + 'a> FilePath<'a, P> { .map_or_else(|| PathBuf::new(), PathBuf::from) } + pub fn as_slug + ?Sized>(&self, path: &P2) -> Option { + let path = path.as_ref(); + let ext = path.extension(); + let file_name: Option = 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::>() + .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>(&self, value: &'a P2) -> Result { 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 + 'a> FilePath<'a, P> { value.as_ref().strip_prefix(&self.root_dir) } } - - - diff --git a/src/iter_ext.rs b/src/iter_ext.rs new file mode 100644 index 0000000..3f38adb --- /dev/null +++ b/src/iter_ext.rs @@ -0,0 +1,56 @@ +pub(crate) trait Head: Iterator { + fn head(self) -> Option<(Self::Item, Self)> + where + Self: Sized; +} + +impl 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 { + iter: I, + f: F, + accum: V, +} + +impl MapFold { + pub fn new(iter: I, init: V, f: F) -> MapFold { + Self { + iter, + f, + accum: init, + } + } +} + +impl V, V: Clone> Iterator for MapFold { + type Item = V; + + fn next(&mut self) -> Option { + self.accum = (self.f)(&self.accum, &self.iter.next()?); + Some(self.accum.clone()) + } +} + +pub(crate) trait MapFoldExt { + type Item; + + fn map_fold(self, init: B, f: F) -> MapFold + where + Self: Sized, + F: FnMut(&B, &Self::Item) -> B, + { + MapFold::new(self, init, f) + } +} + +impl MapFoldExt for I { + type Item = I::Item; +} diff --git a/src/main.rs b/src/main.rs index 0502c5a..40367a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(err: E) -> Error { Error::new(err.to_string(), err) } @@ -57,15 +63,40 @@ fn get_files + std::fmt::Debug>(path: &P) -> Result, Ok(files) } +fn try_find_file(path: &Path) -> Option { + 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::(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::(f.as_str()) + .unwrap() + .merge(ConfigDeserializer::default()) + }, + ) + .into() } fn context_from_meta_files<'a, T: AsRef>( @@ -82,10 +113,17 @@ fn context_from_meta_files<'a, T: AsRef>( 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>( Ok(()) } -const DEFAULT_THEME: &'static str = "base16-ocean.dark"; -const CONTENT_EXT: [&'static str; 4] = ["md", "tera", "html", "htm"]; - fn main() -> Result<(), Box> { 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> { 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> { html.add_context(&ctx); parser.push(&mut html); + //println!("{output_path:?}"); Roxy::process_file(&file, &output_path, &mut parser).unwrap(); } diff --git a/src/result_ext.rs b/src/result_ext.rs new file mode 100644 index 0000000..77a1426 --- /dev/null +++ b/src/result_ext.rs @@ -0,0 +1,9 @@ +pub(crate) trait ResultExt: Sized { + fn then_err_into>(self, op: impl FnOnce(T) -> Result) -> Result; +} + +impl ResultExt for Result { + fn then_err_into>(self, op: impl FnOnce(T) -> Result) -> Result { + op(self?) + } +}