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);
+}