From: Huck Boles Date: Sun, 21 May 2023 00:54:30 +0000 (-0500) Subject: refactored: moved builder functions into methods for MetaFiles X-Git-Url: https://git.huck.website/?a=commitdiff_plain;h=15a922d9a8a5ad8060fcb7fa33205d6f2e3590d9;p=metaforge.git refactored: moved builder functions into methods for MetaFiles --- diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index 694abd4..0000000 --- a/src/builder.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod array; -mod pattern; -mod source; -mod variable; - -use pattern::*; -use source::*; -use variable::*; - -#[cfg(test)] -mod tests; - -use crate::{log, MetaError, MetaFile, Scope}; -use eyre::Result; - -pub fn build_metafile(file: &MetaFile) -> Result> { - log!(file.opts, format!("building {}", file.path.display()), 1); - - if file.header.blank { - return Ok(String::new()); - } else if file.header.ignore { - return Err(Box::new(MetaError::Ignored)); - } - - let html = get_source_html(file).map_err(MetaError::from)?; - - let pattern = get_pattern("base", file).map_err(MetaError::from)?; - let mut base = crate::parse_string(pattern, file.opts).map_err(MetaError::from)?; - - base.merge(file); - base.patterns.insert(Scope::into_global("SOURCE"), html); - - let output = metafile_to_string(&base).map_err(MetaError::from)?; - - Ok(output) -} diff --git a/src/builder/array.rs b/src/builder/array.rs deleted file mode 100644 index e4871d1..0000000 --- a/src/builder/array.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{log, MetaError, MetaFile, Scope, Src}; -use eyre::Result; -use std::collections::HashMap; - -pub fn expand_arrays(input: String, file: &MetaFile) -> Result { - log!( - file.opts, - format!("expanding arrays in {}", file.path.display()), - 2 - ); - - let map: HashMap = file - .source - .iter() - // filter out arrays from source vec - .filter_map(|x| { - if let Src::Arr(array) = x { - Some(array) - } else { - None - } - }) - // make a hash map of [keys in source] -> [defined arrays] - .map(|key| { - // concat array to pattern name to get key in HashMap - let name = file.name().unwrap_or_default(); - let long_key = name + "." + key; - - let value = if let Some(val) = file.get_arr(&Scope::into_global(&long_key)) { - val - } else if let Some(val) = file.get_arr(&Scope::into_local(&long_key)) { - val - } else if let Some(val) = file.get_arr(&Scope::into_global(key)) { - val - } else if let Some(val) = file.get_arr(&Scope::into_local(key)) { - val - } else if file.opts.undefined { - panic!( - "{}", - MetaError::UndefinedExpand { - val: key.to_string(), - path: file.path.to_string_lossy().to_string(), - } - ) - } else { - &[] - }; - (key.to_string(), value) - }) - .collect(); - - // loop to duplicate the output template for each array member - let mut expanded = String::new(); - let size = match get_array_size(&map, file.header.equal_arrays) { - Ok(num) => Ok(num), - Err(e) => match e.as_ref() { - &MetaError::Array => Err(MetaError::UnequalArrays { - path: file.path.to_string_lossy().to_string(), - }), - _ => Err(MetaError::Unknown), - }, - }?; - for i in 0..size { - // get a fresh copy of the file - let mut str = input.clone(); - // replace each key in the file - for (key, val) in map.iter() { - if let Some(value) = val.get(i) { - str = str.replace(&format!("-{{{key}}}"), value); - } - } - // concatenate to final file - expanded.push_str(&str); - } - - Ok(expanded) -} - -fn get_array_size( - map: &HashMap, - same_size: bool, -) -> Result> { - if same_size { - let mut size = (0, false); - for val in map.values() { - if !size.1 { - size = (val.len(), true); - } else if size.0 != val.len() { - return Err(Box::new(MetaError::Array)); - } - } - return Ok(size.0); - } - - let mut max = 0; - for val in map.values() { - if max < val.len() { - max = val.len(); - } - } - Ok(max) -} diff --git a/src/builder/pattern.rs b/src/builder/pattern.rs deleted file mode 100644 index b9cda17..0000000 --- a/src/builder/pattern.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::{log, MetaError, MetaFile, Scope}; -use eyre::Result; -use std::fs; - -pub fn get_pattern(key: &str, file: &MetaFile) -> Result { - log!(file.opts, format!("expanding {key}"), 2); - // SOURCE is already expanded in the initial build_metafile() call - if key == "SOURCE" { - if let Some(source) = file.patterns.get(&Scope::into_global("SOURCE")) { - return Ok(source.to_string()); - } else { - return Ok(String::new()); - } - } - - let mut filename = if let Some(name) = file.get_pat(&Scope::into_local(key)) { - Ok(name.to_string()) - } else if let Some(name) = file.get_pat(&Scope::into_global(key)) { - Ok(name.to_string()) - } else if file.header.panic_default { - Err(MetaError::UndefinedDefault { - pattern: key.to_string(), - path: file.path.to_string_lossy().to_string(), - }) - } else { - // anything not defined should have a default.meta file to fall back to - Ok("default".to_string()) - }?; - - // BLANK returns nothing, so no more processing needs to be done - if filename == "BLANK" { - return Ok(String::default()); - }; - - // DEFAULT override for patterns overriding globals - if filename == "DEFAULT" { - filename = "default".to_string(); - } - - // if we're building the base pattern we need to wait on - // parsing/expansion so we can build and convert source to html - // for the SOURCE pattern. we just want to return the string right now - if key == "base" { - let pattern_path = key.to_string() + "/" + &filename; - let mut path = file.opts.pattern.join(pattern_path); - path.set_extension("meta"); - - return match fs::read_to_string(&path) { - Ok(str) => Ok(str), - Err(_) => Err(MetaError::FileNotFound { - path: path.to_string_lossy().to_string(), - } - .into()), - }; - } - - let pattern_path = key.replace('.', "/") + "/" + &filename; - let mut path = file.opts.pattern.join(pattern_path); - path.set_extension("meta"); - - let mut pattern = MetaFile::build(path, file.opts)?; - - // copy over maps for expanding contained variables - pattern.merge(file); - - super::metafile_to_string(&pattern) -} diff --git a/src/builder/source.rs b/src/builder/source.rs deleted file mode 100644 index 820da11..0000000 --- a/src/builder/source.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::{log, MetaError, MetaFile, Src}; -use eyre::Result; -use pandoc::{InputFormat, InputKind, OutputFormat, OutputKind, Pandoc}; - -use super::array::*; -use super::*; - -pub fn get_source_html(file: &MetaFile) -> Result { - let string = metafile_to_string(file)?; - - if file.opts.no_pandoc || !file.header.pandoc || string.is_empty() { - return Ok(string); - } - - let input: InputFormat; - let output: OutputFormat; - if let Ok(io) = get_pandoc_io(file) { - input = io.0; - output = io.1; - } else { - // don't run pandoc if a filetype that isn't supported gets requested - return Ok(string); - } - - log!(file.opts, "calling pandoc", 3); - - let mut pandoc = Pandoc::new(); - pandoc - .set_input(InputKind::Pipe(string)) - .set_output(OutputKind::Pipe) - .set_input_format(input, vec![]) - .set_output_format(output, vec![]); - - if let pandoc::PandocOutput::ToBuffer(s) = pandoc.execute()? { - Ok(s) - } else { - Err(MetaError::Pandoc { file: file.name()? }.into()) - } -} - -pub fn metafile_to_string(file: &MetaFile) -> Result { - if file.header.blank { - return Ok(String::new()); - } - - let mut output = String::default(); - let mut arrays = false; - - for section in file.source.iter() { - let sec = match section { - // concatenate any char sequences - Src::Str(str) => str.to_string(), - // expand all variables and recursively expand patterns - Src::Var(key) => get_variable(key, file)?, - Src::Pat(key) => get_pattern(key, file)?, - Src::Arr(key) => { - arrays = true; - // comments have already been removed at this point, - // so we use them to mark keys for array substitution - format!("-{{{key}}}") - } - }; - - output.push_str(&sec); - } - - if arrays { - expand_arrays(output, file) - } else { - Ok(output) - } -} - -fn get_pandoc_io( - file: &MetaFile, -) -> Result<(pandoc::InputFormat, pandoc::OutputFormat), Box> { - let mut source_type = ""; - if !file.header.source.is_empty() { - source_type = &file.header.source; - } else if !file.opts.input.is_empty() { - source_type = &file.opts.input; - } - - let input = match source_type { - "markdown" => Ok(InputFormat::Markdown), - "html" => Ok(InputFormat::Html), - "org" => Ok(InputFormat::Org), - "json" => Ok(InputFormat::Json), - "latex" => Ok(InputFormat::Latex), - _ => Err(Box::new(MetaError::Filetype)), - }?; - - let mut filetype = ""; - if !file.header.filetype.is_empty() { - filetype = &file.header.filetype; - } else if !file.opts.input.is_empty() { - filetype = &file.opts.output; - } - - let output = match filetype { - "html" => Ok(OutputFormat::Html), - "markdown" => Ok(OutputFormat::Markdown), - "man" => Ok(OutputFormat::Man), - "txt" => Ok(OutputFormat::Plain), - "org" => Ok(OutputFormat::Org), - "json" => Ok(OutputFormat::Json), - "latex" => Ok(OutputFormat::Latex), - "asciidoc" => Ok(OutputFormat::Asciidoc), - "pdf" => Ok(OutputFormat::Pdf), - _ => Err(Box::new(MetaError::Filetype)), - }?; - - Ok((input, output)) -} diff --git a/src/builder/variable.rs b/src/builder/variable.rs deleted file mode 100644 index b419577..0000000 --- a/src/builder/variable.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{log, MetaError, MetaFile, Scope}; -use eyre::Result; - -pub fn get_variable(key: &str, file: &MetaFile) -> Result { - log!( - file.opts, - format!("substituting {key} in {}", file.path.display()), - 2 - ); - let long_key = file.name()? + "." + key; - if let Some(val) = file.get_var(&Scope::into_local(&long_key)) { - Ok(val.clone()) - } else if let Some(val) = file.get_var(&Scope::into_global(&long_key)) { - Ok(val.clone()) - } else if let Some(val) = file.get_var(&Scope::into_local(key)) { - Ok(val.clone()) - } else if let Some(val) = file.get_var(&Scope::into_global(key)) { - Ok(val.clone()) - } else if file.opts.undefined || file.header.panic_undefined { - return Err(MetaError::UndefinedExpand { - val: key.to_string(), - path: file.name()?, - } - .into()); - } else { - Ok(String::new()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 27f6513..379d142 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,14 @@ extern crate pest; #[macro_use] extern crate pest_derive; -mod builder; mod error; mod metafile; mod options; mod parser; -pub use builder::*; +#[cfg(test)] +mod tests; + pub use error::*; pub use metafile::*; pub use options::*; @@ -18,6 +19,15 @@ use clap::Parser; use eyre::Result; use std::fs; +#[macro_export] +macro_rules! log { + ($opts:expr, $string:expr, $level:expr) => { + if $opts.verbose >= $level && !$opts.quiet { + println!("{}", $string); + } + }; +} + pub fn get_opts() -> Result { let opts = Options::try_from(Opts::parse())?; @@ -65,12 +75,12 @@ pub fn single_file(opts: &Options) -> Result { let file = parse_string(source, opts)?; - Ok(build_metafile(&file)?) + Ok(file.construct()?) } pub fn new_site(opts: &Options) -> Result<()> { log!( - &opts, + opts, format!("building new site skeleton in {}", opts.root.display()), 1 ); diff --git a/src/metafile/dir.rs b/src/metafile/dir.rs index e8ab48f..c24654d 100644 --- a/src/metafile/dir.rs +++ b/src/metafile/dir.rs @@ -1,4 +1,4 @@ -use crate::{build_metafile, MetaError, Options}; +use crate::{MetaError, Options}; use eyre::Result; use std::{fs, path::PathBuf}; @@ -61,7 +61,7 @@ impl<'a> DirNode<'a> { pub fn build_files(&mut self) -> Result<()> { for file in self.files.iter_mut() { file.merge(&self.global); - match build_metafile(file) { + match file.construct() { Ok(str) => { fs::write(file.dest()?, str)?; } diff --git a/src/metafile/file.rs b/src/metafile/file.rs index d7b7162..bfe86b4 100644 --- a/src/metafile/file.rs +++ b/src/metafile/file.rs @@ -1,5 +1,12 @@ -use crate::{parse_string, MetaError, Options}; +mod arrays; +mod attributes; +mod patterns; +mod source; +mod variables; + +use crate::{log, parse_string, MetaError, Options}; use eyre::Result; +use pandoc::{InputFormat, InputKind, OutputFormat, OutputKind, Pandoc}; use std::{collections::HashMap, path::PathBuf}; use super::*; @@ -15,6 +22,18 @@ pub struct MetaFile<'a> { } impl<'a> MetaFile<'a> { + pub fn new(opts: &'a Options) -> Self { + Self { + opts, + path: PathBuf::new(), + header: Header::new(), + variables: HashMap::new(), + arrays: HashMap::new(), + patterns: HashMap::new(), + source: Vec::new(), + } + } + pub fn build(path: PathBuf, opts: &'a Options) -> Result { let str = match std::fs::read_to_string(&path) { Ok(str) => str, @@ -35,85 +54,26 @@ impl<'a> MetaFile<'a> { Ok(metafile) } - pub fn new(opts: &'a Options) -> Self { - Self { - opts, - path: PathBuf::new(), - header: Header::new(), - variables: HashMap::new(), - arrays: HashMap::new(), - patterns: HashMap::new(), - source: Vec::new(), - } - } - - pub fn dest(&self) -> Result { - let mut path = self - .opts - .build - .join(self.path.strip_prefix(&self.opts.source)?); - path.set_extension(&self.header.filetype); - - Ok(path) - } + pub fn construct(&self) -> Result> { + log!(self.opts, format!("building {}", self.path.display()), 1); - pub fn name(&self) -> Result { - if self.path.starts_with(&self.opts.source) { - // in source dir, we want the file name without the '.meta' extension - let name: String = self - .path - .strip_prefix(&self.opts.source)? - .components() - .map(|x| { - x.as_os_str() - .to_string_lossy() - .to_string() - .replace(".meta", "") - }) - .collect::>() - .join("."); - Ok(name) - } else if self.path.starts_with(&self.opts.pattern) { - // in pattern dir, we want the parent dir - let name = self.path.strip_prefix(&self.opts.pattern)?; - let name = name - .parent() - .map(|s| s.to_string_lossy().to_string().replace('/', ".")) - .unwrap_or_default(); - Ok(name) - } else { - Err(MetaError::Name { - file: self.path.to_string_lossy().to_string(), - } - .into()) + if self.header.blank { + return Ok(String::new()); + } else if self.header.ignore { + return Err(Box::new(MetaError::Ignored)); } - } - - pub fn get_var(&self, key: &Scope) -> Option<&String> { - self.variables.get(key) - } - pub fn get_arr(&self, key: &Scope) -> Option<&[String]> { - self.arrays.get(key).map(|a| &a[..]) - } + let html = self.to_html().map_err(MetaError::from)?; - pub fn get_pat(&self, key: &Scope) -> Option<&String> { - self.patterns.get(key) - } + let pattern = self.get_pattern("base").map_err(MetaError::from)?; + let mut base = crate::parse_string(pattern, self.opts).map_err(MetaError::from)?; - pub fn var_defined(&self, key: &str) -> bool { - self.variables.contains_key(&Scope::into_local(key)) - || self.variables.contains_key(&Scope::into_global(key)) - } + base.merge(self); + base.patterns.insert(Scope::into_global("SOURCE"), html); - pub fn arr_defined(&self, key: &str) -> bool { - self.arrays.contains_key(&Scope::into_local(key)) - || self.arrays.contains_key(&Scope::into_global(key)) - } + let output = base.get_source().map_err(MetaError::from)?; - pub fn pat_defined(&self, key: &str) -> bool { - self.patterns.contains_key(&Scope::into_local(key)) - || self.patterns.contains_key(&Scope::into_global(key)) + Ok(output) } pub fn merge(&mut self, other: &Self) { diff --git a/src/metafile/file/arrays.rs b/src/metafile/file/arrays.rs new file mode 100644 index 0000000..ab130ef --- /dev/null +++ b/src/metafile/file/arrays.rs @@ -0,0 +1,88 @@ +use super::*; + +impl<'a> MetaFile<'a> { + pub fn expand_arrays(&self, input: String) -> Result { + log!( + self.opts, + format!("expanding arrays in {}", self.path.display()), + 2 + ); + + let map: HashMap = self + .source + .iter() + // filter out arrays from source vec + .filter_map(|x| { + if let Src::Arr(array) = x { + Some(array) + } else { + None + } + }) + // make a hash map of [keys in source] -> [defined arrays] + .map(|key| { + // concat array to pattern name to get key in HashMap + let name = self.name().unwrap_or_default(); + let long_key = name + "." + key; + + let value = if let Some(val) = self.arrays.get(&Scope::into_global(&long_key)) { + &val[..] + } else if let Some(val) = self.arrays.get(&Scope::into_local(&long_key)) { + &val[..] + } else if let Some(val) = self.arrays.get(&Scope::into_global(key)) { + &val[..] + } else if let Some(val) = self.arrays.get(&Scope::into_local(key)) { + &val[..] + } else if self.opts.undefined { + panic!( + "{}", + MetaError::UndefinedExpand { + val: key.to_string(), + path: self.path.to_string_lossy().to_string(), + } + ) + } else { + &[] + }; + (key.to_string(), value) + }) + .collect(); + + // loop to duplicate the output template for each array member + let mut expanded = String::new(); + let size = if self.header.equal_arrays { + let mut size = (0, false); + for val in map.values() { + if !size.1 { + size = (val.len(), true); + } else if size.0 != val.len() { + return Err(eyre::Error::from(MetaError::Array)); + } + } + Ok::(size.0) + } else { + let mut max = 0; + for val in map.values() { + if max < val.len() { + max = val.len(); + } + } + Ok(max) + }?; + + for i in 0..size { + // get a fresh copy of the file + let mut str = input.clone(); + // replace each key in the file + for (key, val) in map.iter() { + if let Some(value) = val.get(i) { + str = str.replace(&format!("-{{{key}}}"), value); + } + } + // concatenate to final file + expanded.push_str(&str); + } + + Ok(expanded) + } +} diff --git a/src/metafile/file/attributes.rs b/src/metafile/file/attributes.rs new file mode 100644 index 0000000..2fecd5e --- /dev/null +++ b/src/metafile/file/attributes.rs @@ -0,0 +1,45 @@ +use super::*; + +impl<'a> MetaFile<'a> { + pub fn dest(&self) -> Result { + let mut path = self + .opts + .build + .join(self.path.strip_prefix(&self.opts.source)?); + path.set_extension(&self.header.filetype); + + Ok(path) + } + + pub fn name(&self) -> Result { + if self.path.starts_with(&self.opts.source) { + // in source dir, we want the file name without the '.meta' extension + let name: String = self + .path + .strip_prefix(&self.opts.source)? + .components() + .map(|x| { + x.as_os_str() + .to_string_lossy() + .to_string() + .replace(".meta", "") + }) + .collect::>() + .join("."); + Ok(name) + } else if self.path.starts_with(&self.opts.pattern) { + // in pattern dir, we want the parent dir + let name = self.path.strip_prefix(&self.opts.pattern)?; + let name = name + .parent() + .map(|s| s.to_string_lossy().to_string().replace('/', ".")) + .unwrap_or_default(); + Ok(name) + } else { + Err(MetaError::Name { + file: self.path.to_string_lossy().to_string(), + } + .into()) + } + } +} diff --git a/src/metafile/file/patterns.rs b/src/metafile/file/patterns.rs new file mode 100644 index 0000000..b3a2871 --- /dev/null +++ b/src/metafile/file/patterns.rs @@ -0,0 +1,67 @@ +use super::*; + +impl<'a> MetaFile<'a> { + pub fn get_pattern(&self, key: &str) -> Result { + log!(self.opts, format!("expanding {key}"), 2); + // SOURCE is already expanded in the initial construct() call + if key == "SOURCE" { + if let Some(source) = self.patterns.get(&Scope::into_global("SOURCE")) { + return Ok(source.to_string()); + } else { + return Ok(String::new()); + } + } + + let mut filename = if let Some(name) = self.patterns.get(&Scope::into_local(key)) { + Ok(name.to_string()) + } else if let Some(name) = self.patterns.get(&Scope::into_global(key)) { + Ok(name.to_string()) + } else if self.header.panic_default { + Err(MetaError::UndefinedDefault { + pattern: key.to_string(), + path: self.path.to_string_lossy().to_string(), + }) + } else { + // anything not defined should have a default.meta file to fall back to + Ok("default".to_string()) + }?; + + // BLANK returns nothing, so no more processing needs to be done + if filename == "BLANK" { + return Ok(String::default()); + }; + + // DEFAULT override for patterns overriding globals + if filename == "DEFAULT" { + filename = "default".to_string(); + } + + // if we're building the base pattern we need to wait on + // parsing/expansion so we can build and convert source to html + // for the SOURCE pattern. we just want to return the string right now + if key == "base" { + let pattern_path = key.to_string() + "/" + &filename; + let mut path = self.opts.pattern.join(pattern_path); + path.set_extension("meta"); + + return match std::fs::read_to_string(&path) { + Ok(str) => Ok(str), + Err(_) => Err(MetaError::FileNotFound { + path: path.to_string_lossy().to_string(), + } + .into()), + }; + } + + let pattern_path = key.replace('.', "/") + "/" + &filename; + let mut path = self.opts.pattern.join(pattern_path); + path.set_extension("meta"); + + let mut pattern = MetaFile::build(path, self.opts)?; + + // copy over maps for expanding contained variables + pattern.merge(self); + + pattern.get_source() + } +} diff --git a/src/metafile/file/source.rs b/src/metafile/file/source.rs new file mode 100644 index 0000000..6ffad58 --- /dev/null +++ b/src/metafile/file/source.rs @@ -0,0 +1,109 @@ +use super::*; + +impl<'a> MetaFile<'a> { + pub fn to_html(&self) -> Result { + let string = self.get_source()?; + + if self.opts.no_pandoc || !self.header.pandoc || string.is_empty() { + return Ok(string); + } + + let input: InputFormat; + let output: OutputFormat; + if let Ok(io) = self.pandoc_io() { + input = io.0; + output = io.1; + } else { + // don't run pandoc if a filetype that isn't supported gets requested + return Ok(string); + } + + log!(self.opts, "calling pandoc", 3); + + let mut pandoc = Pandoc::new(); + pandoc + .set_input(InputKind::Pipe(string)) + .set_output(OutputKind::Pipe) + .set_input_format(input, vec![]) + .set_output_format(output, vec![]); + + if let pandoc::PandocOutput::ToBuffer(s) = pandoc.execute()? { + Ok(s) + } else { + Err(MetaError::Pandoc { file: self.name()? }.into()) + } + } + + fn pandoc_io(&self) -> Result<(pandoc::InputFormat, pandoc::OutputFormat), Box> { + let mut source_type = ""; + if !self.header.source.is_empty() { + source_type = &self.header.source; + } else if !self.opts.input.is_empty() { + source_type = &self.opts.input; + } + + let input = match source_type { + "markdown" => Ok(InputFormat::Markdown), + "html" => Ok(InputFormat::Html), + "org" => Ok(InputFormat::Org), + "json" => Ok(InputFormat::Json), + "latex" => Ok(InputFormat::Latex), + _ => Err(Box::new(MetaError::Filetype)), + }?; + + let mut filetype = ""; + if !self.header.filetype.is_empty() { + filetype = &self.header.filetype; + } else if !self.opts.input.is_empty() { + filetype = &self.opts.output; + } + + let output = match filetype { + "html" => Ok(OutputFormat::Html), + "markdown" => Ok(OutputFormat::Markdown), + "man" => Ok(OutputFormat::Man), + "txt" => Ok(OutputFormat::Plain), + "org" => Ok(OutputFormat::Org), + "json" => Ok(OutputFormat::Json), + "latex" => Ok(OutputFormat::Latex), + "asciidoc" => Ok(OutputFormat::Asciidoc), + "pdf" => Ok(OutputFormat::Pdf), + _ => Err(Box::new(MetaError::Filetype)), + }?; + + Ok((input, output)) + } + + pub fn get_source(&self) -> Result { + if self.header.blank { + return Ok(String::new()); + } + + let mut output = String::default(); + let mut arrays = false; + + for section in self.source.iter() { + let sec = match section { + // concatenate any char sequences + Src::Str(str) => str.to_string(), + // expand all variables and recursively expand patterns + Src::Var(key) => self.get_variable(key)?, + Src::Pat(key) => self.get_pattern(key)?, + Src::Arr(key) => { + arrays = true; + // comments have already been removed at this point, + // so we use them to mark keys for array substitution + format!("-{{{key}}}") + } + }; + + output.push_str(&sec); + } + + if arrays { + self.expand_arrays(output) + } else { + Ok(output) + } + } +} diff --git a/src/metafile/file/variables.rs b/src/metafile/file/variables.rs new file mode 100644 index 0000000..033f38c --- /dev/null +++ b/src/metafile/file/variables.rs @@ -0,0 +1,33 @@ +use super::*; + +impl<'a> MetaFile<'a> { + pub fn get_variable(&self, key: &str) -> Result { + log!( + self.opts, + format!( + "substituting {} in {}", + key.to_string(), + self.path.display() + ), + 2 + ); + let long_key = self.name()? + "." + &key.to_string(); + if let Some(val) = self.variables.get(&Scope::into_local(&long_key)) { + Ok(val.clone()) + } else if let Some(val) = self.variables.get(&Scope::into_global(&long_key)) { + Ok(val.clone()) + } else if let Some(val) = self.variables.get(&Scope::into_local(key)) { + Ok(val.clone()) + } else if let Some(val) = self.variables.get(&Scope::into_global(key)) { + Ok(val.clone()) + } else if self.opts.undefined || self.header.panic_undefined { + return Err(MetaError::UndefinedExpand { + val: key.to_string(), + path: self.name()?, + } + .into()); + } else { + Ok(String::new()) + } + } +} diff --git a/src/options.rs b/src/options.rs index 3fbdd4f..b66300a 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,14 +1,149 @@ -pub mod arg_parser; -pub mod opt_struct; +use clap::Parser; +use eyre::Result; +use std::path::PathBuf; -pub use arg_parser::*; -pub use opt_struct::*; +#[derive(Parser, Debug)] +#[command(author = "Huck Boles")] +#[command(version = "0.1.1")] +#[command(about = "A customizable template driven static site generator")] +#[command(long_about = None)] +pub struct Opts { + /// root directory [current_dir] + #[arg(short, long, value_name = "ROOT_DIR")] + pub root: Option, + /// source file directory [current_dir/source] + #[arg(short, long, value_name = "SOURCE_DIR")] + pub source: Option, + /// build directory [current_dir/build] + #[arg(short, long, value_name = "BUILD_DIR")] + pub build: Option, + /// pattern directory [current_dir/pattern] + #[arg(short, long, value_name = "PATTERN_DIR")] + pub pattern: Option, + /// only build a single file + #[arg(short, long, value_name = "FILENAME")] + pub file: Option, + /// create a new skeleton directory + #[arg(long, default_value_t = false)] + pub new: bool, + /// enable extra output. repeated flags give more info + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, + /// minimal output + #[arg(short, long, default_value_t = false)] + pub quiet: bool, + /// don't stop on file failure [false] + #[arg(long, default_value_t = false)] + pub force: bool, + /// stop on undefined variables and arrays [false] + #[arg(long, default_value_t = false)] + pub undefined: bool, + /// clean build directory before building site [false] + #[arg(long, default_value_t = false)] + pub clean: bool, + /// don't call pandoc on source files + #[arg(long, default_value_t = false)] + pub no_pandoc: bool, + /// output filetype [html] + #[arg(short, long, value_name = "OUTPUT_FILETYPE")] + pub output: Option, + /// input filetype [markdown] + #[arg(short, long, value_name = "INPUT_FILETYPE")] + pub input: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct Options { + pub root: PathBuf, + pub source: PathBuf, + pub build: PathBuf, + pub pattern: PathBuf, + pub file: Option, + pub input: String, + pub output: String, + pub verbose: u8, + pub quiet: bool, + pub force: bool, + pub undefined: bool, + pub clean: bool, + pub no_pandoc: bool, + pub new: bool, +} -#[macro_export] -macro_rules! log { - ($opts:expr, $string:expr, $level:expr) => { - if $opts.verbose >= $level && !$opts.quiet { - println!("{}", $string); +impl Options { + pub fn new() -> Self { + Self { + root: PathBuf::new(), + source: PathBuf::new(), + build: PathBuf::new(), + pattern: PathBuf::new(), + file: None, + input: String::default(), + output: String::default(), + verbose: 0, + quiet: false, + force: false, + undefined: false, + clean: false, + no_pandoc: false, + new: false, } - }; + } +} + +impl TryFrom for Options { + type Error = eyre::Error; + fn try_from(value: crate::Opts) -> Result { + let mut opts = Options::new(); + + opts.verbose = value.verbose; + opts.quiet = value.quiet; + opts.force = value.force; + opts.undefined = value.undefined; + opts.clean = value.clean; + opts.no_pandoc = value.no_pandoc; + opts.new = value.new; + + if let Some(root) = value.root.as_deref() { + opts.root = PathBuf::from(root).canonicalize()?; + } else { + opts.root = std::env::current_dir()?; + } + + if let Some(source) = value.source.as_deref() { + opts.source = PathBuf::from(source).canonicalize()?; + } else { + opts.source = opts.root.join("source"); + } + + if let Some(build) = value.build.as_deref() { + opts.build = PathBuf::from(build).canonicalize()?; + } else { + opts.build = opts.root.join("build"); + } + + if let Some(pattern) = value.pattern.as_deref() { + opts.pattern = PathBuf::from(pattern).canonicalize()?; + } else { + opts.pattern = opts.root.join("pattern"); + } + + if let Some(file) = value.file.as_deref() { + opts.file = Some(PathBuf::from(file).canonicalize()?); + } + + if let Some(input) = value.input { + opts.input = input; + } else { + opts.input = String::from("html"); + } + + if let Some(output) = value.output { + opts.output = output; + } else { + opts.output = String::from("markdown"); + } + + Ok(opts) + } } diff --git a/src/options/arg_parser.rs b/src/options/arg_parser.rs deleted file mode 100644 index 3093c2e..0000000 --- a/src/options/arg_parser.rs +++ /dev/null @@ -1,51 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[command(author = "Huck Boles")] -#[command(version = "0.1.1")] -#[command(about = "A customizable template driven static site generator")] -#[command(long_about = None)] -pub struct Opts { - /// root directory [current_dir] - #[arg(short, long, value_name = "ROOT_DIR")] - pub root: Option, - /// source file directory [current_dir/source] - #[arg(short, long, value_name = "SOURCE_DIR")] - pub source: Option, - /// build directory [current_dir/build] - #[arg(short, long, value_name = "BUILD_DIR")] - pub build: Option, - /// pattern directory [current_dir/pattern] - #[arg(short, long, value_name = "PATTERN_DIR")] - pub pattern: Option, - /// only build a single file - #[arg(short, long, value_name = "FILENAME")] - pub file: Option, - /// create a new skeleton directory - #[arg(long, default_value_t = false)] - pub new: bool, - /// enable extra output. repeated flags give more info - #[arg(short, long, action = clap::ArgAction::Count)] - pub verbose: u8, - /// minimal output - #[arg(short, long, default_value_t = false)] - pub quiet: bool, - /// don't stop on file failure [false] - #[arg(long, default_value_t = false)] - pub force: bool, - /// stop on undefined variables and arrays [false] - #[arg(long, default_value_t = false)] - pub undefined: bool, - /// clean build directory before building site [false] - #[arg(long, default_value_t = false)] - pub clean: bool, - /// don't call pandoc on source files - #[arg(long, default_value_t = false)] - pub no_pandoc: bool, - /// output filetype [html] - #[arg(short, long, value_name = "OUTPUT_FILETYPE")] - pub output: Option, - /// input filetype [markdown] - #[arg(short, long, value_name = "INPUT_FILETYPE")] - pub input: Option, -} diff --git a/src/options/opt_struct.rs b/src/options/opt_struct.rs deleted file mode 100644 index 0f6da5a..0000000 --- a/src/options/opt_struct.rs +++ /dev/null @@ -1,98 +0,0 @@ -use eyre::Result; -use std::path::PathBuf; - -#[derive(Debug, Clone, Default)] -pub struct Options { - pub root: PathBuf, - pub source: PathBuf, - pub build: PathBuf, - pub pattern: PathBuf, - pub file: Option, - pub input: String, - pub output: String, - pub verbose: u8, - pub quiet: bool, - pub force: bool, - pub undefined: bool, - pub clean: bool, - pub no_pandoc: bool, - pub new: bool, -} - -impl Options { - pub fn new() -> Self { - Self { - root: PathBuf::new(), - source: PathBuf::new(), - build: PathBuf::new(), - pattern: PathBuf::new(), - file: None, - input: String::default(), - output: String::default(), - verbose: 0, - quiet: false, - force: false, - undefined: false, - clean: false, - no_pandoc: false, - new: false, - } - } -} - -impl TryFrom for Options { - type Error = eyre::Error; - fn try_from(value: crate::Opts) -> Result { - let mut opts = Options::new(); - - opts.verbose = value.verbose; - opts.quiet = value.quiet; - opts.force = value.force; - opts.undefined = value.undefined; - opts.clean = value.clean; - opts.no_pandoc = value.no_pandoc; - opts.new = value.new; - - if let Some(root) = value.root.as_deref() { - opts.root = PathBuf::from(root).canonicalize()?; - } else { - opts.root = std::env::current_dir()?; - } - - if let Some(source) = value.source.as_deref() { - opts.source = PathBuf::from(source).canonicalize()?; - } else { - opts.source = opts.root.join("source"); - } - - if let Some(build) = value.build.as_deref() { - opts.build = PathBuf::from(build).canonicalize()?; - } else { - opts.build = opts.root.join("build"); - } - - if let Some(pattern) = value.pattern.as_deref() { - opts.pattern = PathBuf::from(pattern).canonicalize()?; - } else { - opts.pattern = opts.root.join("pattern"); - } - - if let Some(file) = value.file.as_deref() { - opts.file = Some(PathBuf::from(file).canonicalize()?); - } - - if let Some(input) = value.input { - opts.input = input; - } else { - opts.input = String::from("html"); - } - - if let Some(output) = value.output { - opts.output = output; - } else { - opts.output = String::from("markdown"); - } - - Ok(opts) - } -} diff --git a/src/builder/tests.rs b/src/tests.rs similarity index 97% rename from src/builder/tests.rs rename to src/tests.rs index 1d7d2ed..179dc70 100644 --- a/src/builder/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use crate::{build_metafile, MetaFile, Options}; +use crate::{MetaFile, Options}; use eyre::{Result, WrapErr}; use std::{error::Error, fs, path::PathBuf}; @@ -18,7 +18,7 @@ fn unit_test(test: (&str, &str)) -> Result<()> { file_path.set_extension("meta"); let file = MetaFile::build(file_path, &opts)?; - let output = build_metafile(&file).wrap_err_with(|| test.0.to_string())?; + let output = file.construct().wrap_err_with(|| test.0.to_string())?; if output == test.1 { Ok(())