Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add include by anchor in preprocessor (partial include) #851

Merged
merged 2 commits into from
Jul 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"), "");
}
}