feat(elements): cleanup and minor refactor

This commit is contained in:
PoiScript 2019-08-09 22:15:06 +08:00
parent c1465a6d77
commit 9bc260627b
20 changed files with 802 additions and 835 deletions

View file

@ -2,71 +2,7 @@ use std::borrow::Cow;
use nom::{bytes::complete::tag_no_case, character::complete::alpha1, sequence::preceded, IResult}; use nom::{bytes::complete::tag_no_case, character::complete::alpha1, sequence::preceded, IResult};
use crate::parsers::{take_lines_till, take_until_eol}; use crate::parsers::{line, take_lines_while};
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Block<'a> {
pub name: Cow<'a, str>,
pub args: Option<Cow<'a, str>>,
}
impl Block<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, (Block<'_>, &str)> {
let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?;
let (input, args) = take_until_eol(input)?;
let end_line = format!(r"#+END_{}", name);
let (input, contents) =
take_lines_till(|line| line.eq_ignore_ascii_case(&end_line))(input)?;
Ok((
input,
(
Block {
name: name.into(),
args: if args.is_empty() {
None
} else {
Some(args.into())
},
},
contents,
),
))
}
}
#[test]
fn parse() {
assert_eq!(
Block::parse("#+BEGIN_SRC\n#+END_SRC"),
Ok((
"",
(
Block {
name: "SRC".into(),
args: None,
},
""
)
))
);
assert_eq!(
Block::parse("#+BEGIN_SRC javascript \nconsole.log('Hello World!');\n#+END_SRC\n"),
Ok((
"",
(
Block {
name: "SRC".into(),
args: Some("javascript".into()),
},
"console.log('Hello World!');\n"
)
))
);
// TODO: more testing
}
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
@ -129,3 +65,60 @@ pub struct SourceBlock<'a> {
pub language: Cow<'a, str>, pub language: Cow<'a, str>,
pub arguments: Cow<'a, str>, pub arguments: Cow<'a, str>,
} }
pub(crate) fn parse_block_element(input: &str) -> IResult<&str, (&str, Option<&str>, &str)> {
let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?;
let (input, args) = line(input)?;
let end_line = format!(r"#+END_{}", name);
let (input, contents) =
take_lines_while(|line| !line.trim().eq_ignore_ascii_case(&end_line))(input)?;
let (input, _) = line(input)?;
Ok((
input,
(
name,
if args.trim().is_empty() {
None
} else {
Some(args.trim())
},
contents,
),
))
}
#[test]
fn parse() {
assert_eq!(
parse_block_element(
r#"#+BEGIN_SRC
#+END_SRC"#
),
Ok(("", ("SRC".into(), None, "")))
);
assert_eq!(
parse_block_element(
r#"#+begin_src
#+end_src"#
),
Ok(("", ("src".into(), None, "")))
);
assert_eq!(
parse_block_element(
r#"#+BEGIN_SRC javascript
console.log('Hello World!');
#+END_SRC
"#
),
Ok((
"",
(
"SRC".into(),
Some("javascript".into()),
"console.log('Hello World!');\n"
)
))
);
// TODO: more testing
}

View file

@ -8,7 +8,7 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::{Datetime, Element, Timestamp}; use crate::elements::{Datetime, Timestamp};
use crate::parsers::eol; use crate::parsers::eol;
/// clock elements /// clock elements
@ -40,7 +40,7 @@ pub enum Clock<'a> {
} }
impl Clock<'_> { impl Clock<'_> {
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, Clock<'_>> {
let (input, _) = tag("CLOCK:")(input)?; let (input, _) = tag("CLOCK:")(input)?;
let (input, _) = space0(input)?; let (input, _) = space0(input)?;
let (input, timestamp) = Timestamp::parse_inactive(input)?; let (input, timestamp) = Timestamp::parse_inactive(input)?;
@ -60,13 +60,13 @@ impl Clock<'_> {
let (input, _) = eol(input)?; let (input, _) = eol(input)?;
Ok(( Ok((
input, input,
Element::Clock(Clock::Closed { Clock::Closed {
start, start,
end, end,
repeater, repeater,
delay, delay,
duration: duration.into(), duration: duration.into(),
}), },
)) ))
} }
Timestamp::Inactive { Timestamp::Inactive {
@ -77,11 +77,11 @@ impl Clock<'_> {
let (input, _) = eol(input)?; let (input, _) = eol(input)?;
Ok(( Ok((
input, input,
Element::Clock(Clock::Running { Clock::Running {
start, start,
repeater, repeater,
delay, delay,
}), },
)) ))
} }
_ => unreachable!( _ => unreachable!(
@ -148,7 +148,7 @@ fn parse() {
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"), Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Ok(( Ok((
"", "",
Element::Clock(Clock::Running { Clock::Running {
start: Datetime { start: Datetime {
year: 2003, year: 2003,
month: 9, month: 9,
@ -159,14 +159,14 @@ fn parse() {
}, },
repeater: None, repeater: None,
delay: None, delay: None,
}) }
)) ))
); );
assert_eq!( assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"), Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"),
Ok(( Ok((
"", "",
Element::Clock(Clock::Closed { Clock::Closed {
start: Datetime { start: Datetime {
year: 2003, year: 2003,
month: 9, month: 9,
@ -186,7 +186,7 @@ fn parse() {
repeater: None, repeater: None,
delay: None, delay: None,
duration: "1:00".into(), duration: "1:00".into(),
}) }
)) ))
); );
} }

