-use crate::MetaFile;
-use color_eyre::Result;
-
mod array;
mod pattern;
mod source;
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<String> {
if file.header.blank {
return Ok(String::new());
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)?;
-use crate::{MetaFile, Src, Sub};
+use crate::{MetaFile, Scope, Src};
use color_eyre::Result;
use std::collections::HashMap;
.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
// 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();
-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<String> {
// 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" {
-use crate::{MetaFile, Src, Sub};
+use crate::{MetaFile, Src};
use color_eyre::{eyre::bail, Result};
use super::array::*;
pub fn get_source_html(file: &MetaFile) -> Result<String> {
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);
}
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 {
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()?;
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);
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", "<html>\n\n</html>\n"));
+ tests.push(("find_dest", "<html>\n</html>\n"));
tests.push(("blank/blank_pattern", ""));
tests.push(("blank/blank_variable", "<html>\n</html>\n"));
tests.push(("blank/blank_array", "<html>\n</html>\n"));
- tests.push(("blank/comment", "<html>\n\n</html>\n"));
+ tests.push(("blank/comment", "<html>\n</html>\n"));
tests.push((
"blank/inline_comment",
"<html>\n<p>inline comment</p>\n</html>\n",
tests.push(("header/pandoc", "# This should not become html\n"));
tests.push(("header/blank", ""));
+ let mut err = false;
+ let mut errs: Vec<Box<dyn Error>> = 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(())
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();
-use crate::MetaFile;
+use crate::{MetaFile, Scope};
use color_eyre::{eyre::bail, Result};
pub fn get_variable(key: &str, file: &MetaFile) -> Result<String> {
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())
}
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(),
+ }
+ }
+}
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<MetaFile> = Vec::new();
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<String, String>,
- pub arrays: HashMap<String, Vec<String>>,
- pub patterns: HashMap<String, String>,
+ pub variables: HashMap<Scope, String>,
+ pub arrays: HashMap<Scope, Vec<String>>,
+ pub patterns: HashMap<Scope, String>,
pub source: Vec<Src>,
}
}
}
- 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);
+ }
}
--- /dev/null
+#[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())
+ }
+}
-use crate::Rule;
+use crate::{Rule, Scope};
use pest::iterators::{Pair, Pairs};
use std::collections::HashMap;
-pub fn parse_array_defs(pairs: Pairs<Rule>) -> HashMap<String, Vec<String>> {
+pub fn parse_array_defs(pairs: Pairs<Rule>) -> HashMap<Scope, Vec<String>> {
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<Rule>) -> (String, Vec<String>) {
+fn parse_assign_array(pair: Pair<Rule>) -> (Scope, Vec<String>) {
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<Rule>) -> Vec<String> {
-use crate::Rule;
+use crate::{Rule, Scope};
use pest::iterators::{Pair, Pairs};
use std::collections::HashMap;
-pub fn parse_defs(pairs: Pairs<Rule>) -> HashMap<String, String> {
+pub fn parse_defs(pairs: Pairs<Rule>) -> HashMap<Scope, String> {
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<Rule>) -> (&str, &str) {
+fn parse_assign(pair: Pair<Rule>) -> (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)
+ }
}
COMMENT = _{ "-{" ~ (!"}" ~ ANY)* ~ "}" }
sigil = _{ ("$" | "@" | "&" | "#" | "-") ~ "{" }
+scope = _{ "!" | "*" }
raw_char = _{ !(sigil) ~ ANY }
char_seq = ${ raw_char+ }
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 }
use crate::{
parser::{Pair, Pairs},
- source, Rule, Src,
+ Rule, Src,
};
pub fn parse_source(pairs: Pairs<Rule>) -> Vec<Src> {
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!(),
}
metaforge::build_dir(&opts)?;
- assert!(opts.build.join("benchmark.html").exists());
assert!(opts.build.join("unit_tests").exists());
assert!(opts
.build