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
**/*.rs.bk
# Added Manually
.idea
examples/readme/book
examples/default-value/book
target

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

@ -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 <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
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
[[#<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
```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)

@ -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::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<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
P1: AsRef<Path>,
P2: AsRef<Path>,
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
<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::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<Link<'a>> {
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: <VecDeque<&str> 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::<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 value = split_n.next().unwrap();
(key, value)
})
.collect::<Vec<_>>();
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::<Vec<_>>());
// 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 <file>}}
(_, _, 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 \{{#<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) {
/*
This looks like
{{#template
<file>
<args>
}}
*/
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::<Vec<_>>(),
// This looks like {{#template <file> <args>}}
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::<Vec<_>>(),
};
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<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 {
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<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)]
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<Args<'a>> {
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<_>>(), 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]
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<_>>(), vec![]);
}
@ -361,6 +333,24 @@ mod link_tests {
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]
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::<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]
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<_>>(), 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![]);
let s = "Some random text with {{#width 550...";
let s = "Some random text with [[#width 550...";
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![]);
}
#[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<_>>(), 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::<Vec<_>>();
@ -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::<Vec<_>>();
let res2 = extract_args(s2).collect::<Vec<_>>();
@ -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::<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]
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
);
}
}

@ -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