View file

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::parsers::{eol, take_lines_till}; use crate::parsers::{eol, line, take_lines_while};
use nom::{ use nom::{
bytes::complete::{tag, take_while1}, bytes::complete::{tag, take_while1},
@ -24,7 +24,9 @@ impl Drawer<'_> {
tag(":"), tag(":"),
)(input)?; )(input)?;
let (input, _) = eol(input)?; let (input, _) = eol(input)?;
let (input, contents) = take_lines_till(|line| line.eq_ignore_ascii_case(":END:"))(input)?; let (input, contents) =
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() }, contents)))
} }

View file

@ -1,7 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::elements::Element; use crate::parsers::{line, take_lines_while};
use crate::parsers::{take_lines_till, take_until_eol};
use nom::{ use nom::{
bytes::complete::tag_no_case, bytes::complete::tag_no_case,
@ -20,25 +19,26 @@ pub struct DynBlock<'a> {
impl DynBlock<'_> { impl DynBlock<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, (Element<'_>, &str)> { pub(crate) fn parse(input: &str) -> IResult<&str, (DynBlock<'_>, &str)> {
let (input, _) = tag_no_case("#+BEGIN:")(input)?; let (input, _) = tag_no_case("#+BEGIN:")(input)?;
let (input, _) = space1(input)?; let (input, _) = space1(input)?;
let (input, name) = alpha1(input)?; let (input, name) = alpha1(input)?;
let (input, args) = take_until_eol(input)?; let (input, args) = line(input)?;
let (input, contents) =
let (input, contents) = take_lines_till(|line| line.eq_ignore_ascii_case("#+END:"))(input)?; take_lines_while(|line| !line.trim().eq_ignore_ascii_case("#+END:"))(input)?;
let (input, _) = line(input)?;
Ok(( Ok((
input, input,
( (
Element::DynBlock(DynBlock { DynBlock {
block_name: name.into(), block_name: name.into(),
arguments: if args.is_empty() { arguments: if args.trim().is_empty() {
None None
} else { } else {
Some(args.into()) Some(args.trim().into())
},
}, },
}),
contents, contents,
), ),
)) ))
@ -49,14 +49,19 @@ impl DynBlock<'_> {
fn parse() { fn parse() {
// TODO: testing // TODO: testing
assert_eq!( assert_eq!(
DynBlock::parse("#+BEGIN: clocktable :scope file\nCONTENTS\n#+END:\n"), DynBlock::parse(
r#"#+BEGIN: clocktable :scope file
CONTENTS
#+END:
"#
),
Ok(( Ok((
"", "",
( (
Element::DynBlock(DynBlock { DynBlock {
block_name: "clocktable".into(), block_name: "clocktable".into(),
arguments: Some(":scope file".into()), arguments: Some(":scope file".into()),
}), },
"CONTENTS\n" "CONTENTS\n"
) )
)) ))

View file

@ -2,7 +2,7 @@ use bytecount::count;
use memchr::memchr_iter; use memchr::memchr_iter;
#[inline] #[inline]
pub(crate) fn parse(text: &str, marker: u8) -> Option<(&str, &str)> { pub(crate) fn parse_emphasis(text: &str, marker: u8) -> Option<(&str, &str)> {
debug_assert!(text.len() >= 3); debug_assert!(text.len() >= 3);
let bytes = text.as_bytes(); let bytes = text.as_bytes();
@ -35,19 +35,14 @@ fn validate_marker(pos: usize, text: &str) -> bool {
} }
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse; assert_eq!(parse_emphasis("*bold*", b'*'), Some(("", "bold")));
assert_eq!(parse_emphasis("*bo*ld*", b'*'), Some(("", "bo*ld")));
assert_eq!(parse("*bold*", b'*'), Some(("", "bold"))); assert_eq!(parse_emphasis("*bo\nld*", b'*'), Some(("", "bo\nld")));
assert_eq!(parse("*bo*ld*", b'*'), Some(("", "bo*ld"))); assert_eq!(parse_emphasis("*bold*a", b'*'), None);
assert_eq!(parse("*bo\nld*", b'*'), Some(("", "bo\nld"))); assert_eq!(parse_emphasis("*bold*", b'/'), None);
assert_eq!(parse("*bold*a", b'*'), None); assert_eq!(parse_emphasis("*bold *", b'*'), None);
assert_eq!(parse("*bold*", b'/'), None); assert_eq!(parse_emphasis("* bold*", b'*'), None);
assert_eq!(parse("*bold *", b'*'), None); assert_eq!(parse_emphasis("*b\nol\nd*", b'*'), None);
assert_eq!(parse("* bold*", b'*'), None);
assert_eq!(parse("*b\nol\nd*", b'*'), None);
}
} }

View file

@ -1,12 +1,13 @@
use std::borrow::Cow; use std::borrow::Cow;
use memchr::memchr;
use nom::{ use nom::{
bytes::complete::{tag, take_while1}, bytes::complete::{tag, take_while1},
sequence::delimited, sequence::delimited,
IResult, IResult,
}; };
use crate::parsers::line;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -14,29 +15,24 @@ pub struct FnDef<'a> {
pub label: Cow<'a, str>, pub label: Cow<'a, str>,
} }
fn parse_label(input: &str) -> IResult<&str, &str> { impl FnDef<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, (FnDef<'_>, &str)> {
let (input, label) = delimited( let (input, label) = delimited(
tag("[fn:"), tag("[fn:"),
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'), take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
tag("]"), tag("]"),
)(input)?; )(input)?;
let (input, content) = line(input)?;
Ok((input, label)) Ok((
} input,
(
impl FnDef<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, FnDef<'_>, &str)> {
let (tail, label) = parse_label(text).ok()?;
let end = memchr(b'\n', tail.as_bytes()).unwrap_or_else(|| tail.len());
Some((
&tail[end..],
FnDef { FnDef {
label: label.into(), label: label.into(),
}, },
&tail[0..end], content,
),
)) ))
} }
} }
@ -45,39 +41,46 @@ impl FnDef<'_> {
fn parse() { fn parse() {
assert_eq!( assert_eq!(
FnDef::parse("[fn:1] https://orgmode.org"), FnDef::parse("[fn:1] https://orgmode.org"),
Some(("", FnDef { label: "1".into() }, " https://orgmode.org")) Ok(("", (FnDef { label: "1".into() }, " https://orgmode.org")))
); );
assert_eq!( assert_eq!(
FnDef::parse("[fn:word_1] https://orgmode.org"), FnDef::parse("[fn:word_1] https://orgmode.org"),
Some(( Ok((
"", "",
(
FnDef { FnDef {
label: "word_1".into() label: "word_1".into()
}, },
" https://orgmode.org" " https://orgmode.org"
)
)) ))
); );
assert_eq!( assert_eq!(
FnDef::parse("[fn:WORD-1] https://orgmode.org"), FnDef::parse("[fn:WORD-1] https://orgmode.org"),
Some(( Ok((
"", "",
(
FnDef { FnDef {
label: "WORD-1".into() label: "WORD-1".into()
}, },
" https://orgmode.org" " https://orgmode.org"
)
)) ))
); );
assert_eq!( assert_eq!(
FnDef::parse("[fn:WORD]"), FnDef::parse("[fn:WORD]"),
Some(( Ok((
"", "",
(
FnDef { FnDef {
label: "WORD".into() label: "WORD".into()
}, },
"" ""
)
)) ))
); );
assert_eq!(FnDef::parse("[fn:] https://orgmode.org"), None);
assert_eq!(FnDef::parse("[fn:wor d] https://orgmode.org"), None); assert!(FnDef::parse("[fn:] https://orgmode.org").is_err());
assert_eq!(FnDef::parse("[fn:WORD https://orgmode.org"), None); assert!(FnDef::parse("[fn:wor d] https://orgmode.org").is_err());
assert!(FnDef::parse("[fn:WORD https://orgmode.org").is_err());
} }

