2018-01-06 05:03:30 +08:00
|
|
|
use std::ops::{Range, RangeFrom, RangeTo, RangeFull};
|
2015-12-31 05:40:23 +08:00
|
|
|
use std::path::{Path, PathBuf};
|
2017-07-04 07:04:18 +08:00
|
|
|
use regex::{CaptureMatches, Captures, Regex};
|
|
|
|
use utils::fs::file_to_string;
|
2018-01-06 05:03:30 +08:00
|
|
|
use utils::take_lines;
|
2017-07-04 07:04:18 +08:00
|
|
|
use errors::*;
|
2015-12-31 05:40:23 +08:00
|
|
|
|
2018-01-08 00:21:46 +08:00
|
|
|
use super::Preprocessor;
|
|
|
|
use book::{Book, BookItem};
|
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
const ESCAPE_CHAR: char = '\\';
|
2016-01-01 02:25:02 +08:00
|
|
|
|
2018-01-08 00:21:46 +08:00
|
|
|
pub struct ReplaceAllPreprocessor {
|
|
|
|
pub src_dir: PathBuf
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Preprocessor for ReplaceAllPreprocessor {
|
|
|
|
fn run(&self, book: &mut Book) -> Result<()> {
|
|
|
|
for section in &mut book.sections {
|
|
|
|
match *section {
|
|
|
|
BookItem::Chapter(ref mut ch) => {
|
|
|
|
let base = ch.path.parent()
|
|
|
|
.map(|dir| self.src_dir.join(dir))
|
|
|
|
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
|
2018-01-08 00:43:34 +08:00
|
|
|
let content = replace_all(&ch.content, base)?;
|
|
|
|
ch.content = content
|
2018-01-08 00:21:46 +08:00
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
|
2017-05-19 19:04:37 +08:00
|
|
|
// When replacing one thing in a string by something with a different length,
|
|
|
|
// the indices after that will not correspond,
|
|
|
|
// we therefore have to store the difference to correct this
|
2016-01-01 02:25:02 +08:00
|
|
|
let mut previous_end_index = 0;
|
|
|
|
let mut replaced = String::new();
|
2015-12-31 05:40:23 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
for playpen in find_links(s) {
|
2016-01-01 02:25:02 +08:00
|
|
|
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
2017-07-04 07:04:18 +08:00
|
|
|
replaced.push_str(&playpen.render_with_path(&path)?);
|
2016-01-01 02:25:02 +08:00
|
|
|
previous_end_index = playpen.end_index;
|
2015-12-31 05:40:23 +08:00
|
|
|
}
|
|
|
|
|
2016-01-01 02:25:02 +08:00
|
|
|
replaced.push_str(&s[previous_end_index..]);
|
2017-07-04 07:04:18 +08:00
|
|
|
Ok(replaced)
|
|
|
|
}
|
2016-01-01 02:25:02 +08:00
|
|
|
|
2018-01-06 05:03:30 +08:00
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
2017-07-04 07:04:18 +08:00
|
|
|
enum LinkType<'a> {
|
|
|
|
Escaped,
|
2018-01-06 05:03:30 +08:00
|
|
|
IncludeRange(PathBuf, Range<usize>),
|
|
|
|
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
|
|
|
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
|
|
|
IncludeRangeFull(PathBuf, RangeFull),
|
2017-07-04 07:04:18 +08:00
|
|
|
Playpen(PathBuf, Vec<&'a str>),
|
2015-12-31 05:40:23 +08:00
|
|
|
}
|
|
|
|
|
2018-01-06 05:03:30 +08:00
|
|
|
fn parse_include_path(path: &str) -> LinkType<'static> {
|
|
|
|
let mut parts = path.split(':');
|
|
|
|
let path = parts.next().unwrap().into();
|
|
|
|
let start = parts.next().and_then(|s| s.parse::<usize>().ok());
|
|
|
|
let end = parts.next().and_then(|s| s.parse::<usize>().ok());
|
|
|
|
match start {
|
|
|
|
Some(start) => {
|
|
|
|
match end {
|
|
|
|
Some(end) => LinkType::IncludeRange(path, Range{ start: start, end: end}),
|
|
|
|
None => LinkType::IncludeRangeFrom(path, RangeFrom{ start: start }),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
match end {
|
|
|
|
Some(end) => LinkType::IncludeRangeTo(path, RangeTo{ end: end }),
|
|
|
|
None => LinkType::IncludeRangeFull(path, RangeFull),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
2017-07-04 07:04:18 +08:00
|
|
|
struct Link<'a> {
|
2016-01-01 02:25:02 +08:00
|
|
|
start_index: usize,
|
|
|
|
end_index: usize,
|
2017-07-04 07:04:18 +08:00
|
|
|
link: LinkType<'a>,
|
|
|
|
link_text: &'a str,
|
2015-12-31 05:40:23 +08:00
|
|
|
}
|
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
impl<'a> Link<'a> {
|
|
|
|
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
|
|
|
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
|
|
|
(_, Some(typ), Some(rest)) => {
|
|
|
|
let mut path_props = rest.as_str().split_whitespace();
|
2018-01-06 05:03:30 +08:00
|
|
|
let file_arg = path_props.next();
|
2017-07-04 07:04:18 +08:00
|
|
|
let props: Vec<&str> = path_props.collect();
|
2016-01-01 08:40:37 +08:00
|
|
|
|
2018-01-06 05:03:30 +08:00
|
|
|
match (typ.as_str(), file_arg) {
|
|
|
|
("include", Some(pth)) => Some(parse_include_path(pth)),
|
|
|
|
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
2017-07-04 07:04:18 +08:00
|
|
|
_ => None,
|
2016-03-18 05:31:28 +08:00
|
|
|
}
|
2017-10-03 19:40:23 +08:00
|
|
|
}
|
2018-01-06 05:03:30 +08:00
|
|
|
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(
|
|
|
|
LinkType::Escaped,
|
|
|
|
),
|
2017-07-04 07:04:18 +08:00
|
|
|
_ => None,
|
|
|
|
};
|
2015-12-31 05:40:23 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
link_type.and_then(|lnk| {
|
2017-10-03 19:40:23 +08:00
|
|
|
cap.get(0).map(|mat| {
|
2018-01-06 05:03:30 +08:00
|
|
|
Link {
|
|
|
|
start_index: mat.start(),
|
|
|
|
end_index: mat.end(),
|
|
|
|
link: lnk,
|
|
|
|
link_text: mat.as_str(),
|
|
|
|
}
|
|
|
|
})
|
2017-07-04 07:04:18 +08:00
|
|
|
})
|
2015-12-31 05:40:23 +08:00
|
|
|
}
|
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
|
|
|
let base = base.as_ref();
|
|
|
|
match self.link {
|
|
|
|
// omit the escape char
|
|
|
|
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
2018-01-06 05:03:30 +08:00
|
|
|
LinkType::IncludeRange(ref pat, ref range) => {
|
|
|
|
file_to_string(base.join(pat))
|
|
|
|
.map(|s| take_lines(&s, range.clone()))
|
|
|
|
.chain_err(|| {
|
|
|
|
format!("Could not read file for link {}", self.link_text)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
|
|
|
file_to_string(base.join(pat))
|
|
|
|
.map(|s| take_lines(&s, range.clone()))
|
|
|
|
.chain_err(|| {
|
|
|
|
format!("Could not read file for link {}", self.link_text)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
|
|
|
file_to_string(base.join(pat))
|
|
|
|
.map(|s| take_lines(&s, range.clone()))
|
|
|
|
.chain_err(|| {
|
|
|
|
format!("Could not read file for link {}", self.link_text)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
LinkType::IncludeRangeFull(ref pat, _) => {
|
|
|
|
file_to_string(base.join(pat))
|
|
|
|
.chain_err(|| {
|
|
|
|
format!("Could not read file for link {}", self.link_text)
|
|
|
|
})
|
2017-10-03 19:40:23 +08:00
|
|
|
}
|
2017-07-04 07:04:18 +08:00
|
|
|
LinkType::Playpen(ref pat, ref attrs) => {
|
2017-10-03 19:40:23 +08:00
|
|
|
let contents = file_to_string(base.join(pat)).chain_err(|| {
|
2018-01-06 05:03:30 +08:00
|
|
|
format!("Could not read file for link {}", self.link_text)
|
|
|
|
})?;
|
2017-07-04 07:04:18 +08:00
|
|
|
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
2018-01-06 05:03:30 +08:00
|
|
|
Ok(format!(
|
|
|
|
"```{}{}\n{}\n```\n",
|
|
|
|
ftype,
|
|
|
|
attrs.join(","),
|
|
|
|
contents
|
|
|
|
))
|
2017-10-03 19:40:23 +08:00
|
|
|
}
|
2017-07-04 07:04:18 +08:00
|
|
|
}
|
|
|
|
}
|
2015-12-31 05:40:23 +08:00
|
|
|
}
|
2015-12-31 19:00:09 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
struct LinkIter<'a>(CaptureMatches<'a, 'a>);
|
2015-12-31 19:00:09 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
impl<'a> Iterator for LinkIter<'a> {
|
|
|
|
type Item = Link<'a>;
|
|
|
|
fn next(&mut self) -> Option<Link<'a>> {
|
|
|
|
for cap in &mut self.0 {
|
|
|
|
if let Some(inc) = Link::from_capture(cap) {
|
|
|
|
return Some(inc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2015-12-31 19:00:09 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
fn find_links(contents: &str) -> LinkIter {
|
|
|
|
// lazily compute following regex
|
|
|
|
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
|
|
|
lazy_static! {
|
|
|
|
static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
|
|
|
|
\\\{\{\#.*\}\} # match escaped link
|
|
|
|
| # or
|
|
|
|
\{\{\s* # link opening parens and whitespace
|
|
|
|
\#([a-zA-Z0-9]+) # link type
|
2017-08-01 19:16:47 +08:00
|
|
|
\s+ # separating whitespace
|
2017-07-04 07:04:18 +08:00
|
|
|
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
|
|
|
\s*\}\} # whitespace and link closing parens
|
|
|
|
").unwrap();
|
|
|
|
}
|
|
|
|
LinkIter(RE.captures_iter(contents))
|
|
|
|
}
|
2015-12-31 19:00:09 +08:00
|
|
|
|
2016-03-18 05:31:28 +08:00
|
|
|
// ---------------------------------------------------------------------------------
|
2015-12-31 19:00:09 +08:00
|
|
|
// Tests
|
|
|
|
//
|
|
|
|
|
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_no_link() {
|
|
|
|
let s = "Some random text without link...";
|
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
2015-12-31 19:00:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_partial_link() {
|
2015-12-31 19:00:09 +08:00
|
|
|
let s = "Some random text with {{#playpen...";
|
2017-07-04 07:04:18 +08:00
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
|
|
let s = "Some random text with {{#include...";
|
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
|
|
let s = "Some random text with \\{{#include...";
|
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
2015-12-31 19:00:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_empty_link() {
|
|
|
|
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
|
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
2015-12-31 19:00:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_unknown_link_type() {
|
|
|
|
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
|
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
2015-12-31 19:00:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_simple_link() {
|
|
|
|
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
|
|
|
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
|
|
|
|
assert_eq!(res,
|
|
|
|
vec![Link {
|
|
|
|
start_index: 22,
|
|
|
|
end_index: 42,
|
|
|
|
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
|
|
|
link_text: "{{#playpen file.rs}}",
|
|
|
|
},
|
|
|
|
Link {
|
|
|
|
start_index: 47,
|
|
|
|
end_index: 68,
|
|
|
|
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
|
|
|
link_text: "{{#playpen test.rs }}",
|
|
|
|
}]);
|
2016-01-01 08:40:37 +08:00
|
|
|
}
|
|
|
|
|
2018-01-06 05:03:30 +08:00
|
|
|
#[test]
|
|
|
|
fn test_find_links_with_range() {
|
|
|
|
let s = "Some random text with {{#include file.rs:10:20}}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
vec![
|
|
|
|
Link {
|
|
|
|
start_index: 22,
|
|
|
|
end_index: 48,
|
|
|
|
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 10..20),
|
|
|
|
link_text: "{{#include file.rs:10:20}}",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_find_links_with_from_range() {
|
|
|
|
let s = "Some random text with {{#include file.rs:10:}}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
vec![
|
|
|
|
Link {
|
|
|
|
start_index: 22,
|
|
|
|
end_index: 46,
|
|
|
|
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 10..),
|
|
|
|
link_text: "{{#include file.rs:10:}}",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_find_links_with_to_range() {
|
|
|
|
let s = "Some random text with {{#include file.rs::20}}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
vec![
|
|
|
|
Link {
|
|
|
|
start_index: 22,
|
|
|
|
end_index: 46,
|
|
|
|
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
|
|
|
link_text: "{{#include file.rs::20}}",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_find_links_with_full_range() {
|
|
|
|
let s = "Some random text with {{#include file.rs::}}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
vec![
|
|
|
|
Link {
|
|
|
|
start_index: 22,
|
|
|
|
end_index: 44,
|
|
|
|
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
|
|
|
link_text: "{{#include file.rs::}}",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-01-01 08:40:37 +08:00
|
|
|
#[test]
|
2017-07-04 07:04:18 +08:00
|
|
|
fn test_find_links_escaped_link() {
|
2016-01-01 08:40:37 +08:00
|
|
|
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
|
|
|
|
assert_eq!(res,
|
|
|
|
vec![Link {
|
|
|
|
start_index: 38,
|
|
|
|
end_index: 68,
|
|
|
|
link: LinkType::Escaped,
|
|
|
|
link_text: "\\{{#playpen file.rs editable}}",
|
|
|
|
}]);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_find_playpens_with_properties() {
|
2017-10-03 19:40:23 +08:00
|
|
|
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n \
|
|
|
|
text {{#playpen my.rs editable no_run should_panic}} ...";
|
2017-07-04 07:04:18 +08:00
|
|
|
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(res,
|
|
|
|
vec![Link {
|
|
|
|
start_index: 38,
|
|
|
|
end_index: 68,
|
|
|
|
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
|
|
|
link_text: "{{#playpen file.rs editable }}",
|
|
|
|
},
|
|
|
|
Link {
|
2017-10-03 19:40:23 +08:00
|
|
|
start_index: 89,
|
|
|
|
end_index: 136,
|
|
|
|
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
|
|
|
vec!["editable", "no_run", "should_panic"]),
|
2017-07-04 07:04:18 +08:00
|
|
|
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
|
|
|
}]);
|
|
|
|
}
|
2016-01-01 08:40:37 +08:00
|
|
|
|
2017-07-04 07:04:18 +08:00
|
|
|
#[test]
|
|
|
|
fn test_find_all_link_types() {
|
2017-10-03 19:40:23 +08:00
|
|
|
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
|
|
|
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable no_run \
|
|
|
|
should_panic}} ...";
|
2017-07-04 07:04:18 +08:00
|
|
|
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
|
|
println!("\nOUTPUT: {:?}\n", res);
|
|
|
|
assert_eq!(res.len(), 3);
|
|
|
|
assert_eq!(res[0],
|
|
|
|
Link {
|
|
|
|
start_index: 38,
|
|
|
|
end_index: 58,
|
2018-01-06 05:03:30 +08:00
|
|
|
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
2017-07-04 07:04:18 +08:00
|
|
|
link_text: "{{#include file.rs}}",
|
|
|
|
});
|
|
|
|
assert_eq!(res[1],
|
|
|
|
Link {
|
|
|
|
start_index: 63,
|
|
|
|
end_index: 112,
|
|
|
|
link: LinkType::Escaped,
|
|
|
|
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
|
|
|
});
|
|
|
|
assert_eq!(res[2],
|
|
|
|
Link {
|
|
|
|
start_index: 130,
|
|
|
|
end_index: 177,
|
2017-10-03 19:40:23 +08:00
|
|
|
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
|
|
|
vec!["editable", "no_run", "should_panic"]),
|
2017-07-04 07:04:18 +08:00
|
|
|
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
|
|
|
});
|
2015-12-31 19:00:09 +08:00
|
|
|
}
|