Skip to content

Commit ac1749f

Browse files
carols10centsDylan-DPC
authored andcommitted
Implement a rustdoc_include preprocessor (#1003)
* Allow underscores in the link type name * Add some tests for include anchors * Include parts of Rust files and hide the rest Fixes #618. * Increase min supported Rust version to 1.35 * Add a test for a behavior of rustdoc_include I want to depend on At first I thought this was a bug, but then I looked at some use cases we have in TRPL and decided this was a feature that I'd like to use.
1 parent 8cdeb12 commit ac1749f

14 files changed

+2023
-377
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ matrix:
1717
env: TARGET=x86_64-unknown-linux-gnu
1818
- rust: nightly
1919
env: TARGET=x86_64-unknown-linux-gnu
20-
- rust: 1.34.0 # Minimum required Rust version
20+
- rust: 1.35.0 # Minimum required Rust version
2121
env: TARGET=x86_64-unknown-linux-gnu
2222

2323
- rust: stable

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ There are multiple ways to install mdBook.
4141

4242
2. **From Crates.io**
4343

44-
This requires at least [Rust] 1.34 and Cargo to be installed. Once you have installed
44+
This requires at least [Rust] 1.35 and Cargo to be installed. Once you have installed
4545
Rust, type the following in the terminal:
4646

4747
```

book-example/src/format/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ This controls the build process of your book.
8181

8282
The following preprocessors are available and included by default:
8383

84-
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
84+
- `links`: Expand the `{{ #playpen }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
8585
helpers in a chapter to include the contents of a file.
8686
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
8787
to say, all `README.md` would be rendered to an index file `index.html` in the

book-example/src/format/mdbook.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## Hiding code lines
44

55
There is a feature in mdBook that lets you hide code lines by prepending them
6-
with a `#`.
6+
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
7+
8+
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
79

810
```bash
911
# fn main() {
@@ -107,6 +109,70 @@ This is the full file.
107109

108110
Lines containing anchor patterns inside the included anchor are ignored.
109111

112+
## Including a file but initially hiding all except specified lines
113+
114+
The `rustdoc_include` helper is for including code from external Rust files that contain complete
115+
examples, but only initially showing particular lines specified with line numbers or anchors in the
116+
same way as with `include`.
117+
118+
The lines not in the line number range or between the anchors will still be included, but they will
119+
be prefaced with `#`. This way, a reader can expand the snippet to see the complete example, and
120+
Rustdoc will use the complete example when you run `mdbook test`.
121+
122+
For example, consider a file named `file.rs` that contains this Rust program:
123+
124+
```rust
125+
fn main() {
126+
let x = add_one(2);
127+
assert_eq!(x, 3);
128+
}
129+
130+
fn add_one(num: i32) -> i32 {
131+
num + 1
132+
}
133+
```
134+
135+
We can include a snippet that initially shows only line 2 by using this syntax:
136+
137+
````hbs
138+
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
139+
140+
```rust
141+
\{{#rustdoc_include file.rs:2}}
142+
```
143+
````
144+
145+
This would have the same effect as if we had manually inserted the code and hidden all but line 2
146+
using `#`:
147+
148+
````hbs
149+
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
150+
151+
```rust
152+
# fn main() {
153+
let x = add_one(2);
154+
# assert_eq!(x, 3);
155+
# }
156+
#
157+
# fn add_one(num: i32) -> i32 {
158+
# num + 1
159+
#}
160+
```
161+
````
162+
163+
That is, it looks like this (click the "expand" icon to see the rest of the file):
164+
165+
```rust
166+
# fn main() {
167+
let x = add_one(2);
168+
# assert_eq!(x, 3);
169+
# }
170+
#
171+
# fn add_one(num: i32) -> i32 {
172+
# num + 1
173+
#}
174+
```
175+
110176
## Inserting runnable Rust files
111177

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

src/preprocess/links.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::errors::*;
2-
use crate::utils::{take_anchored_lines, take_lines};
2+
use crate::utils::{
3+
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
4+
take_rustdoc_include_lines,
5+
};
36
use regex::{CaptureMatches, Captures, Regex};
47
use std::fs;
58
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
@@ -11,8 +14,15 @@ use crate::book::{Book, BookItem};
1114
const ESCAPE_CHAR: char = '\\';
1215
const MAX_LINK_NESTED_DEPTH: usize = 10;
1316

14-
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
15-
/// helpers in a chapter.
17+
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
18+
///
19+
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
20+
///. lines, or only between the specified anchors.
21+
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
22+
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
23+
/// This hides the lines from initial display but shows them when the reader expands the code
24+
/// block and provides them to Rustdoc for testing.
25+
/// - `{{# playpen}}` - Insert runnable Rust files
1626
#[derive(Default)]
1727
pub struct LinkPreprocessor;
1828

@@ -104,6 +114,7 @@ enum LinkType<'a> {
104114
Escaped,
105115
Include(PathBuf, RangeOrAnchor),
106116
Playpen(PathBuf, Vec<&'a str>),
117+
RustdocInclude(PathBuf, RangeOrAnchor),
107118
}
108119

109120
#[derive(PartialEq, Debug, Clone)]
@@ -172,6 +183,7 @@ impl<'a> LinkType<'a> {
172183
LinkType::Escaped => None,
173184
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
174185
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
186+
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
175187
}
176188
}
177189
}
@@ -222,6 +234,15 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
222234
LinkType::Include(path, range_or_anchor)
223235
}
224236

237+
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
238+
let mut parts = path.splitn(2, ':');
239+
240+
let path = parts.next().unwrap().into();
241+
let range_or_anchor = parse_range_or_anchor(parts.next());
242+
243+
LinkType::RustdocInclude(path, range_or_anchor)
244+
}
245+
225246
#[derive(PartialEq, Debug, Clone)]
226247
struct Link<'a> {
227248
start_index: usize,
@@ -241,6 +262,7 @@ impl<'a> Link<'a> {
241262
match (typ.as_str(), file_arg) {
242263
("include", Some(pth)) => Some(parse_include_path(pth)),
243264
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
265+
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
244266
_ => None,
245267
}
246268
}
@@ -281,6 +303,26 @@ impl<'a> Link<'a> {
281303
)
282304
})
283305
}
306+
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
307+
let target = base.join(pat);
308+
309+
fs::read_to_string(&target)
310+
.map(|s| match range_or_anchor {
311+
RangeOrAnchor::Range(range) => {
312+
take_rustdoc_include_lines(&s, range.clone())
313+
}
314+
RangeOrAnchor::Anchor(anchor) => {
315+
take_rustdoc_include_anchored_lines(&s, anchor)
316+
}
317+
})
318+
.chain_err(|| {
319+
format!(
320+
"Could not read file for link {} ({})",
321+
self.link_text,
322+
target.display(),
323+
)
324+
})
325+
}
284326
LinkType::Playpen(ref pat, ref attrs) => {
285327
let target = base.join(pat);
286328

@@ -326,7 +368,7 @@ fn find_links(contents: &str) -> LinkIter<'_> {
326368
\\\{\{\#.*\}\} # match escaped link
327369
| # or
328370
\{\{\s* # link opening parens and whitespace
329-
\#([a-zA-Z0-9]+) # link type
371+
\#([a-zA-Z0-9_]+) # link type
330372
\s+ # separating whitespace
331373
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
332374
\s*\}\} # whitespace and link closing parens"

src/utils/mod.rs

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

14-
pub use self::string::{take_anchored_lines, take_lines};
14+
pub use self::string::{
15+
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
16+
take_rustdoc_include_lines,
17+
};
1518

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

0 commit comments

Comments
 (0)