View file

@ -7,8 +7,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -21,9 +19,9 @@ pub struct InlineCall<'a> {
pub end_header: Option<Cow<'a, str>>, pub end_header: Option<Cow<'a, str>>,
} }
impl<'a> InlineCall<'a> { impl InlineCall<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, InlineCall<'_>> {
let (input, name) = preceded( let (input, name) = preceded(
tag("call_"), tag("call_"),
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'), take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
@ -43,12 +41,12 @@ impl<'a> InlineCall<'a> {
Ok(( Ok((
input, input,
Element::InlineCall(InlineCall { InlineCall {
name: name.into(), name: name.into(),
arguments: arguments.into(), arguments: arguments.into(),
inside_header: inside_header.map(Into::into), inside_header: inside_header.map(Into::into),
end_header: end_header.map(Into::into), end_header: end_header.map(Into::into),
}), },
)) ))
} }
} }
@ -59,48 +57,48 @@ fn parse() {
InlineCall::parse("call_square(4)"), InlineCall::parse("call_square(4)"),
Ok(( Ok((
"", "",
Element::InlineCall(InlineCall { InlineCall {
name: "square".into(), name: "square".into(),
arguments: "4".into(), arguments: "4".into(),
inside_header: None, inside_header: None,
end_header: None, end_header: None,
}), }
)) ))
); );
assert_eq!( assert_eq!(
InlineCall::parse("call_square[:results output](4)"), InlineCall::parse("call_square[:results output](4)"),
Ok(( Ok((
"", "",
Element::InlineCall(InlineCall { InlineCall {
name: "square".into(), name: "square".into(),
arguments: "4".into(), arguments: "4".into(),
inside_header: Some(":results output".into()), inside_header: Some(":results output".into()),
end_header: None, end_header: None,
}), },
)) ))
); );
assert_eq!( assert_eq!(
InlineCall::parse("call_square(4)[:results html]"), InlineCall::parse("call_square(4)[:results html]"),
Ok(( Ok((
"", "",
Element::InlineCall(InlineCall { InlineCall {
name: "square".into(), name: "square".into(),
arguments: "4".into(), arguments: "4".into(),
inside_header: None, inside_header: None,
end_header: Some(":results html".into()), end_header: Some(":results html".into()),
}), },
)) ))
); );
assert_eq!( assert_eq!(
InlineCall::parse("call_square[:results output](4)[:results html]"), InlineCall::parse("call_square[:results output](4)[:results html]"),
Ok(( Ok((
"", "",
Element::InlineCall(InlineCall { InlineCall {
name: "square".into(), name: "square".into(),
arguments: "4".into(), arguments: "4".into(),
inside_header: Some(":results output".into()), inside_header: Some(":results output".into()),
end_header: Some(":results html".into()), end_header: Some(":results html".into()),
}), },
)) ))
); );
} }

