[TEM #2] - Add happy path component tests, Improve regex, Add comments,

pull/4/head
sgoudham 2 years ago
parent a6fc4c216b
commit 0839dceac1
Signed by: hammy
GPG Key ID: 44E818FD5457EEA4

@ -6,7 +6,10 @@ use mdbook::errors::Result;
use mdbook::preprocess::{Preprocessor, PreprocessorContext}; use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::BookItem; use mdbook::BookItem;
use crate::utils::{FileReader, SystemFileReader};
mod links; mod links;
mod utils;
const MAX_LINK_NESTED_DEPTH: usize = 10; const MAX_LINK_NESTED_DEPTH: usize = 10;
@ -36,7 +39,13 @@ impl Preprocessor for Template {
.map(|dir| src_dir.join(dir)) .map(|dir| src_dir.join(dir))
.expect("All book items have a parent"); .expect("All book items have a parent");
let content = replace_template(&chapter.content, base, source, 0); let content = replace_template(
&chapter.content,
&SystemFileReader::default(),
base,
source,
0,
);
chapter.content = content; chapter.content = content;
} }
} }
@ -50,10 +59,17 @@ impl Preprocessor for Template {
} }
} }
fn replace_template<P1, P2>(chapter_content: &str, base: P1, source: P2, depth: usize) -> String fn replace_template<P1, P2, FR>(
chapter_content: &str,
file_reader: &FR,
base: P1,
source: P2,
depth: usize,
) -> String
where where
P1: AsRef<Path>, P1: AsRef<Path>,
P2: AsRef<Path>, P2: AsRef<Path>,
FR: FileReader,
{ {
let path = base.as_ref(); let path = base.as_ref();
let source = source.as_ref(); let source = source.as_ref();
@ -64,12 +80,13 @@ where
for link in links::extract_template_links(chapter_content) { for link in links::extract_template_links(chapter_content) {
replaced.push_str(&chapter_content[previous_end_index..link.start_index]); replaced.push_str(&chapter_content[previous_end_index..link.start_index]);
match link.replace_args(&path) { match link.replace_args(&path, file_reader) {
Ok(new_content) => { Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH { if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = link.link_type.relative_path(path) { if let Some(rel_path) = link.link_type.relative_path(path) {
replaced.push_str(&replace_template( replaced.push_str(&replace_template(
&new_content, &new_content,
file_reader,
rel_path, rel_path,
source, source,
depth + 1, depth + 1,
@ -100,3 +117,172 @@ where
replaced.push_str(&chapter_content[previous_end_index..]); replaced.push_str(&chapter_content[previous_end_index..]);
replaced replaced
} }
#[cfg(test)]
mod lib_tests {
use std::collections::HashMap;
use std::path::PathBuf;
use crate::replace_template;
use crate::utils::TestFileReader;
#[test]
fn test_happy_path_escaped() {
let start = r"
Example Text
```hbs
\{{#template template.md}} << an escaped link!
```";
let end = r"
Example Text
```hbs
{{#template template.md}} << an escaped link!
```";
assert_eq!(
replace_template(start, &TestFileReader::default(), "", "", 0),
end
);
}
#[test]
fn test_happy_path_simple() {
let start_chapter_content = "{{#template footer.md}}";
let end_chapter_content = "Designed & Created With Love From - Goudham & Hazel";
let file_name = PathBuf::from("footer.md");
let template_file_contents =
"Designed & Created With Love From - Goudham & Hazel".to_string();
let map = HashMap::from([(file_name, template_file_contents)]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_with_args() {
let start_chapter_content = "{{#template footer.md authors=Goudham & Hazel}}";
let end_chapter_content = "Designed & Created With Love From - Goudham & Hazel";
let file_name = PathBuf::from("footer.md");
let template_file_contents = "Designed & Created With Love From - {{$authors}}".to_string();
let map = HashMap::from([(file_name, template_file_contents)]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_new_lines() {
let start_chapter_content = r"
Some content...
{{#template footer.md authors=Goudham & Hazel}}";
let end_chapter_content = r"
Some content...
- - - -
Designed & Created With Love From Goudham & Hazel";
let file_name = PathBuf::from("footer.md");
let template_file_contents = r"- - - -
Designed & Created With Love From {{$authors}}"
.to_string();
let map = HashMap::from([(file_name, template_file_contents)]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_multiple() {
let start_chapter_content = r"
{{#template header.md title=Example Title}}
Some content...
{{#template
footer.md
authors=Goudham & Hazel}}";
let end_chapter_content = r"
# Example Title
Some content...
- - - -
Designed & Created With Love From Goudham & Hazel";
let header_file_name = PathBuf::from("header.md");
let header_contents = r"# {{$title}}".to_string();
let footer_file_name = PathBuf::from("footer.md");
let footer_contents = r"- - - -
Designed & Created With Love From {{$authors}}"
.to_string();
let map = HashMap::from([
(footer_file_name, footer_contents),
(header_file_name, header_contents),
]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_with_default_values() {
let start_chapter_content = "{{#template footer.md}}";
let end_chapter_content = "Designed By - Goudham";
let file_name = PathBuf::from("footer.md");
let template_file_contents = "Designed By - {{$authors Goudham}}".to_string();
let map = HashMap::from([(file_name, template_file_contents)]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_with_overridden_default_values() {
let start_chapter_content = "{{#template footer.md authors=Hazel}}";
let end_chapter_content = "Designed By - Hazel";
let file_name = PathBuf::from("footer.md");
let template_file_contents = "Designed By - {{$authors Goudham}}".to_string();
let map = HashMap::from([(file_name, template_file_contents)]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
#[test]
fn test_happy_path_nested() {
let start_chapter_content = r"
{{#template header.md title=Example Title}}
Some content...";
let end_chapter_content = r"
# Example Title
<img src='example.png' alt='Example Title'>
Some content...";
let header_file_name = PathBuf::from("header.md");
let header_contents = r"# {{$title}}
{{#template image.md}}"
.to_string();
let image_file_name = PathBuf::from("image.md");
let image_contents = r"<img src='example.png' alt='Example Title'>".to_string();
let map = HashMap::from([
(image_file_name, image_contents),
(header_file_name, header_contents),
]);
let file_reader = &TestFileReader::from(map);
let actual_chapter_content =
replace_template(start_chapter_content, file_reader, "", "", 0);
assert_eq!(actual_chapter_content, end_chapter_content);
}
}

@ -1,12 +1,12 @@
use std::collections::{HashMap, VecDeque}; use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Context;
use fancy_regex::{CaptureMatches, Captures, Regex}; use fancy_regex::{CaptureMatches, Captures, Regex};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mdbook::errors::Result; use mdbook::errors::Result;
use crate::FileReader;
const ESCAPE_CHAR: char = '\\'; const ESCAPE_CHAR: char = '\\';
const LINE_BREAKS: &[char] = &['\n', '\r']; const LINE_BREAKS: &[char] = &['\n', '\r'];
@ -14,7 +14,7 @@ lazy_static! {
// https://stackoverflow.com/questions/22871602/optimizing-regex-to-fine-key-value-pairs-space-delimited // https://stackoverflow.com/questions/22871602/optimizing-regex-to-fine-key-value-pairs-space-delimited
static ref TEMPLATE_ARGS: Regex = Regex::new(r"(?<=\s|\A)([^\s=]+)=(.*?)(?=(?:\s[^\s=]+=|$))").unwrap(); static ref TEMPLATE_ARGS: Regex = Regex::new(r"(?<=\s|\A)([^\s=]+)=(.*?)(?=(?:\s[^\s=]+=|$))").unwrap();
// r"(?x)\\\{\{\#.*\}\}|\{\{\s*\#(template)\s+([a-zA-Z0-9_^'<>().:*+|\\\/?-]+)\s+([^}]+)\}\}" // r"(?x)\\\{\{\#.*\}\}|\{\{\s*\#(template)\s+([\S]+)\s*\}\}|\{\{\s*\#(template)\s+([\S]+)\s+([^}]+)\}\}"
static ref TEMPLATE: Regex = Regex::new( static ref TEMPLATE: Regex = Regex::new(
r"(?x) # enable insignificant whitespace mode r"(?x) # enable insignificant whitespace mode
@ -27,7 +27,16 @@ lazy_static! {
\{\{\s* # link opening parens and whitespace(s) \{\{\s* # link opening parens and whitespace(s)
\#(template) # link type - template \#(template) # link type - template
\s+ # separating whitespace \s+ # separating whitespace
([\w'<>.:^\-\(\)\*\+\|\\\/\?]+) # relative path to template file ([\S]+) # relative path to template file
\s* # optional separating whitespaces(s)
\}\} # link closing parens
| # or
\{\{\s* # link opening parens and whitespace(s)
\#(template) # link type - template
\s+ # separating whitespace
([\S]+) # relative path to template file
\s+ # separating whitespace(s) \s+ # separating whitespace(s)
([^}]+) # get all template arguments ([^}]+) # get all template arguments
\}\} # link closing parens" \}\} # link closing parens"
@ -73,52 +82,48 @@ impl<'a> Link<'a> {
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> { fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
let mut all_args = HashMap::with_capacity(20); let mut all_args = HashMap::with_capacity(20);
let link_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) { // https://regex101.com/r/OBywLv/1
(Some(mat), _, _, _) if mat.as_str().contains(LINE_BREAKS) => { let link_type = match (
cap.get(0),
cap.get(1),
cap.get(2),
cap.get(3),
cap.get(4),
cap.get(5),
) {
// This looks like {{#template <file>}}
(_, _, Some(file), None, None, None) => {
Some(LinkType::Template(PathBuf::from(file.as_str())))
}
// This looks like \{{#<whatever string>}}
(Some(mat), _, _, _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => {
Some(LinkType::Escaped)
}
(_, None, None, _, Some(file), Some(args)) => {
let split_args = match args.as_str().contains(LINE_BREAKS) {
/* /*
Given a template string that looks like: This looks like
{{#template {{#template
footer.md <file>
path=../images <args>
author=Hazel
}} }}
The resulting args: <VecDeque<&str> will look like:
["{{#template", "footer.md", "path=../images", "author=Hazel", "}}"]
*/ */
let mut args = mat true => args
.as_str() .as_str()
.lines() .split(LINE_BREAKS)
.map(|line| { .map(|str| str.trim())
line.trim_end_matches(LINE_BREAKS) .filter(|trimmed| !trimmed.is_empty())
.trim_start_matches(LINE_BREAKS) .map(|mat| {
}) let mut split_n = mat.splitn(2, '=');
.collect::<VecDeque<_>>();
// Remove {{#template
args.pop_front();
// Remove ending }}
args.pop_back();
// Store relative path of template file
let file = args.pop_front().unwrap();
let split_args = args
.into_iter()
.map(|arg| {
let mut split_n = arg.splitn(2, '=');
let key = split_n.next().unwrap().trim(); let key = split_n.next().unwrap().trim();
let value = split_n.next().unwrap(); let value = split_n.next().unwrap();
(key, value) (key, value)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>(),
all_args.extend(split_args);
Some(LinkType::Template(PathBuf::from(file.trim()))) // This looks like {{#template <file> <args>}}
} false => TEMPLATE_ARGS
(_, _, Some(file), Some(args)) => { .captures_iter(args.as_str())
let matches = TEMPLATE_ARGS.captures_iter(args.as_str());
let split_args = matches
.into_iter() .into_iter()
.map(|mat| { .map(|mat| {
let mut split_n = mat.unwrap().get(0).unwrap().as_str().splitn(2, '='); let mut split_n = mat.unwrap().get(0).unwrap().as_str().splitn(2, '=');
@ -126,14 +131,12 @@ impl<'a> Link<'a> {
let value = split_n.next().unwrap(); let value = split_n.next().unwrap();
(key, value) (key, value)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>(),
all_args.extend(split_args); };
all_args.extend(split_args);
Some(LinkType::Template(PathBuf::from(file.as_str()))) Some(LinkType::Template(PathBuf::from(file.as_str())))
} }
(Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => {
Some(LinkType::Escaped)
}
_ => None, _ => None,
}; };
@ -148,20 +151,16 @@ impl<'a> Link<'a> {
}) })
} }
pub(crate) fn replace_args<P: AsRef<Path>>(&self, base: P) -> Result<String> { pub(crate) fn replace_args<P, FR>(&self, base: P, file_reader: &FR) -> Result<String>
where
P: AsRef<Path>,
FR: FileReader,
{
match self.link_type { match self.link_type {
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
LinkType::Template(ref pat) => { LinkType::Template(ref pat) => {
let target = base.as_ref().join(pat); let target = base.as_ref().join(pat);
let contents = file_reader.read_to_string(&target, self.link_text)?;
let contents = fs::read_to_string(&target).with_context(|| {
format!(
"Could not read template file {} ({})",
self.link_text,
target.display(),
)
})?;
Ok(Args::replace(contents.as_str(), &self.args)) Ok(Args::replace(contents.as_str(), &self.args))
} }
} }
@ -245,11 +244,15 @@ impl<'a> Args<'a> {
} }
fn from_capture(cap: Captures<'a>) -> Option<Args<'a>> { fn from_capture(cap: Captures<'a>) -> Option<Args<'a>> {
// https://regex101.com/r/lKSOOl/3
let arg_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) { let arg_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) {
// This looks like {{$path}}
(_, Some(argument), None, None) => Some(ArgsType::Plain(argument.as_str())), (_, Some(argument), None, None) => Some(ArgsType::Plain(argument.as_str())),
// This looks like {{$path ../images}}
(_, _, Some(argument), Some(default_value)) => { (_, _, Some(argument), Some(default_value)) => {
Some(ArgsType::Default(argument.as_str(), default_value.as_str())) Some(ArgsType::Default(argument.as_str(), default_value.as_str()))
} }
// This looks like \{{$any string}}
(Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => { (Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => {
Some(ArgsType::Escaped) Some(ArgsType::Escaped)
} }
@ -299,22 +302,6 @@ mod link_tests {
use std::path::PathBuf; use std::path::PathBuf;
use crate::links::{extract_args, extract_template_links, Args, ArgsType, Link, LinkType}; use crate::links::{extract_args, extract_template_links, Args, ArgsType, Link, LinkType};
use crate::replace_template;
#[test]
fn test_escaped_template_link() {
let start = r"
Example Text
```hbs
\{{#template template.md}} << an escaped link!
```";
let end = r"
Example Text
```hbs
{{#template template.md}} << an escaped link!
```";
assert_eq!(replace_template(start, "", "", 0), end);
}
#[test] #[test]
fn test_extract_zero_template_links() { fn test_extract_zero_template_links() {
@ -322,12 +309,6 @@ mod link_tests {
assert_eq!(extract_template_links(s).collect::<Vec<_>>(), vec![]) assert_eq!(extract_template_links(s).collect::<Vec<_>>(), vec![])
} }
#[test]
fn test_extract_zero_template_links_without_args() {
let s = "{{#template templates/footer.md}}";
assert_eq!(extract_template_links(s).collect::<Vec<_>>(), vec![])
}
#[test] #[test]
fn test_extract_template_links_partial_match() { fn test_extract_template_links_partial_match() {
let s = "Some random text with {{#template..."; let s = "Some random text with {{#template...";
@ -342,7 +323,7 @@ mod link_tests {
#[test] #[test]
fn test_extract_template_links_empty() { fn test_extract_template_links_empty() {
let s = "Some random text with {{#template}} and {{#template }} {{}} {{#}}..."; let s = "Some random text with {{}} {{#}}...";
assert_eq!(extract_template_links(s).collect::<Vec<_>>(), vec![]); assert_eq!(extract_template_links(s).collect::<Vec<_>>(), vec![]);
} }
@ -352,6 +333,24 @@ mod link_tests {
assert!(extract_template_links(s).collect::<Vec<_>>() == vec![]); assert!(extract_template_links(s).collect::<Vec<_>>() == vec![]);
} }
#[test]
fn test_extract_zero_template_links_without_args() {
let s = "{{#template templates/footer.md}}";
let res = extract_template_links(s).collect::<Vec<_>>();
assert_eq!(
res,
vec![Link {
start_index: 0,
end_index: 33,
link_type: LinkType::Template(PathBuf::from("templates/footer.md")),
link_text: "{{#template templates/footer.md}}",
args: HashMap::new()
},]
);
}
#[test] #[test]
fn test_extract_template_links_simple() { fn test_extract_template_links_simple() {
let s = let s =
@ -361,13 +360,22 @@ mod link_tests {
assert_eq!( assert_eq!(
res, res,
vec![Link { vec![
Link {
start_index: 22,
end_index: 43,
link_type: LinkType::Template(PathBuf::from("file.rs")),
link_text: "{{#template file.rs}}",
args: HashMap::new()
},
Link {
start_index: 48, start_index: 48,
end_index: 79, end_index: 79,
link_type: LinkType::Template(PathBuf::from("test.rs")), link_type: LinkType::Template(PathBuf::from("test.rs")),
link_text: "{{#template test.rs lang=rust}}", link_text: "{{#template test.rs lang=rust}}",
args: HashMap::from([("lang", "rust")]) args: HashMap::from([("lang", "rust")])
},] },
]
); );
} }
@ -489,6 +497,26 @@ year=2022
); );
} }
#[test]
fn test_extract_template_links_with_newlines_malformed() {
let s = "{{#template test.rs
lang=rust
year=2022}}";
let res = extract_template_links(s).collect::<Vec<_>>();
assert_eq!(
res,
vec![Link {
start_index: 0,
end_index: 58,
link_type: LinkType::Template(PathBuf::from("test.rs")),
link_text: "{{#template test.rs \n lang=rust\n year=2022}}",
args: HashMap::from([("lang", "rust"), ("year", "2022")]),
},]
);
}
#[test] #[test]
fn test_extract_zero_args() { fn test_extract_zero_args() {
let s = "This is some text without any template links"; let s = "This is some text without any template links";

Loading…
Cancel
Save