diff --git a/src/parser/drawer.rs b/src/parser/drawer.rs index d36c260d..7afd5536 100644 --- a/src/parser/drawer.rs +++ b/src/parser/drawer.rs @@ -13,7 +13,9 @@ use nom::sequence::tuple; use super::affiliated_keyword::parse_affiliated_keywords; use super::org_source::OrgSource; +use super::paragraph::empty_paragraph; use super::util::maybe_consume_trailing_whitespace_if_not_exiting; +use crate::context::bind_context; use crate::context::parser_with_context; use crate::context::ContextElement; use crate::context::ExitClass; @@ -22,7 +24,6 @@ use crate::context::RefContext; use crate::error::CustomError; use crate::error::Res; use crate::parser::element_parser::element; -use crate::parser::util::blank_line; use crate::parser::util::exit_matcher_parser; use crate::parser::util::get_consumed; use crate::parser::util::immediate_in_section; @@ -31,7 +32,6 @@ use crate::parser::util::WORD_CONSTITUENT_CHARACTERS; use crate::types::Drawer; use crate::types::Element; use crate::types::Keyword; -use crate::types::Paragraph; #[cfg_attr( feature = "tracing", @@ -107,23 +107,14 @@ fn children<'b, 'g, 'r, 's>( let element_matcher = parser_with_context!(element(true))(context); let exit_matcher = parser_with_context!(exit_matcher_parser)(context); - let (remaining, children) = match tuple(( - not(exit_matcher), - blank_line, - many_till(blank_line, exit_matcher), - ))(input) + if let Ok((remaining, (_not_exit, empty_para))) = + tuple((not(exit_matcher), bind_context!(empty_paragraph, context)))(input) { - Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => { - let source = get_consumed(input, remain); - let element = Element::Paragraph(Paragraph::of_text(source.into(), first_line.into())); - (remain, vec![element]) - } - Err(_) => { - let (remaining, (children, _exit_contents)) = - many_till(element_matcher, exit_matcher)(input)?; - (remaining, children) - } - }; + return Ok((remaining, vec![Element::Paragraph(empty_para)])); + } + + let (remaining, (children, _exit_contents)) = many_till(element_matcher, exit_matcher)(input)?; + Ok((remaining, children)) } diff --git a/src/parser/paragraph.rs b/src/parser/paragraph.rs index badfea43..237dce1c 100644 --- a/src/parser/paragraph.rs +++ b/src/parser/paragraph.rs @@ -1,6 +1,8 @@ use nom::branch::alt; +use nom::character::complete::space1; use nom::combinator::consumed; use nom::combinator::eof; +use nom::combinator::opt; use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many1; @@ -13,6 +15,7 @@ use super::org_source::OrgSource; use super::util::blank_line; use super::util::get_consumed; use super::util::maybe_consume_trailing_whitespace_if_not_exiting; +use super::util::org_line_ending; use crate::context::parser_with_context; use crate::context::ContextElement; use crate::context::ExitClass; @@ -72,6 +75,57 @@ where )) } +#[cfg_attr( + feature = "tracing", + tracing::instrument(ret, level = "debug", skip(context)) +)] +pub(crate) fn empty_paragraph<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, Paragraph<'s>> { + // If it is just a single newline then source, contents, and post-blank are "\n". + // If it has multiple newlines then contents is the first "\n" and post-blank is all the new lines. + // If there are any spaces on the first line then post-blank excludes the first line. + + let exit_matcher = parser_with_context!(exit_matcher_parser)(context); + + let (remaining, first_line_with_spaces) = + opt(recognize(tuple((space1, org_line_ending))))(input)?; + + let post_blank_begin = remaining; + + if let Some(first_line_with_spaces) = first_line_with_spaces { + let (remaining, _additional_lines) = + recognize(many_till(blank_line, exit_matcher))(remaining)?; + let post_blank = get_consumed(post_blank_begin, remaining); + let source = get_consumed(input, remaining); + Ok(( + remaining, + Paragraph::of_text_full( + Into::<&str>::into(source), + Into::<&str>::into(first_line_with_spaces), + Some(Into::<&str>::into(first_line_with_spaces)), + Some(Into::<&str>::into(post_blank)), + ), + )) + } else { + let (remaining, first_line) = blank_line(remaining)?; + let (remaining, _additional_lines) = + recognize(many_till(blank_line, exit_matcher))(remaining)?; + let post_blank = get_consumed(post_blank_begin, remaining); + let source = get_consumed(input, remaining); + Ok(( + remaining, + Paragraph::of_text_full( + Into::<&str>::into(source), + Into::<&str>::into(first_line), + Some(Into::<&str>::into(first_line)), + Some(Into::<&str>::into(post_blank)), + ), + )) + } +} + #[cfg_attr( feature = "tracing", tracing::instrument(ret, level = "debug", skip(context)) @@ -99,6 +153,7 @@ mod tests { use crate::context::List; use crate::parser::element_parser::element; use crate::parser::org_source::OrgSource; + use crate::parser::paragraph::empty_paragraph; use crate::types::StandardProperties; #[test] @@ -115,4 +170,17 @@ mod tests { assert_eq!(first_paragraph.get_source(), "foo bar baz\n\n"); assert_eq!(second_paragraph.get_source(), "lorem ipsum"); } + + #[test] + fn paragraph_whitespace() { + let input = OrgSource::new("\n"); + let global_settings = GlobalSettings::default(); + let initial_context = ContextElement::document_context(); + let initial_context = Context::new(&global_settings, List::new(&initial_context)); + let paragraph_matcher = bind_context!(empty_paragraph, &initial_context); + let (remaining, paragraph) = paragraph_matcher(input).expect("Parse paragraph"); + assert_eq!(Into::<&str>::into(remaining), ""); + assert_eq!(paragraph.get_source(), "\n"); + assert_eq!(paragraph.get_contents(), Some("\n")); + } } diff --git a/src/parser/property_drawer.rs b/src/parser/property_drawer.rs index 40197f43..4191f60d 100644 --- a/src/parser/property_drawer.rs +++ b/src/parser/property_drawer.rs @@ -78,7 +78,11 @@ pub(crate) fn property_drawer<'b, 'g, 'r, 's>( PropertyDrawer { source: source.into(), children, - contents: Some(contents.into()), + contents: if contents.len() > 0 { + Some(contents.into()) + } else { + None + }, post_blank: post_blank.map(Into::<&str>::into), }, )) diff --git a/src/types/lesser_element.rs b/src/types/lesser_element.rs index 49a4d760..ab696b4d 100644 --- a/src/types/lesser_element.rs +++ b/src/types/lesser_element.rs @@ -204,10 +204,29 @@ impl<'s> Paragraph<'s> { /// /// This is used for elements that support an "empty" content like greater blocks. pub(crate) fn of_text(source: &'s str, body: &'s str) -> Self { + // TODO: This should be replaced with of_text_full. Paragraph { source, - contents: None, // TODO - post_blank: None, // TODO + contents: None, + post_blank: None, + affiliated_keywords: AffiliatedKeywords::default(), + children: vec![Object::PlainText(PlainText { source: body })], + } + } + + /// Generate a paragraph of the passed in text with no additional properties. + /// + /// This is used for elements that support an "empty" content like greater blocks. + pub(crate) fn of_text_full( + source: &'s str, + body: &'s str, + contents: Option<&'s str>, + post_blank: Option<&'s str>, + ) -> Self { + Paragraph { + source, + contents, + post_blank, affiliated_keywords: AffiliatedKeywords::default(), children: vec![Object::PlainText(PlainText { source: body })], }