View file

@ -7,8 +7,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -21,7 +19,7 @@ pub struct InlineSrc<'a> {
impl InlineSrc<'_> { impl InlineSrc<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, InlineSrc<'_>> {
let (input, _) = tag("src_")(input)?; let (input, _) = tag("src_")(input)?;
let (input, lang) = let (input, lang) =
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?; take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?;
@ -35,11 +33,11 @@ impl InlineSrc<'_> {
Ok(( Ok((
input, input,
Element::InlineSrc(InlineSrc { InlineSrc {
lang: lang.into(), lang: lang.into(),
options: options.map(Into::into), options: options.map(Into::into),
body: body.into(), body: body.into(),
}), },
)) ))
} }
} }
@ -50,22 +48,22 @@ fn parse() {
InlineSrc::parse("src_C{int a = 0;}"), InlineSrc::parse("src_C{int a = 0;}"),
Ok(( Ok((
"", "",
Element::InlineSrc(InlineSrc { InlineSrc {
lang: "C".into(), lang: "C".into(),
options: None, options: None,
body: "int a = 0;".into() body: "int a = 0;".into()
}), },
)) ))
); );
assert_eq!( assert_eq!(
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"), InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
Ok(( Ok((
"", "",
Element::InlineSrc(InlineSrc { InlineSrc {
lang: "xml".into(), lang: "xml".into(),
options: Some(":exports code".into()), options: Some(":exports code".into()),
body: "<tag>text</tag>".into(), body: "<tag>text</tag>".into(),
}), },
)) ))
); );

View file

@ -7,8 +7,7 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element; use crate::parsers::line;
use crate::parsers::take_until_eol;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
@ -27,132 +26,52 @@ pub struct BabelCall<'a> {
pub value: Cow<'a, str>, pub value: Cow<'a, str>,
} }
impl Keyword<'_> { pub(crate) fn parse_keyword(input: &str) -> IResult<&str, (&str, Option<&str>, &str)> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("#+")(input)?; let (input, _) = tag("#+")(input)?;
let (input, key) = let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
let (input, optional) = opt(delimited( let (input, optional) = opt(delimited(
tag("["), tag("["),
take_till(|c| c == ']' || c == '\n'), take_till(|c| c == ']' || c == '\n'),
tag("]"), tag("]"),
))(input)?; ))(input)?;
let (input, _) = tag(":")(input)?; let (input, _) = tag(":")(input)?;
let (input, value) = take_until_eol(input)?; let (input, value) = line(input)?;
if key.eq_ignore_ascii_case("CALL") { Ok((input, (key, optional, value.trim())))
Ok((
input,
Element::BabelCall(BabelCall {
value: value.into(),
}),
))
} else {
Ok((
input,
Element::Keyword(Keyword {
key: key.into(),
optional: optional.map(Into::into),
value: value.into(),
}),
))
}
}
} }
#[test] #[test]
fn parse() { fn parse() {
assert_eq!(parse_keyword("#+KEY:"), Ok(("", ("KEY", None, ""))));
assert_eq!( assert_eq!(
Keyword::parse("#+KEY:"), parse_keyword("#+KEY: VALUE"),
Ok(( Ok(("", ("KEY", None, "VALUE")))
"",
Element::Keyword(Keyword {
key: "KEY".into(),
optional: None,
value: "".into(),
})
))
); );
assert_eq!( assert_eq!(
Keyword::parse("#+KEY: VALUE"), parse_keyword("#+K_E_Y: VALUE"),
Ok(( Ok(("", ("K_E_Y", None, "VALUE")))
"",
Element::Keyword(Keyword {
key: "KEY".into(),
optional: None,
value: "VALUE".into(),
})
))
); );
assert_eq!( assert_eq!(
Keyword::parse("#+K_E_Y: VALUE"), parse_keyword("#+KEY:VALUE\n"),
Ok(( Ok(("", ("KEY", None, "VALUE")))
"",
Element::Keyword(Keyword {
key: "K_E_Y".into(),
optional: None,
value: "VALUE".into(),
})
))
); );
assert_eq!( assert!(parse_keyword("#+KE Y: VALUE").is_err());
Keyword::parse("#+KEY:VALUE\n"), assert!(parse_keyword("#+ KEY: VALUE").is_err());
Ok((
"", assert_eq!(parse_keyword("#+RESULTS:"), Ok(("", ("RESULTS", None, ""))));
Element::Keyword(Keyword {
key: "KEY".into(),
optional: None,
value: "VALUE".into(),
})
))
);
assert!(Keyword::parse("#+KE Y: VALUE").is_err());
assert!(Keyword::parse("#+ KEY: VALUE").is_err());
assert_eq!( assert_eq!(
Keyword::parse("#+RESULTS:"), parse_keyword("#+ATTR_LATEX: :width 5cm\n"),
Ok(( Ok(("", ("ATTR_LATEX", None, ":width 5cm")))
"",
Element::Keyword(Keyword {
key: "RESULTS".into(),
optional: None,
value: "".into(),
})
))
); );
assert_eq!( assert_eq!(
Keyword::parse("#+ATTR_LATEX: :width 5cm\n"), parse_keyword("#+CALL: double(n=4)"),
Ok(( Ok(("", ("CALL", None, "double(n=4)")))
"",
Element::Keyword(Keyword {
key: "ATTR_LATEX".into(),
optional: None,
value: ":width 5cm".into(),
})
))
); );
assert_eq!( assert_eq!(
Keyword::parse("#+CALL: double(n=4)"), parse_keyword("#+CAPTION[Short caption]: Longer caption."),
Ok(( Ok(("", ("CAPTION", Some("Short caption"), "Longer caption.",)))
"",
Element::BabelCall(BabelCall {
value: "double(n=4)".into(),
})
))
);
assert_eq!(
Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
Ok((
"",
Element::Keyword(Keyword {
key: "CAPTION".into(),
optional: Some("Short caption".into()),
value: "Longer caption.".into(),
})
))
); );
} }

