commit 7a08dc915e546affdd3679d8011253819c8c29ba
parent 9f192ee4eca807893380b5e2a39ab4de60321045
Author: Demonstrandum <samuel@knutsen.co>
Date: Fri, 28 Jun 2024 19:55:02 +0100
bug: html void tags would stop any further generation.
Due to using `return Ok(())` instead of a `continue` after finishing
generation early for html void elements, the html generation would just
stop after the first such tag. This was fixed with a refactor, taking
the individual node generation out into its own function, and converting
all the old `continue`s into `return`s.
Diffstat:
5 files changed, 281 insertions(+), 274 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -162,7 +162,7 @@ dependencies = [
[[package]]
name = "seam"
-version = "0.2.1"
+version = "0.2.2"
dependencies = [
"chrono",
"colored",
diff --git a/Cargo.toml b/Cargo.toml
@@ -4,12 +4,12 @@ description = "Symbolic Expressions As Markup."
keywords = ["markup", "lisp", "macro", "symbolic-expression", "sexp"]
license-file = "LICENSE"
homepage = "https://git.knutsen.co/seam"
-version = "0.2.1"
+version = "0.2.2"
authors = ["Demonstrandum <samuel@knutsen.co>"]
edition = "2021"
[features]
-default = ["debug"]
+# default = ["debug"]
debug = []
[lib]
diff --git a/src/assemble/html.rs b/src/assemble/html.rs
@@ -98,189 +98,193 @@ impl<'a> MarkupDisplay for HTMLFormatter<'a> {
Ok(doc)
}
- fn generate(&self, f : Formatter)
- -> Result<(), GenerationError> {
+ fn generate(&self, f: Formatter) -> Result<(), GenerationError> {
let mut tree_iter = self.tree.iter().peekable();
while let Some(node) = tree_iter.next() {
- match node {
- ParseNode::Symbol(node)
- | ParseNode::Number(node) => {
- write!(f, "{}", node.leading_whitespace)?;
- write!(f, "{}", escape_xml(&node.value))?;
- },
- ParseNode::String(node) => {
- write!(f, "{}", node.leading_whitespace)?;
- write!(f, "{}", escape_xml(&node.value))?;
- },
- ParseNode::List { nodes: list, leading_whitespace, end_token, .. } => {
- write!(f, "{}", leading_whitespace)?;
- let head = list.first();
- let tag: &str; // html <tag> name.
- if let Some(head_node) = head {
- if let ParseNode::Symbol(head_symbol) = head_node {
- tag = &head_symbol.value;
- write!(f, "<{}", tag)?;
- } else {
- // Error, tags can only have symbol values.
- return Err(GenerationError::new("HTML",
- "HTML tags can only be given as symbols.",
- head_node.site()));
- }
- } else {
- // Error, empty tags not supported.
- return Err(GenerationError::new("HTML",
- "Empty lists cannot be converted into a valid HTML tag.",
- node.site()));
- }
- let tag = tag.to_ascii_lowercase();
+ generate_html_node(f, node)?;
+ }
+ Ok(())
+ }
+}
- let mut rest = &list[1..];
+fn generate_html_node<'a>(f: Formatter, node: &'a ParseNode<'a>) -> Result<(), GenerationError<'a>> {
+ match node {
+ ParseNode::Symbol(node)
+ | ParseNode::Number(node) => {
+ write!(f, "{}", node.leading_whitespace)?;
+ write!(f, "{}", escape_xml(&node.value))?;
+ },
+ ParseNode::String(node) => {
+ write!(f, "{}", node.leading_whitespace)?;
+ write!(f, "{}", escape_xml(&node.value))?;
+ },
+ ParseNode::List { nodes: list, leading_whitespace, end_token, .. } => {
+ write!(f, "{}", leading_whitespace)?;
+ let head = list.first();
+ let tag: &str; // html <tag> name.
+ if let Some(head_node) = head {
+ if let ParseNode::Symbol(head_symbol) = head_node {
+ tag = &head_symbol.value;
+ write!(f, "<{}", tag)?;
+ } else {
+ // Error, tags can only have symbol values.
+ return Err(GenerationError::new("HTML",
+ "HTML tags can only be given as symbols.",
+ head_node.site()));
+ }
+ } else {
+ // Error, empty tags not supported.
+ return Err(GenerationError::new("HTML",
+ "Empty lists cannot be converted into a valid HTML tag.",
+ node.site()));
+ }
+ let tag = tag.to_ascii_lowercase();
- // Declarations behave differently.
- if tag.as_bytes()[0] == '!' as u8 {
- while !rest.is_empty() {
- if let Some(node) = rest[0].symbolic() {
- write!(f, " {}", node.value)?;
- } else {
- return Err(GenerationError::new("HTML",
- "Non-symbolic item in declaration",
- &rest[0].site()));
- }
- rest = &rest[1..];
- }
- write!(f, ">")?;
- continue;
- }
+ let mut rest = &list[1..];
- while let Some(ParseNode::Attribute { node, keyword, .. }) = rest.first() {
- if let Some(atom) = (*node).atomic() {
- write!(f, " {}=\"{}\"", keyword, atom.value)?;
- rest = &rest[1..];
- } else {
- // Error! Cannot be non atomic.
- return Err(GenerationError::new("HTML",
- "Attribute cannot contain non-atomic data.",
- &(*node).site()));
- }
+ // Declarations behave differently.
+ if tag.as_bytes()[0] == '!' as u8 {
+ while !rest.is_empty() {
+ if let Some(node) = rest[0].symbolic() {
+ write!(f, " {}", node.value)?;
+ } else {
+ return Err(GenerationError::new("HTML",
+ "Non-symbolic item in declaration",
+ &rest[0].site()));
}
- write!(f, ">")?;
+ rest = &rest[1..];
+ }
+ write!(f, ">")?;
+ return Ok(());
+ }
- // Check early if this tag is a void element.
- if VOID_ELEMENTS.binary_search(&tag.as_str()).is_ok() {
- // Void elements cannot have children.
- if let Some(child_node) = rest.first() {
- return Err(GenerationError::new("HTML",
- &format!("A void element such as `<{}>' cannot have children.", tag),
- child_node.site()));
- }
- // Finished: void elements dont get a closing tag.
- return Ok(());
- }
+ while let Some(ParseNode::Attribute { node, keyword, .. }) = rest.first() {
+ if let Some(atom) = (*node).atomic() {
+ write!(f, " {}=\"{}\"", keyword, atom.value)?;
+ rest = &rest[1..];
+ } else {
+ // Error! Cannot be non atomic.
+ return Err(GenerationError::new("HTML",
+ "Attribute cannot contain non-atomic data.",
+ &(*node).site()));
+ }
+ }
+ write!(f, ">")?;
- // The first node to a tag should have its whitespace supressed!
- // e.g. `(p hello world)` -> `<p>hello world</p>`.
- // But if there's a new line, its likely it should be carreid through.
- // e.g.
- // ```
- // (div
- // hello)
- // ```
- // ->
- // ```
- // <div>
- // hello
- // </div>
- let rest_with_preserved_whitespace = rest;
- let mut rest: Vec<ParseNode<'a>> = rest_with_preserved_whitespace.to_vec();
- let mut is_first_node_on_next_line = false;
- if let Some(first_node) = rest.get_mut(0) {
- is_first_node_on_next_line = first_node.leading_whitespace().contains('\n');
- if !is_first_node_on_next_line {
- first_node.set_leading_whitespace("".to_owned());
- }
- }
+ // Check early if this tag is a void element.
+ if VOID_ELEMENTS.binary_search(&tag.as_str()).is_ok() {
+ // Void elements cannot have children.
+ if let Some(child_node) = rest.first() {
+ return Err(GenerationError::new("HTML",
+ &format!("A void element such as `<{}>' cannot have children.", tag),
+ child_node.site()));
+ }
+ // Finished: void elements don't get a closing tag.
+ return Ok(());
+ }
- // Handle tags which *do not* contain HTML as syntax:
- // <pre>, <style>, <script>, <math>, <svg>, <textarea>, <title>
- // Specifically:
- // - <svg> and <math> contain XML, not HTML;
- // - <pre>, <textarea> and <title> contain raw text, not parsed as HTML;
- // - <pre> will display raw text found in source code;
- // - <textarea> and <title> however, are escapable (evaluete macros);
- // - <script> contains JavaScript, maybe we will parse this in the future!;
- // - <style> contains CSS, which we have our own parser for already.
- match tag.as_str() {
- "pre" => { // <pre> should preserve the raw text in the *source* file.
- // Find beginning and end byte offset of first and last token inside
- // of `(pre ...)` and simply clone the text between those offsets.
- let pre = raw_text(rest_with_preserved_whitespace.first(), end_token);
- write!(f, "{}", pre)?;
- },
- "textarea" | "title" => { // Not eaw source-code, but plain-text.
- // We have to reconsititute what the source-code would look like if all
- // macros were expanded by hand, and read as raw source code.
- let sexp_fmt = SExpFormatter::new(rest.into_boxed_slice());
- let sexp_fmt = Box::leak(Box::new(sexp_fmt)); // TODO: Store.
- sexp_fmt.generate(f)?;
- },
- "style" => { // <style> tag needs to generate CSS.
- // When just a string is passed, don't convert. Assume raw CSS.
- if let Some(ParseNode::String(string_node)) = rest.first() {
- if rest.len() != 1 {
- // FIXME: Leak doesn't really matter, but should really be a better way.
- let second_node = Box::leak(Box::new(rest[1].to_owned()));
- return Err(GenerationError::new("HTML+CSS",
- "A `style' tag can either have S-expression CSS rules, or\
- a single string containing raw CSS be passed in.\n\
- A string was passed in, but excess expressions were passed \
- in after that!",
- second_node.site()));
- }
- // Otherwise, write that raw CSS.
- write!(f, "{}", string_node.value)?;
- } else {
- writeln!(f, "")?;
- let css_fmt = CSSFormatter::new(rest.into_boxed_slice());
- let css_fmt = Box::leak(Box::new(css_fmt)); // FIXME: store formatter.
- css_fmt.generate(f)?;
- }
- },
- "script" => {
- // TODO: Generating JavaScript from S-expressions is not implemented.
- // For now, just treat it as a raw-text tag (a la <pre>).
- let sexp_fmt = SExpFormatter::new(rest.into_boxed_slice());
- let sexp_fmt = Box::leak(Box::new(sexp_fmt)); // TODO: Store.
- sexp_fmt.generate(f)?;
- },
- "math" | "svg" => { // <math> and <svg> are subsets of XML.
- let xml_fmt = XMLFormatter::new(rest.into_boxed_slice());
- let xml_fmt = Box::leak(Box::new(xml_fmt)); // FIXME: store formatter.
- xml_fmt.generate(f)?;
- },
- _ => { // Tag contains regular old HTML.
- let html_fmt = HTMLFormatter::new(rest.into_boxed_slice());
- let html_fmt = Box::leak(Box::new(html_fmt)); // FIXME: store formatter.
- html_fmt.generate(f)?;
- },
- }
- // Closing tag should be equally as spaced as opening tag (?)
- if end_token.leading_whitespace.is_empty() {
- if is_first_node_on_next_line || tag == "style" {
- write!(f, "{}", leading_whitespace)?;
+ // The first node to a tag should have its whitespace supressed!
+ // e.g. `(p hello world)` -> `<p>hello world</p>`.
+ // But if there's a new line, its likely it should be carreid through.
+ // e.g.
+ // ```
+ // (div
+ // hello)
+ // ```
+ // ->
+ // ```
+ // <div>
+ // hello
+ // </div>
+ let rest_with_preserved_whitespace = rest;
+ let mut rest: Vec<ParseNode<'a>> = rest_with_preserved_whitespace.to_vec();
+ let mut is_first_node_on_next_line = false;
+ if let Some(first_node) = rest.get_mut(0) {
+ is_first_node_on_next_line = first_node.leading_whitespace().contains('\n');
+ if !is_first_node_on_next_line {
+ first_node.set_leading_whitespace("".to_owned());
+ }
+ }
+
+ // Handle tags which *do not* contain HTML as syntax:
+ // <pre>, <style>, <script>, <math>, <svg>, <textarea>, <title>
+ // Specifically:
+ // - <svg> and <math> contain XML, not HTML;
+ // - <pre>, <textarea> and <title> contain raw text, not parsed as HTML;
+ // - <pre> will display raw text found in source code;
+ // - <textarea> and <title> however, are escapable (evaluete macros);
+ // - <script> contains JavaScript, maybe we will parse this in the future!;
+ // - <style> contains CSS, which we have our own parser for already.
+ match tag.as_str() {
+ "pre" => { // <pre> should preserve the raw text in the *source* file.
+ // Find beginning and end byte offset of first and last token inside
+ // of `(pre ...)` and simply clone the text between those offsets.
+ let pre = raw_text(rest_with_preserved_whitespace.first(), end_token);
+ write!(f, "{}", pre)?;
+ },
+ "textarea" | "title" => { // Not eaw source-code, but plain-text.
+ // We have to reconsititute what the source-code would look like if all
+ // macros were expanded by hand, and read as raw source code.
+ let sexp_fmt = SExpFormatter::new(rest.into_boxed_slice());
+ let sexp_fmt = Box::leak(Box::new(sexp_fmt)); // TODO: Store.
+ sexp_fmt.generate(f)?;
+ },
+ "style" => { // <style> tag needs to generate CSS.
+ // When just a string is passed, don't convert. Assume raw CSS.
+ if let Some(ParseNode::String(string_node)) = rest.first() {
+ if rest.len() != 1 {
+ // FIXME: Leak doesn't really matter, but should really be a better way.
+ let second_node = Box::leak(Box::new(rest[1].to_owned()));
+ return Err(GenerationError::new("HTML+CSS",
+ "A `style' tag can either have S-expression CSS rules, or\
+ a single string containing raw CSS be passed in.\n\
+ A string was passed in, but excess expressions were passed \
+ in after that!",
+ second_node.site()));
}
+ // Otherwise, write that raw CSS.
+ write!(f, "{}", string_node.value)?;
} else {
- write!(f, "{}", end_token.leading_whitespace)?;
+ writeln!(f, "")?;
+ let css_fmt = CSSFormatter::new(rest.into_boxed_slice());
+ let css_fmt = Box::leak(Box::new(css_fmt)); // FIXME: store formatter.
+ css_fmt.generate(f)?;
}
-
- write!(f, "</{}>", tag)?;
},
- ParseNode::Attribute { ref site, .. } =>
- return Err(GenerationError::new("HTML",
- "Unexpected attribute encountered.", site))
+ "script" => {
+ // TODO: Generating JavaScript from S-expressions is not implemented.
+ // For now, just treat it as a raw-text tag (a la <pre>).
+ let sexp_fmt = SExpFormatter::new(rest.into_boxed_slice());
+ let sexp_fmt = Box::leak(Box::new(sexp_fmt)); // TODO: Store.
+ sexp_fmt.generate(f)?;
+ },
+ "math" | "svg" => { // <math> and <svg> are subsets of XML.
+ let xml_fmt = XMLFormatter::new(rest.into_boxed_slice());
+ let xml_fmt = Box::leak(Box::new(xml_fmt)); // FIXME: store formatter.
+ xml_fmt.generate(f)?;
+ },
+ _ => { // Tag contains regular old HTML.
+ let html_fmt = HTMLFormatter::new(rest.into_boxed_slice());
+ let html_fmt = Box::leak(Box::new(html_fmt)); // FIXME: store formatter.
+ html_fmt.generate(f)?;
+ },
}
- }
- Ok(())
+ // Closing tag should be equally as spaced as opening tag (?)
+ if end_token.leading_whitespace.is_empty() {
+ if is_first_node_on_next_line || tag == "style" {
+ write!(f, "{}", leading_whitespace)?;
+ }
+ } else {
+ write!(f, "{}", end_token.leading_whitespace)?;
+ }
+
+ write!(f, "</{}>", tag)?;
+ },
+ ParseNode::Attribute { ref site, .. } =>
+ return Err(GenerationError::new("HTML",
+ "Unexpected attribute encountered.", site))
}
+ Ok(())
}
/// Get raw text in source-file between a `start_node` and some `end_token`.
diff --git a/src/assemble/xml.rs b/src/assemble/xml.rs
@@ -11,21 +11,6 @@ impl<'a> XMLFormatter<'a> {
pub fn new(tree: ParseTree<'a>) -> Self {
Self { tree }
}
-
- fn display_attribute(&'a self, attr: &'a parser::ParseNode<'a>)
- -> Result<String, GenerationError> {
- let parser::ParseNode::Attribute { keyword, node, .. } = attr else {
- panic!("Passed non-attribute to display_attribute.")
- };
- if let Some(symbol) = (*node).atomic() {
- Ok(format!("{}=\"{}\"", keyword, symbol.value))
- } else {
- Err(GenerationError::new("XML",
- "Attribute can only contain symbols, numbers or strings",
- &(*node).site()))
- }
- }
-
}
pub const DEFAULT : &str =
@@ -61,100 +46,118 @@ impl<'a> MarkupDisplay for XMLFormatter<'a> {
fn generate(&self, f : Formatter) -> Result<(), GenerationError> {
let mut tree_iter = self.tree.iter().peekable();
while let Some(node) = tree_iter.next() {
- match node {
- ParseNode::Symbol(node)
- | ParseNode::Number(node) => {
- write!(f, "{}", node.leading_whitespace)?;
- write!(f, "{}", escape_xml(&node.value))?;
- },
- ParseNode::String(node) => {
- write!(f, "{}", node.leading_whitespace)?;
- write!(f, "{}", escape_xml(&node.value))?
- },
- ParseNode::List { nodes: list, leading_whitespace, end_token, .. } => {
- write!(f, "{}", leading_whitespace)?;
- let head = list.first();
- let tag: &str; // xml <tag> name.
- if let Some(head_node) = head {
- if let ParseNode::Symbol(head_symbol) = head_node {
- tag = &head_symbol.value;
- write!(f, "<{}", tag)?;
- } else {
- // Error, tags can only have symbol values.
- return Err(GenerationError::new("XML",
- "XML tags can only be given as symbols.",
- head_node.site()));
- }
- } else {
- // Error, empty tags not supported.
- return Err(GenerationError::new("XML",
- "Empty lists cannot be converted into a valid XML tag.",
- node.site()));
- }
+ generate_xml_node(f, node)?;
+ }
+ Ok(())
+ }
+}
- let mut rest = &list[1..];
-
- // Declarations behave differently.
- let front = tag.as_bytes()[0] as char;
- if front == '!' || front == '?' {
- while !rest.is_empty() {
- if let Some(node) = rest[0].symbolic() {
- write!(f, "{}", node.value)?;
- } else if let attr@ParseNode::Attribute { .. } = &rest[0] {
- write!(f, " {}", self.display_attribute(attr)?)?;
- } else {
- return Err(GenerationError::new("XML",
- "Only identifiers and attributes are allowed in declarations.",
- &rest[0].site()));
- }
- rest = &rest[1..];
- }
- if front == '?' {
- write!(f, " ?>")?;
- } else {
- write!(f, ">")?;
- }
- continue;
- }
+fn generate_xml_node<'a>(f: Formatter, node: &'a ParseNode<'a>) -> Result<(), GenerationError<'a>> {
+ match node {
+ ParseNode::Symbol(node)
+ | ParseNode::Number(node) => {
+ write!(f, "{}", node.leading_whitespace)?;
+ write!(f, "{}", escape_xml(&node.value))?;
+ },
+ ParseNode::String(node) => {
+ write!(f, "{}", node.leading_whitespace)?;
+ write!(f, "{}", escape_xml(&node.value))?
+ },
+ ParseNode::List { nodes: list, leading_whitespace, end_token, .. } => {
+ write!(f, "{}", leading_whitespace)?;
+ let head = list.first();
+ let tag: &str; // xml <tag> name.
+ if let Some(head_node) = head {
+ if let ParseNode::Symbol(head_symbol) = head_node {
+ tag = &head_symbol.value;
+ write!(f, "<{}", tag)?;
+ } else {
+ // Error, tags can only have symbol values.
+ return Err(GenerationError::new("XML",
+ "XML tags can only be given as symbols.",
+ head_node.site()));
+ }
+ } else {
+ // Error, empty tags not supported.
+ return Err(GenerationError::new("XML",
+ "Empty lists cannot be converted into a valid XML tag.",
+ node.site()));
+ }
- while let Some(attr@ParseNode::Attribute { .. }) = rest.first() {
- write!(f, " {}", self.display_attribute(&attr)?)?;
- rest = &rest[1..];
- }
- write!(f, ">")?;
+ let mut rest = &list[1..];
- // See similar comment for HTML generation:
- // We strip leading whitespace from the first child element in a tag.
- // This is more natural w.r.t. the S-exp syntax.
- let mut rest = rest.to_vec();
- let mut is_first_node_on_next_line = false;
- if let Some(first_node) = rest.get_mut(0) {
- is_first_node_on_next_line = first_node.leading_whitespace().contains('\n');
- if !is_first_node_on_next_line {
- first_node.set_leading_whitespace("".to_owned());
- }
+ // Declarations behave differently.
+ let front = tag.as_bytes()[0] as char;
+ if front == '!' || front == '?' {
+ while !rest.is_empty() {
+ if let Some(node) = rest[0].symbolic() {
+ write!(f, "{}", node.value)?;
+ } else if let attr@ParseNode::Attribute { .. } = &rest[0] {
+ write!(f, " {}", display_attribute(attr)?)?;
+ } else {
+ return Err(GenerationError::new("XML",
+ "Only identifiers and attributes are allowed in declarations.",
+ &rest[0].site()));
}
+ rest = &rest[1..];
+ }
+ if front == '?' {
+ write!(f, " ?>")?;
+ } else {
+ write!(f, ">")?;
+ }
+ return Ok(());
+ }
- let xml_fmt = XMLFormatter::new(rest.to_owned().into_boxed_slice());
- let xml_fmt = Box::leak(Box::new(xml_fmt)); // FIXME: store formatter.
- xml_fmt.generate(f)?;
+ while let Some(attr@ParseNode::Attribute { .. }) = rest.first() {
+ write!(f, " {}", display_attribute(&attr)?)?;
+ rest = &rest[1..];
+ }
+ write!(f, ">")?;
+
+ // See similar comment for HTML generation:
+ // We strip leading whitespace from the first child element in a tag.
+ // This is more natural w.r.t. the S-exp syntax.
+ let mut rest = rest.to_vec();
+ let mut is_first_node_on_next_line = false;
+ if let Some(first_node) = rest.get_mut(0) {
+ is_first_node_on_next_line = first_node.leading_whitespace().contains('\n');
+ if !is_first_node_on_next_line {
+ first_node.set_leading_whitespace("".to_owned());
+ }
+ }
- // Closing tag should be equally as spaced as opening tag (?)
- if end_token.leading_whitespace.is_empty() {
- if is_first_node_on_next_line || tag == "style" {
- write!(f, "{}", leading_whitespace)?;
- }
- } else {
- write!(f, "{}", end_token.leading_whitespace)?;
- }
+ let xml_fmt = XMLFormatter::new(rest.to_owned().into_boxed_slice());
+ let xml_fmt = Box::leak(Box::new(xml_fmt)); // FIXME: store formatter.
+ xml_fmt.generate(f)?;
- write!(f, "</{}>", tag)?;
- },
- _ => return Err(GenerationError::new("XML",
- &format!("Unexpected {} node when generating.", node.node_type()),
- &node.site()))
+ // Closing tag should be equally as spaced as opening tag (?)
+ if end_token.leading_whitespace.is_empty() {
+ if is_first_node_on_next_line || tag == "style" {
+ write!(f, "{}", leading_whitespace)?;
+ }
+ } else {
+ write!(f, "{}", end_token.leading_whitespace)?;
}
- }
- Ok(())
+
+ write!(f, "</{}>", tag)?;
+ },
+ _ => return Err(GenerationError::new("XML",
+ &format!("Unexpected {} node when generating.", node.node_type()),
+ &node.site()))
+ }
+ Ok(())
+}
+
+fn display_attribute<'a>(attr: &'a parser::ParseNode<'a>) -> Result<String, GenerationError> {
+ let parser::ParseNode::Attribute { keyword, node, .. } = attr else {
+ panic!("Passed non-attribute to display_attribute.")
+ };
+ if let Some(symbol) = (*node).atomic() {
+ Ok(format!("{}=\"{}\"", keyword, symbol.value))
+ } else {
+ Err(GenerationError::new("XML",
+ "Attribute can only contain symbols, numbers or strings",
+ &(*node).site()))
}
}
diff --git a/src/lib.rs b/src/lib.rs
@@ -9,7 +9,7 @@ use parse::{expander, parser, lexer};
use std::{fs, io, path::Path};
-pub const VERSION : (u8, u8, u8) = (0, 2, 0);
+pub const VERSION: (u8, u8, u8) = (0, 2, 2);
pub fn tree_builder<'a, P: AsRef<Path>>(source_path: Option<P>, string: String)
-> expander::Expander<'a> {