Skip to content

Commit 4b1b97b

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 4b1b97b

File tree

5 files changed

+345
-24
lines changed

5 files changed

+345
-24
lines changed

guide/src/format/mdbook.md

+6
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

+38-8
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

+103-12
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,54 @@ 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+
let ws = line.chars().take_while(|c| c.is_whitespace());
28+
if let Some(common) = common_ws {
29+
common_ws = Some(
30+
common
31+
.chars()
32+
.zip(ws)
33+
.take_while(|(a, b)| a == b)
34+
.map(|(a, _b)| a)
35+
.collect(),
36+
);
37+
} else {
38+
common_ws = Some(ws.collect())
39+
}
40+
}
41+
common_ws.unwrap_or_else(String::new)
42+
}
43+
44+
fn calculate_shift(lines: &[String], shift: Shift) -> ExplicitShift {
45+
match shift {
46+
Shift::None => ExplicitShift::None,
47+
Shift::Left(l) => ExplicitShift::Left(l),
48+
Shift::Right(r) => ExplicitShift::Right(r),
49+
Shift::Auto => ExplicitShift::Left(common_leading_ws(lines).len()),
50+
}
51+
}
52+
53+
fn shift_line(l: &str, shift: ExplicitShift) -> Cow<'_, str> {
1654
match shift {
17-
Shift::None => Cow::Borrowed(l),
18-
Shift::Right(shift) => {
55+
ExplicitShift::None => Cow::Borrowed(l),
56+
ExplicitShift::Right(shift) => {
1957
let indent = " ".repeat(shift);
2058
Cow::Owned(format!("{indent}{l}"))
2159
}
22-
Shift::Left(skip) => {
60+
ExplicitShift::Left(skip) => {
2361
if l.chars().take(skip).any(|c| !c.is_whitespace()) {
2462
log::error!("left-shifting away non-whitespace");
2563
}
@@ -30,6 +68,7 @@ fn shift_line(l: &str, shift: Shift) -> Cow<'_, str> {
3068
}
3169

3270
fn shift_lines(lines: &[String], shift: Shift) -> Vec<Cow<'_, str>> {
71+
let shift = calculate_shift(lines, shift);
3372
lines.iter().map(|l| shift_line(l, shift)).collect()
3473
}
3574

@@ -160,20 +199,44 @@ pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
160199
#[cfg(test)]
161200
mod tests {
162201
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,
202+
common_leading_ws, shift_line, take_anchored_lines, take_anchored_lines_with_shift,
203+
take_lines, take_lines_with_shift, take_rustdoc_include_anchored_lines,
204+
take_rustdoc_include_lines, ExplicitShift, Shift,
166205
};
167206

207+
#[test]
208+
fn common_leading_ws_test() {
209+
let tests = [
210+
([" line1", " line2", " line3"], " "),
211+
([" line1", " line2", "line3"], ""),
212+
(["\t\tline1", "\t\t line2", "\t\tline3"], "\t\t"),
213+
(["\t line1", " \tline2", " \t\tline3"], ""),
214+
];
215+
for (lines, want) in tests {
216+
let lines = lines.into_iter().map(|l| l.to_string()).collect::<Vec<_>>();
217+
let got = common_leading_ws(&lines);
218+
assert_eq!(got, want, "for input {lines:?}");
219+
}
220+
}
221+
168222
#[test]
169223
fn shift_line_test() {
170224
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");
225+
assert_eq!(shift_line(s, ExplicitShift::None), s);
226+
assert_eq!(
227+
shift_line(s, ExplicitShift::Left(4)),
228+
"Line with 4 space intro"
229+
);
230+
assert_eq!(
231+
shift_line(s, ExplicitShift::Left(2)),
232+
" Line with 4 space intro"
233+
);
175234
assert_eq!(
176-
shift_line(s, Shift::Right(2)),
235+
shift_line(s, ExplicitShift::Left(6)),
236+
"ne with 4 space intro"
237+
);
238+
assert_eq!(
239+
shift_line(s, ExplicitShift::Right(2)),
177240
" Line with 4 space intro"
178241
);
179242
}
@@ -207,6 +270,10 @@ mod tests {
207270
take_lines_with_shift(s, 1..3, Shift::Right(2)),
208271
" ipsum\n dolor"
209272
);
273+
assert_eq!(
274+
take_lines_with_shift(s, 1..3, Shift::Auto),
275+
"ipsum\n dolor"
276+
);
210277
assert_eq!(take_lines_with_shift(s, 3.., Shift::None), " sit\n amet");
211278
assert_eq!(
212279
take_lines_with_shift(s, 3.., Shift::Right(1)),
@@ -217,6 +284,10 @@ mod tests {
217284
take_lines_with_shift(s, ..3, Shift::None),
218285
" Lorem\n ipsum\n dolor"
219286
);
287+
assert_eq!(
288+
take_lines_with_shift(s, ..3, Shift::Auto),
289+
"Lorem\nipsum\n dolor"
290+
);
220291
assert_eq!(
221292
take_lines_with_shift(s, ..3, Shift::Right(4)),
222293
" Lorem\n ipsum\n dolor"
@@ -226,6 +297,10 @@ mod tests {
226297
"rem\nsum\ndolor"
227298
);
228299
assert_eq!(take_lines_with_shift(s, .., Shift::None), s);
300+
assert_eq!(
301+
take_lines_with_shift(s, .., Shift::Auto),
302+
"Lorem\nipsum\n dolor\nsit\namet"
303+
);
229304
// corner cases
230305
assert_eq!(take_lines_with_shift(s, 4..3, Shift::None), "");
231306
assert_eq!(take_lines_with_shift(s, 4..3, Shift::Left(2)), "");
@@ -307,6 +382,10 @@ mod tests {
307382
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
308383
"dolor\nsit\namet"
309384
);
385+
assert_eq!(
386+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
387+
"dolor\nsit\namet"
388+
);
310389
assert_eq!(
311390
take_anchored_lines_with_shift(s, "something", Shift::None),
312391
""
@@ -333,6 +412,10 @@ mod tests {
333412
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
334413
"dolor\nsit\namet"
335414
);
415+
assert_eq!(
416+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
417+
"dolor\nsit\namet"
418+
);
336419
assert_eq!(
337420
take_anchored_lines_with_shift(s, "test", Shift::Left(4)),
338421
"lor\nt\net"
@@ -359,6 +442,10 @@ mod tests {
359442
take_anchored_lines_with_shift(s, "test", Shift::Left(2)),
360443
"ipsum\ndolor\nsit\namet"
361444
);
445+
assert_eq!(
446+
take_anchored_lines_with_shift(s, "test", Shift::Auto),
447+
"ipsum\ndolor\nsit\namet"
448+
);
362449
assert_eq!(
363450
take_anchored_lines_with_shift(s, "something", Shift::None),
364451
""
@@ -371,6 +458,10 @@ mod tests {
371458
take_anchored_lines_with_shift(s, "something", Shift::Left(2)),
372459
""
373460
);
461+
assert_eq!(
462+
take_anchored_lines_with_shift(s, "something", Shift::Auto),
463+
""
464+
);
374465

375466
// Include non-ASCII.
376467
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

+4
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)