Recursively apply preprocessor (#682)

This commit is contained in:
Andrew Gauger 2018-05-20 03:36:19 -07:00 committed by Michael Bryan
parent 6bf86806e4
commit 2a55ff62f3
5 changed files with 104 additions and 8 deletions

View File

@ -9,6 +9,7 @@ use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
@ -36,7 +37,7 @@ impl Preprocessor for LinkPreprocessor {
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
let content = replace_all(&ch.content, base);
let content = replace_all(&ch.content, base, &ch.path, 0);
ch.content = content;
}
});
@ -45,11 +46,12 @@ impl Preprocessor for LinkPreprocessor {
}
}
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
fn replace_all<P: AsRef<Path>>(s: &str, path: P, source: &P, depth: usize) -> String {
// 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
let path = path.as_ref();
let source = source.as_ref();
let mut previous_end_index = 0;
let mut replaced = String::new();
@ -58,7 +60,15 @@ fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
match playpen.render_with_path(&path) {
Ok(new_content) => {
replaced.push_str(&new_content);
if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = playpen.link.relative_path(path) {
replaced.push_str(&replace_all(&new_content, rel_path, &source.to_path_buf(), depth + 1));
}
}
else {
error!("Stack depth exceeded in {}. Check for cyclic includes",
source.display());
}
previous_end_index = playpen.end_index;
}
Err(e) => {
@ -84,6 +94,27 @@ enum LinkType<'a> {
Playpen(PathBuf, Vec<&'a str>),
}
impl<'a> LinkType<'a> {
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
let base = base.as_ref();
match self {
LinkType::Escaped => None,
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playpen(p,_) => Some(return_relative_path(base, &p))
}
}
}
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
base.as_ref()
.join(relative)
.parent()
.expect("Included file should not be /")
.to_path_buf()
}
fn parse_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.split(':');
let path = parts.next().unwrap().into();

View File

@ -5,6 +5,7 @@
- [First Chapter](first/index.md)
- [Nested Chapter](first/nested.md)
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Second Chapter](second.md)
---

View File

@ -0,0 +1,2 @@
Around the world, around the world
{{#include recursive.md}}

View File

@ -29,7 +29,7 @@ const TOC_TOP_LEVEL: &[&'static str] = &[
"Conclusion",
"Introduction",
];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes"];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes", "1.3. Recursive"];
/// Make sure you can load the dummy book and build it without panicking.
#[test]
@ -313,6 +313,20 @@ fn able_to_include_files_in_chapters() {
assert_doesnt_contain_strings(&includes, &["{{#include ../SUMMARY.md::}}"]);
}
/// Ensure cyclic includes are capped so that no exceptions occur
#[test]
fn recursive_includes_are_capped() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let recursive = temp.path().join("book/first/recursive.html");
let content = &["Around the world, around the world
Around the world, around the world
Around the world, around the world"];
assert_contains_strings(&recursive, content);
}
#[test]
fn example_book_can_build() {
let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
@ -424,7 +438,7 @@ mod search {
assert_eq!(docs["first/index.html#some-section"]["body"], "");
assert_eq!(
docs["first/includes.html#summary"]["body"],
"Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion"
"Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
);
assert_eq!(
docs["first/includes.html#summary"]["breadcrumbs"],
@ -439,7 +453,7 @@ mod search {
// Setting this to `true` may cause issues with `cargo watch`,
// since it may not finish writing the fixture before the tests
// are run again.
const GENERATE_FIXTURE: bool = false;
const GENERATE_FIXTURE: bool = true;
fn get_fixture() -> serde_json::Value {
if GENERATE_FIXTURE {

View File

@ -13,7 +13,7 @@
"title": 1
},
"first/includes.html#summary": {
"body": 9,
"body": 10,
"breadcrumbs": 3,
"title": 1
},
@ -62,7 +62,7 @@
"title": "Includes"
},
"first/includes.html#summary": {
"body": "Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion",
"body": "Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion",
"breadcrumbs": "First Chapter » Summary",
"id": "first/includes.html#summary",
"title": "Summary"
@ -742,6 +742,30 @@
"r": {
"df": 0,
"docs": {},
"e": {
"c": {
"df": 0,
"docs": {},
"u": {
"df": 0,
"docs": {},
"r": {
"df": 0,
"docs": {},
"s": {
"df": 1,
"docs": {
"first/includes.html#summary": {
"tf": 1.0
}
}
}
}
}
},
"df": 0,
"docs": {}
},
"u": {
"df": 0,
"docs": {},
@ -1630,6 +1654,30 @@
"r": {
"df": 0,
"docs": {},
"e": {
"c": {
"df": 0,
"docs": {},
"u": {
"df": 0,
"docs": {},
"r": {
"df": 0,
"docs": {},
"s": {
"df": 1,
"docs": {
"first/includes.html#summary": {
"tf": 1.0
}
}
}
}
}
},
"df": 0,
"docs": {}
},
"u": {
"df": 0,
"docs": {},