From: Huck Boles Date: Wed, 17 May 2023 16:05:03 +0000 (-0500) Subject: restructure X-Git-Url: https://git.huck.website/?a=commitdiff_plain;h=ee6cda6d75bb751f89be42bf3aa694c7fca0e218;p=metaforge.git restructure --- diff --git a/src/builder.rs b/src/builder.rs index 6e93bb3..79dfe27 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,6 +1,16 @@ -use crate::{MetaFile, Src, Sub}; -use color_eyre::{eyre::bail, Result}; -use std::{collections::HashMap, fs}; +use crate::MetaFile; +use color_eyre::Result; + +mod array; +mod pattern; +mod source; +mod variable; + +use pattern::*; +use source::*; + +#[cfg(test)] +mod tests; pub fn build_metafile(file: &MetaFile) -> Result { if file.header.blank { @@ -10,7 +20,7 @@ pub fn build_metafile(file: &MetaFile) -> Result { let html = get_source_html(file)?; let pattern = get_pattern("base", file)?; - let mut base = crate::parse_file(pattern, file.opts)?; + let mut base = crate::parse_string(pattern, file.opts)?; base.merge(file); base.patterns.insert("SOURCE".to_string(), html); @@ -19,319 +29,3 @@ pub fn build_metafile(file: &MetaFile) -> Result { Ok(output) } - -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() { - match section { - // concatenate any char sequences - Src::Str(str) => { - output.push_str(str); - } - // expand all variables and recursively expand patterns - Src::Sub(sub) => { - let expanded = match sub { - Sub::Var(key) => get_variable(key, file)?, - Sub::Pat(key) => get_pattern(key, file)?, - Sub::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(&expanded); - } - } - } - - if arrays { - expand_arrays(output, file) - } else { - Ok(output) - } -} - -fn get_source_html(file: &MetaFile) -> Result { - let string = metafile_to_string(file)?; - - if file.opts.no_pandoc || !file.header.pandoc { - return Ok(string); - } - - let mut pandoc = pandoc::Pandoc::new(); - pandoc - .set_input(pandoc::InputKind::Pipe(string)) - .set_output(pandoc::OutputKind::Pipe) - .set_input_format(pandoc::InputFormat::Markdown, vec![]) - .set_output_format(pandoc::OutputFormat::Html, vec![]); - - if let Ok(pandoc::PandocOutput::ToBuffer(html)) = pandoc.execute() { - Ok(html) - } else { - bail!("pandoc could not write to buffer") - } -} - -fn get_pattern(key: &str, file: &MetaFile) -> Result { - // SOURCE is already expanded in the initial build_metafile() call - if key == "SOURCE" { - if let Some(source) = file.patterns.get("SOURCE") { - return Ok(source.to_string()); - } - } - - let mut filename: String; - if let Some(name) = file.get_pat(key) { - filename = name.to_string(); - } else { - // anything not defined should have a default.meta file to fall back to - filename = "default".to_string() - } - - // BLANK returns nothing, so no more processing needs to be done - if filename == "BLANK" { - return Ok(String::from("")); - }; - - // DEFAULT override for patterns overriding globals - if filename == "DEFAULT" { - filename = "default".to_string(); - } - - // if we're building from base pattern we need to wait on - // parsing/expansion so we can build and convert source to html - // 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(_) => bail!("could not find base file {}", path.display()), - }; - } - - 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); - - metafile_to_string(&pattern) -} - -fn get_variable(key: &str, file: &MetaFile) -> Result { - let long_key = file.name()? + "." + key; - if let Some(val) = file.get_var(&long_key) { - Ok(val.clone()) - } else if let Some(val) = file.get_var(key) { - Ok(val.clone()) - } else if file.opts.undefined { - bail!("undefined variable: {}, {}", key, long_key) - } else { - Ok(String::new()) - } -} - -fn expand_arrays(input: String, file: &MetaFile) -> Result { - let map: HashMap = file - .source - .iter() - // filter out arrays from source vec - .filter_map(|x| { - if let Src::Sub(Sub::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(); - let long_key = name + "." + key; - - let value: &[String]; - if let Some(val) = file.get_arr(&long_key) { - value = val; - } else if let Some(val) = file.get_arr(key) { - value = val; - } else if file.opts.undefined { - panic!("undefined array called: {}, {}", key, long_key); - } else { - value = &[]; - } - - (key.to_string(), value) - }) - .collect(); - - // loop to duplicate the output template for each array member - let mut expanded = String::new(); - for i in 0..get_max_size(&map) { - // 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_max_size(map: &HashMap) -> usize { - let mut max = 0; - for val in map.values() { - if max < val.len() { - max = val.len(); - } - } - max -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::Options; - use std::path::PathBuf; - - fn unit_test(test: &str, result: &str) -> Result<()> { - let dir = PathBuf::from("files/test_site").canonicalize()?; - - let mut opts = Options::new(); - opts.root = dir.clone(); - opts.source = dir.join("source"); - opts.build = dir.join("build"); - opts.pattern = dir.join("pattern"); - opts.clean = true; - - let test_dir = opts.source.join("unit_tests"); - let mut file_path = test_dir.join(test); - file_path.set_extension("meta"); - let file = MetaFile::build(file_path, &opts)?; - - let output = build_metafile(&file)?; - - assert_eq!(output, result); - - Ok(()) - } - - #[test] - fn test_find_dest() -> Result<()> { - unit_test("find_dest", "\n\n\n") - } - - #[test] - fn test_blank() -> Result<()> { - unit_test("blank/blank_pattern", "")?; - unit_test("blank/blank_variable", "\n\n")?; - unit_test("blank/blank_array", "\n\n")?; - Ok(()) - } - - #[test] - fn test_comment() -> Result<()> { - unit_test("blank/comment", "\n\n\n")?; - unit_test( - "blank/inline_comment", - "\n

inline comment

\n\n", - )?; - Ok(()) - } - - #[test] - fn test_expand() -> Result<()> { - unit_test( - "expand/variable_in_source", - "\n

GOOD

\n\n", - )?; - unit_test("expand/variable_in_pattern", "\nGOOD\n")?; - unit_test("expand/array_in_source", "\n

12345

