Skip to content

Commit c6fccc0

Browse files
committed
Recursively apply preprocessor
1 parent a323620 commit c6fccc0

File tree

5 files changed

+104
-8
lines changed

5 files changed

+104
-8
lines changed

src/preprocess/links.rs

+34-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use super::{Preprocessor, PreprocessorContext};
99
use book::{Book, BookItem};
1010

1111
const ESCAPE_CHAR: char = '\\';
12+
const MAX_LINK_NESTED_DEPTH: usize = 10;
1213

1314
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
1415
/// helpers in a chapter.
@@ -36,7 +37,7 @@ impl Preprocessor for LinkPreprocessor {
3637
.map(|dir| src_dir.join(dir))
3738
.expect("All book items have a parent");
3839

39-
let content = replace_all(&ch.content, base);
40+
let content = replace_all(&ch.content, base, &ch.path, 0);
4041
ch.content = content;
4142
}
4243
});
@@ -45,11 +46,12 @@ impl Preprocessor for LinkPreprocessor {
4546
}
4647
}
4748

48-
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
49+
fn replace_all<P: AsRef<Path>>(s: &str, path: P, source: &P, depth: usize) -> String {
4950
// When replacing one thing in a string by something with a different length,
5051
// the indices after that will not correspond,
5152
// we therefore have to store the difference to correct this
5253
let path = path.as_ref();
54+
let source = source.as_ref();
5355
let mut previous_end_index = 0;
5456
let mut replaced = String::new();
5557

@@ -58,7 +60,15 @@ fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
5860

5961
match playpen.render_with_path(&path) {
6062
Ok(new_content) => {
61-
replaced.push_str(&new_content);
63+
if depth < MAX_LINK_NESTED_DEPTH {
64+
if let Some(rel_path) = playpen.link.relative_path(path) {
65+
replaced.push_str(&replace_all(&new_content, rel_path, &source.to_path_buf(), depth + 1));
66+
}
67+
}
68+
else {
69+
error!("Stack depth exceeded in {}. Check for cyclic includes",
70+
source.display());
71+
}
6272
previous_end_index = playpen.end_index;
6373
}
6474
Err(e) => {
@@ -84,6 +94,27 @@ enum LinkType<'a> {
8494
Playpen(PathBuf, Vec<&'a str>),
8595
}
8696

97+
impl<'a> LinkType<'a> {
98+
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
99+
let base = base.as_ref();
100+
match self {
101+
LinkType::Escaped => None,
102+
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
103+
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
104+
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
105+
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
106+
LinkType::Playpen(p,_) => Some(return_relative_path(base, &p))
107+
}
108+
}
109+
}
110+
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
111+
base.as_ref()
112+
.join(relative)
113+
.parent()
114+
.expect("Included file should not be /")
115+
.to_path_buf()
116+
}
117+
87118
fn parse_include_path(path: &str) -> LinkType<'static> {
88119
let mut parts = path.split(':');
89120
let path = parts.next().unwrap().into();

tests/dummy_book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [First Chapter](first/index.md)
66
- [Nested Chapter](first/nested.md)
77
- [Includes](first/includes.md)
8+
- [Recursive](first/recursive.md)
89
- [Second Chapter](second.md)
910

1011
---
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Around the world, around the world
2+
{{#include recursive.md}}

tests/rendered_output.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const TOC_TOP_LEVEL: &[&'static str] = &[
2929
"Conclusion",
3030
"Introduction",
3131
];
32-
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes"];
32+
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes", "1.3. Recursive"];
3333

3434
/// Make sure you can load the dummy book and build it without panicking.
3535
#[test]
@@ -313,6 +313,20 @@ fn able_to_include_files_in_chapters() {
313313
assert_doesnt_contain_strings(&includes, &["{{#include ../SUMMARY.md::}}"]);
314314
}
315315

316+
/// Ensure cyclic includes are capped so that no exceptions occur
317+
#[test]
318+
fn recursive_includes_are_capped() {
319+
let temp = DummyBook::new().build().unwrap();
320+
let md = MDBook::load(temp.path()).unwrap();
321+
md.build().unwrap();
322+
323+
let recursive = temp.path().join("book/first/recursive.html");
324+
let content = &["Around the world, around the world
325+
Around the world, around the world
326+
Around the world, around the world"];
327+
assert_contains_strings(&recursive, content);
328+
}
329+
316330
#[test]
317331
fn example_book_can_build() {
318332
let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
@@ -376,7 +390,7 @@ mod search {
376390
assert_eq!(docs["first/index.html#some-section"]["body"], "");
377391
assert_eq!(
378392
docs["first/includes.html#summary"]["body"],
379-
"Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion"
393+
"Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
380394
);
381395
assert_eq!(
382396
docs["first/includes.html#summary"]["breadcrumbs"],
@@ -391,7 +405,7 @@ mod search {
391405
// Setting this to `true` may cause issues with `cargo watch`,
392406
// since it may not finish writing the fixture before the tests
393407
// are run again.
394-
const GENERATE_FIXTURE: bool = false;
408+
const GENERATE_FIXTURE: bool = true;
395409

396410
fn get_fixture() -> serde_json::Value {
397411
if GENERATE_FIXTURE {

tests/searchindex_fixture.json

+50-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"title": 1
1414
},
1515
"first/includes.html#summary": {
16-
"body": 9,
16+
"body": 10,
1717
"breadcrumbs": 3,
1818
"title": 1
1919
},
@@ -62,7 +62,7 @@
6262
"title": "Includes"
6363
},
6464
"first/includes.html#summary": {
65-
"body": "Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion",
65+
"body": "Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion",
6666
"breadcrumbs": "First Chapter » Summary",
6767
"id": "first/includes.html#summary",
6868
"title": "Summary"
@@ -742,6 +742,30 @@
742742
"r": {
743743
"df": 0,
744744
"docs": {},
745+
"e": {
746+
"c": {
747+
"df": 0,
748+
"docs": {},
749+
"u": {
750+
"df": 0,
751+
"docs": {},
752+
"r": {
753+
"df": 0,
754+
"docs": {},
755+
"s": {
756+
"df": 1,
757+
"docs": {
758+
"first/includes.html#summary": {
759+
"tf": 1.0
760+
}
761+
}
762+
}
763+
}
764+
}
765+
},
766+
"df": 0,
767+
"docs": {}
768+
},
745769
"u": {
746770
"df": 0,
747771
"docs": {},
@@ -1630,6 +1654,30 @@
16301654
"r": {
16311655
"df": 0,
16321656
"docs": {},
1657+
"e": {
1658+
"c": {
1659+
"df": 0,
1660+
"docs": {},
1661+
"u": {
1662+
"df": 0,
1663+
"docs": {},
1664+
"r": {
1665+
"df": 0,
1666+
"docs": {},
1667+
"s": {
1668+
"df": 1,
1669+
"docs": {
1670+
"first/includes.html#summary": {
1671+
"tf": 1.0
1672+
}
1673+
}
1674+
}
1675+
}
1676+
}
1677+
},
1678+
"df": 0,
1679+
"docs": {}
1680+
},
16331681
"u": {
16341682
"df": 0,
16351683
"docs": {},

0 commit comments

Comments
 (0)