-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<String> {
if file.header.blank {
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);
Ok(output)
}
-
-fn metafile_to_string(file: &MetaFile) -> Result<String> {
- 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<String> {
- 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<String> {
- // 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<String> {
- 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<String> {
- let map: HashMap<String, &[String]> = 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<String, &[String]>) -> 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", "<html>\n\n</html>\n")
- }
-
- #[test]
- fn test_blank() -> Result<()> {
- unit_test("blank/blank_pattern", "")?;
- unit_test("blank/blank_variable", "<html>\n</html>\n")?;
- unit_test("blank/blank_array", "<html>\n</html>\n")?;
- Ok(())
- }
-
- #[test]
- fn test_comment() -> Result<()> {
- unit_test("blank/comment", "<html>\n\n</html>\n")?;
- unit_test(
- "blank/inline_comment",
- "<html>\n<p>inline comment</p>\n</html>\n",
- )?;
- Ok(())
- }
-
- #[test]
- fn test_expand() -> Result<()> {
- unit_test(
- "expand/variable_in_source",
- "<html>\n<p>GOOD</p>\n</html>\n",
- )?;
- unit_test("expand/variable_in_pattern", "<html>\nGOOD</html>\n")?;
- unit_test("expand/array_in_source", "<html>\n<p>12345</p>\n</html>\n")?;
- unit_test("expand/array_in_pattern", "<html>\n12345</html>\n")?;
- unit_test("expand/pattern_in_source", "<p>GOOD</p>\n")?;
- unit_test("expand/pattern_in_pattern", "<html>\nGOOD\nGOOD\n</html>\n")?;
- Ok(())
- }
-
- #[test]
- fn test_override() -> Result<()> {
- unit_test("override/variable", "<html>\n<p>GOOD</p>\n</html>\n")?;
- unit_test("override/pattern", "<html>\nGOOD\nGOOD\n</html>\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"))?,
- "<p>GOOD GOOD</p>\n"
- );
-
- assert_eq!(
- fs::read_to_string(dir.join("build/unit_tests/global/variable.html"))?,
- "<p>GOODGOOD</p>\n"
- );
-
- Ok(())
- }
-}
--- /dev/null
+use crate::{MetaFile, Src, Sub};
+use color_eyre::Result;
+use std::collections::HashMap;
+
+pub fn expand_arrays(input: String, file: &MetaFile) -> Result<String> {
+ let map: HashMap<String, &[String]> = 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<String, &[String]>) -> usize {
+ let mut max = 0;
+ for val in map.values() {
+ if max < val.len() {
+ max = val.len();
+ }
+ }
+ max
+}
--- /dev/null
+use crate::MetaFile;
+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") {
+ 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)
+}
--- /dev/null
+use crate::{MetaFile, Src, Sub};
+use color_eyre::{eyre::bail, Result};
+
+use super::array::*;
+use super::*;
+
+pub fn get_source_html(file: &MetaFile) -> Result<String> {
+ 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<String> {
+ 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)
+ }
+}
--- /dev/null
+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", "<html>\n\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/inline_comment",
+ "<html>\n<p>inline comment</p>\n</html>\n",
+ ));
+ tests.push((
+ "expand/variable_in_source",
+ "<html>\n<p>GOOD</p>\n</html>\n",
+ ));
+ tests.push(("expand/variable_in_pattern", "<html>\nGOOD</html>\n"));
+ tests.push(("expand/array_in_source", "<html>\n<p>12345</p>\n</html>\n"));
+ tests.push(("expand/array_in_pattern", "<html>\n12345</html>\n"));
+ tests.push(("expand/pattern_in_source", "<p>GOOD</p>\n"));
+ tests.push(("expand/pattern_in_pattern", "<html>\nGOOD\nGOOD\n</html>\n"));
+ tests.push(("override/variable", "<html>\n<p>GOOD</p>\n</html>\n"));
+ tests.push(("override/pattern", "<html>\nGOOD\nGOOD\n</html>\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"))?,
+ "<p>GOOD GOOD</p>\n"
+ );
+
+ assert_eq!(
+ std::fs::read_to_string(dir.join("build/unit_tests/global/variable.html"))?,
+ "<p>GOODGOOD</p>\n"
+ );
+
+ Ok(())
+}
--- /dev/null
+use crate::MetaFile;
+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) {
+ 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())
+ }
+}
-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<HashMap<String, String>> for Header {
- fn from(value: HashMap<String, String>) -> 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<String, String>,
- pub arrays: HashMap<String, Vec<String>>,
- pub patterns: HashMap<String, String>,
- pub source: Vec<Src>,
-}
-
-impl<'a> MetaFile<'a> {
- pub fn build(path: PathBuf, opts: &'a Options) -> Result<Self> {
- 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<PathBuf> {
- 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<String> {
- 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::<Vec<String>>()
- .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())};
);
Arr(String),
Pat(String),
}
-
-#[derive(Debug, Clone)]
-pub struct DirNode<'a> {
- path: PathBuf,
- opts: &'a Options,
- global: MetaFile<'a>,
- files: Vec<MetaFile<'a>>,
- dirs: Vec<DirNode<'a>>,
-}
-
-impl<'a> DirNode<'a> {
- pub fn build(path: PathBuf, opts: &'a Options) -> Result<Self> {
- 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<MetaFile> = Vec::new();
- let dirs: Vec<DirNode> = 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(())
- }
-}
--- /dev/null
+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<MetaFile<'a>>,
+ dirs: Vec<DirNode<'a>>,
+}
+
+impl<'a> DirNode<'a> {
+ pub fn build(path: PathBuf, opts: &'a Options) -> Result<Self> {
+ 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<MetaFile> = Vec::new();
+ let dirs: Vec<DirNode> = 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(())
+ }
+}
--- /dev/null
+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<String, String>,
+ pub arrays: HashMap<String, Vec<String>>,
+ pub patterns: HashMap<String, String>,
+ pub source: Vec<Src>,
+}
+
+impl<'a> MetaFile<'a> {
+ pub fn build(path: PathBuf, opts: &'a Options) -> Result<Self> {
+ 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<PathBuf> {
+ 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<String> {
+ 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::<Vec<String>>()
+ .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()),
+ };
+ }
+ }
+}
--- /dev/null
+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<HashMap<String, String>> for Header {
+ fn from(value: HashMap<String, String>) -> 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
+ }
+}
--- /dev/null
+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(())
+}
-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<String>,
- /// Source file directory [CURRENT_DIR/source]
- #[arg(short, long, value_name = "SOURCE_DIR")]
- source: Option<String>,
- /// Build directory [CURRENT_DIR/build]
- #[arg(short, long, value_name = "BUILD_DIR")]
- build: Option<String>,
- /// Pattern directory [CURRENT_DIR/pattern]
- #[arg(short, long, value_name = "PATTERN_DIR")]
- pattern: Option<String>,
- /// 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<Opts> for Options {
- type Error = color_eyre::eyre::Error;
- fn try_from(value: Opts) -> Result<Self, Self::Error> {
- 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 {
--- /dev/null
+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<String>,
+ /// Source file directory [CURRENT_DIR/source]
+ #[arg(short, long, value_name = "SOURCE_DIR")]
+ pub source: Option<String>,
+ /// Build directory [CURRENT_DIR/build]
+ #[arg(short, long, value_name = "BUILD_DIR")]
+ pub build: Option<String>,
+ /// Pattern directory [CURRENT_DIR/pattern]
+ #[arg(short, long, value_name = "PATTERN_DIR")]
+ pub pattern: Option<String>,
+ /// 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,
+}
--- /dev/null
+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<crate::Opts> for Options {
+ type Error = color_eyre::eyre::Error;
+ fn try_from(value: crate::Opts) -> Result<Self, Self::Error> {
+ 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)
+ }
+}
-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<MetaFile> {
+pub fn parse_string(file: String, opts: &Options) -> Result<MetaFile> {
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<Rule>, opts: &'a Options) -> MetaFile<'a> {
+fn parse_file<'a>(pair: Pair<Rule>, opts: &'a Options) -> MetaFile<'a> {
let mut meta_file = MetaFile::new(opts);
if Rule::file == pair.as_rule() {
meta_file
}
-
-fn parse_defs(pairs: Pairs<Rule>) -> HashMap<String, 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
-}
-
-fn parse_header_defs(pairs: Pairs<Rule>) -> HashMap<String, String> {
- 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<Rule>) -> HashMap<String, 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
-}
-
-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())),
- // anything that isn't a substitution is a char_seq inside source
- _ => unreachable!(),
- }
- }
-
- vec
-}
-
-fn parse_sub(pair: Pair<Rule>) -> &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<Rule>) -> (&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<Rule>) -> (&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<Rule>) -> (String, Vec<String>) {
- 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<Rule>) -> Vec<String> {
- let mut vec: Vec<String> = 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']}"#);
- }
-}
--- /dev/null
+use crate::Rule;
+use pest::iterators::{Pair, Pairs};
+use std::collections::HashMap;
+
+pub fn parse_array_defs(pairs: Pairs<Rule>) -> HashMap<String, 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
+}
+
+fn parse_assign_array(pair: Pair<Rule>) -> (String, Vec<String>) {
+ 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<Rule>) -> Vec<String> {
+ let mut vec: Vec<String> = 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
+}
--- /dev/null
+use crate::Rule;
+use pest::iterators::{Pair, Pairs};
+use std::collections::HashMap;
+
+pub fn parse_defs(pairs: Pairs<Rule>) -> HashMap<String, 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
+}
+
+fn parse_assign(pair: Pair<Rule>) -> (&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)
+}
--- /dev/null
+use crate::Rule;
+use pest::iterators::{Pair, Pairs};
+use std::collections::HashMap;
+
+pub fn parse_header_defs(pairs: Pairs<Rule>) -> HashMap<String, String> {
+ 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<Rule>) -> (&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)
+}
--- /dev/null
+use crate::{
+ parser::{Pair, Pairs},
+ source, 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())),
+ // anything that isn't a substitution is a char_seq inside source
+ _ => unreachable!(),
+ }
+ }
+
+ vec
+}
+
+fn parse_sub(pair: Pair<Rule>) -> &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!(),
+ }
+}
--- /dev/null
+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']}"#);
+}