From 138595ef3c221bb97e77211ecc980da0d70073b6 Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Thu, 18 May 2023 17:44:20 -0500 Subject: [PATCH] added: scope structure to definitions --- src/builder.rs | 9 +++--- src/builder/array.rs | 24 +++++++------- src/builder/pattern.rs | 17 +++++----- src/builder/source.rs | 33 +++++++++---------- src/builder/tests.rs | 52 +++++++++++++++++++++++++----- src/builder/variable.rs | 12 ++++--- src/metafile.rs | 39 +++++++++++++++-------- src/metafile/dir.rs | 2 +- src/metafile/file.rs | 70 +++++++++++++++++++++++++++-------------- src/metafile/scope.rs | 43 +++++++++++++++++++++++++ src/parser/array.rs | 31 ++++++++++++------ src/parser/def_block.rs | 53 ++++++++++++++++++++----------- src/parser/meta.pest | 9 +++--- src/parser/source.rs | 10 +++--- tests/build_dir.rs | 1 - 15 files changed, 277 insertions(+), 128 deletions(-) create mode 100644 src/metafile/scope.rs diff --git a/src/builder.rs b/src/builder.rs index 79dfe27..6005a38 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,6 +1,3 @@ -use crate::MetaFile; -use color_eyre::Result; - mod array; mod pattern; mod source; @@ -8,10 +5,14 @@ mod variable; use pattern::*; use source::*; +use variable::*; #[cfg(test)] mod tests; +use crate::{MetaFile, Scope}; +use color_eyre::Result; + pub fn build_metafile(file: &MetaFile) -> Result { if file.header.blank { return Ok(String::new()); @@ -23,7 +24,7 @@ pub fn build_metafile(file: &MetaFile) -> Result { let mut base = crate::parse_string(pattern, file.opts)?; base.merge(file); - base.patterns.insert("SOURCE".to_string(), html); + base.patterns.insert(Scope::into_global("SOURCE"), html); let output = metafile_to_string(&base)?; diff --git a/src/builder/array.rs b/src/builder/array.rs index b1ff436..be1a126 100644 --- a/src/builder/array.rs +++ b/src/builder/array.rs @@ -1,4 +1,4 @@ -use crate::{MetaFile, Src, Sub}; +use crate::{MetaFile, Scope, Src}; use color_eyre::Result; use std::collections::HashMap; @@ -8,7 +8,7 @@ pub fn expand_arrays(input: String, file: &MetaFile) -> Result { .iter() // filter out arrays from source vec .filter_map(|x| { - if let Src::Sub(Sub::Arr(array)) = x { + if let Src::Arr(array) = x { Some(array) } else { None @@ -17,20 +17,22 @@ pub fn expand_arrays(input: String, file: &MetaFile) -> Result { // 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 name = file.name().unwrap_or_default(); 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; + let value = if let Some(val) = file.get_arr(&Scope::into_global(long_key.to_string())) { + val + } else if let Some(val) = file.get_arr(&Scope::into_local(long_key.to_string())) { + 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!("undefined array called: {}, {}", key, long_key); } else { - value = &[]; - } - + &[] + }; (key.to_string(), value) }) .collect(); diff --git a/src/builder/pattern.rs b/src/builder/pattern.rs index 5576584..e36b556 100644 --- a/src/builder/pattern.rs +++ b/src/builder/pattern.rs @@ -1,22 +1,25 @@ -use crate::MetaFile; +use crate::{MetaFile, Scope}; 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") { + if let Some(source) = file.patterns.get(&Scope::into_global("SOURCE")) { return Ok(source.to_string()); + } else { + return Ok(String::new()); } } - let mut filename: String; - if let Some(name) = file.get_pat(key) { - filename = name.to_string(); + let mut filename = if let Some(name) = file.get_pat(&Scope::into_local(key)) { + name.to_string() + } else if let Some(name) = file.get_pat(&Scope::into_global(key)) { + name.to_string() } else { // anything not defined should have a default.meta file to fall back to - filename = "default".to_string() - } + "default".to_string() + }; // BLANK returns nothing, so no more processing needs to be done if filename == "BLANK" { diff --git a/src/builder/source.rs b/src/builder/source.rs index 48a5517..588d743 100644 --- a/src/builder/source.rs +++ b/src/builder/source.rs @@ -1,4 +1,4 @@ -use crate::{MetaFile, Src, Sub}; +use crate::{MetaFile, Src}; use color_eyre::{eyre::bail, Result}; use super::array::*; @@ -7,7 +7,7 @@ use super::*; pub fn get_source_html(file: &MetaFile) -> Result { let string = metafile_to_string(file)?; - if file.opts.no_pandoc || !file.header.pandoc { + if file.opts.no_pandoc || !file.header.pandoc || string == "" { return Ok(string); } @@ -34,26 +34,21 @@ pub fn metafile_to_string(file: &MetaFile) -> Result { let mut arrays = false; for section in file.source.iter() { - match section { + let sec = match section { // concatenate any char sequences - Src::Str(str) => { - output.push_str(str); - } + Src::Str(str) => str.to_string(), // 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); + 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 { diff --git a/src/builder/tests.rs b/src/builder/tests.rs index 0d6feb0..3011058 100644 --- a/src/builder/tests.rs +++ b/src/builder/tests.rs @@ -1,6 +1,6 @@ use crate::{build_metafile, MetaFile, Options}; use color_eyre::{eyre::WrapErr, Result}; -use std::path::PathBuf; +use std::{error::Error, fs, path::PathBuf}; fn unit_test(test: (&str, &str)) -> Result<()> { let dir = PathBuf::from("files/test_site").canonicalize()?; @@ -11,6 +11,7 @@ fn unit_test(test: (&str, &str)) -> Result<()> { opts.build = dir.join("build"); opts.pattern = dir.join("pattern"); opts.clean = true; + opts.undefined = true; let test_dir = opts.source.join("unit_tests"); let mut file_path = test_dir.join(test.0); @@ -19,19 +20,39 @@ fn unit_test(test: (&str, &str)) -> Result<()> { let output = build_metafile(&file).wrap_err_with(|| test.0.to_string())?; - assert_eq!(output, test.1); + if output == test.1 { + Ok(()) + } else { + let err = color_eyre::eyre::eyre!("{} - failed", test.0); + eprintln!("{:?}", err); + eprintln!("\nTEST:\n{}\nOUTPUT:\n{}", test.1, output); + Err(err) + } +} + +fn clean_build_dir() -> Result<()> { + let build = PathBuf::from("files/test_site") + .canonicalize()? + .join("build"); + + if build.exists() { + fs::remove_dir_all(&build)?; + } + fs::create_dir_all(&build)?; Ok(()) } #[test] fn builder_tests() -> Result<()> { + clean_build_dir()?; + let mut tests: Vec<(&str, &str)> = Vec::new(); - tests.push(("find_dest", "\n\n\n")); + tests.push(("find_dest", "\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/comment", "\n\n")); tests.push(( "blank/inline_comment", "\n

