From 2a55ff62f36245d384f9d270e403dfc395190f52 Mon Sep 17 00:00:00 2001 From: Andrew Gauger Date: Sun, 20 May 2018 03:36:19 -0700 Subject: [PATCH] Recursively apply preprocessor (#682) --- src/preprocess/links.rs | 37 ++++++++++++++++-- tests/dummy_book/src/SUMMARY.md | 1 + tests/dummy_book/src/first/recursive.md | 2 + tests/rendered_output.rs | 20 ++++++++-- tests/searchindex_fixture.json | 52 ++++++++++++++++++++++++- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 tests/dummy_book/src/first/recursive.md diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index e5371975..fe0342b8 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -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>(s: &str, path: P) -> String { +fn replace_all>(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>(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>(self, base: P) -> Option { + 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>(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(); diff --git a/tests/dummy_book/src/SUMMARY.md b/tests/dummy_book/src/SUMMARY.md index 920885e6..ccf3401a 100644 --- a/tests/dummy_book/src/SUMMARY.md +++ b/tests/dummy_book/src/SUMMARY.md @@ -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) --- diff --git a/tests/dummy_book/src/first/recursive.md b/tests/dummy_book/src/first/recursive.md new file mode 100644 index 00000000..cb82a52f --- /dev/null +++ b/tests/dummy_book/src/first/recursive.md @@ -0,0 +1,2 @@ +Around the world, around the world +{{#include recursive.md}} diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 3b3938d0..a95fe96e 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -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 { diff --git a/tests/searchindex_fixture.json b/tests/searchindex_fixture.json index 41793110..aa9493bd 100644 --- a/tests/searchindex_fixture.json +++ b/tests/searchindex_fixture.json @@ -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": {},