View file

@ -7,8 +7,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -20,7 +18,7 @@ pub struct Link<'a> {
impl Link<'_> { impl Link<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, Link<'_>> {
let (input, path) = delimited( let (input, path) = delimited(
tag("[["), tag("[["),
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'), take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
@ -34,10 +32,10 @@ impl Link<'_> {
let (input, _) = tag("]")(input)?; let (input, _) = tag("]")(input)?;
Ok(( Ok((
input, input,
Element::Link(Link { Link {
path: path.into(), path: path.into(),
desc: desc.map(Into::into), desc: desc.map(Into::into),
}), },
)) ))
} }
} }
@ -48,20 +46,20 @@ fn parse() {
Link::parse("[[#id]]"), Link::parse("[[#id]]"),
Ok(( Ok((
"", "",
Element::Link(Link { Link {
path: "#id".into(), path: "#id".into(),
desc: None desc: None
},) }
)) ))
); );
assert_eq!( assert_eq!(
Link::parse("[[#id][desc]]"), Link::parse("[[#id][desc]]"),
Ok(( Ok((
"", "",
Element::Link(Link { Link {
path: "#id".into(), path: "#id".into(),
desc: Some("desc".into()) desc: Some("desc".into())
}) }
)) ))
); );
assert!(Link::parse("[[#id][desc]").is_err()); assert!(Link::parse("[[#id][desc]").is_err());

View file

@ -7,8 +7,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -20,7 +18,7 @@ pub struct Macros<'a> {
impl Macros<'_> { impl Macros<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, Macros<'_>> {
let (input, _) = tag("{{{")(input)?; let (input, _) = tag("{{{")(input)?;
let (input, name) = verify( let (input, name) = verify(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'), take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
@ -31,10 +29,10 @@ impl Macros<'_> {
Ok(( Ok((
input, input,
Element::Macros(Macros { Macros {
name: name.into(), name: name.into(),
arguments: arguments.map(Into::into), arguments: arguments.map(Into::into),
}), },
)) ))
} }
} }
@ -45,30 +43,30 @@ fn parse() {
Macros::parse("{{{poem(red,blue)}}}"), Macros::parse("{{{poem(red,blue)}}}"),
Ok(( Ok((
"", "",
Element::Macros(Macros { Macros {
name: "poem".into(), name: "poem".into(),
arguments: Some("red,blue".into()) arguments: Some("red,blue".into())
}) }
)) ))
); );
assert_eq!( assert_eq!(
Macros::parse("{{{poem())}}}"), Macros::parse("{{{poem())}}}"),
Ok(( Ok((
"", "",
Element::Macros(Macros { Macros {
name: "poem".into(), name: "poem".into(),
arguments: Some(")".into()) arguments: Some(")".into())
}) }
)) ))
); );
assert_eq!( assert_eq!(
Macros::parse("{{{author}}}"), Macros::parse("{{{author}}}"),
Ok(( Ok((
"", "",
Element::Macros(Macros { Macros {
name: "author".into(), name: "author".into(),
arguments: None arguments: None
}) }
)) ))
); );
assert!(Macros::parse("{{{0uthor}}}").is_err()); assert!(Macros::parse("{{{0uthor}}}").is_err());

View file

