diff --git a/src/input/wrapped_input.rs b/src/input/wrapped_input.rs new file mode 100644 index 00000000..5495fe79 --- /dev/null +++ b/src/input/wrapped_input.rs @@ -0,0 +1,180 @@ +use std::ops::RangeBounds; + +use nom::{Compare, InputTake, Slice}; + +#[derive(Debug)] +struct WrappedInput<'s> { + full_source: &'s str, + start: usize, + end: usize, //exclusive + preceding_line_break: Option, +} + +impl<'s> WrappedInput<'s> { + /// Returns a wrapped string that keeps track of values we need for parsing org-mode. + /// + /// Only call this on the full original string. Calling this on a substring can result in invalid values. + pub fn new(input: &'s str) -> Self { + WrappedInput { + full_source: input, + start: 0, + end: input.len(), + preceding_line_break: None, + } + } + + /// Get the text since the line break preceding the start of this WrappedInput. + pub fn text_since_line_break(&self) -> &'s str { + let start = self.preceding_line_break.unwrap_or(0); + &self.full_source[start..self.start] + } +} + +impl<'s> InputTake for WrappedInput<'s> { + fn take(&self, count: usize) -> Self { + self.slice(..count) + } + + fn take_split(&self, count: usize) -> (Self, Self) { + (self.slice(count..), self.slice(..count)) + } +} + +impl<'s, O: Into<&'s str>> Compare for WrappedInput<'s> { + fn compare(&self, t: O) -> nom::CompareResult { + (&self.full_source[self.start..self.end]).compare(t.into()) + } + + fn compare_no_case(&self, t: O) -> nom::CompareResult { + (&self.full_source[self.start..self.end]).compare_no_case(t.into()) + } +} + +impl<'s> From<&'s str> for WrappedInput<'s> { + fn from(value: &'s str) -> Self { + WrappedInput::new(value) + } +} + +impl<'s> From<&WrappedInput<'s>> for &'s str { + fn from(value: &WrappedInput<'s>) -> Self { + &value.full_source[value.start..value.end] + } +} + +impl<'s, R> Slice for WrappedInput<'s> +where + R: RangeBounds, +{ + fn slice(&self, range: R) -> Self { + let new_start = match range.start_bound() { + std::ops::Bound::Included(idx) => self.start + idx, + std::ops::Bound::Excluded(idx) => self.start + idx - 1, + std::ops::Bound::Unbounded => self.start, + }; + let new_end = match range.end_bound() { + std::ops::Bound::Included(idx) => self.start + idx + 1, + std::ops::Bound::Excluded(idx) => self.start + idx, + std::ops::Bound::Unbounded => self.end, + }; + if new_start < self.start { + panic!("Attempted to extend before the start of the WrappedInput.") + } + if new_end > self.end { + panic!("Attempted to extend past the end of the WrappedInput.") + } + + let skipped_text = &self.full_source[self.start..new_start]; + let last_line_feed = skipped_text + .rfind('\n') + .map(|idx| idx + self.preceding_line_break.unwrap_or(0) + 1); + + // TODO: calculate updated values for WrappedInput + WrappedInput { + full_source: self.full_source, + start: new_start, + end: new_end, + preceding_line_break: last_line_feed, + } + } +} + +impl<'s> std::fmt::Display for WrappedInput<'s> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + Into::<&str>::into(self).fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn range() { + let input = WrappedInput::new("foo bar baz"); + let output = input.slice(4..7); + assert_eq!(output.to_string(), "bar"); + } + + #[test] + fn range_to() { + let input = WrappedInput::new("foo bar baz"); + let output = input.slice(..7); + assert_eq!(output.to_string(), "foo bar"); + } + + #[test] + fn range_from() { + let input = WrappedInput::new("foo bar baz"); + let output = input.slice(4..); + assert_eq!(output.to_string(), "bar baz"); + } + + #[test] + fn full_range() { + let input = WrappedInput::new("foo bar baz"); + let output = input.slice(..); + assert_eq!(output.to_string(), "foo bar baz"); + } + + #[test] + fn nested_range() { + let input = WrappedInput::new("lorem foo bar baz ipsum"); + let first_cut = input.slice(6..17); + let output = first_cut.slice(4..7); + assert_eq!(first_cut.to_string(), "foo bar baz"); + assert_eq!(output.to_string(), "bar"); + } + + #[test] + #[should_panic] + fn out_of_bounds() { + let input = WrappedInput::new("lorem foo bar baz ipsum"); + input.slice(6..30); + } + + #[test] + #[should_panic] + fn out_of_nested_bounds() { + let input = WrappedInput::new("lorem foo bar baz ipsum"); + let first_cut = input.slice(6..17); + first_cut.slice(4..14); + } + + #[test] + fn line_break() { + let input = WrappedInput::new("lorem\nfoo\nbar\nbaz\nipsum"); + assert_eq!(input.slice(5..).preceding_line_break, None); + assert_eq!(input.slice(6..).preceding_line_break, Some(6)); + assert_eq!(input.slice(6..).slice(10..).preceding_line_break, Some(14)); + } + + #[test] + fn text_since_line_break() { + let input = WrappedInput::new("lorem\nfoo\nbar\nbaz\nipsum"); + assert_eq!(input.text_since_line_break(), ""); + assert_eq!(input.slice(5..).text_since_line_break(), "lorem"); + assert_eq!(input.slice(6..).text_since_line_break(), ""); + assert_eq!(input.slice(6..).slice(10..).text_since_line_break(), "ba"); + } +}