-
Notifications
You must be signed in to change notification settings - Fork 290
Fix empty Text event emitted with trim_text_end (#984) #997
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ macro_rules! impl_buffered_source { | |
| &mut self, | ||
| buf: &'b mut Vec<u8>, | ||
| position: &mut u64, | ||
| trim_text_end: bool, | ||
| ) -> ReadTextResult<'b, &'b mut Vec<u8>> { | ||
| let mut read = 0; | ||
| let start = buf.len(); | ||
|
|
@@ -83,6 +84,9 @@ macro_rules! impl_buffered_source { | |
| read += i as u64; | ||
|
|
||
| *position += read; | ||
| if trim_text_end && buf[start..].iter().all(|&b| is_whitespace(b)) { | ||
| return ReadTextResult::Markup(buf); | ||
| } | ||
| return match std::str::from_utf8(&buf[start..]) { | ||
| Ok(s) => ReadTextResult::UpToMarkup(s), | ||
| Err(e) => ReadTextResult::Err(e.into()), | ||
|
|
@@ -95,6 +99,9 @@ macro_rules! impl_buffered_source { | |
| read += i as u64; | ||
|
|
||
| *position += read; | ||
| if trim_text_end && buf[start..].iter().all(|&b| is_whitespace(b)) { | ||
| return ReadTextResult::Ref(buf); | ||
| } | ||
|
Comment on lines
+102
to
+104
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is wrong. We do not want to trim spaces before entity references, otherwise
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, but given the existing tests pass, we probably need some additional test cases around all of this. |
||
| return match std::str::from_utf8(&buf[start..]) { | ||
| Ok(s) => ReadTextResult::UpToRef(s), | ||
| Err(e) => ReadTextResult::Err(e.into()), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -325,7 +325,11 @@ macro_rules! read_event_impl { | |
| $reader.skip_whitespace(&mut $self.state.offset) $(.$await)? ?; | ||
| } | ||
|
|
||
| match $reader.read_text($buf, &mut $self.state.offset) $(.$await)? { | ||
| match $reader.read_text( | ||
| $buf, | ||
| &mut $self.state.offset, | ||
| $self.state.config.trim_text_end, | ||
| ) $(.$await)? { | ||
| ReadTextResult::Markup(buf) => { | ||
| $self.state.state = ParseState::InsideMarkup; | ||
| // Pass `buf` to the next next iteration of parsing loop | ||
|
|
@@ -340,15 +344,10 @@ macro_rules! read_event_impl { | |
| } | ||
| ReadTextResult::UpToMarkup(bytes) => { | ||
| $self.state.state = ParseState::InsideMarkup; | ||
| // FIXME: Can produce an empty event if: | ||
| // - event contains only spaces | ||
| // - trim_text_start = false | ||
| // - trim_text_end = true | ||
| Ok(Event::Text($self.state.emit_text(bytes)?)) | ||
| } | ||
| ReadTextResult::UpToRef(bytes) => { | ||
| $self.state.state = ParseState::InsideRef; | ||
| // Return Text event with `bytes` content or Eof if bytes is empty | ||
| Ok(Event::Text($self.state.emit_text(bytes)?)) | ||
| } | ||
| ReadTextResult::UpToEof(bytes) => { | ||
|
|
@@ -493,17 +492,19 @@ macro_rules! read_to_end { | |
| // it is important that this position indicates beginning of the End event. | ||
| // If between last event and the End event would be only spaces, then we | ||
| // take position before the spaces, but spaces would be skipped without | ||
| // generating event if `trim_text_start` is set to `true`. To prevent that | ||
| // we temporary disable start text trimming. | ||
| // generating an event if text trimming is enabled. To prevent that we | ||
| // temporarily disable text trimming. | ||
| // | ||
| // We also cannot take position after getting End event, because if | ||
| // `trim_markup_names_in_closing_tags` is set to `true` (which is the default), | ||
| // we do not known the real size of the End event that it is occupies in | ||
| // the source and cannot correct the position after the End event. | ||
| // So, we in any case should tweak parser configuration. | ||
| let config = $self.config_mut(); | ||
| let trim = config.trim_text_start; | ||
| let trim_start = config.trim_text_start; | ||
| let trim_end = config.trim_text_end; | ||
| config.trim_text_start = false; | ||
| config.trim_text_end = false; | ||
|
|
||
| let start = $self.buffer_position(); | ||
| let mut depth = 0; | ||
|
|
@@ -512,20 +513,26 @@ macro_rules! read_to_end { | |
| let end = $self.buffer_position(); | ||
| match $self.$read_event($buf) $(.$await)? { | ||
| Err(e) => { | ||
| $self.config_mut().trim_text_start = trim; | ||
| let config = $self.config_mut(); | ||
| config.trim_text_start = trim_start; | ||
| config.trim_text_end = trim_end; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really like the clear->restore pattern, but it was already like this. Would be nice to refactor this away
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I also does not like it here... I was thinking about removing auto-trim altogether and making it an explicit method call on the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I probably don't agree with that, at least in this specific case it structurally changes what comes out of the parser. If someone doesn't want to deal with whitespace-only text events at all (and I understand that completely, it's really quite annoying, I use trim for precisely that reason) manual trim wouldn't help.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have an old partial commit from 2022 which changes the configuration options slightly. I think this would make a lot more sense (but I think might be more difficult to implement now, will need to think about how to make it work) - /// Trims leading whitespace in Text events, skip the element if text is empty
- pub trim_text_start: bool,
- /// Trims trailing whitespace in Text events.
- pub trim_text_end: bool,
+ /// Preserves whitespace-only text elements between events (e.g. indented "pretty" formatting).
+ pub preserve_indentation: bool,
+ /// Trims leading and trailing whitespace in Text events.
+ pub trim_text: bool,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can file an issue for that if you agree.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I doubt that we can reliable detect indentation and I think that is not the work for parser at all. |
||
| return Err(e); | ||
| } | ||
|
|
||
| Ok(Event::Start(e)) if e.name() == $end => depth += 1, | ||
| Ok(Event::End(e)) if e.name() == $end => { | ||
| if depth == 0 { | ||
| $self.config_mut().trim_text_start = trim; | ||
| let config = $self.config_mut(); | ||
| config.trim_text_start = trim_start; | ||
| config.trim_text_end = trim_end; | ||
| break start..end; | ||
| } | ||
| depth -= 1; | ||
| } | ||
| Ok(Event::Eof) => { | ||
| $self.config_mut().trim_text_start = trim; | ||
| let config = $self.config_mut(); | ||
| config.trim_text_start = trim_start; | ||
| config.trim_text_end = trim_end; | ||
| return Err(Error::missed_end($end)); | ||
| } | ||
| _ => (), | ||
|
|
@@ -998,16 +1005,19 @@ impl<R> Reader<R> { | |
| /// Result of an attempt to read XML textual data from the source. | ||
| #[derive(Debug)] | ||
| enum ReadTextResult<'r, B> { | ||
| /// Start of markup (`<` character) was found in the first byte. `<` was consumed. | ||
| /// Contains buffer that should be returned back to the next iteration cycle | ||
| /// to satisfy borrow checker requirements. | ||
| /// The reader is positioned at `<` (start of markup). `<` was not consumed. | ||
| /// Returned either when `<` was the first byte, or when `trim_text_end` is | ||
| /// enabled and only whitespace preceded it (that whitespace was consumed and | ||
| /// discarded). Contains buffer that should be returned back to the next | ||
| /// iteration cycle to satisfy borrow checker requirements. | ||
| Markup(B), | ||
| /// Start of reference (`&` character) was found in the first byte. | ||
| /// `&` was not consumed. | ||
| /// Contains buffer that should be returned back to the next iteration cycle | ||
| /// to satisfy borrow checker requirements. | ||
| /// The reader is positioned at `&` (start of a reference). `&` was not consumed. | ||
| /// Returned either when `&` was the first byte, or when `trim_text_end` is | ||
| /// enabled and only whitespace preceded it (that whitespace was consumed and | ||
| /// discarded). Contains buffer that should be returned back to the next | ||
| /// iteration cycle to satisfy borrow checker requirements. | ||
| Ref(B), | ||
| /// Contains text block up to start of markup (`<` character). `<` was consumed. | ||
| /// Contains text block up to start of markup (`<` character). `<` was not consumed. | ||
| UpToMarkup(&'r str), | ||
| /// Contains text block up to start of reference (`&` character). | ||
| /// `&` was not consumed. | ||
|
|
@@ -1069,9 +1079,16 @@ trait XmlSource<'r, B> { | |
| /// - `buf`: Buffer that could be filled from an input (`Self`) and | ||
| /// from which [events] could borrow their data | ||
| /// - `position`: Will be increased by amount of bytes consumed | ||
| /// - `trim_text_end`: When true, a whitespace-only text block is suppressed | ||
| /// (returns [`ReadTextResult::Markup`] or [`ReadTextResult::Ref`] instead) | ||
| /// | ||
| /// [events]: crate::events::Event | ||
| fn read_text(&mut self, buf: B, position: &mut u64) -> ReadTextResult<'r, B>; | ||
| fn read_text( | ||
| &mut self, | ||
| buf: B, | ||
| position: &mut u64, | ||
| trim_text_end: bool, | ||
| ) -> ReadTextResult<'r, B>; | ||
|
|
||
| /// Read input until end of general reference (the `;`) is found, start of | ||
| /// another general reference (the `&`) is found or end of input is reached. | ||
|
|
@@ -1646,7 +1663,7 @@ mod test { | |
| let mut input = b"".as_ref(); | ||
| // ^= 1 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::UpToEof(bytes) => assert_eq!(bytes, ""), | ||
| x => panic!("Expected `UpToEof(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -1660,7 +1677,7 @@ mod test { | |
| let mut input = b"<".as_ref(); | ||
| // ^= 1 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::Markup(b) => assert_eq!(b, $buf), | ||
| x => panic!("Expected `Markup(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -1674,7 +1691,7 @@ mod test { | |
| let mut input = b"&".as_ref(); | ||
| // ^= 1 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::Ref(b) => assert_eq!(b, $buf), | ||
| x => panic!("Expected `Ref(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -1688,7 +1705,7 @@ mod test { | |
| let mut input = b"a<".as_ref(); | ||
| // ^= 2 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::UpToMarkup(bytes) => assert_eq!(bytes, "a"), | ||
| x => panic!("Expected `UpToMarkup(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -1702,7 +1719,7 @@ mod test { | |
| let mut input = b"a&".as_ref(); | ||
| // ^= 2 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::UpToRef(bytes) => assert_eq!(bytes, "a"), | ||
| x => panic!("Expected `UpToRef(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -1716,7 +1733,7 @@ mod test { | |
| let mut input = b"a".as_ref(); | ||
| // ^= 2 | ||
|
|
||
| match $source(&mut input).read_text(buf, &mut position) $(.$await)? { | ||
| match $source(&mut input).read_text(buf, &mut position, false) $(.$await)? { | ||
| ReadTextResult::UpToEof(bytes) => assert_eq!(bytes, "a"), | ||
| x => panic!("Expected `UpToEof(_)`, but got `{:?}`", x), | ||
| } | ||
|
|
@@ -2060,7 +2077,7 @@ mod test { | |
|
|
||
| /// Ensures, that no empty `Text` events are generated | ||
| mod $read_event { | ||
| use crate::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event}; | ||
| use crate::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesRef, BytesStart, BytesText, Event}; | ||
| use crate::reader::Reader; | ||
| use pretty_assertions::assert_eq; | ||
|
|
||
|
|
@@ -2150,6 +2167,55 @@ mod test { | |
| ); | ||
| } | ||
|
|
||
| #[$test] | ||
| $($async)? fn trim_text_end_skips_empty_text_before_markup() { | ||
| let mut reader = Reader::from_str(" <tag/>"); | ||
| reader.config_mut().trim_text_end = true; | ||
|
|
||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Empty(BytesStart::new("tag")) | ||
| ); | ||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Eof | ||
| ); | ||
| } | ||
|
|
||
| #[$test] | ||
| $($async)? fn trim_text_end_skips_empty_text_before_reference() { | ||
| let mut reader = Reader::from_str(" &"); | ||
| reader.config_mut().trim_text_end = true; | ||
|
|
||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::GeneralRef(BytesRef::new("amp")) | ||
| ); | ||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Eof | ||
| ); | ||
| } | ||
|
|
||
| #[$test] | ||
| $($async)? fn trim_text_end_preserves_non_empty_text() { | ||
| let mut reader = Reader::from_str(" text <tag/>"); | ||
| reader.config_mut().trim_text_end = true; | ||
|
|
||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Text(BytesText::new(" text")) | ||
| ); | ||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Empty(BytesStart::new("tag")) | ||
| ); | ||
| assert_eq!( | ||
| reader.$read_event($buf) $(.$await)? .unwrap(), | ||
| Event::Eof | ||
| ); | ||
| } | ||
|
|
||
| #[$test] | ||
| $($async)? fn empty() { | ||
| let mut reader = Reader::from_str("<tag/>"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this is not the correct place for trim. I would trim spaces before comments and CDATA sections. It seems totally wrong to me.