inline comment

\n\n", @@ -50,8 +71,23 @@ fn builder_tests() -> Result<()> { tests.push(("header/pandoc", "# This should not become html\n")); tests.push(("header/blank", "")); + let mut err = false; + let mut errs: Vec> = Vec::new(); for test in tests.iter() { - unit_test(*test)?; + match unit_test(*test) { + Ok(_) => continue, + Err(e) => { + err = true; + errs.push(e.into()); + } + } + } + + if err { + for e in errs.iter() { + eprintln!("{}", e.to_string()); + } + return Err(color_eyre::eyre::eyre!("failed tests")); } Ok(()) @@ -71,16 +107,16 @@ fn test_filetype_header() -> Result<()> { assert_eq!( file.dest()?, - PathBuf::from( - "/home/huck/repos/metaforge/files/test_site/build/unit_tests/header/filetype.rss" - ) + opts.build.join("unit_tests/header/filetype.rss") ); Ok(()) } #[test] +#[ignore = "interferes with unit_tests"] fn test_global() -> Result<()> { + clean_build_dir()?; let dir = PathBuf::from("files/test_site/").canonicalize()?; let mut opts = Options::new(); diff --git a/src/builder/variable.rs b/src/builder/variable.rs index 260cf97..369f383 100644 --- a/src/builder/variable.rs +++ b/src/builder/variable.rs @@ -1,14 +1,18 @@ -use crate::MetaFile; +use crate::{MetaFile, Scope}; 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) { + if let Some(val) = file.get_var(&Scope::into_local(long_key.to_string())) { Ok(val.clone()) - } else if let Some(val) = file.get_var(key) { + } else if let Some(val) = file.get_var(&Scope::into_global(long_key.to_string())) { + 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 { - bail!("undefined variable: {}, {}", key, long_key) + bail!("undefined variable: {}, {}", key.to_string(), &long_key) } else { Ok(String::new()) } diff --git a/src/metafile.rs b/src/metafile.rs index ced91bd..5efa2cc 100644 --- a/src/metafile.rs +++ b/src/metafile.rs @@ -1,31 +1,44 @@ mod dir; mod file; mod header; +mod scope; pub use dir::*; pub use file::*; pub use header::*; +pub use scope::*; #[cfg(test)] mod tests; -#[macro_export] -macro_rules! source ( - (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())}; -); - #[derive(Debug, Clone, PartialEq)] pub enum Src { Str(String), - Sub(Sub), -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Sub { Var(String), Arr(String), Pat(String), } + +impl Src { + pub fn to_var(var: impl ToString) -> Self { + Src::Var(var.to_string()) + } + + pub fn to_arr(arr: impl ToString) -> Self { + Src::Arr(arr.to_string()) + } + + pub fn to_pat(pat: impl ToString) -> Self { + Src::Pat(pat.to_string()) + } + + pub fn to_str(str: impl ToString) -> Self { + Src::Str(str.to_string()) + } + + pub fn to_string(&self) -> String { + match self { + Src::Var(x) | Src::Arr(x) | Src::Pat(x) | Src::Str(x) => x.to_string(), + } + } +} diff --git a/src/metafile/dir.rs b/src/metafile/dir.rs index 7f36ef6..066adcf 100644 --- a/src/metafile/dir.rs +++ b/src/metafile/dir.rs @@ -19,7 +19,7 @@ impl<'a> DirNode<'a> { let build_dir = opts.build.join(path.strip_prefix(&opts.source)?); if !build_dir.exists() { - fs::create_dir(build_dir)?; + fs::create_dir_all(build_dir)?; } let files: Vec = Vec::new(); diff --git a/src/metafile/file.rs b/src/metafile/file.rs index a3fe74a..c944638 100644 --- a/src/metafile/file.rs +++ b/src/metafile/file.rs @@ -3,15 +3,14 @@ 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 variables: HashMap, + pub arrays: HashMap>, + pub patterns: HashMap, pub source: Vec, } @@ -77,36 +76,61 @@ impl<'a> MetaFile<'a> { } } - pub fn get_var(&self, key: &str) -> Option<&String> { + pub fn get_var(&self, key: &Scope) -> Option<&String> { self.variables.get(key) } - pub fn get_arr(&self, key: &str) -> Option<&[String]> { + pub fn get_arr(&self, key: &Scope) -> Option<&[String]> { self.arrays.get(key).map(|a| &a[..]) } - pub fn get_pat(&self, key: &str) -> Option<&String> { + pub fn get_pat(&self, key: &Scope) -> 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()), - }; + pub fn var_defined(&self, key: &str) -> bool { + if self.variables.contains_key(&Scope::into_local(key)) + || self.variables.contains_key(&Scope::into_global(key)) + { + true + } else { + false } - for (key, val) in other.arrays.iter() { - match self.arrays.get(key) { - Some(_) => continue, - None => self.arrays.insert(key.to_string(), val.to_vec()), - }; + } + + pub fn arr_defined(&self, key: &str) -> bool { + if self.arrays.contains_key(&Scope::into_local(key)) + || self.arrays.contains_key(&Scope::into_global(key)) + { + true + } else { + false } - for (key, val) in other.patterns.iter() { - match self.patterns.get(key) { - Some(_) => continue, - None => self.patterns.insert(key.to_string(), val.to_string()), - }; + } + + pub fn pat_defined(&self, key: &str) -> bool { + if self.patterns.contains_key(&Scope::into_local(key)) + || self.patterns.contains_key(&Scope::into_global(key)) + { + true + } else { + false } } + + pub fn merge(&mut self, other: &Self) { + macro_rules! merge ( + ($m:ident) => { + for (key, val) in other.$m.iter() { + if key.is_global() && !self.$m.contains_key(key) { + self.$m.insert(key.clone(), val.clone()); + } + } + }; + ); + + merge!(variables); + merge!(arrays); + merge!(patterns); + } } diff --git a/src/metafile/scope.rs b/src/metafile/scope.rs new file mode 100644 index 0000000..3af714b --- /dev/null +++ b/src/metafile/scope.rs @@ -0,0 +1,43 @@ +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum Scope { + Local(String), + Global(String), +} + +impl Scope { + pub fn into_local(str: impl ToString) -> Scope { + Scope::Local(str.to_string()) + } + + pub fn into_global(str: impl ToString) -> Scope { + Scope::Global(str.to_string()) + } + + pub fn to_string(&self) -> String { + match self { + Scope::Local(x) | Scope::Global(x) => x.to_string(), + } + } + + pub fn is_global(&self) -> bool { + match self { + Scope::Local(_) => false, + Scope::Global(_) => true, + } + } + + pub fn is_local(&self) -> bool { + match self { + Scope::Local(_) => true, + Scope::Global(_) => false, + } + } + + pub fn to_local(&self) -> Scope { + Scope::Local(self.to_string()) + } + + pub fn to_global(&self) -> Scope { + Scope::Global(self.to_string()) + } +} diff --git a/src/parser/array.rs b/src/parser/array.rs index 5120511..bb72a23 100644 --- a/src/parser/array.rs +++ b/src/parser/array.rs @@ -1,32 +1,43 @@ -use crate::Rule; +use crate::{Rule, Scope}; use pest::iterators::{Pair, Pairs}; use std::collections::HashMap; -pub fn parse_array_defs(pairs: Pairs) -> 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.insert(key, val); } } map } -fn parse_assign_array(pair: Pair) -> (String, Vec) { +fn parse_assign_array(pair: Pair) -> (Scope, Vec) { let mut key = ""; let mut val = Vec::default(); + let mut global = true; 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()); + match pair.as_rule() { + Rule::scope => { + if pair.as_str() == "*" { + global = false; + } else { + global = true; + } + } + Rule::key => key = pair.as_str(), + Rule::value => val = parse_array(pair.into_inner()), + _ => unreachable!(), } } - (key.to_string(), val) + if global { + (Scope::into_global(key), val) + } else { + (Scope::into_local(key), val) + } } fn parse_array(pairs: Pairs) -> Vec { diff --git a/src/parser/def_block.rs b/src/parser/def_block.rs index 7917998..df5485d 100644 --- a/src/parser/def_block.rs +++ b/src/parser/def_block.rs @@ -1,39 +1,56 @@ -use crate::Rule; +use crate::{Rule, Scope}; use pest::iterators::{Pair, Pairs}; use std::collections::HashMap; -pub fn parse_defs(pairs: Pairs) -> 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.insert(key, val.to_string()); } } map } -fn parse_assign(pair: Pair) -> (&str, &str) { +fn parse_assign(pair: Pair) -> (Scope, &str) { let mut key = ""; let mut val = ""; + let mut global = true; + let mut trim = true; 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); + match pair.as_rule() { + Rule::scope => { + if pair.as_str() == "*" { + global = false; + } else { + global = true; + } + } + Rule::key => key = pair.as_str(), + Rule::value => { + val = pair.as_str(); + if val == "BLANK" || val == "DEFAULT" { + trim = false; + } } - // 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]; + // nothing else is an acceptable assignment + _ => unreachable!(), } } - (key, val) + if trim { + // 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 = &val[1..val.len() - 1]; + } + + if global { + (Scope::into_global(key), val) + } else { + (Scope::into_local(key), val) + } } diff --git a/src/parser/meta.pest b/src/parser/meta.pest index 438e703..e4e2073 100644 --- a/src/parser/meta.pest +++ b/src/parser/meta.pest @@ -2,6 +2,7 @@ WHITESPACE = _{ " " | "\t" | NEWLINE } COMMENT = _{ "-{" ~ (!"}" ~ ANY)* ~ "}" } sigil = _{ ("$" | "@" | "&" | "#" | "-") ~ "{" } +scope = _{ "!" | "*" } raw_char = _{ !(sigil) ~ ANY } char_seq = ${ raw_char+ } @@ -23,18 +24,18 @@ key_chars = @{ (ASCII_ALPHANUMERIC | "_" | ".")* } key = @{ key_chars } value = ${ string | array | "BLANK" | "DEFAULT" } -assign = { "*"? ~ key ~ "=" ~ value } +assign = { scope? ~ key ~ "=" ~ value } def_block = _{ sigil ~ assign* ~ "}" } var_def = { &("$") ~ def_block } arr_def = { &("@") ~ def_block } pat_def = { &("&") ~ def_block } -definition = _{ var_def | arr_def | pat_def } +definition = _{ scope? ~ (var_def | arr_def | pat_def) } header_value = ${ string | "true" | "false" | "DEFAULT" } -header_assign = { "!"? ~ key ~ "=" ~ header_value } +header_assign = { scope? ~ key ~ "=" ~ header_value } header_block = _{ sigil ~ header_assign* ~ "}" } -header = { &("#") ~ header_block } +header = { scope? ~ &("#") ~ header_block } substitution = _{ sigil ~ key ~ "}" } var_sub = { &("$") ~ substitution } diff --git a/src/parser/source.rs b/src/parser/source.rs index 36a2c44..fd38521 100644 --- a/src/parser/source.rs +++ b/src/parser/source.rs @@ -1,16 +1,16 @@ use crate::{ parser::{Pair, Pairs}, - source, Rule, Src, + 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())), + Rule::var_sub => vec.push(Src::to_var(parse_sub(pair))), + Rule::arr_sub => vec.push(Src::to_arr(parse_sub(pair))), + Rule::pat_sub => vec.push(Src::to_pat(parse_sub(pair))), + Rule::char_seq => vec.push(Src::to_str(pair.as_str())), // anything that isn't a substitution is a char_seq inside source _ => unreachable!(), } diff --git a/tests/build_dir.rs b/tests/build_dir.rs index 505f9c4..6ab839e 100644 --- a/tests/build_dir.rs +++ b/tests/build_dir.rs @@ -15,7 +15,6 @@ fn build_test_site() -> Result<()> { metaforge::build_dir(&opts)?; - assert!(opts.build.join("benchmark.html").exists()); assert!(opts.build.join("unit_tests").exists()); assert!(opts .build -- 2.44.2