Skip to content

Commit e7c3d02

Browse files
authored
Merge pull request #851 from CBenoit/master
Add include by anchor in preprocessor (partial include)
2 parents d6088c8 + d8a68ba commit e7c3d02

File tree

4 files changed

+163
-8
lines changed

4 files changed

+163
-8
lines changed

book-example/src/format/mdbook.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,50 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
6363
first line is omitted. The last command includes the excerpt of `file.rs`
6464
consisting of lines 2 to 10.
6565

66+
To avoid breaking your book when modifying included files, you can also
67+
include a specific section using anchors instead of line numbers.
68+
An anchor is a pair of matching lines. The line beginning an anchor must
69+
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
70+
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
71+
any kind of commented line.
72+
73+
Consider the following file to include:
74+
```rs
75+
/* ANCHOR: all */
76+
77+
// ANCHOR: component
78+
struct Paddle {
79+
hello: f32,
80+
}
81+
// ANCHOR_END: component
82+
83+
////////// ANCHOR: system
84+
impl System for MySystem { ... }
85+
////////// ANCHOR_END: system
86+
87+
/* ANCHOR_END: all */
88+
```
89+
90+
Then in the book, all you have to do is:
91+
````hbs
92+
Here is a component:
93+
```rust,no_run,noplaypen
94+
\{{#include file.rs:component}}
95+
```
96+
97+
Here is a system:
98+
```rust,no_run,noplaypen
99+
\{{#include file.rs:system}}
100+
```
101+
102+
This is the full file.
103+
```rust,no_run,noplaypen
104+
\{{#include file.rs:all}}
105+
```
106+
````
107+
108+
Lines containing anchor patterns inside the included anchor are ignored.
109+
66110
## Inserting runnable Rust files
67111

68112
With the following syntax, you can insert runnable Rust files into your book:

src/preprocess/links.rs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::errors::*;
2-
use crate::utils::take_lines;
2+
use crate::utils::{take_anchored_lines, take_lines};
33
use regex::{CaptureMatches, Captures, Regex};
44
use std::fs;
55
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
@@ -106,6 +106,7 @@ enum LinkType<'a> {
106106
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
107107
IncludeRangeTo(PathBuf, RangeTo<usize>),
108108
IncludeRangeFull(PathBuf, RangeFull),
109+
IncludeAnchor(PathBuf, String),
109110
Playpen(PathBuf, Vec<&'a str>),
110111
}
111112

@@ -118,6 +119,7 @@ impl<'a> LinkType<'a> {
118119
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
119120
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
120121
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
122+
LinkType::IncludeAnchor(p, _) => Some(return_relative_path(base, &p)),
121123
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
122124
}
123125
}
@@ -133,11 +135,21 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
133135
fn parse_include_path(path: &str) -> LinkType<'static> {
134136
let mut parts = path.split(':');
135137
let path = parts.next().unwrap().into();
136-
// subtract 1 since line numbers usually begin with 1
137-
let start = parts
138-
.next()
139-
.and_then(|s| s.parse::<usize>().ok())
140-
.map(|val| val.saturating_sub(1));
138+
139+
let next_element = parts.next();
140+
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
141+
// subtract 1 since line numbers usually begin with 1
142+
Some(value.saturating_sub(1))
143+
} else if let Some(anchor) = next_element {
144+
if anchor == "" {
145+
None
146+
} else {
147+
return LinkType::IncludeAnchor(path, String::from(anchor));
148+
}
149+
} else {
150+
None
151+
};
152+
141153
let end = parts.next();
142154
let has_end = end.is_some();
143155
let end = end.and_then(|s| s.parse::<usize>().ok());
@@ -258,6 +270,19 @@ impl<'a> Link<'a> {
258270
)
259271
})
260272
}
273+
LinkType::IncludeAnchor(ref pat, ref anchor) => {
274+
let target = base.join(pat);
275+
276+
fs::read_to_string(&target)
277+
.map(|s| take_anchored_lines(&s, anchor))
278+
.chain_err(|| {
279+
format!(
280+
"Could not read file for link {} ({})",
281+
self.link_text,
282+
target.display(),
283+
)
284+
})
285+
}
261286
LinkType::Playpen(ref pat, ref attrs) => {
262287
let target = base.join(pat);
263288

@@ -482,6 +507,25 @@ mod tests {
482507
);
483508
}
484509