\n\n")?; - unit_test("expand/array_in_pattern", "\n12345\n")?; - unit_test("expand/pattern_in_source", "

GOOD

\n")?; - unit_test("expand/pattern_in_pattern", "\nGOOD\nGOOD\n\n")?; - Ok(()) - } - - #[test] - fn test_override() -> Result<()> { - unit_test("override/variable", "\n

GOOD

\n\n")?; - unit_test("override/pattern", "\nGOOD\nGOOD\n\n")?; - Ok(()) - } - - #[test] - fn test_headers() -> Result<()> { - unit_test("header/pandoc", "# This should not become html\n")?; - unit_test("header/blank", "")?; - - Ok(()) - } - - #[test] - fn test_filetype_header() -> Result<()> { - let dir = PathBuf::from("files/test_site").canonicalize()?; - - let mut opts = Options::new(); - opts.root = dir.clone(); - opts.source = dir.join("source"); - opts.build = dir.join("build"); - - let path = opts.source.join("unit_tests/header/filetype.meta"); - let file = MetaFile::build(path, &opts)?; - - assert_eq!( - file.dest()?, - PathBuf::from( - "/home/huck/repos/metaforge/files/test_site/build/unit_tests/header/filetype.rss" - ) - ); - - Ok(()) - } - - #[test] - fn test_global() -> Result<()> { - let dir = PathBuf::from("files/test_site/").canonicalize()?; - - let mut opts = Options::new(); - opts.root = dir.clone(); - opts.source = dir.join("source"); - opts.build = dir.join("build"); - opts.pattern = dir.join("pattern"); - - let mut dir_node = crate::DirNode::build(dir.join("source/unit_tests/global"), &opts)?; - let global = MetaFile::build(dir.join("source/default.meta"), &opts)?; - dir_node.map(&global)?; - dir_node.build_dir()?; - - assert_eq!( - fs::read_to_string(dir.join("build/unit_tests/global/pattern.html"))?, - "

GOOD GOOD

\n" - ); - - assert_eq!( - fs::read_to_string(dir.join("build/unit_tests/global/variable.html"))?, - "

GOODGOOD

\n" - ); - - Ok(()) - } -} diff --git a/src/builder/array.rs b/src/builder/array.rs new file mode 100644 index 0000000..b1ff436 --- /dev/null +++ b/src/builder/array.rs @@ -0,0 +1,64 @@ +use crate::{MetaFile, Src, Sub}; +use color_eyre::Result; +use std::collections::HashMap; + +pub fn expand_arrays(input: String, file: &MetaFile) -> Result { + let map: HashMap = file + .source + .iter() + // filter out arrays from source vec + .filter_map(|x| { + if let Src::Sub(Sub::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(); + let long_key = name + "." + key; + + let value: &[String]; + if let Some(val) = file.get_arr(&long_key) { + value = val; + } else if let Some(val) = file.get_arr(key) { + value = val; + } else if file.opts.undefined { + panic!("undefined array called: {}, {}", key, long_key); + } else { + value = &[]; + } + + (key.to_string(), value) + }) + .collect(); + + // loop to duplicate the output template for each array member + let mut expanded = String::new(); + for i in 0..get_max_size(&map) { + // 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_max_size(map: &HashMap) -> usize { + let mut max = 0; + for val in map.values() { + if max < val.len() { + max = val.len(); + } + } + max +} diff --git a/src/builder/pattern.rs b/src/builder/pattern.rs new file mode 100644 index 0000000..5576584 --- /dev/null +++ b/src/builder/pattern.rs @@ -0,0 +1,55 @@ +use crate::MetaFile; +use color_eyre::{eyre::bail, Result}; +use std::fs; + +pub fn get_pattern(key: &str, file: &MetaFile) -> Result { + // SOURCE is already expanded in the initial build_metafile() call + if key == "SOURCE" { + if let Some(source) = file.patterns.get("SOURCE") { + return Ok(source.to_string()); + } + } + + let mut filename: String; + if let Some(name) = file.get_pat(key) { + filename = name.to_string(); + } else { + // anything not defined should have a default.meta file to fall back to + filename = "default".to_string() + } + + // BLANK returns nothing, so no more processing needs to be done + if filename == "BLANK" { + return Ok(String::from("")); + }; + + // DEFAULT override for patterns overriding globals + if filename == "DEFAULT" { + filename = "default".to_string(); + } + + // if we're building from base pattern we need to wait on + // parsing/expansion so we can build and convert source to html + // 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(_) => bail!("could not find base file {}", path.display()), + }; + } + + 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 new file mode 100644 index 0000000..48a5517 --- /dev/null +++ b/src/builder/source.rs @@ -0,0 +1,64 @@ +use crate::{MetaFile, Src, Sub}; +use color_eyre::{eyre::bail, Result}; + +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 { + return Ok(string); + } + + let mut pandoc = pandoc::Pandoc::new(); + pandoc + .set_input(pandoc::InputKind::Pipe(string)) + .set_output(pandoc::OutputKind::Pipe) + .set_input_format(pandoc::InputFormat::Markdown, vec![]) + .set_output_format(pandoc::OutputFormat::Html, vec![]); + + if let Ok(pandoc::PandocOutput::ToBuffer(html)) = pandoc.execute() { + Ok(html) + } else { + bail!("pandoc could not write to buffer") + } +} + +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() { + match section { + // concatenate any char sequences + Src::Str(str) => { + output.push_str(str); + } + // expand all variables and recursively expand patterns + Src::Sub(sub) => { + let expanded = match sub { + Sub::Var(key) => super::variable::get_variable(key, file)?, + Sub::Pat(key) => get_pattern(key, file)?, + Sub::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(&expanded); + } + } + } + + if arrays { + expand_arrays(output, file) + } else { + Ok(output) + } +} diff --git a/src/builder/tests.rs b/src/builder/tests.rs new file mode 100644 index 0000000..0d6feb0 --- /dev/null +++ b/src/builder/tests.rs @@ -0,0 +1,108 @@ +use crate::{build_metafile, MetaFile, Options}; +use color_eyre::{eyre::WrapErr, Result}; +use std::path::PathBuf; + +fn unit_test(test: (&str, &str)) -> Result<()> { + let dir = PathBuf::from("files/test_site").canonicalize()?; + + let mut opts = Options::new(); + opts.root = dir.clone(); + opts.source = dir.join("source"); + opts.build = dir.join("build"); + opts.pattern = dir.join("pattern"); + opts.clean = true; + + let test_dir = opts.source.join("unit_tests"); + let mut file_path = test_dir.join(test.0); + file_path.set_extension("meta"); + let file = MetaFile::build(file_path, &opts)?; + + let output = build_metafile(&file).wrap_err_with(|| test.0.to_string())?; + + assert_eq!(output, test.1); + + Ok(()) +} + +#[test] +fn builder_tests() -> Result<()> { + let mut tests: Vec<(&str, &str)> = Vec::new(); + tests.push(("find_dest", "\n\n\n")); + tests.push(("blank/blank_pattern", "")); + tests.push(("blank/blank_variable", "\n\n")); + tests.push(("blank/blank_array", "\n\n")); + tests.push(("blank/comment", "\n\n\n")); + tests.push(( + "blank/inline_comment", + "\n

