diff --git a/src/elements/clock.rs b/src/elements/clock.rs index 39e9542..e2d1c0a 100644 --- a/src/elements/clock.rs +++ b/src/elements/clock.rs @@ -1,8 +1,8 @@ -use nom::sequence::separated_pair; use nom::{ bytes::complete::tag, character::complete::{char, digit1, space0}, combinator::{peek, recognize}, + sequence::separated_pair, IResult, }; @@ -14,20 +14,25 @@ use crate::parsers::eol; /// there are two types of clock: *closed* clock and *running* clock. #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] #[derive(Debug)] pub enum Clock<'a> { /// closed Clock Closed { start: Datetime<'a>, end: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, duration: &'a str, }, /// running Clock Running { start: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, }, } diff --git a/src/elements/drawer.rs b/src/elements/drawer.rs index a7228f7..01e52c2 100644 --- a/src/elements/drawer.rs +++ b/src/elements/drawer.rs @@ -1,4 +1,3 @@ -use crate::elements::Element; use crate::parsers::{eol, take_lines_till}; use nom::{ @@ -16,7 +15,7 @@ pub struct Drawer<'a> { impl Drawer<'_> { #[inline] - pub(crate) fn parse(input: &str) -> IResult<&str, (Element<'_>, &str)> { + pub(crate) fn parse(input: &str) -> IResult<&str, (Drawer<'_>, &str)> { let (input, name) = delimited( tag(":"), take_while1(|c: char| c.is_ascii_alphabetic() || c == '-' || c == '_'), @@ -25,7 +24,7 @@ impl Drawer<'_> { let (input, _) = eol(input)?; let (input, contents) = take_lines_till(|line| line.eq_ignore_ascii_case(":END:"))(input)?; - Ok((input, (Element::Drawer(Drawer { name }), contents))) + Ok((input, (Drawer { name }, contents))) } } @@ -33,12 +32,6 @@ impl Drawer<'_> { fn parse() { assert_eq!( Drawer::parse(":PROPERTIES:\n :CUSTOM_ID: id\n :END:"), - Ok(( - "", - ( - Element::Drawer(Drawer { name: "PROPERTIES" }), - " :CUSTOM_ID: id\n" - ) - )) + Ok(("", (Drawer { name: "PROPERTIES" }, " :CUSTOM_ID: id\n"))) ) } diff --git a/src/elements/inline_call.rs b/src/elements/inline_call.rs index 662e61a..afb8cf9 100644 --- a/src/elements/inline_call.rs +++ b/src/elements/inline_call.rs @@ -1,7 +1,7 @@ use nom::{ bytes::complete::{tag, take_till}, combinator::opt, - sequence::delimited, + sequence::{delimited, preceded}, IResult, }; @@ -19,19 +19,25 @@ pub struct InlineCall<'a> { pub end_header: Option<&'a str>, } -fn header(input: &str) -> IResult<&str, &str> { - delimited(tag("["), take_till(|c| c == ']' || c == '\n'), tag("]"))(input) -} - impl<'a> InlineCall<'a> { #[inline] pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { - let (input, _) = tag("call_")(input)?; - let (input, name) = take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')')(input)?; - let (input, inside_header) = opt(header)(input)?; + let (input, name) = preceded( + tag("call_"), + take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'), + )(input)?; + let (input, inside_header) = opt(delimited( + tag("["), + take_till(|c| c == ']' || c == '\n'), + tag("]"), + ))(input)?; let (input, arguments) = delimited(tag("("), take_till(|c| c == ')' || c == '\n'), tag(")"))(input)?; - let (input, end_header) = opt(header)(input)?; + let (input, end_header) = opt(delimited( + tag("["), + take_till(|c| c == ']' || c == '\n'), + tag("]"), + ))(input)?; Ok(( input, diff --git a/src/elements/list.rs b/src/elements/list.rs index 326f501..83ba1dd 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -18,45 +18,46 @@ impl List { .unwrap_or((0, text)); let ordered = is_item(tail)?; - let bytes = text.as_bytes(); - let mut lines = memchr_iter(b'\n', bytes) - .map(|i| i + 1) - .chain(once(text.len())); - let mut pos = lines.next()?; - while let Some(i) = lines.next() { - let line = &text[pos..i]; - return if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) { - // this line is no empty + let mut last_end = 0; + let mut start = 0; + + for i in memchr_iter(b'\n', text.as_bytes()) + .map(|i| i + 1) + .chain(once(text.len())) + { + let line = &text[start..i]; + if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) { if line_indent < indent || (line_indent == indent && is_item(&line[line_indent..]).is_none()) { - Some((&text[pos..], List { indent, ordered }, &text[0..pos])) + return Some(( + &text[start..], + List { indent, ordered }, + &text[0..start - 1], + )); } else { - pos = i; + last_end = 0; + start = i; continue; } - } else if let Some(next_i) = lines.next() { - // this line is empty - let line = &text[i..next_i]; - if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) { - if line_indent < indent - || (line_indent == indent && is_item(&line[line_indent..]).is_none()) - { - Some((&text[pos..], List { indent, ordered }, &text[0..pos])) - } else { - pos = next_i; - continue; - } - } else { - Some((&text[next_i..], List { indent, ordered }, &text[0..pos])) - } } else { - Some((&text[i..], List { indent, ordered }, &text[0..pos])) - }; + // this line is empty + if last_end != 0 { + return Some((&text[i..], List { indent, ordered }, &text[0..last_end])); + } else { + last_end = start; + start = i; + continue; + } + } } - Some((&text[pos..], List { indent, ordered }, &text[0..pos])) + if last_end != 0 { + Some(("", List { indent, ordered }, &text[0..last_end])) + } else { + Some(("", List { indent, ordered }, text)) + } } } diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 9fe1593..ad6d127 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -55,7 +55,7 @@ pub use self::{ #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "kebab-case"))] pub enum Element<'a> { SpecialBlock(SpecialBlock<'a>), QuoteBlock(QuoteBlock<'a>), @@ -83,7 +83,6 @@ pub enum Element<'a> { List(List), ListItem(ListItem<'a>), Macros(Macros<'a>), - Planning(Planning<'a>), Snippet(Snippet<'a>), Text { value: &'a str }, Paragraph, @@ -162,7 +161,6 @@ impl_from!( Link, ListItem, Macros, - Planning, QuoteBlock, Snippet, SourceBlock, diff --git a/src/elements/snippet.rs b/src/elements/snippet.rs index b5ab4f0..cc55266 100644 --- a/src/elements/snippet.rs +++ b/src/elements/snippet.rs @@ -1,5 +1,5 @@ use nom::{ - bytes::complete::{tag, take, take_until, take_while1}, + bytes::complete::{tag, take_until, take_while1}, sequence::{delimited, separated_pair}, IResult, }; @@ -24,7 +24,7 @@ impl Snippet<'_> { tag(":"), take_until("@@"), ), - take(2usize), + tag("@@"), )(input)?; Ok((input, Element::Snippet(Snippet { name, value }))) @@ -40,7 +40,7 @@ fn parse() { Element::Snippet(Snippet { name: "html", value: "" - },) + }) )) ); assert_eq!( @@ -50,7 +50,7 @@ fn parse() { Element::Snippet(Snippet { name: "latex", value: "any arbitrary LaTeX code", - },) + }) )) ); assert_eq!( @@ -60,7 +60,7 @@ fn parse() { Element::Snippet(Snippet { name: "html", value: "", - },) + }) )) ); assert_eq!( @@ -70,7 +70,7 @@ fn parse() { Element::Snippet(Snippet { name: "html", value: "

@

", - },) + }) )) ); assert!(Snippet::parse("@@html:@").is_err()); diff --git a/src/elements/timestamp.rs b/src/elements/timestamp.rs index a45e138..a8e4d4f 100644 --- a/src/elements/timestamp.rs +++ b/src/elements/timestamp.rs @@ -22,7 +22,9 @@ pub struct Datetime<'a> { pub month: u8, pub day: u8, pub dayname: &'a str, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub hour: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub minute: Option, } @@ -103,29 +105,37 @@ mod chrono { #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(tag = "timestamp_type", rename_all = "kebab-case"))] #[derive(Debug)] pub enum Timestamp<'a> { Active { start: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, }, Inactive { start: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, }, ActiveRange { start: Datetime<'a>, end: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, }, InactiveRange { start: Datetime<'a>, end: Datetime<'a>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] repeater: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] delay: Option<&'a str>, }, Diary { diff --git a/src/export/html.rs b/src/export/html.rs index 12735b3..7adaa02 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -94,7 +94,6 @@ pub trait HtmlHandler> { Escape(link.desc.unwrap_or(link.path)), )?, Macros(_macros) => (), - Planning(_planning) => (), RadioTarget(_radio_target) => (), Snippet(snippet) => { if snippet.name.eq_ignore_ascii_case("HTML") { diff --git a/src/export/org.rs b/src/export/org.rs index 6becb02..9fcabb8 100644 --- a/src/export/org.rs +++ b/src/export/org.rs @@ -80,7 +80,6 @@ pub trait OrgHandler> { write!(&mut w, "]")?; } Macros(_macros) => (), - Planning(_planning) => (), RadioTarget(_radio_target) => (), Snippet(snippet) => write!(w, "@@{}:{}@@", snippet.name, snippet.value)?, Target(_target) => (), diff --git a/src/iter.rs b/src/iter.rs deleted file mode 100644 index 0c94517..0000000 --- a/src/iter.rs +++ /dev/null @@ -1,25 +0,0 @@ -use indextree::{Arena, NodeEdge, Traverse}; - -use crate::elements::Element; - -#[derive(Debug)] -pub enum Event<'a> { - Start(&'a Element<'a>), - End(&'a Element<'a>), -} - -pub struct Iter<'a> { - pub(crate) arena: &'a Arena>, - pub(crate) traverse: Traverse<'a, Element<'a>>, -} - -impl<'a> Iterator for Iter<'a> { - type Item = Event<'a>; - - fn next(&mut self) -> Option { - self.traverse.next().map(|edge| match edge { - NodeEdge::Start(e) => Event::Start(&self.arena[e].data), - NodeEdge::End(e) => Event::End(&self.arena[e].data), - }) - } -} diff --git a/src/lib.rs b/src/lib.rs index bf70979..c012167 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,7 +219,6 @@ mod config; pub mod elements; pub mod export; -mod iter; mod org; mod parsers; #[cfg(feature = "serde")] @@ -227,5 +226,4 @@ mod serde; pub use config::ParseConfig; pub use elements::Element; -pub use iter::{Event, Iter}; pub use org::Org; diff --git a/src/org.rs b/src/org.rs index d98f8fd..b21ffd0 100644 --- a/src/org.rs +++ b/src/org.rs @@ -1,4 +1,4 @@ -use indextree::{Arena, NodeId}; +use indextree::{Arena, NodeEdge, NodeId}; use jetscii::bytes; use memchr::{memchr, memchr2, memchr_iter}; use std::io::{Error, Write}; @@ -6,7 +6,7 @@ use std::io::{Error, Write}; use crate::config::ParseConfig; use crate::elements::*; use crate::export::*; -use crate::iter::{Event, Iter}; +use crate::parsers::skip_empty_lines; pub struct Org<'a> { pub(crate) arena: Arena>, @@ -42,12 +42,18 @@ enum Container<'a> { }, } -impl<'a> Org<'a> { - pub fn parse(text: &'a str) -> Self { +#[derive(Debug)] +pub enum Event<'a> { + Start(&'a Element<'a>), + End(&'a Element<'a>), +} + +impl Org<'_> { + pub fn parse(text: &str) -> Org<'_> { Org::parse_with_config(text, &ParseConfig::default()) } - pub fn parse_with_config(content: &'a str, config: &ParseConfig) -> Self { + pub fn parse_with_config<'a>(content: &'a str, config: &ParseConfig) -> Org<'a> { let mut arena = Arena::new(); let document = arena.new_node(Element::Document); @@ -82,7 +88,8 @@ impl<'a> Org<'a> { node: parent, } => { let mut tail = content; - let (new_tail, title, content) = Title::parse(tail, config); + let (new_tail, title) = Title::parse(tail, config).unwrap(); + let content = title.raw; let node = arena.new_node(Element::Title(title)); parent.append(node, &mut arena).unwrap(); containers.push(Container::Inline { content, node }); @@ -120,11 +127,13 @@ impl<'a> Org<'a> { Org { arena, document } } - pub fn iter(&'a self) -> Iter<'a> { - Iter { - arena: &self.arena, - traverse: self.document.traverse(&self.arena), - } + pub fn iter<'a>(&'a self) -> impl Iterator> + 'a { + self.document + .traverse(&self.arena) + .map(move |edge| match edge { + NodeEdge::Start(e) => Event::Start(&self.arena[e].data), + NodeEdge::End(e) => Event::End(&self.arena[e].data), + }) } pub fn html(&self, wrtier: W) -> Result<(), Error> { @@ -305,8 +314,10 @@ fn parse_block<'a>( } if tail.starts_with(':') { - if let Ok((tail, (drawer, _content))) = Drawer::parse(tail) { - return Some((tail, arena.new_node(drawer))); + if let Ok((tail, (drawer, content))) = Drawer::parse(tail) { + let node = arena.new_node(drawer.into()); + containers.push(Container::Block { content, node }); + return Some((tail, node)); } } @@ -629,28 +640,3 @@ fn parse_list_items<'a>( contents = tail; } } - -fn skip_empty_lines(contents: &str) -> &str { - let mut i = 0; - for pos in memchr_iter(b'\n', contents.as_bytes()) { - if contents.as_bytes()[i..pos] - .iter() - .all(u8::is_ascii_whitespace) - { - i = pos + 1; - } else { - break; - } - } - &contents[i..] -} - -#[test] -fn test_skip_empty_lines() { - assert_eq!(skip_empty_lines("foo"), "foo"); - assert_eq!(skip_empty_lines(" foo"), " foo"); - assert_eq!(skip_empty_lines(" \nfoo\n"), "foo\n"); - assert_eq!(skip_empty_lines(" \n\n\nfoo\n"), "foo\n"); - assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), "foo\n"); - assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " foo\n"); -} diff --git a/src/parsers.rs b/src/parsers.rs index cfea23d..a1ca1e6 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -2,8 +2,11 @@ use memchr::{memchr, memchr_iter}; use nom::{ - bytes::complete::tag, character::complete::space0, error::ErrorKind, error_position, Err, - IResult, + branch::alt, + bytes::complete::{tag, take_till}, + character::complete::space0, + error::ErrorKind, + error_position, Err, IResult, }; pub(crate) fn eol(input: &str) -> IResult<&str, ()> { @@ -43,3 +46,34 @@ pub(crate) fn take_lines_till( } } } + +pub(crate) fn take_one_word(input: &str) -> IResult<&str, &str> { + alt((take_till(|c: char| c == ' ' || c == '\t'), |input| { + Ok(("", input)) + }))(input) +} + +pub(crate) fn skip_empty_lines(contents: &str) -> &str { + let mut i = 0; + for pos in memchr_iter(b'\n', contents.as_bytes()) { + if contents.as_bytes()[i..pos] + .iter() + .all(u8::is_ascii_whitespace) + { + i = pos + 1; + } else { + break; + } + } + &contents[i..] +} + +#[test] +fn test_skip_empty_lines() { + assert_eq!(skip_empty_lines("foo"), "foo"); + assert_eq!(skip_empty_lines(" foo"), " foo"); + assert_eq!(skip_empty_lines(" \nfoo\n"), "foo\n"); + assert_eq!(skip_empty_lines(" \n\n\nfoo\n"), "foo\n"); + assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), "foo\n"); + assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " foo\n"); +}