diff --git a/.gitignore b/.gitignore index 9daf347..6996d41 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +# Added Manually .idea examples/readme/book +examples/default-value/book target \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 48fa6fc..1190cc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook-template" -version = "0.1.0" +version = "1.0.0" edition = "2021" authors = ["Goudham Suresh "] description = "A mdbook preprocessor that allows the re-usability of template files with dynamic arguments" diff --git a/README.md b/README.md index 399360f..a03b3f1 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,21 @@ ## Table of Contents -- [mdbook-template](#mdbook-template) - * [Author Notes](#author-notes) - * [Installation](#installation) - * [About](#about) - * [Format](#format) - * [Valid Configurations](#valid-configurations) - * [Example](#example) - * [License](#license) - * [Contributing](#contributing) - * [Acknowledgement](#acknowledgement) +* [Author Notes](#author-notes) +* [Installation](#installation) +* [About](#about) +* [Format](#format) + + [Template](#template) + + [Arguments](#arguments) + + [Default Values](#default-values) +* [Valid Configurations](#valid-configurations) + + [Template](#template-config) + + [Arguments](#arguments-config) +* [Example](#example) +* [GitHub Actions](#github-actions) +* [License](#license) +* [Contributing](#contributing) +* [Acknowledgement](#acknowledgement) ## Author Notes @@ -39,7 +44,8 @@ $ cargo install mdbook-template [preprocessor.template] ``` -**You're good to go :D Continue building your mdbook normally!** +**You're good to go :D +Continue building your mdbook normally!** ```shell $ mdbook build @@ -74,6 +80,8 @@ Please view the given [example](#example) which demonstrates it in action. ## Format +### Template + The format is as follows ```text @@ -81,60 +89,83 @@ The format is as follows {{#template }} ``` -1. The identifier that this text should be replaced +1. The identifier that tells `mdbook-template` that this text should be replaced by a template 2. The `relative path` to the template file 3. Any arguments that should be substituted within the template file. Arguments should be seperated by whitespace and should be in the `key=value` format. -Arguments to be replaced within the template files should be wrapped in `{}` +### Arguments + +Arguments to be replaced within the template files should be wrapped in `[[# ...]]` +The format is as follows + +```text + 1 +[[#]] +``` + +1. The name of the argument + +### Default Values + +Default values can be set in case some files need dynamic arguments and other don't. +The format is as follows + +```text + 1 2 +[[# ]] +``` + +1. The name of the argument +2. The value that this argument should have by default ## Valid Configurations -```markdown -# Valid +### Template Config +```markdown {{#template file.txt path=../images author=Goudham}} +``` -# Valid - +```markdown {{#template file.txt path=../images author=Goudham }} +``` -# Valid - +```markdown // Not recommended but valid {{#template file.txt path=../images author=Goudham}} +``` -# Valid - +```markdown // Not recommended but valid {{#template file.txt path=../images author=Goudham }} +``` -# Invalid - -// Use {{#include}} for simply including files -{{#template file.txt}} +### Arguments Config -# Invalid +```markdown +\[[#escaped]] +``` -{{#template - file.txt - path=../images - author=Goudham}} +```markdown +[[#width]] +``` -# Invalid +```markdown +[[#width 200px]] +``` -{{#template file.txt - path=../images - author=Goudham -}} +```markdown +// Not recommended but valid +[[ #width 400px ]] ``` ## Example @@ -161,9 +192,9 @@ and the following content `templates/footer.md` ```markdown --- Designed By {authors} -- -![ferris]({path}/ferris.png) -![corro]({path}/corro.png) +-- Designed By [[#authors]] -- +![ferris]([[#path]]/ferris.png) +![corro]([[#path]]/corro.png) ``` `rust.md` @@ -193,11 +224,7 @@ Some Content... Some Content... -{{#template - ../templates/footer.md - path=../images - authors=Goudham, Hazel -}} +{{#template ../templates/footer.md path=../images authors=Goudham, Hazel }} ``` After running `mdbook build` with the mdbook-template preprocessor enabled, the files will have dynamic paths to the @@ -241,6 +268,21 @@ Some Content... Further examples are included within the [examples](/examples) directory which demonstrate a variety of usages. +## GitHub Actions + +Include the following within your `.yml` workflow files if you need `mdbook-template` as an executable to build your +book. + +```yaml +- name: Install mdbook-template + run: | + mkdir mdbook-template + curl -sSL https://github.com/sgoudham/mdbook-template/releases/latest/download/mdbook-template-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook-template + echo `pwd`/mdbook-template >> $GITHUB_PATH +``` + +The above step will ensure the latest version of mdbook-template is retrieved and built. + ## License [MIT License](LICENSE) diff --git a/examples/default-value/book.toml b/examples/default-value/book.toml new file mode 100644 index 0000000..76e9e11 --- /dev/null +++ b/examples/default-value/book.toml @@ -0,0 +1,9 @@ +[book] +authors = ["sgoudham"] +language = "en" +multilingual = false +src = "src" +title = "default-value" + +# Enables the `mdbook-template` preprocessor +[preprocessor.template] \ No newline at end of file diff --git a/examples/default-value/src/SUMMARY.md b/examples/default-value/src/SUMMARY.md new file mode 100644 index 0000000..aab98da --- /dev/null +++ b/examples/default-value/src/SUMMARY.md @@ -0,0 +1,6 @@ +# Summary + +- [Rust](rust.md) +- [Go](go.md) +- [Friends]() + - [Hazel](friends/hazel.md) \ No newline at end of file diff --git a/examples/default-value/src/friends/hazel.md b/examples/default-value/src/friends/hazel.md new file mode 100644 index 0000000..a01fc14 --- /dev/null +++ b/examples/default-value/src/friends/hazel.md @@ -0,0 +1,12 @@ +# Hazel +Some Content... + +Width and Height both overridden to 200x200 ! + +{{#template + ../templates/footer.md + path=../images + authors=Goudham, Hazel + width=200px + height=200px +}} \ No newline at end of file diff --git a/examples/default-value/src/go.md b/examples/default-value/src/go.md new file mode 100644 index 0000000..9215b56 --- /dev/null +++ b/examples/default-value/src/go.md @@ -0,0 +1,6 @@ +# Go +Some Content... + +Width overridden to 200px ! + +{{#template templates/footer.md path=images authors=Goudham, Hazel width=200px}} \ No newline at end of file diff --git a/examples/default-value/src/images/corro.png b/examples/default-value/src/images/corro.png new file mode 100644 index 0000000..a63aa50 Binary files /dev/null and b/examples/default-value/src/images/corro.png differ diff --git a/examples/default-value/src/images/ferris.png b/examples/default-value/src/images/ferris.png new file mode 100644 index 0000000..cbcc88a Binary files /dev/null and b/examples/default-value/src/images/ferris.png differ diff --git a/examples/default-value/src/rust.md b/examples/default-value/src/rust.md new file mode 100644 index 0000000..b3b48ff --- /dev/null +++ b/examples/default-value/src/rust.md @@ -0,0 +1,6 @@ +# Rust +Some Content... + +Both images are defaulted to 400x400 ! + +{{#template templates/footer.md authors=Goudham, Hazel path=images}} \ No newline at end of file diff --git a/examples/default-value/src/templates/footer.md b/examples/default-value/src/templates/footer.md new file mode 100644 index 0000000..3571db8 --- /dev/null +++ b/examples/default-value/src/templates/footer.md @@ -0,0 +1,4 @@ +-- Designed By [[#authors]] -- + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c2b7e2a..62a970e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,10 @@ use mdbook::errors::Result; use mdbook::preprocess::{Preprocessor, PreprocessorContext}; use mdbook::BookItem; +use crate::utils::{FileReader, SystemFileReader}; + mod links; +mod utils; const MAX_LINK_NESTED_DEPTH: usize = 10; @@ -36,7 +39,13 @@ impl Preprocessor for Template { .map(|dir| src_dir.join(dir)) .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; } } @@ -50,10 +59,17 @@ impl Preprocessor for Template { } } -fn replace_template(chapter_content: &str, base: P1, source: P2, depth: usize) -> String +fn replace_template( + chapter_content: &str, + file_reader: &FR, + base: P1, + source: P2, + depth: usize, +) -> String where P1: AsRef, P2: AsRef, + FR: FileReader, { let path = base.as_ref(); let source = source.as_ref(); @@ -64,12 +80,13 @@ where for link in links::extract_template_links(chapter_content) { 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) => { if depth < MAX_LINK_NESTED_DEPTH { if let Some(rel_path) = link.link_type.relative_path(path) { replaced.push_str(&replace_template( &new_content, + file_reader, rel_path, source, depth + 1, @@ -99,4 +116,183 @@ where replaced.push_str(&chapter_content[previous_end_index..]); 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 + Example Title + Some content..."; + let header_file_name = PathBuf::from("header.md"); + let header_contents = r"# [[#title]] + {{#template image.md title=[[#title]]}}" + .to_string(); + let image_file_name = PathBuf::from("image.md"); + let image_contents = r"[[#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); + } + + #[test] + fn test_sad_path_invalid_file() { + let start_chapter_content = "{{#template footer.md}}"; + + let actual_chapter_content = + replace_template(start_chapter_content, &TestFileReader::default(), "", "", 0); + + assert_eq!(actual_chapter_content, start_chapter_content); + } } \ No newline at end of file diff --git a/src/links.rs b/src/links.rs index 632482a..50d3821 100644 --- a/src/links.rs +++ b/src/links.rs @@ -1,12 +1,12 @@ -use std::collections::{HashMap, VecDeque}; -use std::fs; +use std::collections::HashMap; use std::path::{Path, PathBuf}; -use anyhow::Context; use fancy_regex::{CaptureMatches, Captures, Regex}; use lazy_static::lazy_static; use mdbook::errors::Result; +use crate::FileReader; + const ESCAPE_CHAR: char = '\\'; 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 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( r"(?x) # enable insignificant whitespace mode @@ -22,32 +22,49 @@ lazy_static! { \#.* # match any character \}\} # escaped 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* # optional separating whitespaces(s) + \}\} # link closing parens + | # or \{\{\s* # link opening parens and whitespace(s) \#(template) # link type - template \s+ # separating whitespace - ([\w'<>.:^\-\(\)\*\+\|\\\/\?]+) # relative path to template file + ([\S]+) # relative path to template file \s+ # separating whitespace(s) ([^}]+) # get all template arguments \}\} # link closing parens" ) .unwrap(); - // r"(?x)\\\{\{\#.*\}\}|\{\{\s*\#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+)\s*\}\}|\{\{\s*\#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+)\s+([^}]+)\}\}" + // r"(?x)\\\[\[.*\]\]|\[\[\s*\#([\S]+)\s*\]\]|\[\[\s*\#([\S]+)\s+([^]]+)\]\]" static ref ARGS: Regex = Regex::new( r"(?x) # enable insignificant whitespace mode - - \\\{\{ # escaped link opening parens - \#.* # match any character - \}\} # escaped link closing parens - + + \\\[\[ # escaped link opening square brackets + \#.* # match any character + \]\] # escaped link closing parens + | # or - - \{\{\s* # link opening parens and whitespace(s) - \#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+) # arg name + + \[\[\s* # link opening parens and whitespace(s) + \#([\S]+) # arg name \s* # optional separating whitespace(s) - \}\} # link closing parens" + \]\] # link closing parens + + | # or + + \[\[\s* # link opening parens and whitespace(s) + \#([\S]+) # arg name + \s+ # optional separating whitespace(s) + ([^]]+) # match everything after space + \]\] # link closing parens" ) .unwrap(); } @@ -65,55 +82,61 @@ impl<'a> Link<'a> { fn from_capture(cap: Captures<'a>) -> Option> { let mut all_args = HashMap::with_capacity(20); - let link_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) { - (Some(mat), _, _, _) if mat.as_str().contains(LINE_BREAKS) => { - /* - Given a template string that looks like: - {{#template - footer.md - path=../images - author=Hazel - }} - - The resulting args: will look like: - ["{{#template", "footer.md", "path=../images", "author=Hazel", "}}"] - */ - let mut args = mat - .as_str() - .lines() - .map(|line| { - line.trim_end_matches(LINE_BREAKS) - .trim_start_matches(LINE_BREAKS) - }) - .collect::>(); - - // 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 value = split_n.next().unwrap(); - (key, value) - }) - .collect::>(); - all_args.extend(split_args); - - Some(LinkType::Template(PathBuf::from(file.trim()))) - } - (_, _, Some(file), Some(args)) => { - all_args.extend(extract_template_args(args.as_str()).collect::>()); + // https://regex101.com/r/OBywLv/1 + 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 }} + (_, _, Some(file), None, None, None) => { Some(LinkType::Template(PathBuf::from(file.as_str()))) } - (Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => { + // This looks like \{{#}} + (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) { + /* + This looks like + {{#template + + + }} + */ + true => args + .as_str() + .split(LINE_BREAKS) + .map(|str| str.trim()) + .filter(|trimmed| !trimmed.is_empty()) + .map(|mat| { + let mut split_n = mat.splitn(2, '='); + let key = split_n.next().unwrap().trim(); + let value = split_n.next().unwrap(); + (key, value) + }) + .collect::>(), + + // This looks like {{#template }} + false => TEMPLATE_ARGS + .captures_iter(args.as_str()) + .into_iter() + .map(|mat| { + let mut split_n = mat.unwrap().get(0).unwrap().as_str().splitn(2, '='); + let key = split_n.next().unwrap().trim(); + let value = split_n.next().unwrap(); + (key, value) + }) + .collect::>(), + }; + + all_args.extend(split_args); + Some(LinkType::Template(PathBuf::from(file.as_str()))) + } _ => None, }; @@ -128,20 +151,16 @@ impl<'a> Link<'a> { }) } - pub(crate) fn replace_args>(&self, base: P) -> Result { + pub(crate) fn replace_args(&self, base: P, file_reader: &FR) -> Result + where + P: AsRef, + FR: FileReader, + { match self.link_type { LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), LinkType::Template(ref pat) => { let target = base.as_ref().join(pat); - - let contents = fs::read_to_string(&target).with_context(|| { - format!( - "Could not read template file {} ({})", - self.link_text, - target.display(), - ) - })?; - + let contents = file_reader.read_to_string(&target, self.link_text)?; Ok(Args::replace(contents.as_str(), &self.args)) } } @@ -188,26 +207,6 @@ pub(crate) fn extract_template_links(contents: &str) -> LinkIter<'_> { LinkIter(TEMPLATE.captures_iter(contents)) } -struct TemplateArgsIter<'a>(CaptureMatches<'a, 'a>); - -impl<'a> Iterator for TemplateArgsIter<'a> { - type Item = (&'a str, &'a str); - - fn next(&mut self) -> Option { - if let Some(cap) = (&mut self.0).next() { - let mut split_args = cap.unwrap().get(0).unwrap().as_str().splitn(2, '='); - let key = split_args.next().unwrap().trim(); - let value = split_args.next().unwrap(); - return Some((key, value)); - } - None - } -} - -fn extract_template_args(contents: &str) -> TemplateArgsIter<'_> { - TemplateArgsIter(TEMPLATE_ARGS.captures_iter(contents)) -} - #[derive(PartialEq, Debug, Clone)] struct Args<'a> { start_index: usize, @@ -231,12 +230,10 @@ impl<'a> Args<'a> { None => {} Some(value) => replaced.push_str(value), }, - ArgsType::Default(argument, default_value) => { - // [TEM #2] - // check if captured_arg exists within hashmap - // if so, replace arg with corresponding value and push to replaced string - // if not, replace arg with default value and push to replaced string - } + ArgsType::Default(argument, default_value) => match all_args.get(argument) { + None => replaced.push_str(default_value), + Some(value) => replaced.push_str(value), + }, } previous_end_index = captured_arg.end_index; @@ -247,18 +244,16 @@ impl<'a> Args<'a> { } fn from_capture(cap: Captures<'a>) -> Option> { - let arg_type = match (cap.get(0), cap.get(1), cap.get(2)) { - (_, Some(argument), None) => { - println!("Argument -> {:?}", argument); - Some(ArgsType::Plain(argument.as_str())) - } - (_, Some(argument), Some(default_value)) => { - println!("Argument -> {:?}", argument); - println!("Default Value -> {:?}", default_value); + // https://regex101.com/r/lKSOOl/4 + 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())), + // This looks like [[#path ../images]] + (_, _, Some(argument), Some(default_value)) => { Some(ArgsType::Default(argument.as_str(), default_value.as_str())) } - (Some(mat), _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => { - println!("Escaped -> {}", mat.as_str()); + // This looks like \[[#any string]] + (Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => { Some(ArgsType::Escaped) } _ => None, @@ -303,27 +298,10 @@ fn extract_args(contents: &str) -> ArgsIter<'_> { #[cfg(test)] mod link_tests { - use std::any::Any; use std::collections::HashMap; use std::path::PathBuf; 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] fn test_extract_zero_template_links() { @@ -331,12 +309,6 @@ mod link_tests { assert_eq!(extract_template_links(s).collect::>(), vec![]) } - #[test] - fn test_extract_zero_template_links_without_args() { - let s = "{{#template templates/footer.md}}"; - assert_eq!(extract_template_links(s).collect::>(), vec![]) - } - #[test] fn test_extract_template_links_partial_match() { let s = "Some random text with {{#template..."; @@ -351,7 +323,7 @@ mod link_tests { #[test] 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![]); } @@ -361,6 +333,24 @@ mod link_tests { assert!(extract_template_links(s).collect::>() == vec![]); } + #[test] + fn test_extract_zero_template_links_without_args() { + let s = "{{#template templates/footer.md}}"; + + let res = extract_template_links(s).collect::>(); + + 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] fn test_extract_template_links_simple() { let s = @@ -370,13 +360,22 @@ mod link_tests { assert_eq!( res, - vec![Link { - start_index: 48, - end_index: 79, - link_type: LinkType::Template(PathBuf::from("test.rs")), - link_text: "{{#template test.rs lang=rust}}", - args: HashMap::from([("lang", "rust")]) - },] + 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, + end_index: 79, + link_type: LinkType::Template(PathBuf::from("test.rs")), + link_text: "{{#template test.rs lang=rust}}", + args: HashMap::from([("lang", "rust")]) + }, + ] ); } @@ -498,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::>(); + + 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] fn test_extract_zero_args() { let s = "This is some text without any template links"; @@ -506,25 +525,25 @@ year=2022 #[test] fn test_extract_args_partial_match() { - let s = "Some random text with {{#height..."; + let s = "Some random text with [[#height..."; assert_eq!(extract_args(s).collect::>(), vec![]); - let s = "Some random text with {{#image ferris.png..."; + let s = "Some random text with [[#image ferris.png..."; assert_eq!(extract_args(s).collect::>(), vec![]); - let s = "Some random text with {{#width 550..."; + let s = "Some random text with [[#width 550..."; assert_eq!(extract_args(s).collect::>(), vec![]); - let s = "Some random text with \\{{#title..."; + let s = "Some random text with \\[[#title..."; assert_eq!(extract_args(s).collect::>(), vec![]); } #[test] fn test_extract_args_empty() { - let s = "Some random text with {{}} {{#}}..."; + let s = "Some random text with [[]] [[#]]..."; assert_eq!(extract_args(s).collect::>(), vec![]); } #[test] fn test_extract_args_simple() { - let s = "This is some random text with {{#path}} and then some more random text"; + let s = "This is some random text with [[#path]] and then some more random text"; let res = extract_args(s).collect::>(); @@ -534,7 +553,7 @@ year=2022 start_index: 30, end_index: 39, args_type: ArgsType::Plain("path"), - args_text: "{{#path}}" + args_text: "[[#path]]" }] ); } @@ -543,36 +562,20 @@ year=2022 fn test_extract_args_escaped() { let start = r" Example Text - \{{#height 200px}} << an escaped argument! + \[[#height 200px]] << an escaped argument! "; let end = r" Example Text - {{#height 200px}} << an escaped argument! + [[#height 200px]] << an escaped argument! "; assert_eq!(Args::replace(start, &HashMap::<&str, &str>::new()), end); } - #[test] - fn test_replace_args_simple() { - let start = r" - Example Text - {{#height}} << an argument! - "; - let end = r" - Example Text - 200px << an argument! - "; - assert_eq!( - Args::replace(start, &HashMap::from([("height", "200px")])), - end - ); - } - #[test] fn test_extract_args_with_spaces() { - let s1 = "This is some random text with {{ #path }}"; - let s2 = "This is some random text with {{#path }}"; - let s3 = "This is some random text with {{ #path}}"; + let s1 = "This is some random text with [[ #path ]]"; + let s2 = "This is some random text with [[#path ]]"; + let s3 = "This is some random text with [[ #path]]"; let res1 = extract_args(s1).collect::>(); let res2 = extract_args(s2).collect::>(); @@ -584,7 +587,7 @@ year=2022 start_index: 30, end_index: 51, args_type: ArgsType::Plain("path"), - args_text: "{{ #path }}" + args_text: "[[ #path ]]" }] ); @@ -594,7 +597,7 @@ year=2022 start_index: 30, end_index: 46, args_type: ArgsType::Plain("path"), - args_text: "{{#path }}" + args_text: "[[#path ]]" }] ); @@ -604,14 +607,105 @@ year=2022 start_index: 30, end_index: 44, args_type: ArgsType::Plain("path"), - args_text: "{{ #path}}" + args_text: "[[ #path]]" + }] + ); + } + + #[test] + fn test_extract_args_with_default_value() { + let s = "This is some random text with [[#path 200px]] and then some more random text"; + + let res = extract_args(s).collect::>(); + + assert_eq!( + res, + vec![Args { + start_index: 30, + end_index: 45, + args_type: ArgsType::Default("path", "200px"), + args_text: "[[#path 200px]]" + }] + ); + } + + #[test] + fn test_extract_args_with_default_value_and_spaces() { + let s = + "This is some random text with [[ #path 400px ]] and then some more random text"; + + let res = extract_args(s).collect::>(); + + assert_eq!( + res, + vec![Args { + start_index: 30, + end_index: 52, + args_type: ArgsType::Default("path", "400px "), + args_text: "[[ #path 400px ]]" + }] + ); + } + + #[test] + fn test_extract_args_with_multiple_spaced_default_value() { + let s = "[[#title An Amazing Title]]"; + + let res = extract_args(s).collect::>(); + + assert_eq!( + res, + vec![Args { + start_index: 0, + end_index: 27, + args_type: ArgsType::Default("title", "An Amazing Title"), + args_text: "[[#title An Amazing Title]]" }] ); } - // #[test] - fn test_extract_args_with_default_value() {} + #[test] + fn test_replace_args_simple() { + let start = r" + Example Text + [[#height]] << an argument! + "; + let end = r" + Example Text + 200px << an argument! + "; + assert_eq!( + Args::replace(start, &HashMap::from([("height", "200px")])), + end + ); + } + + #[test] + fn test_replace_args_with_default() { + let start = r" + Example Text + [[#height 300px]] << an argument! + "; + let end = r" + Example Text + 300px << an argument! + "; + assert_eq!(Args::replace(start, &HashMap::<&str, &str>::new()), end); + } - // #[test] - fn test_extract_args_with_default_value_and_spaces() {} + #[test] + fn test_replace_args_overriding_default() { + let start = r" + Example Text + [[#height 300px]] << an argument! + "; + let end = r" + Example Text + 200px << an argument! + "; + assert_eq!( + Args::replace(start, &HashMap::from([("height", "200px")])), + end + ); + } } \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5b32eee --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Error, Result}; + +pub(crate) trait FileReader { + fn read_to_string(&self, file_name: &Path, template_text: &str) -> Result; +} + +#[derive(PartialEq, Debug, Clone, Default)] +pub(crate) struct SystemFileReader; + +#[derive(PartialEq, Debug, Clone, Default)] +pub(crate) struct TestFileReader { + pub(crate) captured_contents: HashMap, +} + +impl FileReader for SystemFileReader { + fn read_to_string(&self, file_name: &Path, template_text: &str) -> Result { + fs::read_to_string(file_name).with_context(|| { + format!( + "Could not read template file {} ({})", + template_text, + file_name.display(), + ) + }) + } +} + +impl From> for TestFileReader { + fn from(map: HashMap) -> Self { + TestFileReader { + captured_contents: map, + } + } +} + +impl FileReader for TestFileReader { + fn read_to_string(&self, file_name: &Path, template_text: &str) -> Result { + match self.captured_contents.get(file_name) { + Some(file_contents) => Ok(file_contents.to_string()), + None => Err(Error::msg(format!( + "Could not read template file {} ({})", + template_text, + file_name.display(), + ))), + } + } +} \ No newline at end of file