Implement partial include of source files (#520)
* Implement partial include of source files. The macro `{{include some_file}}` accepts now optional line number arguments, s.t. the specified line range is included. The following forms are supported: * `{{include some_file::}}` is equivalent to `{{include some_file}}` * `{{include some_file:from:}}` includes lines [from, infinity) * `{{include some_file::to}}` includes lines [0, to] * `{{include some_file:from:to}}` includes lines [from, to] * Remove the special case IncludeFull which is IncludeFrom(0). * Use Range, RangeFrom, RangeTo and RangeFull to represent include-ranges. Also: * Move out introduced methods as free functions. * Introduce RangeArgument trait as long it is unstable in stdlib. * Use itertools for joining of lines on the fly. * Split tests. * Simplify include file argument parsing. * Make utils::string private and link collections_range feature issue.
This commit is contained in:
parent
e791f250fa
commit
e461610dab
|
@ -31,6 +31,7 @@ memchr = "2.0.1"
|
||||||
open = "1.1"
|
open = "1.1"
|
||||||
regex = "0.2.1"
|
regex = "0.2.1"
|
||||||
tempdir = "0.3.4"
|
tempdir = "0.3.4"
|
||||||
|
itertools = "0.7.4"
|
||||||
|
|
||||||
# Watch feature
|
# Watch feature
|
||||||
notify = { version = "4.0", optional = true }
|
notify = { version = "4.0", optional = true }
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate error_chain;
|
extern crate error_chain;
|
||||||
extern crate handlebars;
|
extern crate handlebars;
|
||||||
|
extern crate itertools;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use std::ops::{Range, RangeFrom, RangeTo, RangeFull};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use regex::{CaptureMatches, Captures, Regex};
|
use regex::{CaptureMatches, Captures, Regex};
|
||||||
use utils::fs::file_to_string;
|
use utils::fs::file_to_string;
|
||||||
|
use utils::take_lines;
|
||||||
use errors::*;
|
use errors::*;
|
||||||
|
|
||||||
const ESCAPE_CHAR: char = '\\';
|
const ESCAPE_CHAR: char = '\\';
|
||||||
|
@ -22,14 +24,38 @@ pub fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
|
||||||
Ok(replaced)
|
Ok(replaced)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialOrd, PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
enum LinkType<'a> {
|
enum LinkType<'a> {
|
||||||
Escaped,
|
Escaped,
|
||||||
Include(PathBuf),
|
IncludeRange(PathBuf, Range<usize>),
|
||||||
|
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||||
|
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||||
|
IncludeRangeFull(PathBuf, RangeFull),
|
||||||
Playpen(PathBuf, Vec<&'a str>),
|
Playpen(PathBuf, Vec<&'a str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialOrd, PartialEq, Debug, Clone)]
|
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||||
|
let mut parts = path.split(':');
|
||||||
|
let path = parts.next().unwrap().into();
|
||||||
|
let start = parts.next().and_then(|s| s.parse::<usize>().ok());
|
||||||
|
let end = parts.next().and_then(|s| s.parse::<usize>().ok());
|
||||||
|
match start {
|
||||||
|
Some(start) => {
|
||||||
|
match end {
|
||||||
|
Some(end) => LinkType::IncludeRange(path, Range{ start: start, end: end}),
|
||||||
|
None => LinkType::IncludeRangeFrom(path, RangeFrom{ start: start }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
match end {
|
||||||
|
Some(end) => LinkType::IncludeRangeTo(path, RangeTo{ end: end }),
|
||||||
|
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
struct Link<'a> {
|
struct Link<'a> {
|
||||||
start_index: usize,
|
start_index: usize,
|
||||||
end_index: usize,
|
end_index: usize,
|
||||||
|
@ -42,30 +68,30 @@ impl<'a> Link<'a> {
|
||||||
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
||||||
(_, Some(typ), Some(rest)) => {
|
(_, Some(typ), Some(rest)) => {
|
||||||
let mut path_props = rest.as_str().split_whitespace();
|
let mut path_props = rest.as_str().split_whitespace();
|
||||||
let file_path = path_props.next().map(PathBuf::from);
|
let file_arg = path_props.next();
|
||||||
let props: Vec<&str> = path_props.collect();
|
let props: Vec<&str> = path_props.collect();
|
||||||
|
|
||||||
match (typ.as_str(), file_path) {
|
match (typ.as_str(), file_arg) {
|
||||||
("include", Some(pth)) => Some(LinkType::Include(pth)),
|
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth, props)),
|
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(
|
||||||
Some(LinkType::Escaped)
|
LinkType::Escaped,
|
||||||
}
|
),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
link_type.and_then(|lnk| {
|
link_type.and_then(|lnk| {
|
||||||
cap.get(0).map(|mat| {
|
cap.get(0).map(|mat| {
|
||||||
Link {
|
Link {
|
||||||
start_index: mat.start(),
|
start_index: mat.start(),
|
||||||
end_index: mat.end(),
|
end_index: mat.end(),
|
||||||
link: lnk,
|
link: lnk,
|
||||||
link_text: mat.as_str(),
|
link_text: mat.as_str(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,22 +100,44 @@ impl<'a> Link<'a> {
|
||||||
match self.link {
|
match self.link {
|
||||||
// omit the escape char
|
// omit the escape char
|
||||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||||
LinkType::Include(ref pat) => {
|
LinkType::IncludeRange(ref pat, ref range) => {
|
||||||
file_to_string(base.join(pat)).chain_err(|| {
|
file_to_string(base.join(pat))
|
||||||
format!("Could not read file for \
|
.map(|s| take_lines(&s, range.clone()))
|
||||||
link {}",
|
.chain_err(|| {
|
||||||
self.link_text)
|
format!("Could not read file for link {}", self.link_text)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
||||||
|
file_to_string(base.join(pat))
|
||||||
|
.map(|s| take_lines(&s, range.clone()))
|
||||||
|
.chain_err(|| {
|
||||||
|
format!("Could not read file for link {}", self.link_text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
||||||
|
file_to_string(base.join(pat))
|
||||||
|
.map(|s| take_lines(&s, range.clone()))
|
||||||
|
.chain_err(|| {
|
||||||
|
format!("Could not read file for link {}", self.link_text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LinkType::IncludeRangeFull(ref pat, _) => {
|
||||||
|
file_to_string(base.join(pat))
|
||||||
|
.chain_err(|| {
|
||||||
|
format!("Could not read file for link {}", self.link_text)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
LinkType::Playpen(ref pat, ref attrs) => {
|
LinkType::Playpen(ref pat, ref attrs) => {
|
||||||
let contents = file_to_string(base.join(pat)).chain_err(|| {
|
let contents = file_to_string(base.join(pat)).chain_err(|| {
|
||||||
format!("Could not \
|
format!("Could not read file for link {}", self.link_text)
|
||||||
read file \
|
})?;
|
||||||
for link {}",
|
|
||||||
self.link_text)
|
|
||||||
})?;
|
|
||||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||||
Ok(format!("```{}{}\n{}\n```\n", ftype, attrs.join(","), contents))
|
Ok(format!(
|
||||||
|
"```{}{}\n{}\n```\n",
|
||||||
|
ftype,
|
||||||
|
attrs.join(","),
|
||||||
|
contents
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,6 +228,78 @@ fn test_find_links_simple_link() {
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_range() {
|
||||||
|
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 48,
|
||||||
|
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 10..20),
|
||||||
|
link_text: "{{#include file.rs:10:20}}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_from_range() {
|
||||||
|
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 46,
|
||||||
|
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 10..),
|
||||||
|
link_text: "{{#include file.rs:10:}}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_to_range() {
|
||||||
|
let s = "Some random text with {{#include file.rs::20}}...";
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 46,
|
||||||
|
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||||
|
link_text: "{{#include file.rs::20}}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_full_range() {
|
||||||
|
let s = "Some random text with {{#include file.rs::}}...";
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 44,
|
||||||
|
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||||
|
link_text: "{{#include file.rs::}}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_escaped_link() {
|
fn test_find_links_escaped_link() {
|
||||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||||
|
@ -232,7 +352,7 @@ fn test_find_all_link_types() {
|
||||||
Link {
|
Link {
|
||||||
start_index: 38,
|
start_index: 38,
|
||||||
end_index: 58,
|
end_index: 58,
|
||||||
link: LinkType::Include(PathBuf::from("file.rs")),
|
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||||
link_text: "{{#include file.rs}}",
|
link_text: "{{#include file.rs}}",
|
||||||
});
|
});
|
||||||
assert_eq!(res[1],
|
assert_eq!(res[1],
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
mod string;
|
||||||
|
|
||||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||||
OPTION_ENABLE_TABLES};
|
OPTION_ENABLE_TABLES};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub use self::string::{RangeArgument, take_lines};
|
||||||
|
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
// This trait is already contained in the standard lib, however it is unstable.
|
||||||
|
// TODO: Remove when the `collections_range` feature stabilises
|
||||||
|
// (https://github.com/rust-lang/rust/issues/30877)
|
||||||
|
pub trait RangeArgument<T: ?Sized> {
|
||||||
|
fn start(&self) -> Option<&T>;
|
||||||
|
fn end(&self) -> Option<&T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> RangeArgument<T> for RangeFull {
|
||||||
|
fn start(&self) -> Option<&T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn end(&self) -> Option<&T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RangeArgument<T> for RangeFrom<T> {
|
||||||
|
fn start(&self) -> Option<&T> {
|
||||||
|
Some(&self.start)
|
||||||
|
}
|
||||||
|
fn end(&self) -> Option<&T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RangeArgument<T> for RangeTo<T> {
|
||||||
|
fn start(&self) -> Option<&T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn end(&self) -> Option<&T> {
|
||||||
|
Some(&self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RangeArgument<T> for Range<T> {
|
||||||
|
fn start(&self) -> Option<&T> {
|
||||||
|
Some(&self.start)
|
||||||
|
}
|
||||||
|
fn end(&self) -> Option<&T> {
|
||||||
|
Some(&self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take a range of lines from a string.
|
||||||
|
pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
|
||||||
|
let start = *range.start().unwrap_or(&0);
|
||||||
|
let mut lines = s.lines().skip(start);
|
||||||
|
match range.end() {
|
||||||
|
Some(&end) => lines.take(end).join("\n"),
|
||||||
|
None => lines.join("\n"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::take_lines;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn take_lines_test() {
|
||||||
|
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||||
|
assert_eq!(take_lines(s, 0..3), "Lorem\nipsum\ndolor");
|
||||||
|
assert_eq!(take_lines(s, 3..), "sit\namet");
|
||||||
|
assert_eq!(take_lines(s, ..3), "Lorem\nipsum\ndolor");
|
||||||
|
assert_eq!(take_lines(s, ..), s);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue