Merge pull request #4 from sgoudham/TEM-2

pull/6/head v1.0.0
Hamothy 3 years ago committed by GitHub
commit 7263761e32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -8,6 +8,9 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# Added Manually
.idea .idea
examples/readme/book examples/readme/book
examples/default-value/book
target target

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook-template" name = "mdbook-template"
version = "0.1.0" version = "1.0.0"
edition = "2021" edition = "2021"
authors = ["Goudham Suresh <sgoudham@gmail.com>"] authors = ["Goudham Suresh <sgoudham@gmail.com>"]
description = "A mdbook preprocessor that allows the re-usability of template files with dynamic arguments" description = "A mdbook preprocessor that allows the re-usability of template files with dynamic arguments"

@ -9,16 +9,21 @@
## Table of Contents ## Table of Contents
- [mdbook-template](#mdbook-template) * [Author Notes](#author-notes)
* [Author Notes](#author-notes) * [Installation](#installation)
* [Installation](#installation) * [About](#about)
* [About](#about) * [Format](#format)
* [Format](#format) + [Template](#template)
* [Valid Configurations](#valid-configurations) + [Arguments](#arguments)
* [Example](#example) + [Default Values](#default-values)
* [License](#license) * [Valid Configurations](#valid-configurations)
* [Contributing](#contributing) + [Template](#template-config)
* [Acknowledgement](#acknowledgement) + [Arguments](#arguments-config)
* [Example](#example)
* [GitHub Actions](#github-actions)
* [License](#license)
* [Contributing](#contributing)
* [Acknowledgement](#acknowledgement)
## Author Notes ## Author Notes
@ -39,7 +44,8 @@ $ cargo install mdbook-template
[preprocessor.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 ```shell
$ mdbook build $ mdbook build
@ -74,6 +80,8 @@ Please view the given [example](#example) which demonstrates it in action.
## Format ## Format
### Template
The format is as follows The format is as follows
```text ```text
@ -81,60 +89,83 @@ The format is as follows
{{#template <file> <args>}} {{#template <file> <args>}}
``` ```
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 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 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. 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
[[#<name>]]
```
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
[[#<name> <default-value>]]
```
1. The name of the argument
2. The value that this argument should have by default
## Valid Configurations ## Valid Configurations
```markdown ### Template Config
# Valid
```markdown
{{#template file.txt path=../images author=Goudham}} {{#template file.txt path=../images author=Goudham}}
```
# Valid ```markdown
{{#template {{#template
file.txt file.txt
path=../images path=../images
author=Goudham author=Goudham
}} }}
```
# Valid ```markdown
// Not recommended but valid // Not recommended but valid
{{#template file.txt path=../images author=Goudham}} {{#template file.txt path=../images author=Goudham}}
```
# Valid ```markdown
// Not recommended but valid // Not recommended but valid
{{#template {{#template
file.txt file.txt
path=../images path=../images
author=Goudham author=Goudham
}} }}
```
# Invalid ### Arguments Config
// Use {{#include}} for simply including files
{{#template file.txt}}
# Invalid ```markdown
\[[#escaped]]
```
{{#template ```markdown
file.txt [[#width]]
path=../images ```
author=Goudham}}
# Invalid ```markdown
[[#width 200px]]
```
{{#template file.txt ```markdown
path=../images // Not recommended but valid
author=Goudham [[ #width 400px ]]
}}
``` ```
## Example ## Example
@ -161,9 +192,9 @@ and the following content
`templates/footer.md` `templates/footer.md`
```markdown ```markdown
-- Designed By {authors} -- -- Designed By [[#authors]] --
![ferris]({path}/ferris.png) ![ferris]([[#path]]/ferris.png)
![corro]({path}/corro.png) ![corro]([[#path]]/corro.png)
``` ```
`rust.md` `rust.md`
@ -193,11 +224,7 @@ Some Content...
Some Content... Some Content...
{{#template {{#template ../templates/footer.md path=../images authors=Goudham, Hazel }}
../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 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. 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 ## License
[MIT License](LICENSE) [MIT License](LICENSE)

@ -0,0 +1,9 @@
[book]
authors = ["sgoudham"]
language = "en"
multilingual = false
src = "src"
title = "default-value"
# Enables the `mdbook-template` preprocessor
[preprocessor.template]

@ -0,0 +1,6 @@
# Summary
- [Rust](rust.md)
- [Go](go.md)
- [Friends]()
- [Hazel](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
}}

@ -0,0 +1,6 @@
# Go
Some Content...
Width overridden to 200px !
{{#template templates/footer.md path=images authors=Goudham, Hazel width=200px}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@ -0,0 +1,6 @@
# Rust
Some Content...
Both images are defaulted to 400x400 !
{{#template templates/footer.md authors=Goudham, Hazel path=images}}

@ -0,0 +1,4 @@
-- Designed By [[#authors]] --
<img src="[[#path]]/ferris.png" width="[[#width 400px]]" height="[[#height 400px]]">
<img src="[[#path]]/corro.png" width="[[#width 400px]]" height="[[#height 400px]]">

@ -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,182 @@ 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 title=[[#title]]}}"
.to_string();
let image_file_name = PathBuf::from("image.md");
let image_contents = r"<img src='example.png' alt='[[#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);
}
}

@ -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,27 +27,44 @@ 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"
) )
.unwrap(); .unwrap();
// r"(?x)\\\{\{\#.*\}\}|\{\{\s*\#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+)\s*\}\}|\{\{\s*\#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+)\s+([^}]+)\}\}" // r"(?x)\\\[\[.*\]\]|\[\[\s*\#([\S]+)\s*\]\]|\[\[\s*\#([\S]+)\s+([^]]+)\]\]"
static ref ARGS: Regex = Regex::new( static ref ARGS: Regex = Regex::new(
r"(?x) # enable insignificant whitespace mode r"(?x) # enable insignificant whitespace mode
\\\{\{ # escaped link opening parens \\\[\[ # escaped link opening square brackets
\#.* # match any character \#.* # match any character
\}\} # escaped link closing parens \]\] # escaped link closing parens
| # or | # or
\{\{\s* # link opening parens and whitespace(s) \[\[\s* # link opening parens and whitespace(s)
\#([\w'<>.:^\-\(\)\*\+\|\\\/\?]+) # arg name \#([\S]+) # arg name
\s* # optional separating whitespace(s) \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(); .unwrap();
} }
@ -65,55 +82,61 @@ 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, '=');
let key = split_n.next().unwrap().trim();
let value = split_n.next().unwrap();
(key, value)
}) })
.collect::<VecDeque<_>>(); .collect::<Vec<_>>(),
// 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 // This looks like {{#template <file> <args>}}
false => TEMPLATE_ARGS
.captures_iter(args.as_str())
.into_iter() .into_iter()
.map(|arg| { .map(|mat| {
let mut split_n = arg.splitn(2, '='); let mut split_n = mat.unwrap().get(0).unwrap().as_str().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()))) all_args.extend(split_args);
}
(_, _, Some(file), Some(args)) => {
all_args.extend(extract_template_args(args.as_str()).collect::<Vec<_>>());
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,
}; };
@ -128,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))
} }
} }
@ -188,26 +207,6 @@ pub(crate) fn extract_template_links(contents: &str) -> LinkIter<'_> {
LinkIter(TEMPLATE.captures_iter(contents)) 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<Self::Item> {
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)] #[derive(PartialEq, Debug, Clone)]
struct Args<'a> { struct Args<'a> {
start_index: usize, start_index: usize,
@ -231,12 +230,10 @@ impl<'a> Args<'a> {
None => {} None => {}
Some(value) => replaced.push_str(value), Some(value) => replaced.push_str(value),
}, },
ArgsType::Default(argument, default_value) => { ArgsType::Default(argument, default_value) => match all_args.get(argument) {
// [TEM #2] None => replaced.push_str(default_value),
// check if captured_arg exists within hashmap Some(value) => replaced.push_str(value),
// if so, replace arg with corresponding value and push to replaced string },
// if not, replace arg with default value and push to replaced string
}
} }
previous_end_index = captured_arg.end_index; previous_end_index = captured_arg.end_index;
@ -247,18 +244,16 @@ impl<'a> Args<'a> {
} }
fn from_capture(cap: Captures<'a>) -> Option<Args<'a>> { fn from_capture(cap: Captures<'a>) -> Option<Args<'a>> {
let arg_type = match (cap.get(0), cap.get(1), cap.get(2)) { // https://regex101.com/r/lKSOOl/4
(_, Some(argument), None) => { let arg_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) {
println!("Argument -> {:?}", argument); // This looks like [[#path]]
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)) => {
println!("Argument -> {:?}", argument);
println!("Default Value -> {:?}", default_value);
Some(ArgsType::Default(argument.as_str(), default_value.as_str())) Some(ArgsType::Default(argument.as_str(), default_value.as_str()))
} }
(Some(mat), _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => { // This looks like \[[#any string]]
println!("Escaped -> {}", mat.as_str()); (Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => {
Some(ArgsType::Escaped) Some(ArgsType::Escaped)
} }
_ => None, _ => None,
@ -303,27 +298,10 @@ fn extract_args(contents: &str) -> ArgsIter<'_> {
#[cfg(test)] #[cfg(test)]
mod link_tests { mod link_tests {
use std::any::Any;
use std::collections::HashMap; use std::collections::HashMap;
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() {
@ -331,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...";
@ -351,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![]);
} }
@ -361,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 =
@ -370,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")])
},] },
]
); );
} }
@ -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::<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";
@ -506,25 +525,25 @@ year=2022
#[test] #[test]
fn test_extract_args_partial_match() { 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<_>>(), vec![]); assert_eq!(extract_args(s).collect::<Vec<_>>(), 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<_>>(), vec![]); assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]);
let s = "Some random text with {{#width 550..."; let s = "Some random text with [[#width 550...";
assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]); assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]);
let s = "Some random text with \\{{#title..."; let s = "Some random text with \\[[#title...";
assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]); assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]);
} }
#[test] #[test]
fn test_extract_args_empty() { fn test_extract_args_empty() {
let s = "Some random text with {{}} {{#}}..."; let s = "Some random text with [[]] [[#]]...";
assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]); assert_eq!(extract_args(s).collect::<Vec<_>>(), vec![]);
} }
#[test] #[test]
fn test_extract_args_simple() { 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::<Vec<_>>(); let res = extract_args(s).collect::<Vec<_>>();
@ -534,7 +553,7 @@ year=2022
start_index: 30, start_index: 30,
end_index: 39, end_index: 39,
args_type: ArgsType::Plain("path"), args_type: ArgsType::Plain("path"),
args_text: "{{#path}}" args_text: "[[#path]]"
}] }]
); );
} }
@ -543,36 +562,20 @@ year=2022
fn test_extract_args_escaped() { fn test_extract_args_escaped() {
let start = r" let start = r"
Example Text Example Text
\{{#height 200px}} << an escaped argument! \[[#height 200px]] << an escaped argument!
"; ";
let end = r" let end = r"
Example Text Example Text
{{#height 200px}} << an escaped argument! [[#height 200px]] << an escaped argument!
"; ";
assert_eq!(Args::replace(start, &HashMap::<&str, &str>::new()), end); 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] #[test]
fn test_extract_args_with_spaces() { fn test_extract_args_with_spaces() {
let s1 = "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 s2 = "This is some random text with [[#path ]]";
let s3 = "This is some random text with {{ #path}}"; let s3 = "This is some random text with [[ #path]]";
let res1 = extract_args(s1).collect::<Vec<_>>(); let res1 = extract_args(s1).collect::<Vec<_>>();
let res2 = extract_args(s2).collect::<Vec<_>>(); let res2 = extract_args(s2).collect::<Vec<_>>();
@ -584,7 +587,7 @@ year=2022
start_index: 30, start_index: 30,
end_index: 51, end_index: 51,
args_type: ArgsType::Plain("path"), args_type: ArgsType::Plain("path"),
args_text: "{{ #path }}" args_text: "[[ #path ]]"
}] }]
); );
@ -594,7 +597,7 @@ year=2022
start_index: 30, start_index: 30,
end_index: 46, end_index: 46,
args_type: ArgsType::Plain("path"), args_type: ArgsType::Plain("path"),
args_text: "{{#path }}" args_text: "[[#path ]]"
}] }]
); );
@ -604,14 +607,105 @@ year=2022
start_index: 30, start_index: 30,
end_index: 44, end_index: 44,
args_type: ArgsType::Plain("path"), 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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] #[test]
fn test_extract_args_with_default_value() {} 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] #[test]
fn test_extract_args_with_default_value_and_spaces() {} 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
);
}
} }

@ -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<String>;
}
#[derive(PartialEq, Debug, Clone, Default)]
pub(crate) struct SystemFileReader;
#[derive(PartialEq, Debug, Clone, Default)]
pub(crate) struct TestFileReader {
pub(crate) captured_contents: HashMap<PathBuf, String>,
}
impl FileReader for SystemFileReader {
fn read_to_string(&self, file_name: &Path, template_text: &str) -> Result<String> {
fs::read_to_string(file_name).with_context(|| {
format!(
"Could not read template file {} ({})",
template_text,
file_name.display(),
)
})
}
}
impl From<HashMap<PathBuf, String>> for TestFileReader {
fn from(map: HashMap<PathBuf, String>) -> Self {
TestFileReader {
captured_contents: map,
}
}
}
impl FileReader for TestFileReader {
fn read_to_string(&self, file_name: &Path, template_text: &str) -> Result<String> {
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(),
))),
}
}
}
Loading…
Cancel
Save