From e461610dab50be895faec8c3bc30c6718ffb843d Mon Sep 17 00:00:00 2001 From: boxdot Date: Fri, 5 Jan 2018 22:03:30 +0100 Subject: [PATCH] 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. --- Cargo.toml | 1 + src/lib.rs | 1 + src/preprocess/links.rs | 180 +++++++++++++++++++++++++++++++++------- src/utils/mod.rs | 2 + src/utils/string.rs | 70 ++++++++++++++++ 5 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 src/utils/string.rs diff --git a/Cargo.toml b/Cargo.toml index b2bae3fe..4c49432e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ memchr = "2.0.1" open = "1.1" regex = "0.2.1" tempdir = "0.3.4" +itertools = "0.7.4" # Watch feature notify = { version = "4.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 47014ff7..72b4df63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,7 @@ #[macro_use] extern crate error_chain; extern crate handlebars; +extern crate itertools; #[macro_use] extern crate lazy_static; #[macro_use] diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index 8d10244b..fbc1a2f5 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -1,6 +1,8 @@ +use std::ops::{Range, RangeFrom, RangeTo, RangeFull}; use std::path::{Path, PathBuf}; use regex::{CaptureMatches, Captures, Regex}; use utils::fs::file_to_string; +use utils::take_lines; use errors::*; const ESCAPE_CHAR: char = '\\'; @@ -22,14 +24,38 @@ pub fn replace_all>(s: &str, path: P) -> Result { Ok(replaced) } -#[derive(PartialOrd, PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone)] enum LinkType<'a> { Escaped, - Include(PathBuf), + IncludeRange(PathBuf, Range), + IncludeRangeFrom(PathBuf, RangeFrom), + IncludeRangeTo(PathBuf, RangeTo), + IncludeRangeFull(PathBuf, RangeFull), 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::().ok()); + let end = parts.next().and_then(|s| s.parse::().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> { start_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)) { (_, Some(typ), Some(rest)) => { 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(); - match (typ.as_str(), file_path) { - ("include", Some(pth)) => Some(LinkType::Include(pth)), - ("playpen", Some(pth)) => Some(LinkType::Playpen(pth, props)), + match (typ.as_str(), file_arg) { + ("include", Some(pth)) => Some(parse_include_path(pth)), + ("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)), _ => None, } } - (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => { - Some(LinkType::Escaped) - } + (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some( + LinkType::Escaped, + ), _ => None, }; link_type.and_then(|lnk| { cap.get(0).map(|mat| { - Link { - start_index: mat.start(), - end_index: mat.end(), - link: lnk, - link_text: mat.as_str(), - } - }) + Link { + start_index: mat.start(), + end_index: mat.end(), + link: lnk, + link_text: mat.as_str(), + } + }) }) } @@ -74,22 +100,44 @@ impl<'a> Link<'a> { match self.link { // omit the escape char LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), - LinkType::Include(ref pat) => { - file_to_string(base.join(pat)).chain_err(|| { - format!("Could not read file for \ - link {}", - self.link_text) - }) + LinkType::IncludeRange(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::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) => { let contents = file_to_string(base.join(pat)).chain_err(|| { - format!("Could not \ - read file \ - for link {}", - self.link_text) - })?; + format!("Could not read file for link {}", self.link_text) + })?; 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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] fn test_find_links_escaped_link() { let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ..."; @@ -232,7 +352,7 @@ fn test_find_all_link_types() { Link { start_index: 38, end_index: 58, - link: LinkType::Include(PathBuf::from("file.rs")), + link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..), link_text: "{{#include file.rs}}", }); assert_eq!(res[1], diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4c265dcc..b541f84a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,9 +1,11 @@ pub mod fs; +mod string; use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES}; use std::borrow::Cow; +pub use self::string::{RangeArgument, take_lines}; /// /// diff --git a/src/utils/string.rs b/src/utils/string.rs new file mode 100644 index 00000000..8877d376 --- /dev/null +++ b/src/utils/string.rs @@ -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 { + fn start(&self) -> Option<&T>; + fn end(&self) -> Option<&T>; +} + +impl RangeArgument for RangeFull { + fn start(&self) -> Option<&T> { + None + } + fn end(&self) -> Option<&T> { + None + } +} + +impl RangeArgument for RangeFrom { + fn start(&self) -> Option<&T> { + Some(&self.start) + } + fn end(&self) -> Option<&T> { + None + } +} + +impl RangeArgument for RangeTo { + fn start(&self) -> Option<&T> { + None + } + fn end(&self) -> Option<&T> { + Some(&self.end) + } +} + +impl RangeArgument for Range { + 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>(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); + } +}