510+
#[test]
511+
fn test_find_links_with_anchor() {
512+
let s = "Some random text with {{#include file.rs:anchor}}...";
513+
let res = find_links(s).collect::<Vec<_>>();
514+
println!("\nOUTPUT: {:?}\n", res);
515+
assert_eq!(
516+
res,
517+
vec![Link {
518+
start_index: 22,
519+
end_index: 49,
520+
link_type: LinkType::IncludeAnchor(
521+
PathBuf::from("file.rs"),
522+
String::from("anchor")
523+
),
524+
link_text: "{{#include file.rs:anchor}}",
525+
}]
526+
);
527+
}
528+
485529
#[test]
486530
fn test_find_links_escaped_link() {
487531
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";

src/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::borrow::Cow;
1111
use std::fmt::Write;
1212
use std::path::Path;
1313

14-
pub use self::string::take_lines;
14+
pub use self::string::{take_anchored_lines, take_lines};
1515

1616
/// Replaces multiple consecutive whitespace characters with a single space character.
1717
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {

src/utils/string.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use itertools::Itertools;
2+
use regex::Regex;
23
use std::ops::Bound::{Excluded, Included, Unbounded};
34
use std::ops::RangeBounds;
45

@@ -17,9 +18,46 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
1718
}
1819
}
1920

21+
/// Take anchored lines from a string.
22+
/// Lines containing anchor are ignored.
23+
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
24+
lazy_static! {
25+
static ref RE_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
26+
static ref RE_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
27+
}
28+
29+
let mut retained = Vec::<&str>::new();
30+
let mut anchor_found = false;
31+
32+
for l in s.lines() {
33+
if anchor_found {
34+
match RE_END.captures(l) {
35+
Some(cap) => {
36+
if &cap["anchor_name"] == anchor {
37+
break;
38+
}
39+
}
40+
None => {
41+
if !RE_START.is_match(l) {
42+
retained.push(l);
43+
}
44+
}
45+
}
46+
} else {
47+
if let Some(cap) = RE_START.captures(l) {
48+
if &cap["anchor_name"] == anchor {
49+
anchor_found = true;
50+
}
51+
}
52+
}
53+
}
54+
55+
retained.join("\n")
56+
}
57+
2058
#[cfg(test)]
2159
mod tests {
22-
use super::take_lines;
60+
use super::{take_anchored_lines, take_lines};
2361

2462
#[test]
2563
fn take_lines_test() {
@@ -32,4 +70,33 @@ mod tests {
3270
assert_eq!(take_lines(s, 4..3), "");
3371
assert_eq!(take_lines(s, ..100), s);
3472
}
73+
74+
#[test]
75+
fn take_anchored_lines_test() {
76+
let s = "Lorem\nipsum\ndolor\nsit\namet";
77+
assert_eq!(take_anchored_lines(s, "test"), "");
78+
79+
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
80+
assert_eq!(take_anchored_lines(s, "test"), "");
81+
82+
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
83+
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
84+
assert_eq!(take_anchored_lines(s, "something"), "");
85+
86+
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
87+
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
88+
assert_eq!(take_anchored_lines(s, "something"), "");
89+
90+
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
91+
assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
92+
assert_eq!(take_anchored_lines(s, "something"), "");
93+
94+
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
95+
assert_eq!(
96+
take_anchored_lines(s, "test2"),
97+
"ipsum\ndolor\nsit\namet\nlorem"
98+
);
99+
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
100+
assert_eq!(take_anchored_lines(s, "something"), "");
101+
}
35102
}

0 commit comments

Comments
 (0)