Skip to content

Commit a886ff5

Browse files
committed
Support {{#shiftinclude auto}}
As well as allowing explicitly-specified shift amounts, also support an "auto" option that strips common leftmost whitespace from an inclusion.
1 parent 6c7b259 commit a886ff5

File tree

5 files changed

+353
-28
lines changed

5 files changed

+353
-28
lines changed

guide/src/format/mdbook.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ using the following syntax:
223223
A positive number for the shift will prepend spaces to all lines; a negative number will remove
224224
the corresponding number of characters from the beginning of each line.
225225

226+
The special `auto` value will remove common initial whitespace from all lines.
227+
228+
```hbs
229+
\{{#shiftinclude auto:file.rs:indentedanchor}}
230+
```
231+
226232
## Including a file but initially hiding all except specified lines
227233

228234
The `rustdoc_include` helper is for including code from external Rust files that contain complete

src/preprocess/links.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,18 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
266266
fn parse_shift_include_path(params: &str) -> LinkType<'static> {
267267
let mut params = params.splitn(2, ':');
268268
let param0 = params.next().unwrap();
269-
let shift: isize = param0.parse().unwrap_or_else(|e| {
270-
log::error!("failed to parse shift amount: {e:?}");
271-
0
272-
});
273-
let shift = match shift.cmp(&0) {
274-
Ordering::Greater => Shift::Right(shift as usize),
275-
Ordering::Equal => Shift::None,
276-
Ordering::Less => Shift::Left(-shift as usize),
269+
let shift = if param0 == "auto" {
270+
Shift::Auto
271+
} else {
272+
let shift: isize = param0.parse().unwrap_or_else(|e| {
273+
log::error!("failed to parse shift amount: {e:?}");
274+
0
275+
});
276+
match shift.cmp(&0) {
277+
Ordering::Greater => Shift::Right(shift as usize),
278+
Ordering::Equal => Shift::None,
279+
Ordering::Less => Shift::Left(-shift as usize),
280+
}
277281
};
278282
let mut parts = params.next().unwrap().splitn(2, ':');
279283

@@ -1002,6 +1006,19 @@ mod tests {
10021006
);
10031007
}
10041008

1009+
#[test]
1010+
fn parse_with_auto_shifted_anchor() {
1011+
let link_type = parse_shift_include_path("auto:arbitrary:some-anchor");
1012+
assert_eq!(
1013+
link_type,
1014+
LinkType::Include(
1015+
PathBuf::from("arbitrary"),
1016+
RangeOrAnchor::Anchor("some-anchor".to_string()),
1017+
Shift::Auto
1018+
)
1019+
);
1020+
}
1021+
10051022
#[test]
10061023
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
10071024
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
@@ -1053,4 +1070,17 @@ mod tests {
10531070
)
10541071
);
10551072
}
1073+
1074+
#[test]
1075+
fn parse_start_and_end_auto_shifted_range() {
1076+
let link_type = parse_shift_include_path("auto:arbitrary:5:10");
1077+
assert_eq!(
1078+
link_type,
1079+
LinkType::Include(
1080+
PathBuf::from("arbitrary"),
1081+
RangeOrAnchor::Range(LineRange::from(4..10)),
1082+
Shift::Auto
1083+
)
1084+
);
1085+
}
10561086
}

src/utils/string.rs

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,58 @@ pub enum Shift {
1010
None,
1111
Left(usize),
1212
Right(usize),
13+
/// Strip leftmost whitespace that is common to all lines.
14+
Auto,
1315
}
1416