@ -23,8 +23,10 @@ mod target;
mod timestamp; mod timestamp;
mod title; mod title;
pub(crate) use block::Block; pub(crate) use self::{
pub(crate) use emphasis::parse as parse_emphasis; block::parse_block_element, emphasis::parse_emphasis, keyword::parse_keyword, rule::parse_rule,
table::parse_table_el,
};
pub use self::{ pub use self::{
block::{ block::{
@ -45,7 +47,6 @@ pub use self::{
macros::Macros, macros::Macros,
planning::Planning, planning::Planning,
radio_target::RadioTarget, radio_target::RadioTarget,
rule::Rule,
snippet::Snippet, snippet::Snippet,
table::{Table, TableRow}, table::{Table, TableRow},
target::Target, target::Target,
@ -53,6 +54,8 @@ pub use self::{
title::Title, title::Title,
}; };
use std::borrow::Cow;
/// Org-mode element enum /// Org-mode element enum
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
@ -95,10 +98,10 @@ pub enum Element<'a> {
Strike, Strike,
Italic, Italic,
Underline, Underline,
Verbatim { value: &'a str }, Verbatim { value: Cow<'a, str> },
Code { value: &'a str }, Code { value: Cow<'a, str> },
Comment { value: &'a str }, Comment { value: Cow<'a, str> },
FixedWidth { value: &'a str }, FixedWidth { value: Cow<'a, str> },
Title(Title<'a>), Title(Title<'a>),
Table(Table<'a>), Table(Table<'a>),
TableRow(TableRow), TableRow(TableRow),
@ -176,6 +179,7 @@ impl_from!(
Target, Target,
Timestamp, Timestamp,
Table, Table,
Title,
VerseBlock; VerseBlock;
RadioTarget, RadioTarget,
List, List,

View file

@ -1,37 +1,28 @@
use nom::{bytes::complete::take_while_m_n, character::complete::space0, IResult}; use nom::{bytes::complete::take_while_m_n, IResult};
use std::usize; use std::usize;
use crate::elements::Element;
use crate::parsers::eol; use crate::parsers::eol;
pub struct Rule; pub(crate) fn parse_rule(input: &str) -> IResult<&str, ()> {
impl Rule {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = space0(input)?;
let (input, _) = take_while_m_n(5, usize::MAX, |c| c == '-')(input)?; let (input, _) = take_while_m_n(5, usize::MAX, |c| c == '-')(input)?;
let (input, _) = eol(input)?; let (input, _) = eol(input)?;
Ok((input, Element::Rule)) Ok((input, ()))
}
} }
#[test] #[test]
fn parse() { fn parse() {
assert_eq!(Rule::parse("-----"), Ok(("", Element::Rule))); assert_eq!(parse_rule("-----"), Ok(("", ())));
assert_eq!(Rule::parse("--------"), Ok(("", Element::Rule))); assert_eq!(parse_rule("--------"), Ok(("", ())));
assert_eq!(Rule::parse(" -----"), Ok(("", Element::Rule))); assert_eq!(parse_rule("-----\n"), Ok(("", ())));
assert_eq!(Rule::parse("\t\t-----"), Ok(("", Element::Rule))); assert_eq!(parse_rule("----- \n"), Ok(("", ())));
assert_eq!(Rule::parse("\t\t-----\n"), Ok(("", Element::Rule))); assert!(parse_rule("").is_err());
assert_eq!(Rule::parse("\t\t----- \n"), Ok(("", Element::Rule))); assert!(parse_rule("----").is_err());
assert!(Rule::parse("").is_err()); assert!(parse_rule("----").is_err());
assert!(Rule::parse("----").is_err()); assert!(parse_rule("None----").is_err());
assert!(Rule::parse(" ----").is_err()); assert!(parse_rule("None ----").is_err());
assert!(Rule::parse(" None----").is_err()); assert!(parse_rule("None------").is_err());
assert!(Rule::parse("None ----").is_err()); assert!(parse_rule("----None----").is_err());
assert!(Rule::parse("None------").is_err()); assert!(parse_rule("\t\t----").is_err());
assert!(Rule::parse("----None----").is_err()); assert!(parse_rule("------None").is_err());
assert!(Rule::parse("\t\t----").is_err()); assert!(parse_rule("----- None").is_err());
assert!(Rule::parse("------None").is_err());
assert!(Rule::parse("----- None").is_err());
} }

View file

@ -6,8 +6,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -18,7 +16,7 @@ pub struct Snippet<'a> {
impl Snippet<'_> { impl Snippet<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, Snippet<'_>> {
let (input, (name, value)) = delimited( let (input, (name, value)) = delimited(
tag("@@"), tag("@@"),
separated_pair( separated_pair(
@ -31,10 +29,10 @@ impl Snippet<'_> {
Ok(( Ok((
input, input,
Element::Snippet(Snippet { Snippet {
name: name.into(), name: name.into(),
value: value.into(), value: value.into(),
}), },
)) ))
} }
} }
@ -45,40 +43,40 @@ fn parse() {
Snippet::parse("@@html:<b>@@"), Snippet::parse("@@html:<b>@@"),
Ok(( Ok((
"", "",
Element::Snippet(Snippet { Snippet {
name: "html".into(), name: "html".into(),
value: "<b>".into() value: "<b>".into()
}) }
)) ))
); );
assert_eq!( assert_eq!(
Snippet::parse("@@latex:any arbitrary LaTeX code@@"), Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
Ok(( Ok((
"", "",
Element::Snippet(Snippet { Snippet {
name: "latex".into(), name: "latex".into(),
value: "any arbitrary LaTeX code".into(), value: "any arbitrary LaTeX code".into(),
}) }
)) ))
); );
assert_eq!( assert_eq!(
Snippet::parse("@@html:@@"), Snippet::parse("@@html:@@"),
Ok(( Ok((
"", "",
Element::Snippet(Snippet { Snippet {
name: "html".into(), name: "html".into(),
value: "".into(), value: "".into(),
}) }
)) ))
); );
assert_eq!( assert_eq!(
Snippet::parse("@@html:<p>@</p>@@"), Snippet::parse("@@html:<p>@</p>@@"),
Ok(( Ok((
"", "",
Element::Snippet(Snippet { Snippet {
name: "html".into(), name: "html".into(),
value: "<p>@</p>".into(), value: "<p>@</p>".into(),
}) }
)) ))
); );
assert!(Snippet::parse("@@html:<b>@").is_err()); assert!(Snippet::parse("@@html:<b>@").is_err());

View file

@ -1,5 +1,12 @@
use std::borrow::Cow; use std::borrow::Cow;
use nom::{
combinator::{peek, verify},
IResult,
};
use crate::parsers::{line, take_lines_while};
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
@ -34,3 +41,35 @@ impl TableRow {
} }
} }
} }
pub(crate) fn parse_table_el(input: &str) -> IResult<&str, &str> {
let (input, _) = peek(verify(line, |s: &str| {
let s = s.trim();
s.starts_with("+-") && s.as_bytes().iter().all(|&c| c == b'+' || c == b'-')
}))(input)?;
take_lines_while(|line| line.starts_with('|') || line.starts_with('+'))(input)
}
#[test]
fn parse_table_el_() {
assert_eq!(
parse_table_el(
r#"+---+
| |
+---+
"#
),
Ok((
r#"
"#,
r#"+---+
| |
+---+
"#
))
);
assert!(parse_table_el("").is_err());
assert!(parse_table_el("+----|---").is_err());
}