inline comment

\n\n", + )); + tests.push(( + "expand/variable_in_source", + "\n

GOOD

\n\n", + )); + tests.push(("expand/variable_in_pattern", "\nGOOD\n")); + tests.push(("expand/array_in_source", "\n

12345

\n\n")); + tests.push(("expand/array_in_pattern", "\n12345\n")); + tests.push(("expand/pattern_in_source", "

GOOD

\n")); + tests.push(("expand/pattern_in_pattern", "\nGOOD\nGOOD\n\n")); + tests.push(("override/variable", "\n

GOOD

\n\n")); + tests.push(("override/pattern", "\nGOOD\nGOOD\n\n")); + tests.push(("header/pandoc", "# This should not become html\n")); + tests.push(("header/blank", "")); + + for test in tests.iter() { + unit_test(*test)?; + } + + Ok(()) +} + +#[test] +fn test_filetype_header() -> Result<()> { + let dir = PathBuf::from("files/test_site").canonicalize()?; + + let mut opts = Options::new(); + opts.root = dir.clone(); + opts.source = dir.join("source"); + opts.build = dir.join("build"); + + let path = opts.source.join("unit_tests/header/filetype.meta"); + let file = MetaFile::build(path, &opts)?; + + assert_eq!( + file.dest()?, + PathBuf::from( + "/home/huck/repos/metaforge/files/test_site/build/unit_tests/header/filetype.rss" + ) + ); + + Ok(()) +} + +#[test] +fn test_global() -> Result<()> { + let dir = PathBuf::from("files/test_site/").canonicalize()?; + + let mut opts = Options::new(); + opts.root = dir.clone(); + opts.source = dir.join("source"); + opts.build = dir.join("build"); + opts.pattern = dir.join("pattern"); + + let mut dir_node = crate::DirNode::build(dir.join("source/unit_tests/global"), &opts)?; + let global = MetaFile::build(dir.join("source/default.meta"), &opts)?; + dir_node.map(&global)?; + dir_node.build_dir()?; + + assert_eq!( + std::fs::read_to_string(dir.join("build/unit_tests/global/pattern.html"))?, + "

GOOD GOOD

\n" + ); + + assert_eq!( + std::fs::read_to_string(dir.join("build/unit_tests/global/variable.html"))?, + "

GOODGOOD

