From 948b1be2dbe757b99179200083ae43c1e8f8bcd0 Mon Sep 17 00:00:00 2001 From: PoiScript Date: Mon, 28 Oct 2019 13:33:18 +0800 Subject: [PATCH] feat: pre_blank and post_blank --- src/elements/block.rs | 60 ++++++++++++-- src/elements/clock.rs | 19 ++++- src/elements/comment.rs | 36 +++++++++ src/elements/drawer.rs | 59 ++++++++++++-- src/elements/dyn_block.rs | 19 ++++- src/elements/fixed_width.rs | 63 +++++++++++++++ src/elements/fn_def.rs | 29 +++++-- src/elements/keyword.rs | 31 ++++--- src/elements/mod.rs | 39 ++++----- src/elements/rule.rs | 62 +++++++------- src/elements/table.rs | 2 +- src/elements/title.rs | 39 ++++++--- src/export/html.rs | 34 ++++---- src/export/org.rs | 157 ++++++++++++++++++++++++++---------- src/headline.rs | 4 +- src/org.rs | 10 ++- src/parsers.rs | 156 +++++++++++++++++++++++------------ src/validate.rs | 4 +- tests/blank.rs | 66 +++++++++++++++ 19 files changed, 678 insertions(+), 211 deletions(-) create mode 100644 src/elements/comment.rs create mode 100644 src/elements/fixed_width.rs create mode 100644 tests/blank.rs diff --git a/src/elements/block.rs b/src/elements/block.rs index acc5610..744430f 100644 --- a/src/elements/block.rs +++ b/src/elements/block.rs @@ -5,7 +5,7 @@ use nom::{ sequence::preceded, IResult, }; -use crate::parsers::{line, take_lines_while}; +use crate::parsers::{blank_lines, line, take_lines_while}; /// Special Block Element #[derive(Debug)] @@ -13,9 +13,14 @@ use crate::parsers::{line, take_lines_while}; #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct SpecialBlock<'a> { /// Optional block parameters + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub parameters: Option>, /// Block name pub name: Cow<'a, str>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl SpecialBlock<'_> { @@ -23,6 +28,8 @@ impl SpecialBlock<'_> { SpecialBlock { name: self.name.into_owned().into(), parameters: self.parameters.map(Into::into).map(Cow::Owned), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -33,13 +40,20 @@ impl SpecialBlock<'_> { #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct QuoteBlock<'a> { /// Optional block parameters + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub parameters: Option>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl QuoteBlock<'_> { pub fn into_owned(self) -> QuoteBlock<'static> { QuoteBlock { parameters: self.parameters.map(Into::into).map(Cow::Owned), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -50,13 +64,20 @@ impl QuoteBlock<'_> { #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct CenterBlock<'a> { /// Optional block parameters + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub parameters: Option>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl CenterBlock<'_> { pub fn into_owned(self) -> CenterBlock<'static> { CenterBlock { parameters: self.parameters.map(Into::into).map(Cow::Owned), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -67,13 +88,20 @@ impl CenterBlock<'_> { #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct VerseBlock<'a> { /// Optional block parameters + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub parameters: Option>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl VerseBlock<'_> { pub fn into_owned(self) -> VerseBlock<'static> { VerseBlock { parameters: self.parameters.map(Into::into).map(Cow::Owned), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -83,9 +111,12 @@ impl VerseBlock<'_> { #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct CommentBlock<'a> { + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub data: Option>, /// Comment, without block's boundaries pub contents: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl CommentBlock<'_> { @@ -93,6 +124,7 @@ impl CommentBlock<'_> { CommentBlock { data: self.data.map(Into::into).map(Cow::Owned), contents: self.contents.into_owned().into(), + post_blank: self.post_blank, } } } @@ -102,9 +134,12 @@ impl CommentBlock<'_> { #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct ExampleBlock<'a> { + #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub data: Option>, /// Block contents pub contents: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl ExampleBlock<'_> { @@ -112,6 +147,7 @@ impl ExampleBlock<'_> { ExampleBlock { data: self.data.map(Into::into).map(Cow::Owned), contents: self.contents.into_owned().into(), + post_blank: self.post_blank, } } } @@ -124,6 +160,8 @@ pub struct ExportBlock<'a> { pub data: Cow<'a, str>, /// Block contents pub contents: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl ExportBlock<'_> { @@ -131,12 +169,13 @@ impl ExportBlock<'_> { ExportBlock { data: self.data.into_owned().into(), contents: self.contents.into_owned().into(), + post_blank: self.post_blank, } } } /// Src Block Element -#[derive(Debug)] +#[derive(Debug, Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] pub struct SourceBlock<'a> { @@ -145,6 +184,8 @@ pub struct SourceBlock<'a> { /// Language of the code in the block pub language: Cow<'a, str>, pub arguments: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl SourceBlock<'_> { @@ -153,6 +194,7 @@ impl SourceBlock<'_> { language: self.language.into_owned().into(), arguments: self.arguments.into_owned().into(), contents: self.contents.into_owned().into(), + post_blank: self.post_blank, } } @@ -164,20 +206,21 @@ impl SourceBlock<'_> { } #[inline] -pub fn parse_block_element(input: &str) -> Option<(&str, (&str, Option<&str>, &str))> { +pub fn parse_block_element(input: &str) -> Option<(&str, (&str, Option<&str>, &str, usize))> { parse_block_element_internal::<()>(input).ok() } #[inline] fn parse_block_element_internal<'a, E: ParseError<&'a str>>( input: &'a str, -) -> IResult<&str, (&str, Option<&str>, &str), E> { +) -> IResult<&str, (&str, Option<&str>, &str, usize), E> { let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?; let (input, args) = line(input)?; let end_line = format!("#+END_{}", name); let (input, contents) = take_lines_while(|line| !line.trim().eq_ignore_ascii_case(&end_line))(input); let (input, _) = line(input)?; + let (input, blank) = blank_lines(input); Ok(( input, @@ -189,6 +232,7 @@ fn parse_block_element_internal<'a, E: ParseError<&'a str>>( Some(args.trim()) }, contents, + blank, ), )) } @@ -202,20 +246,21 @@ fn parse() { r#"#+BEGIN_SRC #+END_SRC"# ), - Ok(("", ("SRC".into(), None, ""))) + Ok(("", ("SRC".into(), None, "", 0))) ); assert_eq!( parse_block_element_internal::>( r#"#+begin_src #+end_src"# ), - Ok(("", ("src".into(), None, ""))) + Ok(("", ("src".into(), None, "", 0))) ); assert_eq!( parse_block_element_internal::>( r#"#+BEGIN_SRC javascript console.log('Hello World!'); #+END_SRC + "# ), Ok(( @@ -223,7 +268,8 @@ console.log('Hello World!'); ( "SRC".into(), Some("javascript".into()), - "console.log('Hello World!');\n" + "console.log('Hello World!');\n", + 1 ) )) ); diff --git a/src/elements/clock.rs b/src/elements/clock.rs index 032e314..a0b0b58 100644 --- a/src/elements/clock.rs +++ b/src/elements/clock.rs @@ -11,7 +11,7 @@ use nom::{ use crate::elements::timestamp::{parse_inactive, Datetime, Timestamp}; -use crate::parsers::eol; +use crate::parsers::{blank_lines, eol}; /// Clock Element #[cfg_attr(test, derive(PartialEq))] @@ -31,6 +31,8 @@ pub enum Clock<'a> { delay: Option>, /// Clock duration duration: Cow<'a, str>, + /// Numbers of blank lines + post_blank: usize, }, /// Running Clock Running { @@ -40,6 +42,8 @@ pub enum Clock<'a> { repeater: Option>, #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] delay: Option>, + /// Numbers of blank lines + post_blank: usize, }, } @@ -56,21 +60,25 @@ impl Clock<'_> { repeater, delay, duration, + post_blank, } => Clock::Closed { start: start.into_owned(), end: end.into_owned(), repeater: repeater.map(Into::into).map(Cow::Owned), delay: delay.map(Into::into).map(Cow::Owned), duration: duration.into_owned().into(), + post_blank, }, Clock::Running { start, repeater, delay, + post_blank, } => Clock::Running { start: start.into_owned(), repeater: repeater.map(Into::into).map(Cow::Owned), delay: delay.map(Into::into).map(Cow::Owned), + post_blank, }, } } @@ -118,6 +126,7 @@ impl Clock<'_> { start, repeater, delay, + .. } => Timestamp::Inactive { start: start.clone(), repeater: repeater.clone(), @@ -144,6 +153,7 @@ fn parse_clock<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Cloc let (input, _) = space0(input)?; let (input, duration) = recognize(separated_pair(digit1, char(':'), digit1))(input)?; let (input, _) = eol(input)?; + let (input, blank) = blank_lines(input); Ok(( input, Clock::Closed { @@ -152,6 +162,7 @@ fn parse_clock<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Cloc repeater, delay, duration: duration.into(), + post_blank: blank, }, )) } @@ -161,12 +172,14 @@ fn parse_clock<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Cloc delay, } => { let (input, _) = eol(input)?; + let (input, blank) = blank_lines(input); Ok(( input, Clock::Running { start, repeater, delay, + post_blank: blank, }, )) } @@ -195,12 +208,13 @@ fn parse() { }, repeater: None, delay: None, + post_blank: 0, } )) ); assert_eq!( parse_clock::>( - "CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00" + "CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00\n\n" ), Ok(( "", @@ -224,6 +238,7 @@ fn parse() { repeater: None, delay: None, duration: "1:00".into(), + post_blank: 1, } )) ); diff --git a/src/elements/comment.rs b/src/elements/comment.rs new file mode 100644 index 0000000..bf50bc4 --- /dev/null +++ b/src/elements/comment.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; + +use crate::parsers::{blank_lines, take_lines_while}; + +#[derive(Debug, Default)] +#[cfg_attr(feature = "ser", derive(serde::Serialize))] +pub struct Comment<'a> { + pub value: Cow<'a, str>, + pub post_blank: usize, +} + +impl Comment<'_> { + pub(crate) fn parse(input: &str) -> Option<(&str, Comment<'_>)> { + let (input, value) = take_lines_while(|line| line == "#" || line.starts_with("# "))(input); + let (input, blank) = blank_lines(input); + + if value.is_empty() { + return None; + } + + Some(( + input, + Comment { + value: value.into(), + post_blank: blank, + }, + )) + } + + pub fn into_owned(self) -> Comment<'static> { + Comment { + value: self.value.into_owned().into(), + post_blank: self.post_blank, + } + } +} diff --git a/src/elements/drawer.rs b/src/elements/drawer.rs index cbdeb08..73eef7d 100644 --- a/src/elements/drawer.rs +++ b/src/elements/drawer.rs @@ -7,15 +7,19 @@ use nom::{ IResult, }; -use crate::parsers::{eol, line, take_lines_while}; +use crate::parsers::{blank_lines, eol, line, take_lines_while}; /// Drawer Element +#[derive(Debug, Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] -#[derive(Debug)] pub struct Drawer<'a> { /// Drawer name pub name: Cow<'a, str>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl Drawer<'_> { @@ -26,6 +30,8 @@ impl Drawer<'_> { pub fn into_owned(self) -> Drawer<'static> { Drawer { name: self.name.into_owned().into(), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -33,6 +39,20 @@ impl Drawer<'_> { #[inline] pub fn parse_drawer<'a, E: ParseError<&'a str>>( input: &'a str, +) -> IResult<&str, (Drawer, &str), E> { + let (input, (mut drawer, content)) = parse_drawer_without_blank(input)?; + + let (content, blank) = blank_lines(content); + drawer.pre_blank = blank; + + let (input, blank) = blank_lines(input); + drawer.post_blank = blank; + + Ok((input, (drawer, content))) +} + +pub fn parse_drawer_without_blank<'a, E: ParseError<&'a str>>( + input: &'a str, ) -> IResult<&str, (Drawer, &str), E> { let (input, name) = delimited( tag(":"), @@ -44,7 +64,17 @@ pub fn parse_drawer<'a, E: ParseError<&'a str>>( take_lines_while(|line| !line.trim().eq_ignore_ascii_case(":END:"))(input); let (input, _) = line(input)?; - Ok((input, (Drawer { name: name.into() }, contents))) + Ok(( + input, + ( + Drawer { + name: name.into(), + pre_blank: 0, + post_blank: 0, + }, + contents, + ), + )) } #[test] @@ -52,24 +82,39 @@ fn parse() { use nom::error::VerboseError; assert_eq!( - parse_drawer::>(":PROPERTIES:\n :CUSTOM_ID: id\n :END:"), + parse_drawer::>( + r#":PROPERTIES: + :CUSTOM_ID: id + :END:"# + ), Ok(( "", ( Drawer { - name: "PROPERTIES".into() + name: "PROPERTIES".into(), + pre_blank: 0, + post_blank: 0 }, " :CUSTOM_ID: id\n" ) )) ); assert_eq!( - parse_drawer::>(":PROPERTIES:\n :END:"), + parse_drawer::>( + r#":PROPERTIES: + + + :END: + +"# + ), Ok(( "", ( Drawer { - name: "PROPERTIES".into() + name: "PROPERTIES".into(), + pre_blank: 2, + post_blank: 1, }, "" ) diff --git a/src/elements/dyn_block.rs b/src/elements/dyn_block.rs index a27052c..77c8fa8 100644 --- a/src/elements/dyn_block.rs +++ b/src/elements/dyn_block.rs @@ -7,18 +7,22 @@ use nom::{ IResult, }; -use crate::parsers::{line, take_lines_while}; +use crate::parsers::{blank_lines, line, take_lines_while}; /// Dynamic Block Element +#[derive(Debug, Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] -#[derive(Debug)] pub struct DynBlock<'a> { /// Block name pub block_name: Cow<'a, str>, /// Block argument #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub arguments: Option>, + /// Numbers of blank lines + pub pre_blank: usize, + /// Numbers of blank lines + pub post_blank: usize, } impl DynBlock<'_> { @@ -30,6 +34,8 @@ impl DynBlock<'_> { DynBlock { block_name: self.block_name.into_owned().into(), arguments: self.arguments.map(Into::into).map(Cow::Owned), + pre_blank: self.pre_blank, + post_blank: self.post_blank, } } } @@ -44,7 +50,9 @@ fn parse_dyn_block<'a, E: ParseError<&'a str>>( let (input, args) = line(input)?; let (input, contents) = take_lines_while(|line| !line.trim().eq_ignore_ascii_case("#+END:"))(input); + let (contents, pre_blank) = blank_lines(contents); let (input, _) = line(input)?; + let (input, post_blank) = blank_lines(input); Ok(( input, @@ -56,6 +64,8 @@ fn parse_dyn_block<'a, E: ParseError<&'a str>>( } else { Some(args.trim().into()) }, + pre_blank, + post_blank, }, contents, ), @@ -70,8 +80,11 @@ fn parse() { assert_eq!( parse_dyn_block::>( r#"#+BEGIN: clocktable :scope file + + CONTENTS #+END: + "# ), Ok(( @@ -80,6 +93,8 @@ CONTENTS DynBlock { block_name: "clocktable".into(), arguments: Some(":scope file".into()), + pre_blank: 2, + post_blank: 1, }, "CONTENTS\n" ) diff --git a/src/elements/fixed_width.rs b/src/elements/fixed_width.rs new file mode 100644 index 0000000..bf4feac --- /dev/null +++ b/src/elements/fixed_width.rs @@ -0,0 +1,63 @@ +use std::borrow::Cow; + +use crate::parsers::{blank_lines, take_lines_while}; + +#[derive(Debug, Default)] +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(feature = "ser", derive(serde::Serialize))] +pub struct FixedWidth<'a> { + pub value: Cow<'a, str>, + pub post_blank: usize, +} + +impl FixedWidth<'_> { + pub(crate) fn parse(input: &str) -> Option<(&str, FixedWidth<'_>)> { + let (input, value) = take_lines_while(|line| line == ":" || line.starts_with(": "))(input); + let (input, blank) = blank_lines(input); + + if value.is_empty() { + return None; + } + + Some(( + input, + FixedWidth { + value: value.into(), + post_blank: blank, + }, + )) + } + + pub fn into_owned(self) -> FixedWidth<'static> { + FixedWidth { + value: self.value.into_owned().into(), + post_blank: self.post_blank, + } + } +} + +#[test] +fn parse() { + assert_eq!( + FixedWidth::parse( + r#": A +: +: B +: C + +"# + ), + Some(( + "", + FixedWidth { + value: r#": A +: +: B +: C +"# + .into(), + post_blank: 1 + } + )) + ); +} diff --git a/src/elements/fn_def.rs b/src/elements/fn_def.rs index a3a085e..960357f 100644 --- a/src/elements/fn_def.rs +++ b/src/elements/fn_def.rs @@ -7,15 +7,17 @@ use nom::{ IResult, }; -use crate::parsers::line; +use crate::parsers::{blank_lines, line}; /// Footnote Definition Element #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] -#[derive(Debug)] +#[derive(Debug, Default)] pub struct FnDef<'a> { /// Footnote label, used for refrence pub label: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl FnDef<'_> { @@ -26,6 +28,7 @@ impl FnDef<'_> { pub fn into_owned(self) -> FnDef<'static> { FnDef { label: self.label.into_owned().into(), + post_blank: self.post_blank, } } } @@ -38,12 +41,14 @@ fn parse_fn_def<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, (Fn tag("]"), )(input)?; let (input, content) = line(input)?; + let (input, blank) = blank_lines(input); Ok(( input, ( FnDef { label: label.into(), + post_blank: blank, }, content, ), @@ -56,7 +61,16 @@ fn parse() { assert_eq!( parse_fn_def::>("[fn:1] https://orgmode.org"), - Ok(("", (FnDef { label: "1".into() }, " https://orgmode.org"))) + Ok(( + "", + ( + FnDef { + label: "1".into(), + post_blank: 0 + }, + " https://orgmode.org" + ) + )) ); assert_eq!( parse_fn_def::>("[fn:word_1] https://orgmode.org"), @@ -64,7 +78,8 @@ fn parse() { "", ( FnDef { - label: "word_1".into() + label: "word_1".into(), + post_blank: 0, }, " https://orgmode.org" ) @@ -76,7 +91,8 @@ fn parse() { "", ( FnDef { - label: "WORD-1".into() + label: "WORD-1".into(), + post_blank: 0, }, " https://orgmode.org" ) @@ -88,7 +104,8 @@ fn parse() { "", ( FnDef { - label: "WORD".into() + label: "WORD".into(), + post_blank: 0, }, "" ) diff --git a/src/elements/keyword.rs b/src/elements/keyword.rs index 788c13e..f393771 100644 --- a/src/elements/keyword.rs +++ b/src/elements/keyword.rs @@ -8,7 +8,7 @@ use nom::{ IResult, }; -use crate::parsers::line; +use crate::parsers::{blank_lines, line}; /// Keyword Elemenet #[cfg_attr(test, derive(PartialEq))] @@ -21,6 +21,8 @@ pub struct Keyword<'a> { pub optional: Option>, /// Keyword value pub value: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl Keyword<'_> { @@ -29,6 +31,7 @@ impl Keyword<'_> { key: self.key.into_owned().into(), optional: self.optional.map(Into::into).map(Cow::Owned), value: self.value.into_owned().into(), + post_blank: self.post_blank, } } } @@ -39,25 +42,28 @@ impl Keyword<'_> { #[derive(Debug)] pub struct BabelCall<'a> { pub value: Cow<'a, str>, + /// Numbers of blank lines + pub post_blank: usize, } impl BabelCall<'_> { pub fn into_owned(self) -> BabelCall<'static> { BabelCall { value: self.value.into_owned().into(), + post_blank: self.post_blank, } } } #[inline] -pub fn parse_keyword(input: &str) -> Option<(&str, (&str, Option<&str>, &str))> { +pub fn parse_keyword(input: &str) -> Option<(&str, (&str, Option<&str>, &str, usize))> { parse_keyword_internal::<()>(input).ok() } #[inline] fn parse_keyword_internal<'a, E: ParseError<&'a str>>( input: &'a str, -) -> IResult<&str, (&str, Option<&str>, &str), E> { +) -> IResult<&str, (&str, Option<&str>, &str, usize), E> { let (input, _) = tag("#+")(input)?; let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?; let (input, optional) = opt(delimited( @@ -67,8 +73,9 @@ fn parse_keyword_internal<'a, E: ParseError<&'a str>>( ))(input)?; let (input, _) = tag(":")(input)?; let (input, value) = line(input)?; + let (input, blank) = blank_lines(input); - Ok((input, (key, optional, value.trim()))) + Ok((input, (key, optional, value.trim(), blank))) } #[test] @@ -77,40 +84,40 @@ fn parse() { assert_eq!( parse_keyword_internal::>("#+KEY:"), - Ok(("", ("KEY", None, ""))) + Ok(("", ("KEY", None, "", 0))) ); assert_eq!( parse_keyword_internal::>("#+KEY: VALUE"), - Ok(("", ("KEY", None, "VALUE"))) + Ok(("", ("KEY", None, "VALUE", 0))) ); assert_eq!( parse_keyword_internal::>("#+K_E_Y: VALUE"), - Ok(("", ("K_E_Y", None, "VALUE"))) + Ok(("", ("K_E_Y", None, "VALUE", 0))) ); assert_eq!( parse_keyword_internal::>("#+KEY:VALUE\n"), - Ok(("", ("KEY", None, "VALUE"))) + Ok(("", ("KEY", None, "VALUE", 0))) ); assert!(parse_keyword_internal::>("#+KE Y: VALUE").is_err()); assert!(parse_keyword_internal::>("#+ KEY: VALUE").is_err()); assert_eq!( parse_keyword_internal::>("#+RESULTS:"), - Ok(("", ("RESULTS", None, ""))) + Ok(("", ("RESULTS", None, "", 0))) ); assert_eq!( parse_keyword_internal::>("#+ATTR_LATEX: :width 5cm\n"), - Ok(("", ("ATTR_LATEX", None, ":width 5cm"))) + Ok(("", ("ATTR_LATEX", None, ":width 5cm", 0))) ); assert_eq!( parse_keyword_internal::>("#+CALL: double(n=4)"), - Ok(("", ("CALL", None, "double(n=4)"))) + Ok(("", ("CALL", None, "double(n=4)", 0))) ); assert_eq!( parse_keyword_internal::>("#+CAPTION[Short caption]: Longer caption."), - Ok(("", ("CAPTION", Some("Short caption"), "Longer caption.",))) + Ok(("", ("CAPTION", Some("Short caption"), "Longer caption.", 0))) ); } diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 587a2c5..6d6fdd9 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -2,10 +2,12 @@ pub(crate) mod block; pub(crate) mod clock; +pub(crate) mod comment; pub(crate) mod cookie; pub(crate) mod drawer; pub(crate) mod dyn_block; pub(crate) mod emphasis; +pub(crate) mod fixed_width; pub(crate) mod fn_def; pub(crate) mod fn_ref; pub(crate) mod inline_call; @@ -29,9 +31,11 @@ pub use self::{ SpecialBlock, VerseBlock, }, clock::Clock, + comment::Comment, cookie::Cookie, drawer::Drawer, dyn_block::DynBlock, + fixed_width::FixedWidth, fn_def::FnDef, fn_ref::FnRef, inline_call::InlineCall, @@ -41,6 +45,7 @@ pub use self::{ list::{List, ListItem}, macros::Macros, planning::Planning, + rule::Rule, snippet::Snippet, table::{Table, TableRow}, target::Target, @@ -52,7 +57,6 @@ use std::borrow::Cow; /// Element Enum #[derive(Debug)] -#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", serde(tag = "type", rename_all = "kebab-case"))] pub enum Element<'a> { @@ -70,7 +74,7 @@ pub enum Element<'a> { Cookie(Cookie<'a>), RadioTarget, Drawer(Drawer<'a>), - Document, + Document { pre_blank: usize }, DynBlock(DynBlock<'a>), FnDef(FnDef<'a>), FnRef(FnRef<'a>), @@ -84,8 +88,8 @@ pub enum Element<'a> { Macros(Macros<'a>), Snippet(Snippet<'a>), Text { value: Cow<'a, str> }, - Paragraph, - Rule, + Paragraph { post_blank: usize }, + Rule(Rule), Timestamp(Timestamp<'a>), Target(Target<'a>), Bold, @@ -94,8 +98,8 @@ pub enum Element<'a> { Underline, Verbatim { value: Cow<'a, str> }, Code { value: Cow<'a, str> }, - Comment { value: Cow<'a, str> }, - FixedWidth { value: Cow<'a, str> }, + Comment(Comment<'a>), + FixedWidth(FixedWidth<'a>), Title(Title<'a>), Table(Table<'a>), TableRow(TableRow), @@ -112,13 +116,13 @@ impl Element<'_> { | CenterBlock(_) | VerseBlock(_) | Bold - | Document + | Document { .. } | DynBlock(_) | Headline { .. } | Italic | List(_) | ListItem(_) - | Paragraph + | Paragraph { .. } | Section | Strike | Underline @@ -148,7 +152,7 @@ impl Element<'_> { Cookie(e) => Cookie(e.into_owned()), RadioTarget => RadioTarget, Drawer(e) => Drawer(e.into_owned()), - Document => Document, + Document { pre_blank } => Document { pre_blank }, DynBlock(e) => DynBlock(e.into_owned()), FnDef(e) => FnDef(e.into_owned()), FnRef(e) => FnRef(e.into_owned()), @@ -164,8 +168,8 @@ impl Element<'_> { Text { value } => Text { value: value.into_owned().into(), }, - Paragraph => Paragraph, - Rule => Rule, + Paragraph { post_blank } => Paragraph { post_blank }, + Rule(e) => Rule(e), Timestamp(e) => Timestamp(e.into_owned()), Target(e) => Target(e.into_owned()), Bold => Bold, @@ -178,12 +182,8 @@ impl Element<'_> { Code { value } => Code { value: value.into_owned().into(), }, - Comment { value } => Comment { - value: value.into_owned().into(), - }, - FixedWidth { value } => FixedWidth { - value: value.into_owned().into(), - }, + Comment(e) => Comment(e.into_owned()), + FixedWidth(e) => FixedWidth(e.into_owned()), Title(e) => Title(e.into_owned()), Table(e) => Table(e.into_owned()), TableRow(e) => TableRow(e), @@ -215,12 +215,14 @@ impl_from!( BabelCall, CenterBlock, Clock, + Comment, CommentBlock, Cookie, Drawer, DynBlock, ExampleBlock, ExportBlock, + FixedWidth, FnDef, FnRef, InlineCall, @@ -233,11 +235,12 @@ impl_from!( Snippet, SourceBlock, SpecialBlock, + Table, Target, Timestamp, - Table, Title, VerseBlock; List, + Rule, TableRow ); diff --git a/src/elements/rule.rs b/src/elements/rule.rs index 81d584e..eda64fd 100644 --- a/src/elements/rule.rs +++ b/src/elements/rule.rs @@ -1,19 +1,25 @@ -use std::usize; - use nom::{bytes::complete::take_while_m_n, error::ParseError, IResult}; -use crate::parsers::eol; +use crate::parsers::{blank_lines, eol}; -pub fn parse_rule(input: &str) -> Option<&str> { - parse_rule_internal::<()>(input) - .ok() - .map(|(input, _)| input) +#[derive(Debug, Default)] +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(feature = "ser", derive(serde::Serialize))] +pub struct Rule { + pub post_blank: usize, } -fn parse_rule_internal<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, (), E> { - let (input, _) = take_while_m_n(5, usize::MAX, |c| c == '-')(input)?; +impl Rule { + pub(crate) fn parse(input: &str) -> Option<(&str, Rule)> { + parse_rule::<()>(input).ok() + } +} + +fn parse_rule<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Rule, E> { + let (input, _) = take_while_m_n(5, usize::max_value(), |c| c == '-')(input)?; let (input, _) = eol(input)?; - Ok((input, ())) + let (input, blank) = blank_lines(input); + Ok((input, Rule { post_blank: blank })) } #[test] @@ -21,29 +27,29 @@ fn parse() { use nom::error::VerboseError; assert_eq!( - parse_rule_internal::>("-----"), - Ok(("", ())) + parse_rule::>("-----"), + Ok(("", Rule { post_blank: 0 })) ); assert_eq!( - parse_rule_internal::>("--------"), - Ok(("", ())) + parse_rule::>("--------"), + Ok(("", Rule { post_blank: 0 })) ); assert_eq!( - parse_rule_internal::>("-----\n"), - Ok(("", ())) + parse_rule::>("-----\n\n\n"), + Ok(("", Rule { post_blank: 2 })) ); assert_eq!( - parse_rule_internal::>("----- \n"), - Ok(("", ())) + parse_rule::>("----- \n"), + Ok(("", Rule { post_blank: 0 })) ); - assert!(parse_rule_internal::>("").is_err()); - assert!(parse_rule_internal::>("----").is_err()); - assert!(parse_rule_internal::>("----").is_err()); - assert!(parse_rule_internal::>("None----").is_err()); - assert!(parse_rule_internal::>("None ----").is_err()); - assert!(parse_rule_internal::>("None------").is_err()); - assert!(parse_rule_internal::>("----None----").is_err()); - assert!(parse_rule_internal::>("\t\t----").is_err()); - assert!(parse_rule_internal::>("------None").is_err()); - assert!(parse_rule_internal::>("----- None").is_err()); + assert!(parse_rule::>("").is_err()); + assert!(parse_rule::>("----").is_err()); + assert!(parse_rule::>("----").is_err()); + assert!(parse_rule::>("None----").is_err()); + assert!(parse_rule::>("None ----").is_err()); + assert!(parse_rule::>("None------").is_err()); + assert!(parse_rule::>("----None----").is_err()); + assert!(parse_rule::>("\t\t----").is_err()); + assert!(parse_rule::>("------None").is_err()); + assert!(parse_rule::>("----- None").is_err()); } diff --git a/src/elements/table.rs b/src/elements/table.rs index 522050d..d41d6a5 100644 --- a/src/elements/table.rs +++ b/src/elements/table.rs @@ -58,7 +58,7 @@ impl TableRow { } } -pub(crate) fn parse_table_el(input: &str) -> Option<(&str, &str)> { +pub fn parse_table_el(input: &str) -> Option<(&str, &str)> { parse_table_el_internal::<()>(input).ok() } diff --git a/src/elements/title.rs b/src/elements/title.rs index 9d09fc5..a154426 100644 --- a/src/elements/title.rs +++ b/src/elements/title.rs @@ -16,8 +16,8 @@ use nom::{ use crate::{ config::ParseConfig, - elements::{drawer::parse_drawer, Planning, Timestamp}, - parsers::{line, skip_empty_lines, take_one_word}, + elements::{drawer::parse_drawer_without_blank, Planning, Timestamp}, + parsers::{blank_lines, line, skip_empty_lines, take_one_word}, }; /// Title Elemenet @@ -44,6 +44,8 @@ pub struct Title<'a> { /// Property drawer associated to this headline #[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))] pub properties: HashMap, Cow<'a, str>>, + /// Numbers of blank lines + pub post_blank: usize, } impl Title<'_> { @@ -102,6 +104,7 @@ impl Title<'_> { .into_iter() .map(|(k, v)| (k.into_owned().into(), v.into_owned().into())) .collect(), + post_blank: self.post_blank, } } } @@ -116,6 +119,7 @@ impl Default for Title<'_> { raw: Cow::Borrowed(""), planning: None, properties: HashMap::new(), + post_blank: 0, } } } @@ -166,6 +170,7 @@ fn parse_title<'a, E: ParseError<&'a str>>( .unwrap_or((input, None)); let (input, properties) = opt(parse_properties_drawer)(input)?; + let (input, blank) = blank_lines(input); Ok(( input, @@ -178,6 +183,7 @@ fn parse_title<'a, E: ParseError<&'a str>>( tags, raw: raw.into(), planning, + post_blank: blank, }, raw, ), @@ -188,7 +194,7 @@ fn parse_title<'a, E: ParseError<&'a str>>( fn parse_properties_drawer<'a, E: ParseError<&'a str>>( input: &'a str, ) -> IResult<&str, HashMap, Cow<'_, str>>, E> { - let (input, (drawer, content)) = parse_drawer(input.trim_start())?; + let (input, (drawer, content)) = parse_drawer_without_blank(input.trim_start())?; if drawer.name != "PROPERTIES" { return Err(Err::Error(E::from_error_kind(input, ErrorKind::Tag))); } @@ -236,7 +242,8 @@ fn parse_title_() { raw: "COMMENT Title".into(), tags: vec!["tag".into(), "a2%".into()], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "COMMENT Title" ) @@ -254,7 +261,8 @@ fn parse_title_() { raw: "ToDO [#A] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "ToDO [#A] COMMENT Title" ) @@ -272,7 +280,8 @@ fn parse_title_() { raw: "T0DO [#A] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "T0DO [#A] COMMENT Title" ) @@ -290,7 +299,8 @@ fn parse_title_() { raw: "[#1] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "[#1] COMMENT Title" ) @@ -308,7 +318,8 @@ fn parse_title_() { raw: "[#a] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "[#a] COMMENT Title" ) @@ -326,7 +337,8 @@ fn parse_title_() { raw: "Title :tag:a2%".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "Title :tag:a2%" ) @@ -344,7 +356,8 @@ fn parse_title_() { raw: "Title tag:a2%:".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "Title tag:a2%:" ) @@ -369,7 +382,8 @@ fn parse_title_() { raw: "DONE Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "DONE Title" ) @@ -393,7 +407,8 @@ fn parse_title_() { raw: "Title".into(), tags: vec![], planning: None, - properties: HashMap::new() + properties: HashMap::new(), + post_blank: 0, }, "Title" ) diff --git a/src/export/html.rs b/src/export/html.rs index 5dff5f2..bea3bfa 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -60,7 +60,7 @@ pub trait HtmlHandler>: Default { CenterBlock(_) => write!(w, "
")?, VerseBlock(_) => write!(w, "

