Skip to content

Commit

Permalink
Merge pull request #851 from CBenoit/master
Browse files Browse the repository at this point in the history
Add include by anchor in preprocessor (partial include)
  • Loading branch information
ehuss committed Jul 15, 2019
2 parents d6088c8 + d8a68ba commit e7c3d02
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 8 deletions.
44 changes: 44 additions & 0 deletions book-example/src/format/mdbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,50 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
first line is omitted. The last command includes the excerpt of `file.rs`
consisting of lines 2 to 10.

To avoid breaking your book when modifying included files, you can also
include a specific section using anchors instead of line numbers.
An anchor is a pair of matching lines. The line beginning an anchor must
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
any kind of commented line.

Consider the following file to include:
```rs
/* ANCHOR: all */

// ANCHOR: component
struct Paddle {
hello: f32,
}
// ANCHOR_END: component

////////// ANCHOR: system
impl System for MySystem { ... }
////////// ANCHOR_END: system

/* ANCHOR_END: all */
```

Then in the book, all you have to do is:
````hbs
Here is a component:
```rust,no_run,noplaypen
\{{#include file.rs:component}}
```
Here is a system:
```rust,no_run,noplaypen
\{{#include file.rs:system}}
```
This is the full file.
```rust,no_run,noplaypen
\{{#include file.rs:all}}
```
````

Lines containing anchor patterns inside the included anchor are ignored.

## Inserting runnable Rust files

With the following syntax, you can insert runnable Rust files into your book:
Expand Down
56 changes: 50 additions & 6 deletions src/preprocess/links.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::errors::*;
use crate::utils::take_lines;
use crate::utils::{take_anchored_lines, take_lines};
use regex::{CaptureMatches, Captures, Regex};
use std::fs;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
Expand Down Expand Up @@ -106,6 +106,7 @@ enum LinkType<'a> {
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
IncludeRangeTo(PathBuf, RangeTo<usize>),
IncludeRangeFull(PathBuf, RangeFull),
IncludeAnchor(PathBuf, String),
Playpen(PathBuf, Vec<&'a str>),
}

Expand All @@ -118,6 +119,7 @@ impl<'a> LinkType<'a> {
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeAnchor(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
}
}
Expand All @@ -133,11 +135,21 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
fn parse_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.split(':');
let path = parts.next().unwrap().into();
// subtract 1 since line numbers usually begin with 1
let start = parts
.next()
.and_then(|s| s.parse::<usize>().ok())
.map(|val| val.saturating_sub(1));

let next_element = parts.next();
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
// subtract 1 since line numbers usually begin with 1
Some(value.saturating_sub(1))
} else if let Some(anchor) = next_element {
if anchor == "" {
None
} else {
return LinkType::IncludeAnchor(path, String::from(anchor));
}
} else {
None
};

let end = parts.next();
let has_end = end.is_some();
let end = end.and_then(|s| s.parse::<usize>().ok());
Expand Down Expand Up @@ -258,6 +270,19 @@ impl<'a> Link<'a> {
)
})
}
LinkType::IncludeAnchor(ref pat, ref anchor) => {
let target = base.join(pat);

fs::read_to_string(&target)
.map(|s| take_anchored_lines(&s, anchor))
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::Playpen(ref pat, ref attrs) => {
let target = base.join(pat);

Expand Down Expand Up @@ -482,6 +507,25 @@ mod tests {
);
}

#[test]
fn test_find_links_with_anchor() {
let s = "Some random text with {{#include file.rs:anchor}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 49,
link_type: LinkType::IncludeAnchor(
PathBuf::from("file.rs"),
String::from("anchor")
),
link_text: "{{#include file.rs:anchor}}",
}]
);
}

#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
Expand Down
2 changes: 1 addition & 1 deletion src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::borrow::Cow;
use std::fmt::Write;
use std::path::Path;

pub use self::string::take_lines;
pub use self::string::{take_anchored_lines, take_lines};

/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
Expand Down
69 changes: 68 additions & 1 deletion src/utils/string.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use itertools::Itertools;
use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds;

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

/// Take anchored lines from a string.
/// Lines containing anchor are ignored.
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
lazy_static! {
static ref RE_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
static ref RE_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
}

let mut retained = Vec::<&str>::new();
let mut anchor_found = false;

for l in s.lines() {
if anchor_found {
match RE_END.captures(l) {
Some(cap) => {
if &cap["anchor_name"] == anchor {
break;
}
}
None => {
if !RE_START.is_match(l) {
retained.push(l);
}
}
}
} else {
if let Some(cap) = RE_START.captures(l) {
if &cap["anchor_name"] == anchor {
anchor_found = true;
}
}
}
}

retained.join("\n")
}

#[cfg(test)]
mod tests {
use super::take_lines;
use super::{take_anchored_lines, take_lines};

#[test]
fn take_lines_test() {
Expand All @@ -32,4 +70,33 @@ mod tests {
assert_eq!(take_lines(s, 4..3), "");
assert_eq!(take_lines(s, ..100), s);
}

#[test]
fn take_anchored_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");

let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");

let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");

let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");

let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");

let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
assert_eq!(
take_anchored_lines(s, "test2"),
"ipsum\ndolor\nsit\namet\nlorem"
);
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
}
}

0 comments on commit e7c3d02

Please sign in to comment.