View file

@ -7,8 +7,6 @@ use nom::{
IResult, IResult,
}; };
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)] #[derive(Debug)]
@ -18,7 +16,7 @@ pub struct Target<'a> {
impl Target<'_> { impl Target<'_> {
#[inline] #[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> { pub(crate) fn parse(input: &str) -> IResult<&str, Target<'_>> {
let (input, target) = delimited( let (input, target) = delimited(
tag("<<"), tag("<<"),
verify( verify(
@ -30,9 +28,9 @@ impl Target<'_> {
Ok(( Ok((
input, input,
Element::Target(Target { Target {
target: target.into(), target: target.into(),
}), },
)) ))
} }
} }
@ -43,18 +41,18 @@ fn parse() {
Target::parse("<<target>>"), Target::parse("<<target>>"),
Ok(( Ok((
"", "",
Element::Target(Target { Target {
target: "target".into() target: "target".into()
}) }
)) ))
); );
assert_eq!( assert_eq!(
Target::parse("<<tar get>>"), Target::parse("<<tar get>>"),
Ok(( Ok((
"", "",
Element::Target(Target { Target {
target: "tar get".into() target: "tar get".into()
}) }
)) ))
); );
assert!(Target::parse("<<target >>").is_err()); assert!(Target::parse("<<target >>").is_err());

View file

