From 9a68168f4e8024dc3e0d6847489d400ecfe1a203 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Sat, 30 Mar 2024 08:59:25 +0000 Subject: [PATCH] Add shiftinclude functionality Taken from suggested pull request for upstream: https://github.com/rust-lang/mdBook/pull/2333/ --- src/main.rs | 289 ++++++++++----------------------------- src/string.rs | 365 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 393 insertions(+), 261 deletions(-) diff --git a/src/main.rs b/src/main.rs index f645be5..f66160d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use mdbook::{ use once_cell::sync::Lazy; use regex::{CaptureMatches, Captures, Regex}; use std::{ + cmp::Ordering, fs, io, ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo}, path::{Path, PathBuf}, @@ -20,7 +21,7 @@ use std::{ }; mod string; -use string::{take_anchored_lines, take_lines}; +use string::{take_anchored_lines_with_shift, take_lines_with_shift, Shift}; const ESCAPE_CHAR: char = '\\'; const MAX_LINK_NESTED_DEPTH: usize = 10; @@ -56,7 +57,7 @@ fn main() -> Result<(), Error> { Ok(()) } -/// A pre-processor that acts like `{{#include}}` but allows shifting. +/// A pre-processor for `{{#shiftinclude}}` that acts like `{{#include}}` but allows shifting. #[derive(Default)] pub struct ShiftInclude; @@ -162,7 +163,7 @@ where #[derive(PartialEq, Debug, Clone)] enum LinkType { Escaped, - Include(PathBuf, RangeOrAnchor), + Include(PathBuf, RangeOrAnchor, Shift), } #[derive(PartialEq, Debug, Clone)] @@ -230,7 +231,7 @@ impl LinkType { let base = base.as_ref(); match self { LinkType::Escaped => None, - LinkType::Include(p, _) => Some(return_relative_path(base, &p)), + LinkType::Include(p, _, _) => Some(return_relative_path(base, &p)), } } } @@ -272,13 +273,28 @@ fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor { } } -fn parse_include_path(path: &str) -> LinkType { - let mut parts = path.splitn(2, ':'); +fn parse_shift_include_path(params: &str) -> LinkType { + let mut params = params.splitn(2, ':'); + let param0 = params.next().unwrap(); + let shift = if param0 == "auto" { + Shift::Auto + } else { + let shift: isize = param0.parse().unwrap_or_else(|e| { + log::error!("failed to parse shift amount: {e:?}"); + 0 + }); + match shift.cmp(&0) { + Ordering::Greater => Shift::Right(shift as usize), + Ordering::Equal => Shift::None, + Ordering::Less => Shift::Left(-shift as usize), + } + }; + let mut parts = params.next().unwrap().splitn(2, ':'); let path = parts.next().unwrap().into(); let range_or_anchor = parse_range_or_anchor(parts.next()); - LinkType::Include(path, range_or_anchor) + LinkType::Include(path, range_or_anchor, shift) } #[derive(PartialEq, Debug, Clone)] @@ -297,7 +313,7 @@ impl<'a> Link<'a> { let file_arg = path_props.next(); match (typ.as_str(), file_arg) { - ("include", Some(pth)) => Some(parse_include_path(pth)), + ("shiftinclude", Some(pth)) => Some(parse_shift_include_path(pth)), _ => None, } } @@ -322,13 +338,17 @@ impl<'a> Link<'a> { match self.link_type { // omit the escape char LinkType::Escaped => Ok(self.link_text[1..].to_owned()), - LinkType::Include(ref pat, ref range_or_anchor) => { + LinkType::Include(ref pat, ref range_or_anchor, shift) => { let target = base.join(pat); fs::read_to_string(&target) .map(|s| match range_or_anchor { - RangeOrAnchor::Range(range) => take_lines(&s, range.clone()), - RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor), + RangeOrAnchor::Range(range) => { + take_lines_with_shift(&s, range.clone(), shift) + } + RangeOrAnchor::Anchor(anchor) => { + take_anchored_lines_with_shift(&s, anchor, shift) + } }) .with_context(|| { format!( @@ -385,12 +405,12 @@ mod tests { let start = r" Some text over here. ```hbs - \{{#include file.rs}} << an escaped link! + \{{#shiftinclude 0:file.rs}} << an escaped link! ```"; let end = r" Some text over here. ```hbs - {{#include file.rs}} << an escaped link! + {{#shiftinclude 0:file.rs}} << an escaped link! ```"; assert_eq!(replace_all(start, "", "", 0), end); } @@ -425,133 +445,140 @@ mod tests { #[test] fn test_find_links_with_range() { - let s = "Some random text with {{#include file.rs:10:20}}..."; + let s = "Some random text with {{#shiftinclude 0: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, + end_index: 55, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(9..20)) + RangeOrAnchor::Range(LineRange::from(9..20)), + Shift::None, ), - link_text: "{{#include file.rs:10:20}}", + link_text: "{{#shiftinclude 0:file.rs:10:20}}", }] ); } #[test] fn test_find_links_with_line_number() { - let s = "Some random text with {{#include file.rs:10}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs:10}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 45, + end_index: 52, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(9..10)) + RangeOrAnchor::Range(LineRange::from(9..10)), + Shift::None, ), - link_text: "{{#include file.rs:10}}", + link_text: "{{#shiftinclude 0:file.rs:10}}", }] ); } #[test] fn test_find_links_with_from_range() { - let s = "Some random text with {{#include file.rs:10:}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs:10:}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 46, + end_index: 53, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(9..)) + RangeOrAnchor::Range(LineRange::from(9..)), + Shift::None, ), - link_text: "{{#include file.rs:10:}}", + link_text: "{{#shiftinclude 0:file.rs:10:}}", }] ); } #[test] fn test_find_links_with_to_range() { - let s = "Some random text with {{#include file.rs::20}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs::20}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 46, + end_index: 53, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(..20)) + RangeOrAnchor::Range(LineRange::from(..20)), + Shift::None, ), - link_text: "{{#include file.rs::20}}", + link_text: "{{#shiftinclude 0:file.rs::20}}", }] ); } #[test] fn test_find_links_with_full_range() { - let s = "Some random text with {{#include file.rs::}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs::}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 44, + end_index: 51, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(..)) + RangeOrAnchor::Range(LineRange::from(..)), + Shift::None, ), - link_text: "{{#include file.rs::}}", + link_text: "{{#shiftinclude 0:file.rs::}}", }] ); } #[test] fn test_find_links_with_no_range_specified() { - let s = "Some random text with {{#include file.rs}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 42, + end_index: 49, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Range(LineRange::from(..)) + RangeOrAnchor::Range(LineRange::from(..)), + Shift::None, ), - link_text: "{{#include file.rs}}", + link_text: "{{#shiftinclude 0:file.rs}}", }] ); } #[test] fn test_find_links_with_anchor() { - let s = "Some random text with {{#include file.rs:anchor}}..."; + let s = "Some random text with {{#shiftinclude 0:file.rs:anchor}}..."; let res = find_links(s).collect::>(); println!("\nOUTPUT: {:?}\n", res); assert_eq!( res, vec![Link { start_index: 22, - end_index: 49, + end_index: 56, link_type: LinkType::Include( PathBuf::from("file.rs"), - RangeOrAnchor::Anchor(String::from("anchor")) + RangeOrAnchor::Anchor(String::from("anchor")), + Shift::None, ), - link_text: "{{#include file.rs:anchor}}", + link_text: "{{#shiftinclude 0:file.rs:anchor}}", }] ); } @@ -573,184 +600,4 @@ mod tests { }] ); } - - #[test] - fn parse_without_colon_includes_all() { - let link_type = parse_include_path("arbitrary"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(RangeFull)) - ) - ); - } - - #[test] - fn parse_with_nothing_after_colon_includes_all() { - let link_type = parse_include_path("arbitrary:"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(RangeFull)) - ) - ); - } - - #[test] - fn parse_with_two_colons_includes_all() { - let link_type = parse_include_path("arbitrary::"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(RangeFull)) - ) - ); - } - - #[test] - fn parse_with_garbage_after_two_colons_includes_all() { - let link_type = parse_include_path("arbitrary::NaN"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(RangeFull)) - ) - ); - } - - #[test] - fn parse_with_one_number_after_colon_only_that_line() { - let link_type = parse_include_path("arbitrary:5"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(4..5)) - ) - ); - } - - #[test] - fn parse_with_one_based_start_becomes_zero_based() { - let link_type = parse_include_path("arbitrary:1"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(0..1)) - ) - ); - } - - #[test] - fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() { - let link_type = parse_include_path("arbitrary:0"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(0..1)) - ) - ); - } - - #[test] - fn parse_start_only_range() { - let link_type = parse_include_path("arbitrary:5:"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(4..)) - ) - ); - } - - #[test] - fn parse_start_with_garbage_interpreted_as_start_only_range() { - let link_type = parse_include_path("arbitrary:5:NaN"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(4..)) - ) - ); - } - - #[test] - fn parse_end_only_range() { - let link_type = parse_include_path("arbitrary::5"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(..5)) - ) - ); - } - - #[test] - fn parse_start_and_end_range() { - let link_type = parse_include_path("arbitrary:5:10"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(4..10)) - ) - ); - } - - #[test] - fn parse_with_negative_interpreted_as_anchor() { - let link_type = parse_include_path("arbitrary:-5"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Anchor("-5".to_string()) - ) - ); - } - - #[test] - fn parse_with_floating_point_interpreted_as_anchor() { - let link_type = parse_include_path("arbitrary:-5.7"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Anchor("-5.7".to_string()) - ) - ); - } - - #[test] - fn parse_with_anchor_followed_by_colon() { - let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Anchor("some-anchor".to_string()) - ) - ); - } - - #[test] - fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() { - let link_type = parse_include_path("arbitrary:5:10:17:anything:"); - assert_eq!( - link_type, - LinkType::Include( - PathBuf::from("arbitrary"), - RangeOrAnchor::Range(LineRange::from(4..10)) - ) - ); - } } diff --git a/src/string.rs b/src/string.rs index f593a35..6c5ca85 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,27 +1,101 @@ use once_cell::sync::Lazy; use regex::Regex; +use std::borrow::Cow; use std::ops::Bound::{Excluded, Included, Unbounded}; use std::ops::RangeBounds; -/// Take a range of lines from a string. -pub fn take_lines>(s: &str, range: R) -> String { +/// Indication of whether to shift included text. +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum Shift { + None, + Left(usize), + Right(usize), + /// Strip leftmost whitespace that is common to all lines. + Auto, +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum ExplicitShift { + None, + Left(usize), + Right(usize), +} + +fn common_leading_ws(lines: &[String]) -> String { + let mut common_ws: Option = None; + for line in lines { + if line.is_empty() { + // Don't include empty lines in the calculation. + continue; + } + let ws = line.chars().take_while(|c| c.is_whitespace()); + if let Some(common) = common_ws { + common_ws = Some( + common + .chars() + .zip(ws) + .take_while(|(a, b)| a == b) + .map(|(a, _b)| a) + .collect(), + ); + } else { + common_ws = Some(ws.collect()) + } + } + common_ws.unwrap_or_default() +} + +fn calculate_shift(lines: &[String], shift: Shift) -> ExplicitShift { + match shift { + Shift::None => ExplicitShift::None, + Shift::Left(l) => ExplicitShift::Left(l), + Shift::Right(r) => ExplicitShift::Right(r), + Shift::Auto => ExplicitShift::Left(common_leading_ws(lines).len()), + } +} + +fn shift_line(l: &str, shift: ExplicitShift) -> Cow<'_, str> { + match shift { + ExplicitShift::None => Cow::Borrowed(l), + ExplicitShift::Right(shift) => { + let indent = " ".repeat(shift); + Cow::Owned(format!("{indent}{l}")) + } + ExplicitShift::Left(skip) => { + if l.chars().take(skip).any(|c| !c.is_whitespace()) { + log::error!("left-shifting away non-whitespace"); + } + let rest = l.chars().skip(skip).collect::(); + Cow::Owned(rest) + } + } +} + +fn shift_lines(lines: &[String], shift: Shift) -> Vec> { + let shift = calculate_shift(lines, shift); + lines.iter().map(|l| shift_line(l, shift)).collect() +} + +/// Take a range of lines from a string, shifting all lines left or right. +pub fn take_lines_with_shift>(s: &str, range: R, shift: Shift) -> String { let start = match range.start_bound() { Excluded(&n) => n + 1, Included(&n) => n, Unbounded => 0, }; let lines = s.lines().skip(start); - match range.end_bound() { + let retained = match range.end_bound() { Excluded(end) => lines .take(end.saturating_sub(start)) - .collect::>() - .join("\n"), + .map(|l| l.to_string()) + .collect::>(), Included(end) => lines .take((end + 1).saturating_sub(start)) - .collect::>() - .join("\n"), - Unbounded => lines.collect::>().join("\n"), - } + .map(|l| l.to_string()) + .collect::>(), + Unbounded => lines.map(|l| l.to_string()).collect::>(), + }; + shift_lines(&retained, shift).join("\n") } static ANCHOR_START: Lazy = @@ -29,10 +103,10 @@ static ANCHOR_START: Lazy = static ANCHOR_END: Lazy = Lazy::new(|| Regex::new(r"ANCHOR_END:\s*(?P[\w_-]+)").unwrap()); -/// Take anchored lines from a string. +/// Take anchored lines from a string, shifting all lines left or right. /// Lines containing anchor are ignored. -pub fn take_anchored_lines(s: &str, anchor: &str) -> String { - let mut retained = Vec::<&str>::new(); +pub fn take_anchored_lines_with_shift(s: &str, anchor: &str, shift: Shift) -> String { + let mut retained = Vec::::new(); let mut anchor_found = false; for l in s.lines() { @@ -45,7 +119,7 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String { } None => { if !ANCHOR_START.is_match(l) { - retained.push(l); + retained.push(l.to_string()); } } } @@ -56,52 +130,263 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String { } } - retained.join("\n") + shift_lines(&retained, shift).join("\n") } #[cfg(test)] mod tests { - use super::{take_anchored_lines, take_lines}; + use super::*; + + #[test] + fn common_leading_ws_test() { + let tests = [ + ([" line1", " line2", " line3"], " "), + ([" line1", " line2", "line3"], ""), + (["\t\tline1", "\t\t line2", "\t\tline3"], "\t\t"), + (["\t line1", " \tline2", " \t\tline3"], ""), + ]; + for (lines, want) in tests { + let lines = lines.into_iter().map(|l| l.to_string()).collect::>(); + let got = common_leading_ws(&lines); + assert_eq!(got, want, "for input {lines:?}"); + } + } + + #[test] + fn shift_line_test() { + let s = " Line with 4 space intro"; + assert_eq!(shift_line(s, ExplicitShift::None), s); + assert_eq!( + shift_line(s, ExplicitShift::Left(4)), + "Line with 4 space intro" + ); + assert_eq!( + shift_line(s, ExplicitShift::Left(2)), + " Line with 4 space intro" + ); + assert_eq!( + shift_line(s, ExplicitShift::Left(6)), + "ne with 4 space intro" + ); + assert_eq!( + shift_line(s, ExplicitShift::Right(2)), + " Line with 4 space intro" + ); + } #[test] #[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled - fn take_lines_test() { - let s = "Lorem\nipsum\ndolor\nsit\namet"; - assert_eq!(take_lines(s, 1..3), "ipsum\ndolor"); - assert_eq!(take_lines(s, 3..), "sit\namet"); - assert_eq!(take_lines(s, ..3), "Lorem\nipsum\ndolor"); - assert_eq!(take_lines(s, ..), s); + fn take_lines_with_shift_test() { + let s = " Lorem\n ipsum\n dolor\n sit\n amet"; + assert_eq!( + take_lines_with_shift(s, 1..3, Shift::None), + " ipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, 1..3, Shift::Left(2)), + "ipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, 1..3, Shift::Right(2)), + " ipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, 1..3, Shift::Auto), + "ipsum\n dolor" + ); + assert_eq!(take_lines_with_shift(s, 3.., Shift::None), " sit\n amet"); + assert_eq!( + take_lines_with_shift(s, 3.., Shift::Right(1)), + " sit\n amet" + ); + assert_eq!(take_lines_with_shift(s, 3.., Shift::Left(1)), " sit\n amet"); + assert_eq!( + take_lines_with_shift(s, ..3, Shift::None), + " Lorem\n ipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, ..3, Shift::Auto), + "Lorem\nipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, ..3, Shift::Right(4)), + " Lorem\n ipsum\n dolor" + ); + assert_eq!( + take_lines_with_shift(s, ..3, Shift::Left(4)), + "rem\nsum\ndolor" + ); + assert_eq!(take_lines_with_shift(s, .., Shift::None), s); + assert_eq!( + take_lines_with_shift(s, .., Shift::Auto), + "Lorem\nipsum\n dolor\nsit\namet" + ); // corner cases - assert_eq!(take_lines(s, 4..3), ""); - assert_eq!(take_lines(s, ..100), s); + assert_eq!(take_lines_with_shift(s, 4..3, Shift::None), ""); + assert_eq!(take_lines_with_shift(s, 4..3, Shift::Left(2)), ""); + assert_eq!(take_lines_with_shift(s, 4..3, Shift::Right(2)), ""); + assert_eq!(take_lines_with_shift(s, ..100, Shift::None), s); + assert_eq!( + take_lines_with_shift(s, ..100, Shift::Right(2)), + " Lorem\n ipsum\n dolor\n sit\n amet" + ); + assert_eq!( + take_lines_with_shift(s, ..100, Shift::Left(2)), + "Lorem\nipsum\n dolor\nsit\namet" + ); } #[test] - fn take_anchored_lines_test() { + fn take_anchored_lines_with_shift_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test"), ""); + assert_eq!(take_anchored_lines_with_shift(s, "test", Shift::None), ""); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "" + ); let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test"), ""); + assert_eq!(take_anchored_lines_with_shift(s, "test", Shift::None), ""); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "" + ); - 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\n ipsum\n ANCHOR: test\n dolor\n sit\n amet"; + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::None), + " dolor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + " dolor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Auto), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::None), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::Right(2)), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::Left(2)), + "" + ); - 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\n ipsum\n ANCHOR: test\n dolor\n sit\n amet\n ANCHOR_END: test\n lorem\n ipsum"; + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::None), + " dolor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + " dolor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Auto), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(4)), + "lor\nt\net" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(44)), + "\n\n" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::None), + "" + ); - 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\n ANCHOR: test\n ipsum\n ANCHOR: test\n dolor\n\n\n sit\n amet\n ANCHOR_END: test\n lorem\n ipsum"; + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::None), + " ipsum\n dolor\n\n\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + " ipsum\n dolor\n \n \n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "ipsum\ndolor\n\n\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Auto), + "ipsum\ndolor\n\n\nsit\namet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::None), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::Right(2)), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::Left(2)), + "" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "something", Shift::Auto), + "" + ); - let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum"; + // Include non-ASCII. + 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"; + assert_eq!( + take_anchored_lines_with_shift(s, "test2", Shift::None), + " ípsum\n dôlor\n sit\n amet\n lorem" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test2", Shift::Right(2)), + " ípsum\n dôlor\n sit\n amet\n lorem" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test2", Shift::Left(2)), + "ípsum\ndôlor\nsit\namet\nlorem" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test2", Shift::Left(4)), + "sum\nlor\nt\net\nrem" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::None), + " dôlor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Right(2)), + " dôlor\n sit\n amet" + ); + assert_eq!( + take_anchored_lines_with_shift(s, "test", Shift::Left(2)), + "dôlor\nsit\namet" + ); assert_eq!( - take_anchored_lines(s, "test2"), - "ipsum\ndolor\nsit\namet\nlorem" + take_anchored_lines_with_shift(s, "something", Shift::None), + "" ); - assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something"), ""); } }