15-
fn shift_line(l: &str, shift: Shift) -> Cow<'_, str> {
17+
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
18+
enum ExplicitShift {
19+
None,
20+
Left(usize),
21+
Right(usize),
22+
}
23+
24+
fn common_leading_ws(lines: &[String]) -> String {
25+
let mut common_ws: Option<String> = None;
26+
for line in lines {
27+
if line.is_empty() {
28+
// Don't include empty lines in the calculation.
29+
continue;
30+
}
31+
let ws = line.chars().take_while(|c| c.is_whitespace());
32+
if let Some(common) = common_ws {
33+
common_ws = Some(
34+
common
35+
.chars()
36+
.zip(ws)
37+
.take_while(|(a, b)| a == b)
38+
.map(|(a, _b)| a)
39+
.collect(),
40+
);
41+
} else {
42+
common_ws = Some(ws.collect())
43+
}
44+
}
45+
common_ws.unwrap_or_else(String::new)
46+
}
47+
48+
fn calculate_shift(lines: &[String], shift: Shift) -> ExplicitShift {
1649
match shift {
17-
Shift::None => Cow::Borrowed(l),
18-
Shift::Right(shift) => {
50+
Shift::None => ExplicitShift::None,
51+
Shift::Left(l) => ExplicitShift::Left(l),
52+
Shift::Right(r) => ExplicitShift::Right(r),
53+
Shift::Auto => ExplicitShift::Left(common_leading_ws(lines).len()),
54+
}
55+
}
56+
57+
fn shift_line(l: &str, shift: ExplicitShift) -> Cow<'_, str> {
58+
match shift {
59+
ExplicitShift::None => Cow::Borrowed(l),
60+
ExplicitShift::Right(shift) => {
1961
let indent = " ".repeat(shift);
2062
Cow::Owned(format!("{indent}{l}"))
2163
}
22-
Shift::Left(skip) => {
64+
ExplicitShift::Left(skip) => {
2365
if l.chars().take(skip).any(|c| !c.is_whitespace()) {
2466
log::error!("left-shifting away non-whitespace");
2567
}
@@ -30,6 +72,7 @@ fn shift_line(l: &str, shift: Shift) -> Cow<'_, str> {
3072
}
3173

3274
fn shift_lines(lines: &[String], shift: Shift) -> Vec<Cow<'_, str>> {
75+
let shift = calculate_shift(lines, shift);
3376
lines.iter().map(|l| shift_line(l, shift)).collect()
3477
}
3578

@@ -160,20 +203,44 @@ pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
160203
#[cfg(test)]
161204
mod tests {
162205
use super::{
163-
shift_line, take_anchored_lines, take_anchored_lines_with_shift, take_lines,
164-
take_lines_with_shift, take_rustdoc_include_anchored_lines, take_rustdoc_include_lines,
165-
Shift,
206+
common_leading_ws, shift_line, take_anchored_lines, take_anchored_lines_with_shift,
207+
take_lines, take_lines_with_shift, take_rustdoc_include_anchored_lines,
208+
take_rustdoc_include_lines, ExplicitShift, Shift,
166209
};
167210

211+
#[test]
212+
fn common_leading_ws_test() {
213+
let tests = [
214+
([" line1", " line2", " line3"], " "),
215+
([" line1", " line2", "line3"], ""),
216+
(["\t\tline1", "\t\t line2", "\t\tline3"], "\t\t"),
217+
(["\t line1", " \tline2", " \t\tline3"], ""),
218+
];
219+
for (lines, want) in tests {
220+
let lines = lines.into_iter().map(|l| l.to_string()).collect::<Vec<_>>();
221+
let got = common_leading_ws(&lines);
222+
assert_eq!(got, want, "for input {lines:?}");
223+
}
224+
}
225+
168226
#[test]
169227
fn shift_line_test() {
170228
let s = " Line with 4 space intro";
171-
assert_eq!(shift_line(s, Shift::None), s);
172-
assert_eq!(shift_line(s, Shift::Left(4)), "Line with 4 space intro");
173-
assert_eq!(shift_line(s, Shift::Left(2)), " Line with 4 space intro");
174-
assert_eq!(shift_line(s, Shift::Left(6)), "ne with 4 space intro");
229+
assert_eq!(shift_line(s, ExplicitShift::None), s);
230+
assert_eq!(
231+
shift_line(s, ExplicitShift::Left(4)),
232+
"Line with 4 space intro"
233+
);
234+
assert_eq!(
235+
shift_line(s, ExplicitShift::Left(2)),
236+
" Line with 4 space intro"
237+
);
238+
assert_eq!(
239+
shift_line(s, ExplicitShift::Left(6)),
240+
"ne with 4 space intro"
241+
);
175242
assert_eq!(
176-
shift_line(s, Shift::Right(2)),
243+
shift_line(s, ExplicitShift::Right(2)),
177244
" Line with 4 space intro"
178245
);
179246
}
@@ -207,6 +274,10 @@ mod tests {
207274
take_lines_with_shift(s, 1..3, Shift::Right(2)),
208275
" ipsum\n dolor"
209276
);
277+
assert_eq!(
278+
take_lines_with_shift(s, 1..3, Shift::Auto),
279+
"ipsum\n dolor"
280+
);
210281
assert_eq!(take_lines_with_shift(s, 3.., Shift::None), " sit\n amet");
211282
assert_eq!(
212283
take_lines_with_shift(s, 3.., Shift::Right(1)),
@@ -217,6 +288,10 @@ mod tests {
217288
take_lines_with_shift(s, ..3, Shift::None),
218289
" Lorem\n ipsum\n dolor"
219290
);
291+
assert_eq!(
292+
take_lines_with_shift(s, ..3, Shift::Auto),
293+
"Lorem\nipsum\n dolor"
294+
);
220295
assert_eq!(
221296
take_lines_with_shift(s, ..3, Shift::Right(4)),
222297
" Lorem\n ipsum\n dolor"
@@ -226,6 +301,10 @@ mod tests {
226301
"rem\nsum\ndolor"
227302
);
228303
assert_eq!(take_lines_with_shift(s, .., Shift::None), s);
304+
assert_eq!(
305+
take_lines_with_shift(s, .., Shift::Auto),
306+
"Lorem\nipsum\n dolor\nsit\namet"
307+
);
229308
// corner cases
230309
assert_eq!(take_lines_with_shift(s, 4..3, Shift::None), "");
231310
assert_eq!(take_lines_with_shift(s, 4..3, Shift::Left(2)), "");
@@ -307,6 +386,10 @@ mod tests {
307386
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
308387
"dolor\nsit\namet"
309388
);
389+
assert_eq!(
390+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
391+
"dolor\nsit\namet"
392+
);
310393
assert_eq!(
311394
take_anchored_lines_with_shift(s, "something", Shift::None),
312395
""
@@ -333,6 +416,10 @@ mod tests {
333416
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
334417
"dolor\nsit\namet"
335418
);
419+
assert_eq!(
420+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
421+
"dolor\nsit\namet"
422+
);
336423
assert_eq!(
337424
take_anchored_lines_with_shift(s, "test", Shift::Left(4)),
338425
"lor\nt\net"
@@ -346,18 +433,22 @@ mod tests {
346433
""
347434
);
348435

349-
let s = " Lorem\n ANCHOR: test\n ipsum\n ANCHOR: test\n dolor\n sit\n amet\n ANCHOR_END: test\n lorem\n ipsum";
436+
let s = " Lorem\n ANCHOR: test\n ipsum\n ANCHOR: test\n dolor\n\n\n sit\n amet\n ANCHOR_END: test\n lorem\n ipsum";
350437
assert_eq!(
351438
take_anchored_lines_with_shift(s, "test", Shift::None),
352-
" ipsum\n dolor\n sit\n amet"
439+
" ipsum\n dolor\n\n\n sit\n amet"
353440
);
354441
assert_eq!(
355442
take_anchored_lines_with_shift(s, "test", Shift::Right(2)),
356-
" ipsum\n dolor\n sit\n amet"
443+
" ipsum\n dolor\n \n \n sit\n amet"
357444
);
358445
assert_eq!(
359446
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
360-
"ipsum\ndolor\nsit\namet"
447+
"ipsum\ndolor\n\n\nsit\namet"
448+
);
449+
assert_eq!(
450+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
451+
"ipsum\ndolor\n\n\nsit\namet"
361452
);
362453
assert_eq!(
363454
take_anchored_lines_with_shift(s, "something", Shift::None),
@@ -371,6 +462,10 @@ mod tests {
371462
take_anchored_lines_with_shift(s, "something", Shift::Left(2)),
372463
""
373464
);
465+
assert_eq!(
466+
take_anchored_lines_with_shift(s, "something", Shift::Auto),
467+
""
468+
);
374469

375470
// Include non-ASCII.
376471
let s = " Lorem\n ANCHOR: test2\n ípsum\n ANCHOR: test\n dôlor\n sit\n amet\n ANCHOR_END: test\n lorem\n ANCHOR_END:test2\n ipsum";

tests/dummy_book/src/first/nested.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ assert!($TEST_STATUS);
2424
{{#shiftinclude +2:nested-test-with-anchors.rs:myanchor}}
2525
```
2626

27+
```rust
28+
{{#shiftinclude auto:nested-test-with-anchors.rs:indentedanchor}}
29+
```
30+
2731
## Rustdoc include adds the rest of the file as hidden
2832

2933
```rust

0 commit comments

Comments
 (0)