@ -17,7 +17,7 @@ use std::collections::HashMap;
use crate::config::ParseConfig; use crate::config::ParseConfig;
use crate::elements::{Drawer, Planning}; use crate::elements::{Drawer, Planning};
use crate::parsers::{skip_empty_lines, take_one_word, take_until_eol}; use crate::parsers::{line, skip_empty_lines, take_one_word};
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
@ -108,7 +108,8 @@ fn parse_headline<'a>(
), ),
), ),
))(input)?; ))(input)?;
let (input, tail) = take_until_eol(input)?; let (input, tail) = line(input)?;
let tail = tail.trim();
let (raw, tags) = memrchr(b' ', tail.as_bytes()) let (raw, tags) = memrchr(b' ', tail.as_bytes())
.map(|i| (tail[0..i].trim(), &tail[i + 1..])) .map(|i| (tail[0..i].trim(), &tail[i + 1..]))
.filter(|(_, x)| x.len() > 2 && x.starts_with(':') && x.ends_with(':')) .filter(|(_, x)| x.len() > 2 && x.starts_with(':') && x.ends_with(':'))
@ -130,7 +131,7 @@ fn parse_headline<'a>(
} }
fn parse_properties_drawer(input: &str) -> IResult<&str, HashMap<Cow<'_, str>, Cow<'_, str>>> { fn parse_properties_drawer(input: &str) -> IResult<&str, HashMap<Cow<'_, str>, Cow<'_, str>>> {
let (input, (drawer, content)) = Drawer::parse(input)?; let (input, (drawer, content)) = Drawer::parse(input.trim_start())?;
if drawer.name != "PROPERTIES" { if drawer.name != "PROPERTIES" {
return Err(Err::Error(error_position!(input, ErrorKind::Tag))); return Err(Err::Error(error_position!(input, ErrorKind::Tag)));
} }
@ -146,12 +147,12 @@ fn parse_properties_drawer(input: &str) -> IResult<&str, HashMap<Cow<'_, str>, C
} }
fn parse_node_property(input: &str) -> IResult<&str, (&str, &str)> { fn parse_node_property(input: &str) -> IResult<&str, (&str, &str)> {
let input = skip_empty_lines(input); let input = skip_empty_lines(input).trim_start();
let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| { let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| {
s.trim_end_matches('+') s.trim_end_matches('+')
})(input)?; })(input)?;
let (input, value) = take_until_eol(input)?; let (input, value) = line(input)?;
Ok((input, (name, value))) Ok((input, (name, value.trim())))
} }
impl Title<'_> { impl Title<'_> {
@ -228,6 +229,19 @@ fn parse_headline_() {
); );
} }
#[test]
fn parse_properties_drawer_() {
assert_eq!(
parse_properties_drawer(" :PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
Ok((
"",
vec![("CUSTOM_ID".into(), "id".into())]
.into_iter()
.collect::<HashMap<_, _>>()
))
)
}
// #[test] // #[test]
// fn is_commented() { // fn is_commented() {
// assert!(Title::parse("* COMMENT Title", &CONFIG) // assert!(Title::parse("* COMMENT Title", &CONFIG)

View file

@ -9,7 +9,7 @@ use crate::parsers::*;
pub struct Org<'a> { pub struct Org<'a> {
pub(crate) arena: Arena<Element<'a>>, pub(crate) arena: Arena<Element<'a>>,
document: NodeId, root: NodeId,
} }
#[derive(Debug)] #[derive(Debug)]
@ -25,12 +25,9 @@ impl Org<'_> {
pub fn parse_with_config<'a>(content: &'a str, config: &ParseConfig) -> Org<'a> { pub fn parse_with_config<'a>(content: &'a str, config: &ParseConfig) -> Org<'a> {
let mut arena = Arena::new(); let mut arena = Arena::new();
let document = arena.new_node(Element::Document); let node = arena.new_node(Element::Document);
let containers = &mut vec![Container::Document { let containers = &mut vec![Container::Document { content, node }];
content,
node: document,
}];
while let Some(container) = containers.pop() { while let Some(container) = containers.pop() {
match container { match container {
@ -38,8 +35,7 @@ impl Org<'_> {
parse_section_and_headlines(&mut arena, content, node, containers); parse_section_and_headlines(&mut arena, content, node, containers);
} }
Container::Headline { content, node } => { Container::Headline { content, node } => {
let content = parse_title(&mut arena, content, node, containers, config); parse_headline_content(&mut arena, content, node, containers, config);
parse_section_and_headlines(&mut arena, content, node, containers);
} }
Container::Block { content, node } => { Container::Block { content, node } => {
parse_blocks(&mut arena, content, node, containers); parse_blocks(&mut arena, content, node, containers);
@ -57,28 +53,24 @@ impl Org<'_> {
} }
} }
Org { arena, document } Org { arena, root: node }
} }
pub fn iter<'a>(&'a self) -> impl Iterator<Item = Event<'_>> + 'a { pub fn iter(&self) -> impl Iterator<Item = Event<'_>> + '_ {
self.document self.root.traverse(&self.arena).map(move |edge| match edge {
.traverse(&self.arena)
.map(move |edge| match edge {
NodeEdge::Start(e) => Event::Start(self.arena[e].get()), NodeEdge::Start(e) => Event::Start(self.arena[e].get()),
NodeEdge::End(e) => Event::End(self.arena[e].get()), NodeEdge::End(e) => Event::End(self.arena[e].get()),
}) })
} }
pub fn headlines(&self) -> Vec<HeadlineNode> { pub fn headlines(&self) -> impl Iterator<Item = HeadlineNode> + '_ {
self.document self.root
.descendants(&self.arena) .descendants(&self.arena)
.skip(1) .skip(1)
.filter(|&node| match self.arena[node].get() { .filter_map(move |node| match self.arena[node].get() {
Element::Headline => true, Element::Headline => Some(HeadlineNode(node)),
_ => false, _ => None,
}) })
.map(|node| HeadlineNode(node))
.collect()
} }
pub fn html<W: Write>(&self, wrtier: W) -> Result<(), Error> { pub fn html<W: Write>(&self, wrtier: W) -> Result<(), Error> {
@ -130,6 +122,6 @@ impl Serialize for Org<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde_indextree::Node; use serde_indextree::Node;
serializer.serialize_newtype_struct("Node", &Node::new(self.document, &self.arena)) serializer.serialize_newtype_struct("Org", &Node::new(self.root, &self.arena))
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -23,22 +23,18 @@ test_suite!(
test_suite!( test_suite!(
section_and_headline, section_and_headline,
r#"* Title 1 r#"* title 1
*Section 1* section 1
** Title 2 ** title 2
_Section 2_ section 2
* Title 3 * title 3
/Section 3/ section 3
* Title 4 * title 4
=Section 4="#, section 4"#,
"<main><h1>Title 1</h1>\ "<main><h1>title 1</h1><section><p>section 1</p></section>\
<section><p><b>Section 1</b></p></section>\ <h2>title 2</h2><section><p>section 2</p></section>\
<h2>Title 2</h2>\ <h1>title 3</h1><section><p>section 3</p></section>\
<section><p><u>Section 2</u></p></section>\ <h1>title 4</h1><section><p>section 4</p></section></main>"
<h1>Title 3</h1>\
<section><p><i>Section 3</i></p></section>\
<h1>Title 4</h1>\
<section><p><code>Section 4</code></p></section></main>"
); );
test_suite!( test_suite!(