|
|
|
@ -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
|
|
|
|
|
|
|
|
|
@ -27,7 +27,16 @@ lazy_static! {
|
|
|
|
|
\{\{\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* # 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)
|
|
|
|
|
([^}]+) # get all template arguments
|
|
|
|
|
\}\} # link closing parens"
|
|
|
|
@ -73,67 +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)) => {
|
|
|
|
|
let matches = TEMPLATE_ARGS.captures_iter(args.as_str());
|
|
|
|
|
|
|
|
|
|
let split_args = matches
|
|
|
|
|
.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);
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -148,20 +151,16 @@ impl<'a> Link<'a> {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn replace_args<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
|
|
|
|
pub(crate) fn replace_args<P, FR>(&self, base: P, file_reader: &FR) -> Result<String>
|
|
|
|
|
where
|
|
|
|
|
P: AsRef<Path>,
|
|
|
|
|
FR: FileReader,
|
|
|
|
|
{
|
|
|
|
|
match self.link_type {
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -245,11 +244,15 @@ impl<'a> Args<'a> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn from_capture(cap: Captures<'a>) -> Option<Args<'a>> {
|
|
|
|
|
// https://regex101.com/r/lKSOOl/3
|
|
|
|
|
let arg_type = match (cap.get(0), cap.get(1), cap.get(2), cap.get(3)) {
|
|
|
|
|
// 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()))
|
|
|
|
|
}
|
|
|
|
|
// This looks like \{{$any string}}
|
|
|
|
|
(Some(mat), _, _, _) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
|
|
|
|
Some(ArgsType::Escaped)
|
|
|
|
|
}
|
|
|
|
@ -299,22 +302,6 @@ mod link_tests {
|
|
|
|
|
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() {
|
|
|
|
@ -322,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...";
|
|
|
|
@ -342,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![]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -352,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 =
|
|
|
|
@ -361,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")])
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -489,6 +497,26 @@ year=2022
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_extract_template_links_with_newlines_malformed() {
|
|
|
|
|
let s = "{{#template test.rs
|
|
|
|
|
lang=rust
|
|
|
|
|
year=2022}}";
|
|
|
|
|
|
|
|
|
|
let res = extract_template_links(s).collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
res,
|
|
|
|
|
vec![Link {
|
|
|
|
|
start_index: 0,
|
|
|
|
|
end_index: 58,
|
|
|
|
|
link_type: LinkType::Template(PathBuf::from("test.rs")),
|
|
|
|
|
link_text: "{{#template test.rs \n lang=rust\n year=2022}}",
|
|
|
|
|
args: HashMap::from([("lang", "rust"), ("year", "2022")]),
|
|
|
|
|
},]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_extract_zero_args() {
|
|
|
|
|
let s = "This is some text without any template links";
|
|
|
|
|