\n" + ); + + Ok(()) +} diff --git a/src/builder/variable.rs b/src/builder/variable.rs new file mode 100644 index 0000000..260cf97 --- /dev/null +++ b/src/builder/variable.rs @@ -0,0 +1,15 @@ +use crate::MetaFile; +use color_eyre::{eyre::bail, Result}; + +pub fn get_variable(key: &str, file: &MetaFile) -> Result { + let long_key = file.name()? + "." + key; + if let Some(val) = file.get_var(&long_key) { + Ok(val.clone()) + } else if let Some(val) = file.get_var(key) { + Ok(val.clone()) + } else if file.opts.undefined { + bail!("undefined variable: {}, {}", key, long_key) + } else { + Ok(String::new()) + } +} diff --git a/src/metafile.rs b/src/metafile.rs index 2532382..ced91bd 100644 --- a/src/metafile.rs +++ b/src/metafile.rs @@ -1,161 +1,19 @@ -use crate::{build_metafile, parse_file, Options}; -use color_eyre::{ - eyre::{bail, eyre}, - Result, -}; -use std::collections::HashMap; -use std::{fs, path::PathBuf}; +mod dir; +mod file; +mod header; -#[derive(Debug, Clone, Default)] -pub struct Header { - pub blank: bool, - pub panic_default: bool, - pub panic_undefined: bool, - pub filetype: String, - pub pandoc: bool, -} - -impl Header { - pub fn new() -> Self { - Self { - blank: false, - panic_default: false, - panic_undefined: false, - filetype: String::from("html"), - pandoc: true, - } - } -} - -impl From> for Header { - fn from(value: HashMap) -> Self { - let mut header = Header::new(); - for (key, val) in value.iter() { - match &key[..] { - "blank" => header.blank = val == "true", - "panic_default" => header.panic_default = val == "true", - "panic_undefined" => header.panic_undefined = val == "true", - "pandoc" => header.pandoc = val == "true", - "filetype" => header.filetype = val.to_string(), - _ => continue, - } - } - header - } -} - -#[derive(Debug, Clone)] -pub struct MetaFile<'a> { - pub opts: &'a Options, - pub path: PathBuf, - pub header: Header, - pub variables: HashMap, - pub arrays: HashMap>, - pub patterns: HashMap, - pub source: Vec, -} - -impl<'a> MetaFile<'a> { - pub fn build(path: PathBuf, opts: &'a Options) -> Result { - let str = match fs::read_to_string(&path) { - Ok(str) => str, - Err(_) => bail!("{} does not exist", path.display()), - }; - let mut metafile = parse_file(str, opts)?; - metafile.path = path; - 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); +pub use dir::*; +pub use file::*; +pub use header::*; - 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 { - color_eyre::eyre::bail!("could not get name from: {}", self.path.display()); - } - } - - pub fn get_var(&self, key: &str) -> Option<&String> { - self.variables.get(key) - } - - pub fn get_arr(&self, key: &str) -> Option<&[String]> { - self.arrays.get(key).map(|a| &a[..]) - } - - pub fn get_pat(&self, key: &str) -> Option<&String> { - self.patterns.get(key) - } - - pub fn merge(&mut self, other: &Self) { - for (key, val) in other.variables.iter() { - match self.variables.get(key) { - Some(_) => continue, - None => self.variables.insert(key.to_string(), val.to_string()), - }; - } - for (key, val) in other.arrays.iter() { - match self.arrays.get(key) { - Some(_) => continue, - None => self.arrays.insert(key.to_string(), val.to_vec()), - }; - } - for (key, val) in other.patterns.iter() { - match self.patterns.get(key) { - Some(_) => continue, - None => self.patterns.insert(key.to_string(), val.to_string()), - }; - } - } -} +#[cfg(test)] +mod tests; #[macro_export] macro_rules! source ( - (var($s:expr)) => { Src::Sub(Sub::Var($s.to_string()))}; - (arr($s:expr)) => { Src::Sub(Sub::Arr($s.to_string()))}; - (pat($s:expr)) => { Src::Sub(Sub::Pat($s.to_string()))}; + (var($s:expr)) => { crate::Src::Sub(crate::Sub::Var($s.to_string()))}; + (arr($s:expr)) => { crate::Src::Sub(crate::Sub::Arr($s.to_string()))}; + (pat($s:expr)) => { crate::Src::Sub(crate::Sub::Pat($s.to_string()))}; ($s:expr) => { Src::Str($s.to_string())}; ); @@ -171,119 +29,3 @@ pub enum Sub { Arr(String), Pat(String), } - -#[derive(Debug, Clone)] -pub struct DirNode<'a> { - path: PathBuf, - opts: &'a Options, - global: MetaFile<'a>, - files: Vec>, - dirs: Vec>, -} - -impl<'a> DirNode<'a> { - pub fn build(path: PathBuf, opts: &'a Options) -> Result { - assert!(path.is_dir() && path.exists()); - - let build_dir = opts.build.join(path.strip_prefix(&opts.source)?); - if !build_dir.exists() { - fs::create_dir(build_dir)?; - } - - let files: Vec = Vec::new(); - let dirs: Vec = Vec::new(); - let global = MetaFile::new(opts); - - Ok(Self { - path, - opts, - global, - files, - dirs, - }) - } - - // parses all contained files and directories and pushes - // parsed structures into the files and directories vectors - pub fn map(&mut self, global: &'a MetaFile) -> Result<()> { - for f in fs::read_dir(&self.path)? { - let file = f?.path(); - - if file.is_dir() { - let dir = DirNode::build(file, self.opts)?; - self.dirs.push(dir); - } else if file.file_name().and_then(|f| f.to_str()) == Some("default.meta") { - let mut new_global = MetaFile::build(file, self.opts)?; - new_global.merge(global); - self.global = new_global; - } else if file.extension().and_then(|f| f.to_str()) == Some("meta") { - let file = MetaFile::build(file, self.opts)?; - self.files.push(file) - } - } - - Ok(()) - } - - pub fn build_files(&mut self) -> Result<()> { - for file in self.files.iter_mut() { - file.merge(&self.global); - match build_metafile(file) { - Ok(str) => { - fs::write(file.dest()?, str)?; - } - Err(e) => { - if self.opts.force { - // print a line to stderr about failure but continue with other files - eprintln!("ignoring {}: {}", file.path.display(), e); - continue; - } else { - return Err(e.wrap_err(eyre!("{}:", file.path.display()))); - } - } - } - } - Ok(()) - } - - pub fn build_dir(&'a mut self) -> Result<()> { - self.build_files()?; - - for dir in self.dirs.iter_mut() { - dir.map(&self.global)?; - dir.build_dir()?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_name() -> Result<()> { - let mut opts = Options::new(); - - opts.source = "/tmp/source".into(); - opts.build = "/tmp/build".into(); - opts.pattern = "/tmp/pattern".into(); - - let src_path = PathBuf::from("/tmp/source/test/file.meta"); - let pat1_path = PathBuf::from("/tmp/pattern/base/test.meta"); - let pat2_path = PathBuf::from("/tmp/pattern/test/class/file.meta"); - - let mut src = MetaFile::new(&opts); - src.path = src_path; - let mut pat1 = MetaFile::new(&opts); - pat1.path = pat1_path; - let mut pat2 = MetaFile::new(&opts); - pat2.path = pat2_path; - - assert_eq!(src.name()?, "test.file"); - assert_eq!(pat1.name()?, "base"); - assert_eq!(pat2.name()?, "test.class"); - - Ok(()) - } -} diff --git a/src/metafile/dir.rs b/src/metafile/dir.rs new file mode 100644 index 0000000..7f36ef6 --- /dev/null +++ b/src/metafile/dir.rs @@ -0,0 +1,91 @@ +use crate::{build_metafile, Options}; +use color_eyre::{eyre::eyre, Result}; +use std::{fs, path::PathBuf}; + +use super::*; + +#[derive(Debug, Clone)] +pub struct DirNode<'a> { + path: PathBuf, + opts: &'a Options, + global: MetaFile<'a>, + files: Vec>, + dirs: Vec>, +} + +impl<'a> DirNode<'a> { + pub fn build(path: PathBuf, opts: &'a Options) -> Result { + assert!(path.is_dir() && path.exists()); + + let build_dir = opts.build.join(path.strip_prefix(&opts.source)?); + if !build_dir.exists() { + fs::create_dir(build_dir)?; + } + + let files: Vec = Vec::new(); + let dirs: Vec = Vec::new(); + let global = MetaFile::new(opts); + + Ok(Self { + path, + opts, + global, + files, + dirs, + }) + } + + // parses all contained files and directories and pushes + // parsed structures into the files and directories vectors + pub fn map(&mut self, global: &'a MetaFile) -> Result<()> { + for f in fs::read_dir(&self.path)? { + let file = f?.path(); + + if file.is_dir() { + let dir = DirNode::build(file, self.opts)?; + self.dirs.push(dir); + } else if file.file_name().and_then(|f| f.to_str()) == Some("default.meta") { + let mut new_global = MetaFile::build(file, self.opts)?; + new_global.merge(global); + self.global = new_global; + } else if file.extension().and_then(|f| f.to_str()) == Some("meta") { + let file = MetaFile::build(file, self.opts)?; + self.files.push(file) + } + } + + Ok(()) + } + + pub fn build_files(&mut self) -> Result<()> { + for file in self.files.iter_mut() { + file.merge(&self.global); + match build_metafile(file) { + Ok(str) => { + fs::write(file.dest()?, str)?; + } + Err(e) => { + if self.opts.force { + // print a line to stderr about failure but continue with other files + eprintln!("ignoring {}: {}", file.path.display(), e); + continue; + } else { + return Err(e.wrap_err(eyre!("{}:", file.path.display()))); + } + } + } + } + Ok(()) + } + + pub fn build_dir(&'a mut self) -> Result<()> { + self.build_files()?; + + for dir in self.dirs.iter_mut() { + dir.map(&self.global)?; + dir.build_dir()?; + } + + Ok(()) + } +} diff --git a/src/metafile/file.rs b/src/metafile/file.rs new file mode 100644 index 0000000..a3fe74a --- /dev/null +++ b/src/metafile/file.rs @@ -0,0 +1,112 @@ +use crate::{parse_string, Options}; +use color_eyre::{eyre::bail, Result}; +use std::{collections::HashMap, path::PathBuf}; + +use super::*; + +#[derive(Debug, Clone)] +pub struct MetaFile<'a> { + pub opts: &'a Options, + pub path: PathBuf, + pub header: Header, + pub variables: HashMap, + pub arrays: HashMap>, + pub patterns: HashMap, + pub source: Vec, +} + +impl<'a> MetaFile<'a> { + pub fn build(path: PathBuf, opts: &'a Options) -> Result { + let str = match std::fs::read_to_string(&path) { + Ok(str) => str, + Err(_) => bail!("{} does not exist", path.display()), + }; + let mut metafile = parse_string(str, opts)?; + metafile.path = path; + 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 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 { + color_eyre::eyre::bail!("could not get name from: {}", self.path.display()); + } + } + + pub fn get_var(&self, key: &str) -> Option<&String> { + self.variables.get(key) + } + + pub fn get_arr(&self, key: &str) -> Option<&[String]> { + self.arrays.get(key).map(|a| &a[..]) + } + + pub fn get_pat(&self, key: &str) -> Option<&String> { + self.patterns.get(key) + } + + pub fn merge(&mut self, other: &Self) { + for (key, val) in other.variables.iter() { + match self.variables.get(key) { + Some(_) => continue, + None => self.variables.insert(key.to_string(), val.to_string()), + }; + } + for (key, val) in other.arrays.iter() { + match self.arrays.get(key) { + Some(_) => continue, + None => self.arrays.insert(key.to_string(), val.to_vec()), + }; + } + for (key, val) in other.patterns.iter() { + match self.patterns.get(key) { + Some(_) => continue, + None => self.patterns.insert(key.to_string(), val.to_string()), + }; + } + } +} diff --git a/src/metafile/header.rs b/src/metafile/header.rs new file mode 100644 index 0000000..c00f39e --- /dev/null +++ b/src/metafile/header.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, Default)] +pub struct Header { + pub blank: bool, + pub panic_default: bool, + pub panic_undefined: bool, + pub filetype: String, + pub pandoc: bool, +} + +impl Header { + pub fn new() -> Self { + Self { + blank: false, + panic_default: false, + panic_undefined: false, + filetype: String::from("html"), + pandoc: true, + } + } +} + +impl From> for Header { + fn from(value: HashMap) -> Self { + let mut header = Header::new(); + for (key, val) in value.iter() { + match &key[..] { + "blank" => header.blank = val == "true", + "panic_default" => header.panic_default = val == "true", + "panic_undefined" => header.panic_undefined = val == "true", + "pandoc" => header.pandoc = val == "true", + "filetype" => header.filetype = val.to_string(), + _ => continue, + } + } + header + } +} diff --git a/src/metafile/tests.rs b/src/metafile/tests.rs new file mode 100644 index 0000000..870ba48 --- /dev/null +++ b/src/metafile/tests.rs @@ -0,0 +1,31 @@ +use crate::Options; +use color_eyre::Result; +use std::path::PathBuf; + +use super::*; + +#[test] +fn test_name() -> Result<()> { + let mut opts = Options::new(); + + opts.source = "/tmp/source".into(); + opts.build = "/tmp/build".into(); + opts.pattern = "/tmp/pattern".into(); + + let src_path = PathBuf::from("/tmp/source/test/file.meta"); + let pat1_path = PathBuf::from("/tmp/pattern/base/test.meta"); + let pat2_path = PathBuf::from("/tmp/pattern/test/class/file.meta"); + + let mut src = MetaFile::new(&opts); + src.path = src_path; + let mut pat1 = MetaFile::new(&opts); + pat1.path = pat1_path; + let mut pat2 = MetaFile::new(&opts); + pat2.path = pat2_path; + + assert_eq!(src.name()?, "test.file"); + assert_eq!(pat1.name()?, "base"); + assert_eq!(pat2.name()?, "test.class"); + + Ok(()) +} diff --git a/src/options.rs b/src/options.rs index 4d9f22e..3fbdd4f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,117 +1,8 @@ -use clap::Parser; -use color_eyre::Result; -use std::path::PathBuf; +pub mod arg_parser; +pub mod 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")] - source: Option, - /// Build directory [CURRENT_DIR/build] - #[arg(short, long, value_name = "BUILD_DIR")] - build: Option, - /// Pattern directory [CURRENT_DIR/pattern] - #[arg(short, long, value_name = "PATTERN_DIR")] - pattern: Option, - /// Enable extra output. - /// Repeated flags give more info - #[arg(short, long, action = clap::ArgAction::Count)] - verbose: u8, - /// Minimal output - #[arg(short, long, default_value_t = false)] - quiet: bool, - /// Don't stop on file failure [FALSE] - #[arg(long, default_value_t = false)] - force: bool, - /// Stop on undefined variables and arrays [FALSE] - #[arg(long, default_value_t = false)] - undefined: bool, - /// Clean build directory before building site [FALSE] - #[arg(long, default_value_t = false)] - clean: bool, - /// Don't convert markdown to html. - /// Runs even if pandoc isn't installed [FALSE] - #[arg(long, default_value_t = false)] - no_pandoc: bool, -} - -#[derive(Debug, Clone, Default)] -pub struct Options { - pub root: PathBuf, - pub source: PathBuf, - pub build: PathBuf, - pub pattern: PathBuf, - pub verbose: u8, - pub quiet: bool, - pub force: bool, - pub undefined: bool, - pub clean: bool, - pub no_pandoc: bool, -} - -impl Options { - pub fn new() -> Self { - Self { - root: PathBuf::new(), - source: PathBuf::new(), - build: PathBuf::new(), - pattern: PathBuf::new(), - verbose: 0, - quiet: false, - force: false, - undefined: false, - clean: false, - no_pandoc: false, - } - } -} - -impl TryFrom for Options { - type Error = color_eyre::eyre::Error; - fn try_from(value: Opts) -> Result { - let mut options = Options::new(); - - options.verbose = value.verbose; - options.quiet = value.quiet; - options.force = value.force; - options.undefined = value.undefined; - options.clean = value.clean; - options.no_pandoc = value.no_pandoc; - - if let Some(root) = value.root.as_deref() { - options.root = PathBuf::from(root).canonicalize()?; - } else { - options.root = std::env::current_dir()?; - } - - if let Some(source) = value.source.as_deref() { - options.source = PathBuf::from(source).canonicalize()?; - } else { - options.source = options.root.join("source"); - } - - if let Some(build) = value.build.as_deref() { - options.build = PathBuf::from(build).canonicalize()?; - } else { - options.build = options.root.join("build"); - } - - if let Some(pattern) = value.pattern.as_deref() { - options.pattern = PathBuf::from(pattern).canonicalize()?; - } else { - options.pattern = options.root.join("pattern"); - } - - Ok(options) - } -} +pub use arg_parser::*; +pub use opt_struct::*; #[macro_export] macro_rules! log { diff --git a/src/options/arg_parser.rs b/src/options/arg_parser.rs new file mode 100644 index 0000000..868fdbd --- /dev/null +++ b/src/options/arg_parser.rs @@ -0,0 +1,41 @@ +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, + /// 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 convert markdown to html. + /// Runs even if pandoc isn't installed [FALSE] + #[arg(long, default_value_t = false)] + pub no_pandoc: bool, +} diff --git a/src/options/opt_struct.rs b/src/options/opt_struct.rs new file mode 100644 index 0000000..e37aa63 --- /dev/null +++ b/src/options/opt_struct.rs @@ -0,0 +1,73 @@ +use color_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 verbose: u8, + pub quiet: bool, + pub force: bool, + pub undefined: bool, + pub clean: bool, + pub no_pandoc: bool, +} + +impl Options { + pub fn new() -> Self { + Self { + root: PathBuf::new(), + source: PathBuf::new(), + build: PathBuf::new(), + pattern: PathBuf::new(), + verbose: 0, + quiet: false, + force: false, + undefined: false, + clean: false, + no_pandoc: false, + } + } +} + +impl TryFrom for Options { + type Error = color_eyre::eyre::Error; + fn try_from(value: crate::Opts) -> Result { + let mut options = Options::new(); + + options.verbose = value.verbose; + options.quiet = value.quiet; + options.force = value.force; + options.undefined = value.undefined; + options.clean = value.clean; + options.no_pandoc = value.no_pandoc; + + if let Some(root) = value.root.as_deref() { + options.root = PathBuf::from(root).canonicalize()?; + } else { + options.root = std::env::current_dir()?; + } + + if let Some(source) = value.source.as_deref() { + options.source = PathBuf::from(source).canonicalize()?; + } else { + options.source = options.root.join("source"); + } + + if let Some(build) = value.build.as_deref() { + options.build = PathBuf::from(build).canonicalize()?; + } else { + options.build = options.root.join("build"); + } + + if let Some(pattern) = value.pattern.as_deref() { + options.pattern = PathBuf::from(pattern).canonicalize()?; + } else { + options.pattern = options.root.join("pattern"); + } + + Ok(options) + } +} diff --git a/src/parser.rs b/src/parser.rs index c2c1262..bc7d61c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,26 +1,38 @@ -use crate::{source, Header, MetaFile, Options, Src, Sub}; +use crate::{Header, MetaFile, Options}; use color_eyre::{eyre::WrapErr, Result}; use pest::{ iterators::{Pair, Pairs}, Parser, }; -use std::collections::HashMap; + +mod array; +mod def_block; +mod header; +mod source; + +use array::*; +use def_block::*; +use header::*; +use source::*; + +#[cfg(test)] +mod tests; #[derive(Parser)] -#[grammar = "meta.pest"] +#[grammar = "parser/meta.pest"] pub struct MetaParser; -pub fn parse_file(file: String, opts: &Options) -> Result { +pub fn parse_string(file: String, opts: &Options) -> Result { let meta_source = MetaParser::parse(Rule::file, &file) .wrap_err("parser error")? .next() .unwrap(); - let metafile = parse_pair(meta_source, opts); + let metafile = parse_file(meta_source, opts); Ok(metafile) } -fn parse_pair<'a>(pair: Pair, opts: &'a Options) -> MetaFile<'a> { +fn parse_file<'a>(pair: Pair, opts: &'a Options) -> MetaFile<'a> { let mut meta_file = MetaFile::new(opts); if Rule::file == pair.as_rule() { @@ -44,230 +56,3 @@ fn parse_pair<'a>(pair: Pair, opts: &'a Options) -> MetaFile<'a> { meta_file } - -fn parse_defs(pairs: Pairs) -> HashMap { - let mut map = HashMap::new(); - for pair in pairs { - if Rule::assign == pair.as_rule() { - let (key, val) = parse_assign(pair); - map.insert(key.to_string(), val.to_string()); - } - } - map -} - -fn parse_header_defs(pairs: Pairs) -> HashMap { - let mut map = HashMap::new(); - for pair in pairs { - if Rule::header_assign == pair.as_rule() { - let (key, val) = parse_header_assign(pair); - map.insert(key.to_string(), val.to_string()); - } - } - map -} - -fn parse_array_defs(pairs: Pairs) -> HashMap> { - let mut map = HashMap::new(); - for pair in pairs { - if Rule::assign == pair.as_rule() { - let (key, val) = parse_assign_array(pair); - map.insert(key.to_string(), val); - } - } - map -} - -fn parse_source(pairs: Pairs) -> Vec { - let mut vec = Vec::new(); - for pair in pairs { - match pair.as_rule() { - Rule::var_sub => vec.push(source!(var(parse_sub(pair)))), - Rule::arr_sub => vec.push(source!(arr(parse_sub(pair)))), - Rule::pat_sub => vec.push(source!(pat(parse_sub(pair)))), - Rule::char_seq => vec.push(source!(pair.as_str())), - // anything that isn't a substitution is a char_seq inside source - _ => unreachable!(), - } - } - - vec -} - -fn parse_sub(pair: Pair) -> &str { - match pair.as_rule() { - Rule::var_sub | Rule::arr_sub | Rule::pat_sub => { - let str = pair.as_str(); - // return the value as the inner string for substitution - // all substitutions have the format of - // *{ ... } - // we return everything except: - // first two chars (sigil and preceding brace) - // last char (trailing brace) - &str[2..str.len() - 1] - } - // this function only gets called to parse substituiton patterns - // so anything else should never be called - _ => unreachable!(), - } -} - -fn parse_assign(pair: Pair) -> (&str, &str) { - let mut key = ""; - let mut val = ""; - - for pair in pair.into_inner() { - if Rule::key == pair.as_rule() { - key = pair.as_str(); - } - if Rule::value == pair.as_rule() { - let tmp = pair.as_str(); - // blank and default shoud be handled by whoever is getting the value - if tmp == "BLANK" || tmp == "DEFAULT" { - return (key, tmp); - } - // remove surrounding quotes from values by returning - // everything except first and last characters - // a string is defined as " ... " or ' ... ' - // so it's safe to strip these characters - val = &tmp[1..tmp.len() - 1]; - } - } - - (key, val) -} - -fn parse_header_assign(pair: Pair) -> (&str, &str) { - let mut key = ""; - let mut val = ""; - - for pair in pair.into_inner() { - if Rule::key == pair.as_rule() { - key = pair.as_str(); - } - if Rule::header_value == pair.as_rule() { - let tmp = pair.as_str(); - // blank and default shoud be handled by whoever is getting the value - if tmp == "BLANK" || tmp == "true" || tmp == "false" { - return (key, tmp); - } - // remove surrounding quotes from values by returning - // everything except first and last characters - // a string is defined as " ... " or ' ... ' - // so it's safe to strip these characters - val = &tmp[1..tmp.len() - 1]; - } - } - - (key, val) -} - -fn parse_assign_array(pair: Pair) -> (String, Vec) { - let mut key = ""; - let mut val = Vec::default(); - - for pair in pair.into_inner() { - if Rule::key == pair.as_rule() { - key = pair.as_str(); - } - if Rule::value == pair.as_rule() { - val = parse_array(pair.into_inner()); - } - } - - (key.to_string(), val) -} - -fn parse_array(pairs: Pairs) -> Vec { - let mut vec: Vec = Vec::default(); - - for pair in pairs { - if Rule::string == pair.as_rule() { - let tmp = pair.as_str(); - // remove surrounding quotes from values - // see parse_assign() for reasoning - let val = &tmp[1..tmp.len() - 1]; - vec.push(val.to_string()); - } - } - - vec -} - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! test_str ( - ($s: expr) => { - let opts = Options::new(); - let str = $s.to_string(); - parse_file(str, &opts).unwrap(); - }; - ); - - #[test] - fn no_spaces_in_def() { - test_str!(r#"${v='v'}@{a=['a']}&{p='p'}"#); - } - - #[test] - fn just_source_string() { - test_str!(r#"This is just a &{source} snippet"#); - } - - #[test] - fn one_line() { - test_str!( - r#"${variable = 'var' } @{array = ['array']} &{ pattern = "pattern"} And some extra text"# - ); - } - - #[test] - #[should_panic] - fn key_with_spaces() { - test_str!(r#"${ key with spaces = "value" }"#); - } - - #[test] - #[should_panic] - fn value_missing_quote() { - test_str!(r#"${ key = "value missing quote }"#); - } - - #[test] - #[should_panic] - fn mixed_quotes() { - test_str!(r#"${ key = "value mixing quotes' }"#); - } - - #[test] - #[should_panic] - fn spaces_in_substitution() { - test_str!(r#"This ${variable is not allowed}"#); - } - - #[test] - #[should_panic] - fn missing_closing_brace() { - test_str!(r#"${ key = "value" "#); - } - - #[test] - #[should_panic] - fn map_in_source() { - test_str!(r#"This map: ${ is = "invalid" }"#); - } - - #[test] - #[should_panic] - fn map_source_map() { - test_str!(r#"${var='v'} Some text @{array = ['a']}"#); - } - - #[test] - #[should_panic] - fn header_not_first() { - test_str!(r#"${v='v'} #{ type = 'html'} @{a=['a']}"#); - } -} diff --git a/src/parser/array.rs b/src/parser/array.rs new file mode 100644 index 0000000..5120511 --- /dev/null +++ b/src/parser/array.rs @@ -0,0 +1,46 @@ +use crate::Rule; +use pest::iterators::{Pair, Pairs}; +use std::collections::HashMap; + +pub fn parse_array_defs(pairs: Pairs) -> HashMap> { + let mut map = HashMap::new(); + for pair in pairs { + if Rule::assign == pair.as_rule() { + let (key, val) = parse_assign_array(pair); + map.insert(key.to_string(), val); + } + } + map +} + +fn parse_assign_array(pair: Pair) -> (String, Vec) { + let mut key = ""; + let mut val = Vec::default(); + + for pair in pair.into_inner() { + if Rule::key == pair.as_rule() { + key = pair.as_str(); + } + if Rule::value == pair.as_rule() { + val = parse_array(pair.into_inner()); + } + } + + (key.to_string(), val) +} + +fn parse_array(pairs: Pairs) -> Vec { + let mut vec: Vec = Vec::default(); + + for pair in pairs { + if Rule::string == pair.as_rule() { + let tmp = pair.as_str(); + // remove surrounding quotes from values + // see parse_assign() for reasoning + let val = &tmp[1..tmp.len() - 1]; + vec.push(val.to_string()); + } + } + + vec +} diff --git a/src/parser/def_block.rs b/src/parser/def_block.rs new file mode 100644 index 0000000..7917998 --- /dev/null +++ b/src/parser/def_block.rs @@ -0,0 +1,39 @@ +use crate::Rule; +use pest::iterators::{Pair, Pairs}; +use std::collections::HashMap; + +pub fn parse_defs(pairs: Pairs) -> HashMap { + let mut map = HashMap::new(); + for pair in pairs { + if Rule::assign == pair.as_rule() { + let (key, val) = parse_assign(pair); + map.insert(key.to_string(), val.to_string()); + } + } + map +} + +fn parse_assign(pair: Pair) -> (&str, &str) { + let mut key = ""; + let mut val = ""; + + for pair in pair.into_inner() { + if Rule::key == pair.as_rule() { + key = pair.as_str(); + } + if Rule::value == pair.as_rule() { + let tmp = pair.as_str(); + // blank and default shoud be handled by whoever is getting the value + if tmp == "BLANK" || tmp == "DEFAULT" { + return (key, tmp); + } + // remove surrounding quotes from values by returning + // everything except first and last characters + // a string is defined as " ... " or ' ... ' + // so it's safe to strip these characters + val = &tmp[1..tmp.len() - 1]; + } + } + + (key, val) +} diff --git a/src/parser/header.rs b/src/parser/header.rs new file mode 100644 index 0000000..d487511 --- /dev/null +++ b/src/parser/header.rs @@ -0,0 +1,39 @@ +use crate::Rule; +use pest::iterators::{Pair, Pairs}; +use std::collections::HashMap; + +pub fn parse_header_defs(pairs: Pairs) -> HashMap { + let mut map = HashMap::new(); + for pair in pairs { + if Rule::header_assign == pair.as_rule() { + let (key, val) = parse_header_assign(pair); + map.insert(key.to_string(), val.to_string()); + } + } + map +} + +fn parse_header_assign(pair: Pair) -> (&str, &str) { + let mut key = ""; + let mut val = ""; + + for pair in pair.into_inner() { + if Rule::key == pair.as_rule() { + key = pair.as_str(); + } + if Rule::header_value == pair.as_rule() { + let tmp = pair.as_str(); + // blank and default shoud be handled by whoever is getting the value + if tmp == "BLANK" || tmp == "true" || tmp == "false" { + return (key, tmp); + } + // remove surrounding quotes from values by returning + // everything except first and last characters + // a string is defined as " ... " or ' ... ' + // so it's safe to strip these characters + val = &tmp[1..tmp.len() - 1]; + } + } + + (key, val) +} diff --git a/src/meta.pest b/src/parser/meta.pest similarity index 100% rename from src/meta.pest rename to src/parser/meta.pest diff --git a/src/parser/source.rs b/src/parser/source.rs new file mode 100644 index 0000000..36a2c44 --- /dev/null +++ b/src/parser/source.rs @@ -0,0 +1,38 @@ +use crate::{ + parser::{Pair, Pairs}, + source, Rule, Src, +}; + +pub fn parse_source(pairs: Pairs) -> Vec { + let mut vec = Vec::new(); + for pair in pairs { + match pair.as_rule() { + Rule::var_sub => vec.push(source!(var(parse_sub(pair)))), + Rule::arr_sub => vec.push(source!(arr(parse_sub(pair)))), + Rule::pat_sub => vec.push(source!(pat(parse_sub(pair)))), + Rule::char_seq => vec.push(source!(pair.as_str())), + // anything that isn't a substitution is a char_seq inside source + _ => unreachable!(), + } + } + + vec +} + +fn parse_sub(pair: Pair) -> &str { + match pair.as_rule() { + Rule::var_sub | Rule::arr_sub | Rule::pat_sub => { + let str = pair.as_str(); + // return the value as the inner string for substitution + // all substitutions have the format of + // *{ ... } + // we return everything except: + // first two chars (sigil and preceding brace) + // last char (trailing brace) + &str[2..str.len() - 1] + } + // this function only gets called to parse substituiton patterns + // so anything else should never be called + _ => unreachable!(), + } +} diff --git a/src/parser/tests.rs b/src/parser/tests.rs new file mode 100644 index 0000000..03f929f --- /dev/null +++ b/src/parser/tests.rs @@ -0,0 +1,72 @@ +macro_rules! test_str ( + ($s: expr) => { + let opts = crate::Options::new(); + let str = $s.to_string(); + crate::parse_string(str, &opts).unwrap(); + }; +); + +#[test] +fn no_spaces_in_def() { + test_str!(r#"${v='v'}@{a=['a']}&{p='p'}"#); +} + +#[test] +fn just_source_string() { + test_str!(r#"This is just a &{source} snippet"#); +} + +#[test] +fn one_line() { + test_str!( + r#"${variable = 'var' } @{array = ['array']} &{ pattern = "pattern"} And some extra text"# + ); +} + +#[test] +#[should_panic] +fn key_with_spaces() { + test_str!(r#"${ key with spaces = "value" }"#); +} + +#[test] +#[should_panic] +fn value_missing_quote() { + test_str!(r#"${ key = "value missing quote }"#); +} + +#[test] +#[should_panic] +fn mixed_quotes() { + test_str!(r#"${ key = "value mixing quotes' }"#); +} + +#[test] +#[should_panic] +fn spaces_in_substitution() { + test_str!(r#"This ${variable is not allowed}"#); +} + +#[test] +#[should_panic] +fn missing_closing_brace() { + test_str!(r#"${ key = "value" "#); +} + +#[test] +#[should_panic] +fn map_in_source() { + test_str!(r#"This map: ${ is = "invalid" }"#); +} + +#[test] +#[should_panic] +fn map_source_map() { + test_str!(r#"${var='v'} Some text @{array = ['a']}"#); +} + +#[test] +#[should_panic] +fn header_not_first() { + test_str!(r#"${v='v'} #{ type = 'html'} @{a=['a']}"#); +}