/target
 files/test_site/build
-files/README/build
+docs/build
 files/bench_site/build
 bacon.toml
 
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "anes"
 version = "0.1.6"
 
 [[package]]
 name = "clap"
-version = "4.3.0"
+version = "4.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
+checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28"
 dependencies = [
  "clap_builder",
  "clap_derive",
 
 [[package]]
 name = "clap_builder"
-version = "4.3.0"
+version = "4.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
+checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980"
 dependencies = [
  "anstream",
  "anstyle",
 
 [[package]]
 name = "clap_derive"
-version = "4.3.0"
+version = "4.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b"
+checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b"
 dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.7"
  "typenum",
 ]
 
+[[package]]
+name = "css-minify"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874c6e2d19f8d4a285083b11a3241bfbe01ac3ed85f26e1e6b34888d960552bd"
+dependencies = [
+ "derive_more",
+ "indexmap",
+ "nom",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
 
 [[package]]
 name = "log"
-version = "0.4.17"
+version = "0.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
+checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "memoffset"
 
 [[package]]
 name = "metaforge"
-version = "0.1.1"
+version = "0.1.3"
 dependencies = [
- "clap 4.3.0",
+ "clap 4.3.1",
  "criterion",
  "eyre",
+ "minify-html",
  "pandoc",
  "pest",
  "pest_derive",
  "thiserror",
 ]
 
+[[package]]
+name = "minify-html"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4d9147754a49e80557df835eb59e743eab1bf75410a134f55dc4b9dbb692ad"
+dependencies = [
+ "aho-corasick",
+ "css-minify",
+ "lazy_static",
+ "memchr",
+ "minify-js",
+ "rustc-hash",
+]
+
+[[package]]
+name = "minify-js"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c300f90ba1138b5c5daf5d9441dc9bdc67b808aac22cf638362a2647bc213be4"
+dependencies = [
+ "lazy_static",
+ "parse-js",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.15"
 
 [[package]]
 name = "once_cell"
-version = "1.17.1"
+version = "1.17.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
 
 [[package]]
 name = "oorandom"
  "itertools 0.8.2",
 ]
 
+[[package]]
+name = "parse-js"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30534759e6ad87aa144c396544747e1c25b1020bd133356fd758c8facec764e5"
+dependencies = [
+ "aho-corasick",
+ "lazy_static",
+ "memchr",
+]
+
 [[package]]
 name = "pest"
 version = "2.6.0"
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
 [[package]]
 name = "rustix"
 version = "0.37.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
+[[package]]
+name = "semver"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+
 [[package]]
 name = "serde"
 version = "1.0.163"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.18"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
  "once_cell",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
  "wasm-bindgen-shared",
 ]
 
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [package]
 name = "metaforge"
-version = "0.1.2"
+version = "0.1.3"
 edition = "2021"
 
 [dependencies]
 pest = "2"
 pest_derive = "2"
 rayon = "1.7"
+minify-html = "0.11"
 
 [dev-dependencies]
 criterion = "0.4"
 
-# metaforge - v0.1.2
+# metaforge - v0.1.3
 
 a pattern driven static site generator for extensible snippet insertion.
 
 ## about
 
 metaforge is a static site generator that lets you write something once, and re-use it
-across your site. it requires previous knowledge of html, and doesn't come with pre-made
-themes or templates except for a completely bare skeleton directory. it gives you extremely
-fine grained control over the generated html, customizing each pattern to a source file
+across your site.
+
+metaforge doesn't come with pre-made themes or templates except for a completely bare skeleton directory.
+it gives you extremely fine grained control over the generated html, customizing each pattern to a source file
 with variables and mappable arrays.
 
 metaforge also lets you write metapatterns that contain classes, masking choices to a
 such as an rss feed. metaforge can also technically translate between any two document formats pandoc supports,
 but nothing other than the default markdown to html gets tested.
 
-the full documentation is available in this repository under the **files/README/source/docs**
+the full documentation is available in this repository under the **docs/source**
 directory. it's currently setup as a working example of a metaforge site, so you can poke around
 and see one way a site can be setup.
 
 
 this command can be run as many times as needed to regenerate the documentation, and is
 reccomended after upgrading to a new version to see what's changed. the generated docs will
-be available in **files/README/build**, and can be looked at in any web browser.
+be available in **docs/build**, and can be looked at in any web browser.
 
     opts.clean = true;
     opts.parallel = true;
 
-    c.bench_function("build dir", |b| {
+    c.bench_function("parallel build", |b| {
         if opts.build.exists() {
             std::fs::remove_dir_all(&opts.build).expect("clean build dir");
         }
 
 ${
     author = 'huck boles'
-    version = '0.1.2'
+    version = '0.1.3'
     home = './index.html'
 }
 
     -{ index is a folder up in this directory }
     home = '../index.html'
     -{ using substitutions for example code so it doesn't mess up parsing }
-    var = '${ '
-    arr = '@{ '
-    pat = '&{ '
-    head = '#{ '
-    com = '-{ '
+    var = '${'
+    arr = '@{'
+    pat = '&{'
+    head = '#{'
+    com = '-{'
 }
 
 ${
     title = 'definitions'
     description = 'definining variables and patterns'
-    arr_sub = '@{'
 }
 
 ## rules
 
        foo.bar = [ 'foobar', 'foobaz' ]
        ${com} this will copy pattern/foo/*.meta twice, inserting 'foobar and 'foobaz'
-          once each at the location of ${arr_sub}bar} }
+          once each at the location of ${arr}bar} }
     }
 
     ${com} all of these patterns are only defined for this file }
 
 ${
     title = 'expansions'
     description = 'expanding variables and patterns in a file'
-    var_sub = '${'
-    arr_sub = '@{'
-    pat_sub = '&{'
 }
 
 ## syntax
 
 ### examples
 
-    ...this is a string with a ${var_sub}variable} to be expanded...
+    ...this is a string with a ${var}variable} to be expanded...
 
-    ...this line has a ${pat_sub}pattern} inside of it...
+    ...this line has a ${pat}pattern} inside of it...
 
-    ...this ${arr_sub}array} will be replaced...
+    ...this ${arr}array} will be replaced...
 
 ## behavior
 
            quux = BLANK
         }
 
-    pattern [foo]: <p>${var_sub}baz} ${var}quux}</p>
+    pattern [foo]: <p>${var}baz} ${var}quux}</p>
 
     expanded [foo]: <p>foo </p>
 
-    pattern [bar]: <p>${var_sub}baz} ${var}quux}</p>
+    pattern [bar]: <p>${var}baz} ${var}quux}</p>
 
     expanded [bar]: <p>quux </p>
 
 
 #### example
 
-    pattern [foo]: <p>${arr_sub}bar}</p>
+    pattern [foo]: <p>${arr}bar}</p>
 
     defintion: ${arr} foo.bar = ['foo', 'bar', 'baz'] }
 
 goes through an extra step right before insertion, calling pandoc on the file
 to convert between the chosen filetypes.
 
+***SOURCE*** can also be used as a standin for the source directory while writing expansions,
+allowing patterns to call the same source file every time, or source files to expand other
+source files.
+
+### example
+    ...lorem ${pat}SOURCE.foo.bar} ipsum dolor...
+
 once the filename is determined, it is parsed and expands any contained variables,
 arrays and patterns. if it is a ***SOURCE*** pattern, it is converted to html after
 the expansions. the expanded pattern is then inserted in place of the calling identifier.
 
 ## building
 
-as each file is built, the first thing expanded is the relevant **base/[FILE].meta** pattern,
-so it is required to have at least a **default.meta** in the **pattern/base** directory
+as each source file is built, the first thing expanded is the defined or default
+**[PATTERN]/base/[FILE].meta** pattern, so it is required to have at least a **default.meta**
+in the **pattern/base** directory
 
 ### example
 
-    pattern [base]: <html>${pat_sub}body}</html>
+    pattern [base]: <html>${pat}body}</html>
 
-    pattern [body]: <body>${pat_sub}SOURCE}</body>
+    pattern [body]: <body>${pat}SOURCE}</body>
 
     source [SOURCE]: foo *bar* baz
 
 
         --undefined
             panics and stops building site if any undefined variables are encountered
         --no-pandoc
-            don't call pandoc on source files
-            allows metaforge to run without pandoc installed
+            don't call pandoc on source files. allows metaforge to run without pandoc installed
+        --no-minify
+            don't minify resulting html
 
 - blank = **BOOL** - if true, stops parsing and returns an empty string
 - panic_default = **BOOL** - if true, panics on an undefined default pattern
 - panic_undefined = **BOOL** - if true, panics on an undefined variable or array
+- source = **STRING** - change the the filetype of the source file
+- filetype = **STRING** - change the filetype of the output file
 - equal_arrays = **BOOL** - if true, panics if arrays in the same pattern have different sizes
+- minify = **BOOL** - toggles html minification
+- pandoc = **BOOL** - toggles if pandoc is ran on this file to convert between filetypes, defaults to *true* in **source** dir, and *false* in **pattern** dir.
 
 ### source
 
 - ignore = **BOOL** - stops parsing and skips this file, useful for ignoring directories with scoped definitions
-- source = **STRING** - change the the filetype of the source file
-- filetype = **STRING** - change the filetype of the output file
-- pandoc = **BOOL** - toggles if pandoc is ran on this file to convert between filetypes
+- copy_only = **BOOL** - copys file or directory without processing anything
 
 but can contain anything that you'd like to substitute.
 
 required directories are:
+
 - source (site structure and contents)
 - pattern (patterns for expansion)
 - pattern/base (gets expanded to start building each pattern)
 
        foobaz }
 
 ## layout
-- optional header definition block
-- optional variable, array, pattern definition blocks
+
+*all sections are optional*
+
+- header definition block
+- variable, array, pattern definition blocks
 - source
 
 ### example
 
 
 currently it's unstyled html rendered by your browser, so it's pretty bare-bones.
 
-open **files/README** in metaforge's repository and explore the source and pattern
+open **docs/source** in metaforge's repository and explore the source and pattern
 files to get some examples of how this site works.
 
-you can change anything in the **README** directory and see how it affects the site once
+you can change anything in the **docs** directory and see how it affects the site once
 you rebuild it.
 
 ## contents
+
 - [syntax](docs/syntax.html)
 - [definitions](docs/definitions.html)
 - [expansions](docs/expansions.html)
 - [headers](docs/header.html)
 - [flags](docs/flags.html)
 
+## new in this version
+
+- spaces are correctly preserved between directly adjacent substitutions
+- include from source dir
+- copy_only header directive
+- pandoc header now works on pattern files
+- html minification header and flags
+
 ## versions
+- 0.1.3: grammar and headers
 - 0.1.2: multithreading
 - 0.1.1: initial release
 
--- /dev/null
+&{SOURCE.unit_tests.expand.source_expand}
 
--- /dev/null
+#{ pandoc = true }
+
+# GOOD
 
--- /dev/null
+${ var = 'GOOD' }
+
+&{ test = 'expand_source' }
 
--- /dev/null
+GOOD
+${var}
 
--- /dev/null
+${
+    var1 = 'GOOD'
+    var2 = 'GOOD'
+}
+
+${var1} ${var2}
 
--- /dev/null
+#{ copy_only = true }
+
+variable: ${this} should get copied verbatim
 
--- /dev/null
+&{test = 'pandoc'}
+
+source
 
         })),
     }?;
 
-    let file = parse_string(source, opts)?;
+    let mut file = parse_string(source, opts)?;
 
     Ok(file.construct()?)
 }
 
 #[cfg(test)]
 mod tests;
 
+use std::fmt::Display;
+
 #[derive(Debug, Clone, PartialEq)]
 pub enum Src {
     Str(String),
 }
 
 impl Src {
-    pub fn to_var(var: impl ToString) -> Self {
+    pub fn to_var(var: impl Display) -> Self {
         Src::Var(var.to_string())
     }
 
-    pub fn to_arr(arr: impl ToString) -> Self {
+    pub fn to_arr(arr: impl Display) -> Self {
         Src::Arr(arr.to_string())
     }
 
-    pub fn to_pat(pat: impl ToString) -> Self {
+    pub fn to_pat(pat: impl Display) -> Self {
         Src::Pat(pat.to_string())
     }
 
-    pub fn to_str(str: impl ToString) -> Self {
+    pub fn to_str(str: impl Display) -> Self {
         Src::Str(str.to_string())
     }
 }
 
-impl ToString for Src {
-    fn to_string(&self) -> String {
-        match self {
+impl Display for Src {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let str = match self {
             Src::Var(x) | Src::Arr(x) | Src::Pat(x) | Src::Str(x) => x.to_string(),
-        }
+        };
+
+        write!(f, "{str}")
     }
 }
 
 use crate::{error::*, Options};
 use eyre::Result;
+use minify_html::{minify, Cfg};
 use std::{fs, path::PathBuf};
 
 use super::*;
 
+const HTML_CFG: Cfg = Cfg {
+    do_not_minify_doctype: false,
+    ensure_spec_compliant_unquoted_attribute_values: false,
+    keep_closing_tags: true,
+    keep_html_and_head_opening_tags: true,
+    keep_spaces_between_attributes: false,
+    keep_comments: false,
+    minify_css: true,
+    minify_css_level_1: false,
+    minify_css_level_2: true,
+    minify_css_level_3: false,
+    minify_js: true,
+    remove_bangs: true,
+    remove_processing_instructions: true,
+};
+
 impl<'a> DirNode<'a> {
     pub fn build(path: PathBuf, opts: &'a Options) -> Result<Self> {
         assert!(path.is_dir() && path.exists());
         for f in fs::read_dir(&self.path)? {
             let file = f?.path();
 
+            if self.global.header.copy_only {
+                let dest = self.global.dest()?;
+                fs::copy(file, &dest.parent().unwrap_or(&self.opts.build))?;
+                continue;
+            }
+
             if file.is_dir() {
                 let dir = DirNode::build(file, self.opts)?;
                 self.dirs.push(dir);
             file.merge(&self.global);
             match file.construct() {
                 Ok(str) => {
-                    fs::write(file.dest()?, str)?;
+                    if file.header.minify && self.opts.minify {
+                        fs::write(file.dest()?, minify(str.as_bytes(), &HTML_CFG))?;
+                    } else {
+                        fs::write(file.dest()?, str)?;
+                    }
                 }
                 Err(e) => {
                     // print a line to stderr about failure but continue with other files
 
 use std::{collections::HashMap, path::PathBuf};
 
 use super::*;
+
 #[derive(Debug, Clone)]
 pub struct MetaFile<'a> {
     pub opts: &'a Options,
         Ok(metafile)
     }
 
-    pub fn construct(&self) -> Result<String, Box<MetaError>> {
+    pub fn construct(&mut self) -> Result<String, Box<MetaError>> {
         log!(self.opts, format!("building {}", self.path.display()), 1);
 
         if self.header.blank {
             return Err(Box::new(MetaError::Ignored));
         }
 
-        let html = self.to_html().map_err(MetaError::from)?;
+        if self.header.copy_only {
+            let dest = self.dest().map_err(MetaError::from)?;
+            let source: String = self.source.iter().map(|s| s.to_string()).collect();
+            std::fs::write(dest, source).unwrap();
+            return Err(Box::new(MetaError::Ignored));
+        }
+
+        let src_str: String;
+        if self.header.pandoc.map_or(true, |x| x) {
+            src_str = self.pandoc().map_err(MetaError::from)?;
+        } else {
+            src_str = self.get_source().map_err(MetaError::from)?;
+        }
 
         let pattern = self.get_pattern("base").map_err(MetaError::from)?;
         let mut base = parse_string(pattern, self.opts).map_err(|e| MetaError::ParserError {
         })?;
 
         base.merge(self);
-        base.patterns.insert(Scope::into_global("SOURCE"), html);
+        base.patterns
+            .insert(Scope::create_global("SOURCE"), src_str);
+        let mut base_path = self.opts.pattern.join("base").join(
+            self.patterns
+                .get(&Scope::create_global("base"))
+                .unwrap_or(&"default".into()),
+        );
+
+        base_path.set_extension("meta");
+        base.path = base_path;
 
         let output = base.get_source().map_err(MetaError::from)?;
 
 
 
                 let value = if let Some(val) = self.arrays.get(&name_key) {
                     &val[..]
-                } else if let Some(val) = self.arrays.get(&name_key.to_global()) {
+                } else if let Some(val) = self.arrays.get(&name_key.global()) {
                     &val[..]
                 } else if let Some(val) = self.arrays.get(&class_key) {
                     &val[..]
-                } else if let Some(val) = self.arrays.get(&class_key.to_global()) {
+                } else if let Some(val) = self.arrays.get(&class_key.global()) {
                     &val[..]
-                } else if let Some(val) = self.arrays.get(&Scope::into_global(key)) {
+                } else if let Some(val) = self.arrays.get(&Scope::create_global(key)) {
                     &val[..]
-                } else if let Some(val) = self.arrays.get(&Scope::into_local(key)) {
+                } else if let Some(val) = self.arrays.get(&Scope::create_local(key)) {
                     &val[..]
                 } else if self.opts.undefined {
                     panic!(
 
         log!(self.opts, format!("expanding {key}"), 2);
         // SOURCE is already expanded in the initial construct() call
         if key == "SOURCE" {
-            if let Some(source) = self.patterns.get(&Scope::into_global("SOURCE")) {
+            if let Some(source) = self.patterns.get(&Scope::create_global("SOURCE")) {
                 return Ok(source.to_string());
             } else {
                 return Ok(String::new());
             }
         }
 
-        let mut filename = if let Some(name) = self.patterns.get(&Scope::into_local(key)) {
+        let is_source = key.split('.').next().unwrap_or("") == "SOURCE";
+
+        let mut filename = if let Some(name) = self.patterns.get(&Scope::create_local(key)) {
             Ok(name.to_string())
-        } else if let Some(name) = self.patterns.get(&Scope::into_global(key)) {
+        } else if let Some(name) = self.patterns.get(&Scope::create_global(key)) {
             Ok(name.to_string())
         } else if self
             .opts
             .pattern
             .join(key.replace(".", "/") + ".meta")
             .exists()
+            || is_source
         {
             Ok(String::new())
         } else if self.header.panic_default {
         }
 
         let pattern_path = key.replace('.', "/") + "/" + &filename;
-        let mut path = self.opts.pattern.join(pattern_path);
-        path.set_extension("meta");
 
+        let mut path = if is_source {
+            let pattern_path = pattern_path.replace("SOURCE/", "");
+            self.opts.source.join(pattern_path)
+        } else {
+            self.opts.pattern.join(pattern_path)
+        };
+
+        path.set_extension("meta");
         let mut pattern = MetaFile::build(path, self.opts)?;
 
         // copy over maps for expanding contained variables
         pattern.merge(self);
 
-        pattern.get_source()
+        if pattern.header.pandoc.unwrap_or(false) || is_source {
+            pattern.pandoc()
+        } else {
+            pattern.get_source()
+        }
     }
 }
 
 use super::*;
 
 impl<'a> MetaFile<'a> {
-    pub fn to_html(&self) -> Result<String> {
+    pub fn pandoc(&mut self) -> Result<String> {
         let string = self.get_source()?;
 
-        if self.opts.no_pandoc || !self.header.pandoc || string.is_empty() {
+        if self.opts.no_pandoc || string.is_empty() {
             return Ok(string);
         }
 
             .set_output_format(output, vec![]);
 
         if let pandoc::PandocOutput::ToBuffer(s) = pandoc.execute()? {
+            self.header.pandoc = Some(false);
             Ok(s)
         } else {
             Err(MetaError::Pandoc { file: self.name()? }.into())
 
             2
         );
         let long_key = self.name()? + "." + &key.to_string();
-        if let Some(val) = self.variables.get(&Scope::into_local(&long_key)) {
+        if let Some(val) = self.variables.get(&Scope::create_local(&long_key)) {
             Ok(val.clone())
-        } else if let Some(val) = self.variables.get(&Scope::into_global(&long_key)) {
+        } else if let Some(val) = self.variables.get(&Scope::create_global(&long_key)) {
             Ok(val.clone())
-        } else if let Some(val) = self.variables.get(&Scope::into_local(key)) {
+        } else if let Some(val) = self.variables.get(&Scope::create_local(key)) {
             Ok(val.clone())
-        } else if let Some(val) = self.variables.get(&Scope::into_global(key)) {
+        } else if let Some(val) = self.variables.get(&Scope::create_global(key)) {
             Ok(val.clone())
         } else if self.opts.undefined || self.header.panic_undefined {
             return Err(MetaError::UndefinedExpand {
 
     pub equal_arrays: bool,
     pub filetype: String,
     pub source: String,
-    pub pandoc: bool,
+    pub pandoc: Option<bool>,
     pub ignore: bool,
+    pub copy_only: bool,
+    pub minify: bool,
 }
 
 impl Header {
             equal_arrays: false,
             filetype: String::from("html"),
             source: String::from("markdown"),
-            pandoc: true,
+            pandoc: None,
             ignore: false,
+            copy_only: false,
+            minify: true,
         }
     }
 }
                 "panic_default" => header.panic_default = val == "true",
                 "panic_undefined" => header.panic_undefined = val == "true",
                 "equal_arrays" => header.equal_arrays = val == "true",
-                "pandoc" => header.pandoc = val == "true",
+                "pandoc" => header.pandoc = Some(val == "true"),
                 "filetype" => header.filetype = val.to_string(),
                 "source" => header.source = val.to_string(),
                 "ignore" => header.ignore = val == "true",
+                "copy_only" => header.copy_only = val == "true",
+                "minify" => header.copy_only = val == "true",
                 _ => continue,
             }
         }
 
+use std::fmt::Display;
+
 #[derive(Debug, Clone, Eq, Hash, PartialEq)]
 pub enum Scope {
     Local(String),
 }
 
 impl Scope {
-    pub fn into_local(str: impl ToString) -> Scope {
+    pub fn create_local(str: impl Display) -> Scope {
         Scope::Local(str.to_string())
     }
 
-    pub fn into_global(str: impl ToString) -> Scope {
+    pub fn create_global(str: impl ToString) -> Scope {
         Scope::Global(str.to_string())
     }
 
         }
     }
 
-    pub fn to_local(&self) -> Scope {
+    pub fn local(&self) -> Scope {
         Scope::Local(self.to_string())
     }
 
-    pub fn to_global(&self) -> Scope {
+    pub fn global(&self) -> Scope {
         Scope::Global(self.to_string())
     }
 }
 
-impl ToString for Scope {
-    fn to_string(&self) -> String {
-        match self {
+impl Display for Scope {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let str = match self {
             Scope::Local(x) | Scope::Global(x) => x.to_string(),
-        }
+        };
+
+        write!(f, "{str}")
     }
 }
 
 
 #[derive(Parser, Debug)]
 #[command(author = "huck boles")]
-#[command(version = "0.1.2")]
+#[command(version = "0.1.3")]
 #[command(about = "customizable template driven static site generator")]
 #[command(long_about = None)]
 pub struct Opts {
     /// don't call pandoc on source files
     #[arg(long, default_value_t = false)]
     pub no_pandoc: bool,
+    /// don't minify resulting html
+    #[arg(long, default_value_t = false)]
+    pub no_minify: bool,
 }
 
 #[derive(Debug, Clone, Default)]
     pub clean: bool,
     pub no_pandoc: bool,
     pub new: bool,
+    pub minify: bool,
 }
 
 impl Options {
             clean: false,
             no_pandoc: false,
             new: false,
+            minify: true,
         }
     }
 }
         opts.no_pandoc = value.no_pandoc;
         opts.new = value.new;
         opts.parallel = value.parallel;
+        opts.minify = !value.no_minify;
 
         opts.root = if let Some(root) = value.root.as_deref() {
             PathBuf::from(root).canonicalize()
 
 #[cfg(test)]
 mod tests;
 
-use crate::{log, Header, MetaError, MetaFile, Options};
+use crate::{Header, MetaError, MetaFile, Options};
 use eyre::Result;
 use pest::{
     iterators::{Pair, Pairs},
 pub struct MetaParser;
 
 pub fn parse_string(file: String, opts: &Options) -> Result<MetaFile> {
-    log!(opts, "parsing file", 3);
-
     let pair = MetaParser::parse(Rule::file, &file)?.next().unwrap();
 
     let mut meta_file = MetaFile::new(opts);
 
     }
 
     if global {
-        Ok((Scope::into_global(key), val))
+        Ok((Scope::create_global(key), val))
     } else {
-        Ok((Scope::into_local(key), val))
+        Ok((Scope::create_local(key), val))
     }
 }
 
 
     }
 
     if global {
-        Ok((Scope::into_global(key), val))
+        Ok((Scope::create_global(key), val))
     } else {
-        Ok((Scope::into_local(key), val))
+        Ok((Scope::create_local(key), val))
     }
 }
 
 
 array = _{
     "[" ~ "]"
-  | "[" 
-  ~ WHITESPACE* 
-  ~ string 
-  ~ (WHITESPACE* ~ "," ~ WHITESPACE* ~ string)* 
+  | "["
+  ~ WHITESPACE*
+  ~ string
+  ~ (WHITESPACE* ~ "," ~ WHITESPACE* ~ string)*
   ~ WHITESPACE* ~ ","? ~ WHITESPACE*
   ~ "]"
 }
 var_sub      =  { &("$") ~ substitution }
 arr_sub      =  { &("@") ~ substitution }
 pat_sub      =  { &("&") ~ substitution }
-identifier   = _{ var_sub | pat_sub | arr_sub }
+identifier   = _{ var_sub | pat_sub | arr_sub | COMMENT}
 
-source = { (identifier | char_seq)* }
+source = ${ (identifier | char_seq)* }
 
 file = {
     SOI ~ header? ~ definition* ~ source? ~ EOI
 
-use crate::{MetaFile, Options};
+use crate::{MetaError, MetaFile, Options};
 use eyre::Result;
 use std::{fs, path::PathBuf};
 
             let test_dir = opts.source.join("unit_tests");
             let mut path = test_dir.join($file);
             path.set_extension("meta");
-            let file = MetaFile::build(path, &opts)?;
-            assert_eq!(file.construct()?, $test);
+            let mut file = MetaFile::build(path, &opts)?;
+
+            let str = match file.construct() {
+                Ok(f) => f,
+                Err(e) => match *e {
+                    MetaError::Ignored => return Ok(()),
+                    e => return Err(e.into())
+                }
+
+            };
+
+            assert_eq!(str, $test);
             Ok(())
         }
     };
             let test_dir = opts.source.join("unit_tests");
             let mut path = test_dir.join($file);
             path.set_extension("meta");
-            let file = MetaFile::build(path, &opts).unwrap();
+            let mut file = MetaFile::build(path, &opts).unwrap();
             assert_eq!(file.construct().unwrap(), $test);
         }
     };
 );
 
 unit_test!(blank_pattern, "blank/blank_pattern", "");
-unit_test!(blank_variable, "blank/blank_variable", "<html>\n</html>\n");
-unit_test!(blank_array, "blank/blank_array", "<html>\n</html>\n");
-unit_test!(blank_comment, "blank/comment", "<html>\n</html>\n");
+unit_test!(
+    blank_variable,
+    "blank/blank_variable",
+    "<html>\n\n\n</html>\n"
+);
+unit_test!(blank_array, "blank/blank_array", "<html>\n\n\n</html>\n");
+unit_test!(blank_comment, "blank/comment", "<html>\n\n\n\n</html>\n");
 unit_test!(
     inline_comment,
     "blank/inline_comment",
-    "<html>\n<p>inline comment</p>\n</html>\n"
+    "<html>\n<p>inline comment</p>\n\n\n\n</html>\n"
 );
 unit_test!(
     expand_var_in_src,
     "expand/variable_in_source",
-    "<html>\n<p>GOOD</p>\n</html>\n"
+    "<html>\n<p>GOOD</p>\n\n\n\n</html>\n"
 );
 unit_test!(
     expand_var_in_pat,
     "expand/variable_in_pattern",
-    "<html>\nGOOD</html>\n"
+    "<html>\nGOOD\n\n\n</html>\n"
 );
 unit_test!(
     expand_arr_in_src,
     "expand/array_in_source",
-    "<html>\n<p>12345</p>\n</html>\n"
+    "<html>\n<p>1 2 3 4 5</p>\n\n\n\n</html>\n"
 );
 unit_test!(
     expand_arr_in_pat,
     "expand/array_in_pattern",
-    "<html>\n12345</html>\n"
+    "<html>\n1\n2\n3\n4\n5\n\n\n</html>\n"
 );
 unit_test!(
     expand_pat_in_src,
     "expand/pattern_in_source",
-    "<p>GOOD</p>\n"
+    "<p>GOOD</p>\n\n"
 );
 unit_test!(
     expand_pat_in_pat,
     "expand/pattern_in_pattern",
-    "<html>\nGOOD\nGOOD\n</html>\n"
+    "<html>\nGOOD\nGOOD\n\n\n\n</html>\n"
 );
 unit_test!(
     override_var,
     "override/variable",
-    "<html>\n<p>GOOD</p>\n</html>\n"
+    "<html>\n<p>GOOD</p>\n\n\n\n</html>\n"
 );
 unit_test!(
     override_pat,
     "override/pattern",
-    "<html>\nGOOD\nGOOD\n</html>\n"
+    "<html>\nGOOD\n GOOD\n\n\n\n</html>\n"
 );
 unit_test!(
     header_no_pandoc,
     "header/pandoc",
-    "# This should not become html\n"
+    "# This should not become html\n\n"
 );
 
 unit_test!(header_blank, "header/blank", "");
 unit_test!(
     pat_file,
     "expand/file.meta",
-    "<html>\n<p>GOOD</p>\n</html>\n"
+    "<html>\n<p>GOOD</p>\n\n\n\n</html>\n"
 );
 
 unit_test!(
     direct_call,
     "expand/direct_call",
-    "<html>\n<p>abcd</p>\n</html>\n"
+    "<html>\n<p>a b c d</p>\n\n\n\n</html>\n"
+);
+
+unit_test!(
+    expand_spaces,
+    "expand/spaces",
+    "<html>\n<p>GOOD GOOD</p>\n\n\n\n</html>\n"
+);
+
+unit_test!(
+    copy_header,
+    "header/copy",
+    r#"variable: ${this} should get copied verbatim"#
+);
+
+unit_test!(
+    expandoc,
+    "header/expandoc",
+    "<html>\n<h1 id=\"good\">GOOD</h1>\n\n\n</html>\n"
+);
+
+unit_test!(
+    include_source,
+    "expand/source",
+    "<html>\n<p>GOOD GOOD</p>\n\n\n\n</html>\n"
 );
 
 panic_test!(ignore, "ignore.meta", "");
 
     assert_eq!(
         fs::read_to_string(dir.join("build/unit_tests/global/pattern.html"))?,
-        "<p>GOOD GOOD</p>\n"
+        "<p>GOOD</p><p>GOOD</p>"
     );
 
     assert_eq!(
         fs::read_to_string(dir.join("build/unit_tests/global/variable.html"))?,
-        "<p>GOODGOOD</p>\n"
+        "<p>GOOD GOOD</p>"
     );
 
     Ok(())
 
 #[test]
 #[ignore = "generates README site"]
 fn readme() -> Result<()> {
-    let dir = std::path::PathBuf::from("files/README")
+    let dir = std::path::PathBuf::from("docs")
         .canonicalize()
         .unwrap();