")?, Bold => write!(w, "")?, - Document => write!(w, "

")?, + Document { .. } => write!(w, "
")?, DynBlock(_dyn_block) => (), Headline { .. } => (), List(list) => { @@ -72,7 +72,7 @@ pub trait HtmlHandler>: Default { } Italic => write!(w, "")?, ListItem(_) => write!(w, "
  • ")?, - Paragraph => write!(w, "

    ")?, + Paragraph { .. } => write!(w, "

    ")?, Section => write!(w, "

    ")?, Strike => write!(w, "")?, Underline => write!(w, "")?, @@ -162,13 +162,15 @@ pub trait HtmlHandler>: Default { Verbatim { value } => write!(&mut w, "{}", HtmlEscape(value))?, FnDef(_fn_def) => (), Clock(_clock) => (), - Comment { .. } => (), - FixedWidth { value } => { - write!(w, "
    {}
    ", HtmlEscape(value))? - } + Comment(_) => (), + FixedWidth(fixed_width) => write!( + w, + "
    {}
    ", + HtmlEscape(&fixed_width.value) + )?, Keyword(_keyword) => (), Drawer(_drawer) => (), - Rule => write!(w, "
    ")?, + Rule(_) => write!(w, "
    ")?, Cookie(cookie) => write!(w, "{}", cookie.value)?, Title(title) => write!(w, "", if title.level <= 6 { title.level } else { 6 })?, Table(_) => (), @@ -188,7 +190,7 @@ pub trait HtmlHandler>: Default { CenterBlock(_) => write!(w, "
  • ")?, VerseBlock(_) => write!(w, "

    ")?, Bold => write!(w, "
    ")?, - Document => write!(w, "")?, + Document { .. } => write!(w, "")?, DynBlock(_dyn_block) => (), Headline { .. } => (), List(list) => { @@ -200,7 +202,7 @@ pub trait HtmlHandler>: Default { } Italic => write!(w, "")?, ListItem(_) => write!(w, "")?, - Paragraph => write!(w, "

    ")?, + Paragraph { .. } => write!(w, "

    ")?, Section => write!(w, "")?, Strike => write!(w, "
    ")?, Underline => write!(w, "")?, @@ -337,17 +339,17 @@ mod syntect_handler { write!(w, "
    {}
    ", block.contents)?; } else { write!( - w, - "
    {}
    ", - block.language, - self.highlight(Some(&block.language), &block.contents) - )? + w, + "
    {}
    ", + block.language, + self.highlight(Some(&block.language), &block.contents) + )?; } } - Element::FixedWidth { value } => write!( + Element::FixedWidth(fixed_width) => write!( w, "
    {}
    ", - self.highlight(None, value) + self.highlight(None, fixed_width.value) )?, Element::ExampleBlock(block) => write!( w, diff --git a/src/export/org.rs b/src/export/org.rs index 5ac384d..0146a5e 100644 --- a/src/export/org.rs +++ b/src/export/org.rs @@ -9,46 +9,74 @@ pub trait OrgHandler>: Default { match element { // container elements - SpecialBlock(block) => writeln!(w, "#+BEGIN_{}", block.name)?, - QuoteBlock(_) => write!(w, "#+BEGIN_QUOTE")?, - CenterBlock(_) => write!(w, "#+BEGIN_CENTER")?, - VerseBlock(_) => write!(w, "#+BEGIN_VERSE")?, + SpecialBlock(block) => { + writeln!(w, "#+BEGIN_{}", block.name)?; + write_blank_lines(&mut w, block.pre_blank)?; + } + QuoteBlock(block) => { + writeln!(&mut w, "#+BEGIN_QUOTE")?; + write_blank_lines(&mut w, block.pre_blank)?; + } + CenterBlock(block) => { + writeln!(&mut w, "#+BEGIN_CENTER")?; + write_blank_lines(&mut w, block.pre_blank)?; + } + VerseBlock(block) => { + writeln!(&mut w, "#+BEGIN_VERSE")?; + write_blank_lines(&mut w, block.pre_blank)?; + } Bold => write!(w, "*")?, - Document => (), + Document { pre_blank } => { + write_blank_lines(w, *pre_blank)?; + } DynBlock(dyn_block) => { write!(&mut w, "#+BEGIN: {}", dyn_block.block_name)?; if let Some(parameters) = &dyn_block.arguments { write!(&mut w, " {}", parameters)?; } - writeln!(&mut w)?; + write_blank_lines(&mut w, dyn_block.pre_blank + 1)?; } Headline { .. } => (), List(_list) => (), Italic => write!(w, "/")?, ListItem(list_item) => write!(w, "{}", list_item.bullet)?, - Paragraph => (), + Paragraph { .. } => (), Section => (), Strike => write!(w, "+")?, Underline => write!(w, "_")?, - Drawer(drawer) => writeln!(w, ":{}:", drawer.name)?, + Drawer(drawer) => { + writeln!(&mut w, ":{}:", drawer.name)?; + write_blank_lines(&mut w, drawer.pre_blank)?; + } // non-container elements CommentBlock(block) => { - writeln!(w, "#+BEGIN_COMMENT\n{}\n#+END_COMMENT", block.contents)? + writeln!(&mut w, "#+BEGIN_COMMENT")?; + write!(&mut w, "{}", block.contents)?; + writeln!(&mut w, "#+END_COMMENT")?; + write_blank_lines(&mut w, block.post_blank)?; } ExampleBlock(block) => { - writeln!(w, "#+BEGIN_EXAMPLE\n{}\n#+END_EXAMPLE", block.contents)? + writeln!(&mut w, "#+BEGIN_EXAMPLE")?; + write!(&mut w, "{}", block.contents)?; + writeln!(&mut w, "#+END_EXAMPLE")?; + write_blank_lines(&mut w, block.post_blank)?; + } + ExportBlock(block) => { + writeln!(&mut w, "#+BEGIN_EXPORT {}", block.data)?; + write!(&mut w, "{}", block.contents)?; + writeln!(&mut w, "#+END_EXPORT")?; + write_blank_lines(&mut w, block.post_blank)?; + } + SourceBlock(block) => { + writeln!(&mut w, "#+BEGIN_SRC {}", block.language)?; + write!(&mut w, "{}", block.contents)?; + writeln!(&mut w, "#+END_SRC")?; + write_blank_lines(&mut w, block.post_blank)?; + } + BabelCall(call) => { + writeln!(&mut w, "#+CALL: {}", call.value)?; + write_blank_lines(w, call.post_blank)?; } - ExportBlock(block) => writeln!( - w, - "#+BEGIN_EXPORT {}\n{}\n#+END_EXPORT", - block.data, block.contents - )?, - SourceBlock(block) => writeln!( - w, - "#+BEGIN_SRC {}\n{}\n#+END_SRC", - block.language, block.contents - )?, - BabelCall(_babel_call) => (), InlineSrc(inline_src) => { write!(&mut w, "src_{}", inline_src.lang)?; if let Some(options) = &inline_src.options { @@ -90,7 +118,9 @@ pub trait OrgHandler>: Default { write_timestamp(&mut w, ×tamp)?; } Verbatim { value } => write!(w, "={}=", value)?, - FnDef(_fn_def) => (), + FnDef(fn_def) => { + write_blank_lines(w, fn_def.post_blank)?; + } Clock(clock) => { use crate::elements::Clock; @@ -101,27 +131,42 @@ pub trait OrgHandler>: Default { start, end, duration, + post_blank, .. } => { - write_datetime(&mut w, "[", start, "]--")?; - write_datetime(&mut w, "[", end, "]")?; - writeln!(w, " => {}", duration)?; + write_datetime(&mut w, "[", &start, "]--")?; + write_datetime(&mut w, "[", &end, "]")?; + writeln!(&mut w, " => {}", duration)?; + write_blank_lines(&mut w, *post_blank)?; } - Clock::Running { start, .. } => { - write_datetime(&mut w, "[", start, "]\n")?; + Clock::Running { + start, post_blank, .. + } => { + write_datetime(&mut w, "[", &start, "]\n")?; + write_blank_lines(&mut w, *post_blank)?; } } } - Comment { value } => write!(w, "{}", value)?, - FixedWidth { value } => write!(w, "{}", value)?, + Comment(comment) => { + write!(w, "{}", comment.value)?; + write_blank_lines(&mut w, comment.post_blank)?; + } + FixedWidth(fixed_width) => { + write!(&mut w, "{}", fixed_width.value)?; + write_blank_lines(&mut w, fixed_width.post_blank)?; + } Keyword(keyword) => { write!(&mut w, "#+{}", keyword.key)?; if let Some(optional) = &keyword.optional { write!(&mut w, "[{}]", optional)?; } writeln!(&mut w, ": {}", keyword.value)?; + write_blank_lines(&mut w, keyword.post_blank)?; + } + Rule(rule) => { + writeln!(w, "-----")?; + write_blank_lines(&mut w, rule.post_blank)?; } - Rule => writeln!(w, "-----")?, Cookie(_cookie) => (), Title(title) => { for _ in 0..title.level { @@ -148,28 +193,48 @@ pub trait OrgHandler>: Default { match element { // container elements - SpecialBlock(block) => writeln!(w, "#+END_{}", block.name)?, - QuoteBlock(_) => writeln!(w, "#+END_QUOTE")?, - CenterBlock(_) => writeln!(w, "#+END_CENTER")?, - VerseBlock(_) => writeln!(w, "#+END_VERSE")?, + SpecialBlock(block) => { + writeln!(&mut w, "#+END_{}", block.name)?; + write_blank_lines(&mut w, block.post_blank)?; + } + QuoteBlock(block) => { + writeln!(&mut w, "#+END_QUOTE")?; + write_blank_lines(&mut w, block.post_blank)?; + } + CenterBlock(block) => { + writeln!(&mut w, "#+END_CENTER")?; + write_blank_lines(&mut w, block.post_blank)?; + } + VerseBlock(block) => { + writeln!(&mut w, "#+END_VERSE")?; + write_blank_lines(&mut w, block.post_blank)?; + } Bold => write!(w, "*")?, - Document => (), - DynBlock(_dyn_block) => writeln!(w, "#+END:")?, + Document { .. } => (), + DynBlock(dyn_block) => { + writeln!(w, "#+END:")?; + write_blank_lines(w, dyn_block.post_blank)?; + } Headline { .. } => (), List(_list) => (), Italic => write!(w, "/")?, ListItem(_) => (), - Paragraph => write!(w, "\n\n")?, + Paragraph { post_blank } => { + write_blank_lines(w, post_blank + 1)?; + } Section => (), Strike => write!(w, "+")?, Underline => write!(w, "_")?, - Drawer(_) => writeln!(w, ":END:")?, + Drawer(drawer) => { + writeln!(&mut w, ":END:")?; + write_blank_lines(&mut w, drawer.post_blank)?; + } Title(title) => { if !title.tags.is_empty() { write!(&mut w, " :")?; - } - for tag in &title.tags { - write!(&mut w, "{}:", tag)?; + for tag in &title.tags { + write!(&mut w, "{}:", tag)?; + } } writeln!(&mut w)?; if let Some(planning) = &title.planning { @@ -200,6 +265,7 @@ pub trait OrgHandler>: Default { } writeln!(&mut w, ":END:")?; } + write_blank_lines(&mut w, title.post_blank)?; } Table(_) => (), TableRow(_) => (), @@ -212,7 +278,14 @@ pub trait OrgHandler>: Default { } } -fn write_timestamp(mut w: W, timestamp: &Timestamp) -> std::io::Result<()> { +fn write_blank_lines(mut w: W, count: usize) -> Result<(), Error> { + for _ in 0..count { + writeln!(w)?; + } + Ok(()) +} + +fn write_timestamp(mut w: W, timestamp: &Timestamp) -> Result<(), Error> { match timestamp { Timestamp::Active { start, .. } => { write_datetime(w, "<", start, ">")?; diff --git a/src/headline.rs b/src/headline.rs index e9abd94..88f6f1c 100644 --- a/src/headline.rs +++ b/src/headline.rs @@ -337,7 +337,7 @@ impl Headline { pub fn new<'a>(ttl: Title<'a>, org: &mut Org<'a>) -> Headline { let lvl = ttl.level; let hdl_n = org.arena.new_node(Element::Headline { level: ttl.level }); - let ttl_n = org.arena.new_node(Element::Document); // placeholder + let ttl_n = org.arena.new_node(Element::Document { pre_blank: 0 }); // placeholder hdl_n.append(ttl_n, &mut org.arena); match ttl.raw { @@ -685,7 +685,7 @@ impl Headline { pub fn parent(self, org: &Org) -> Option { org.arena[self.hdl_n].parent().and_then(|n| match org[n] { Element::Headline { level } => Some(Headline::from_node(n, level, org)), - Element::Document => None, + Element::Document { .. } => None, _ => unreachable!(), }) } diff --git a/src/org.rs b/src/org.rs index c5d4632..c7c5c89 100644 --- a/src/org.rs +++ b/src/org.rs @@ -6,7 +6,7 @@ use crate::{ config::{ParseConfig, DEFAULT_CONFIG}, elements::Element, export::{DefaultHtmlHandler, DefaultOrgHandler, HtmlHandler, OrgHandler}, - parsers::{parse_container, Container}, + parsers::{blank_lines, parse_container, Container}, }; pub struct Org<'a> { @@ -24,8 +24,7 @@ impl<'a> Org<'a> { /// Create a new empty `Org` struct pub fn new() -> Org<'static> { let mut arena = Arena::new(); - let root = arena.new_node(Element::Document); - + let root = arena.new_node(Element::Document { pre_blank: 0 }); Org { arena, root } } @@ -36,7 +35,10 @@ impl<'a> Org<'a> { /// Create a new Org struct from parsing `text`, using a custom ParseConfig pub fn parse_with_config(text: &'a str, config: &ParseConfig) -> Org<'a> { - let mut org = Org::new(); + let mut arena = Arena::new(); + let (text, blank) = blank_lines(text); + let root = arena.new_node(Element::Document { pre_blank: blank }); + let mut org = Org { arena, root }; parse_container( &mut org.arena, diff --git a/src/parsers.rs b/src/parsers.rs index 244e8ca..3825338 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -12,10 +12,11 @@ use nom::{bytes::complete::take_while1, combinator::verify, error::ParseError, I use crate::config::ParseConfig; use crate::elements::{ block::parse_block_element, emphasis::parse_emphasis, keyword::parse_keyword, - radio_target::parse_radio_target, rule::parse_rule, table::parse_table_el, BabelCall, - CenterBlock, Clock, CommentBlock, Cookie, Drawer, DynBlock, Element, ExampleBlock, ExportBlock, - FnDef, FnRef, InlineCall, InlineSrc, Keyword, Link, List, ListItem, Macros, QuoteBlock, - Snippet, SourceBlock, SpecialBlock, Table, TableRow, Target, Timestamp, Title, VerseBlock, + radio_target::parse_radio_target, table::parse_table_el, BabelCall, CenterBlock, Clock, + Comment, CommentBlock, Cookie, Drawer, DynBlock, Element, ExampleBlock, ExportBlock, + FixedWidth, FnDef, FnRef, InlineCall, InlineSrc, Keyword, Link, List, ListItem, Macros, + QuoteBlock, Rule, Snippet, SourceBlock, SpecialBlock, Table, TableRow, Target, Timestamp, + Title, VerseBlock, }; pub trait ElementArena<'a> { @@ -216,23 +217,32 @@ pub fn parse_blocks<'a, T: ElementArena<'a>>( .map(|i| i + 1) .unwrap_or_else(|| tail.len()); if tail.as_bytes()[0..i].iter().all(u8::is_ascii_whitespace) { - let node = arena.append_element(Element::Paragraph, parent); + let (tail_, blank) = blank_lines(&tail[i..]); + debug_assert_ne!(tail, tail_); + tail = tail_; + + let node = arena.append_element( + Element::Paragraph { + // including current line (&tail[0..i]) + post_blank: blank + 1, + }, + parent, + ); containers.push(Container::Inline { - content: &text[0..pos].trim_end_matches('\n'), + content: &text[0..pos].trim_end(), node, }); pos = 0; - debug_assert_ne!(tail, skip_empty_lines(&tail[i..])); - tail = skip_empty_lines(&tail[i..]); text = tail; } else if let Some(new_tail) = parse_block(tail, arena, parent, containers) { if pos != 0 { - let node = arena.insert_before_last_child(Element::Paragraph, parent); + let node = + arena.insert_before_last_child(Element::Paragraph { post_blank: 0 }, parent); containers.push(Container::Inline { - content: &text[0..pos].trim_end_matches('\n'), + content: &text[0..pos].trim_end(), node, }); @@ -249,10 +259,10 @@ pub fn parse_blocks<'a, T: ElementArena<'a>>( } if !text.is_empty() { - let node = arena.append_element(Element::Paragraph, parent); + let node = arena.append_element(Element::Paragraph { post_blank: 0 }, parent); containers.push(Container::Inline { - content: &text[0..pos].trim_end_matches('\n'), + content: &text[0..pos].trim_end(), node, }); } @@ -264,11 +274,14 @@ pub fn parse_block<'a, T: ElementArena<'a>>( parent: NodeId, containers: &mut Vec>, ) -> Option<&'a str> { + // footnote definitions must be start at column 0 if let Some((tail, (fn_def, content))) = FnDef::parse(contents) { let node = arena.append_element(fn_def, parent); containers.push(Container::Block { content, node }); return Some(tail); - } else if let Some((tail, list, content)) = List::parse(contents) { + } + + if let Some((tail, list, content)) = List::parse(contents) { let indent = list.indent; let node = arena.append_element(list, parent); containers.push(Container::List { @@ -292,8 +305,8 @@ pub fn parse_block<'a, T: ElementArena<'a>>( None } b'-' => { - let tail = parse_rule(contents)?; - arena.append_element(Element::Rule, parent); + let (tail, rule) = Rule::parse(contents)?; + arena.append_element(rule, parent); Some(tail) } b':' => { @@ -302,9 +315,8 @@ pub fn parse_block<'a, T: ElementArena<'a>>( containers.push(Container::Block { content, node }); Some(tail) } else { - let (tail, value) = parse_fixed_width(contents)?; - let value = value.into(); - arena.append_element(Element::FixedWidth { value }, parent); + let (tail, fixed_width) = FixedWidth::parse(contents)?; + arena.append_element(fixed_width, parent); Some(tail) } } @@ -313,7 +325,7 @@ pub fn parse_block<'a, T: ElementArena<'a>>( Some(tail) } b'#' => { - if let Some((tail, (name, args, content))) = parse_block_element(contents) { + if let Some((tail, (name, args, content, blank))) = parse_block_element(contents) { match_block( arena, parent, @@ -321,17 +333,19 @@ pub fn parse_block<'a, T: ElementArena<'a>>( name.into(), args.map(Into::into), content, + blank, ); Some(tail) } else if let Some((tail, (dyn_block, content))) = DynBlock::parse(contents) { let node = arena.append_element(dyn_block, parent); containers.push(Container::Block { content, node }); Some(tail) - } else if let Some((tail, (key, optional, value))) = parse_keyword(contents) { + } else if let Some((tail, (key, optional, value, blank))) = parse_keyword(contents) { if (&*key).eq_ignore_ascii_case("CALL") { arena.append_element( BabelCall { value: value.into(), + post_blank: blank, }, parent, ); @@ -341,15 +355,15 @@ pub fn parse_block<'a, T: ElementArena<'a>>( key: key.into(), optional: optional.map(Into::into), value: value.into(), + post_blank: blank, }, parent, ); } Some(tail) } else { - let (tail, value) = parse_comment(contents)?; - let value = value.into(); - arena.append_element(Element::Comment { value }, parent); + let (tail, comment) = Comment::parse(contents)?; + arena.append_element(comment, parent); Some(tail) } } @@ -364,14 +378,43 @@ pub fn match_block<'a, T: ElementArena<'a>>( name: Cow<'a, str>, parameters: Option>, content: &'a str, + post_blank: usize, ) { match &*name.to_uppercase() { "CENTER" => { - let node = arena.append_element(CenterBlock { parameters }, parent); + let (content, pre_blank) = blank_lines(content); + let node = arena.append_element( + CenterBlock { + parameters, + pre_blank, + post_blank, + }, + parent, + ); containers.push(Container::Block { content, node }); } "QUOTE" => { - let node = arena.append_element(QuoteBlock { parameters }, parent); + let (content, pre_blank) = blank_lines(content); + let node = arena.append_element( + QuoteBlock { + parameters, + pre_blank, + post_blank, + }, + parent, + ); + containers.push(Container::Block { content, node }); + } + "VERSE" => { + let (content, pre_blank) = blank_lines(content); + let node = arena.append_element( + VerseBlock { + parameters, + pre_blank, + post_blank, + }, + parent, + ); containers.push(Container::Block { content, node }); } "COMMENT" => { @@ -379,6 +422,7 @@ pub fn match_block<'a, T: ElementArena<'a>>( CommentBlock { data: parameters, contents: content.into(), + post_blank, }, parent, ); @@ -388,6 +432,7 @@ pub fn match_block<'a, T: ElementArena<'a>>( ExampleBlock { data: parameters, contents: content.into(), + post_blank, }, parent, ); @@ -397,6 +442,7 @@ pub fn match_block<'a, T: ElementArena<'a>>( ExportBlock { data: parameters.unwrap_or_default(), contents: content.into(), + post_blank, }, parent, ); @@ -416,16 +462,22 @@ pub fn match_block<'a, T: ElementArena<'a>>( arguments, language, contents: content.into(), + post_blank, }, parent, ); } - "VERSE" => { - let node = arena.append_element(VerseBlock { parameters }, parent); - containers.push(Container::Block { content, node }); - } _ => { - let node = arena.append_element(SpecialBlock { parameters, name }, parent); + let (content, pre_blank) = blank_lines(content); + let node = arena.append_element( + SpecialBlock { + parameters, + name, + pre_blank, + post_blank, + }, + parent, + ); containers.push(Container::Block { content, node }); } } @@ -727,26 +779,6 @@ pub fn parse_headline_level(input: &str) -> Option<(&str, usize)> { } } -pub fn parse_fixed_width(input: &str) -> Option<(&str, &str)> { - let (input, content) = take_lines_while(|line| line == ":" || line.starts_with(": "))(input); - - if !content.is_empty() { - Some((input, content)) - } else { - None - } -} - -pub fn parse_comment(input: &str) -> Option<(&str, &str)> { - let (input, content) = take_lines_while(|line| line == "#" || line.starts_with("# "))(input); - - if !content.is_empty() { - Some((input, content)) - } else { - None - } -} - pub fn take_one_word<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> { take_while1(|c: char| !c.is_ascii_whitespace())(input) } @@ -760,3 +792,27 @@ pub fn test_skip_empty_lines() { assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), "foo\n"); assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " foo\n"); } + +pub fn blank_lines(input: &str) -> (&str, usize) { + let bytes = input.as_bytes(); + let mut blank = 0; + let mut last_end = 0; + for i in memchr_iter(b'\n', bytes) { + if bytes[last_end..i].iter().all(u8::is_ascii_whitespace) { + blank += 1; + } else { + break; + } + last_end = 1 + i; + } + (&input[last_end..], blank) +} + +#[test] +pub fn test_blank_lines() { + assert_eq!(blank_lines("foo"), ("foo", 0)); + assert_eq!(blank_lines(" foo"), (" foo", 0)); + assert_eq!(blank_lines(" \t\nfoo\n"), ("foo\n", 1)); + assert_eq!(blank_lines("\n \r\n\nfoo\n"), ("foo\n", 3)); + assert_eq!(blank_lines("\r\n \n \r\n foo\n"), (" foo\n", 3)); +} diff --git a/src/validate.rs b/src/validate.rs index 115a44d..87ea501 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -64,7 +64,7 @@ impl Org<'_> { for node_id in self.root.descendants(&self.arena) { let node = &self.arena[node_id]; match node.get() { - Element::Document => { + Element::Document { .. } => { let mut children = node_id.children(&self.arena); if let Some(node) = children.next() { expect!( @@ -132,7 +132,7 @@ impl Org<'_> { | Element::Comment { .. } | Element::FixedWidth { .. } | Element::Keyword(_) - | Element::Rule + | Element::Rule(_) | Element::Cookie(_) | Element::Table(Table::TableEl { .. }) | Element::TableRow(TableRow::Rule) => { diff --git a/tests/blank.rs b/tests/blank.rs new file mode 100644 index 0000000..8e95686 --- /dev/null +++ b/tests/blank.rs @@ -0,0 +1,66 @@ +use orgize::Org; + +const ORG_STR: &str = r#" + +#+TITLE: org + +#+BEGIN_QUOTE + +CONTENTS + +#+END_QUOTE + +* Headline 1 +SCHEDULED: <2019-10-28 Mon> +:PROPERTIES: +:ID: headline-1 +:END: + +:LOGBOOK: + +CLOCK: [2019-10-28 Mon 08:53] + +CLOCK: [2019-10-28 Mon 08:53]--[2019-10-28 Mon 08:53] => 0:00 + +:END: + +----- + +#+CALL: VALUE + +# +# Comment +# + +#+BEGIN: NAME PARAMETERS + +CONTENTS + +#+END: + +: +: Fixed width +: + +#+BEGIN_COMMENT + +COMMENT + +#+END_COMMENT + +#+BEGIN_EXAMPLE +#+END_EXAMPLE + +"#; + +#[test] +fn blank() { + let org = Org::parse(ORG_STR); + + let mut writer = Vec::new(); + org.org(&mut writer).unwrap(); + + // eprintln!("{}", serde_json::to_string_pretty(&org).unwrap()); + + assert_eq!(String::from_utf8(writer).unwrap(), ORG_STR); +}