chore: setup Cargo workspace

This commit is contained in:
PoiScript 2019-09-13 21:21:21 +08:00
parent 14de34bc88
commit 77eca42760
47 changed files with 45 additions and 40 deletions

View file

@ -1,194 +0,0 @@
use std::borrow::Cow;
use nom::{bytes::complete::tag_no_case, character::complete::alpha1, sequence::preceded, IResult};
use crate::parsers::{line, take_lines_while};
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct SpecialBlock<'a> {
pub parameters: Option<Cow<'a, str>>,
pub name: Cow<'a, str>,
}
impl SpecialBlock<'_> {
pub fn into_owned(self) -> SpecialBlock<'static> {
SpecialBlock {
name: self.name.into_owned().into(),
parameters: self.parameters.map(Into::into).map(Cow::Owned),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct QuoteBlock<'a> {
pub parameters: Option<Cow<'a, str>>,
}
impl QuoteBlock<'_> {
pub fn into_owned(self) -> QuoteBlock<'static> {
QuoteBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct CenterBlock<'a> {
pub parameters: Option<Cow<'a, str>>,
}
impl CenterBlock<'_> {
pub fn into_owned(self) -> CenterBlock<'static> {
CenterBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct VerseBlock<'a> {
pub parameters: Option<Cow<'a, str>>,
}
impl VerseBlock<'_> {
pub fn into_owned(self) -> VerseBlock<'static> {
VerseBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct CommentBlock<'a> {
pub data: Option<Cow<'a, str>>,
pub contents: Cow<'a, str>,
}
impl CommentBlock<'_> {
pub fn into_owned(self) -> CommentBlock<'static> {
CommentBlock {
data: self.data.map(Into::into).map(Cow::Owned),
contents: self.contents.into_owned().into(),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct ExampleBlock<'a> {
pub data: Option<Cow<'a, str>>,
pub contents: Cow<'a, str>,
}
impl ExampleBlock<'_> {
pub fn into_owned(self) -> ExampleBlock<'static> {
ExampleBlock {
data: self.data.map(Into::into).map(Cow::Owned),
contents: self.contents.into_owned().into(),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct ExportBlock<'a> {
pub data: Cow<'a, str>,
pub contents: Cow<'a, str>,
}
impl ExportBlock<'_> {
pub fn into_owned(self) -> ExportBlock<'static> {
ExportBlock {
data: self.data.into_owned().into(),
contents: self.contents.into_owned().into(),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct SourceBlock<'a> {
pub contents: Cow<'a, str>,
pub language: Cow<'a, str>,
pub arguments: Cow<'a, str>,
}
impl SourceBlock<'_> {
pub fn into_owned(self) -> SourceBlock<'static> {
SourceBlock {
language: self.language.into_owned().into(),
arguments: self.arguments.into_owned().into(),
contents: self.contents.into_owned().into(),
}
}
}
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

@ -1,219 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::tag,
character::complete::{char, digit1, space0},
combinator::recognize,
sequence::separated_pair,
IResult,
};
use crate::elements::{Datetime, Timestamp};
use crate::parsers::eol;
/// clock elements
///
/// there are two types of clock: *closed* clock and *running* clock.
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(untagged))]
#[derive(Debug)]
pub enum Clock<'a> {
/// closed Clock
Closed {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
duration: Cow<'a, str>,
},
/// running Clock
Running {
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
}
impl Clock<'_> {
pub(crate) fn parse(input: &str) -> IResult<&str, Clock<'_>> {
let (input, _) = tag("CLOCK:")(input)?;
let (input, _) = space0(input)?;
let (input, timestamp) = Timestamp::parse_inactive(input)?;
match timestamp {
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
} => {
let (input, _) = space0(input)?;
let (input, _) = tag("=>")(input)?;
let (input, _) = space0(input)?;
let (input, duration) =
recognize(separated_pair(digit1, char(':'), digit1))(input)?;
let (input, _) = eol(input)?;
Ok((
input,
Clock::Closed {
start,
end,
repeater,
delay,
duration: duration.into(),
},
))
}
Timestamp::Inactive {
start,
repeater,
delay,
} => {
let (input, _) = eol(input)?;
Ok((
input,
Clock::Running {
start,
repeater,
delay,
},
))
}
_ => unreachable!(
"`Timestamp::parse_inactive` only returns `Timestamp::InactiveRange` or `Timestamp::Inactive`."
),
}
}
pub fn into_onwed(self) -> Clock<'static> {
match self {
Clock::Closed {
start,
end,
repeater,
delay,
duration,
} => 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(),
},
Clock::Running {
start,
repeater,
delay,
} => Clock::Running {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
}
}
/// returns `true` if the clock is running
pub fn is_running(&self) -> bool {
match self {
Clock::Closed { .. } => false,
Clock::Running { .. } => true,
}
}
/// returns `true` if the clock is closed
pub fn is_closed(&self) -> bool {
match self {
Clock::Closed { .. } => true,
Clock::Running { .. } => false,
}
}
/// returns `Some` if the clock is closed, `None` if running
pub fn duration(&self) -> Option<&str> {
match self {
Clock::Closed { duration, .. } => Some(duration),
Clock::Running { .. } => None,
}
}
/// constructs a new timestamp object from the clock
pub fn value(&self) -> Timestamp<'_> {
match &*self {
Clock::Closed {
start,
end,
repeater,
delay,
..
} => Timestamp::InactiveRange {
start: start.clone(),
end: end.clone(),
repeater: repeater.clone(),
delay: delay.clone(),
},
Clock::Running {
start,
repeater,
delay,
} => Timestamp::Inactive {
start: start.clone(),
repeater: repeater.clone(),
delay: delay.clone(),
},
}
}
}
#[test]
fn parse() {
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Ok((
"",
Clock::Running {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
repeater: None,
delay: None,
}
))
);
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"),
Ok((
"",
Clock::Closed {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39)
},
repeater: None,
delay: None,
duration: "1:00".into(),
}
))
);
}

View file

@ -1,116 +0,0 @@
use std::borrow::Cow;
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::digit0,
combinator::recognize,
sequence::{delimited, pair, separated_pair},
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Cookie<'a> {
pub value: Cow<'a, str>,
}
impl Cookie<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Cookie<'_>> {
let (input, value) = recognize(delimited(
tag("["),
alt((
separated_pair(digit0, tag("/"), digit0),
pair(digit0, tag("%")),
)),
tag("]"),
))(input)?;
Ok((
input,
Cookie {
value: value.into(),
},
))
}
pub fn into_owned(self) -> Cookie<'static> {
Cookie {
value: self.value.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
Cookie::parse("[1/10]"),
Ok((
"",
Cookie {
value: "[1/10]".into()
}
))
);
assert_eq!(
Cookie::parse("[1/1000]"),
Ok((
"",
Cookie {
value: "[1/1000]".into()
}
))
);
assert_eq!(
Cookie::parse("[10%]"),
Ok((
"",
Cookie {
value: "[10%]".into()
}
))
);
assert_eq!(
Cookie::parse("[%]"),
Ok((
"",
Cookie {
value: "[%]".into()
}
))
);
assert_eq!(
Cookie::parse("[/]"),
Ok((
"",
Cookie {
value: "[/]".into()
}
))
);
assert_eq!(
Cookie::parse("[100/]"),
Ok((
"",
Cookie {
value: "[100/]".into()
}
))
);
assert_eq!(
Cookie::parse("[/100]"),
Ok((
"",
Cookie {
value: "[/100]".into()
}
))
);
assert!(Cookie::parse("[10% ]").is_err());
assert!(Cookie::parse("[1//100]").is_err());
assert!(Cookie::parse("[1\\100]").is_err());
assert!(Cookie::parse("[10%%]").is_err());
}

View file

@ -1,55 +0,0 @@
use std::borrow::Cow;
use crate::parsers::{eol, line, take_lines_while};
use nom::{
bytes::complete::{tag, take_while1},
sequence::delimited,
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Drawer<'a> {
pub name: Cow<'a, str>,
}
impl Drawer<'_> {
#[inline]
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 == '_'),
tag(":"),
)(input)?;
let (input, _) = eol(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)))
}
pub fn into_owned(self) -> Drawer<'static> {
Drawer {
name: self.name.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
Drawer::parse(":PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
Ok((
"",
(
Drawer {
name: "PROPERTIES".into()
},
" :CUSTOM_ID: id\n"
)
))
)
}

View file

@ -1,76 +0,0 @@
use std::borrow::Cow;
use crate::parsers::{line, take_lines_while};
use nom::{
bytes::complete::tag_no_case,
character::complete::{alpha1, space1},
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct DynBlock<'a> {
pub block_name: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub arguments: Option<Cow<'a, str>>,
}
impl DynBlock<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, (DynBlock<'_>, &str)> {
let (input, _) = tag_no_case("#+BEGIN:")(input)?;
let (input, _) = space1(input)?;
let (input, name) = alpha1(input)?;
let (input, args) = line(input)?;
let (input, contents) =
take_lines_while(|line| !line.trim().eq_ignore_ascii_case("#+END:"))(input)?;
let (input, _) = line(input)?;
Ok((
input,
(
DynBlock {
block_name: name.into(),
arguments: if args.trim().is_empty() {
None
} else {
Some(args.trim().into())
},
},
contents,
),
))
}
pub fn into_owned(self) -> DynBlock<'static> {
DynBlock {
block_name: self.block_name.into_owned().into(),
arguments: self.arguments.map(Into::into).map(Cow::Owned),
}
}
}
#[test]
fn parse() {
// TODO: testing
assert_eq!(
DynBlock::parse(
r#"#+BEGIN: clocktable :scope file
CONTENTS
#+END:
"#
),
Ok((
"",
(
DynBlock {
block_name: "clocktable".into(),
arguments: Some(":scope file".into()),
},
"CONTENTS\n"
)
))
);
}

View file

@ -1,48 +0,0 @@
use bytecount::count;
use memchr::memchr_iter;
#[inline]
pub(crate) fn parse_emphasis(text: &str, marker: u8) -> Option<(&str, &str)> {
debug_assert!(text.len() >= 3);
let bytes = text.as_bytes();
if bytes[1].is_ascii_whitespace() {
return None;
}
for i in memchr_iter(marker, bytes).skip(1) {
if count(&bytes[1..i], b'\n') >= 2 {
break;
} else if validate_marker(i, text) {
return Some((&text[i + 1..], &text[1..i]));
}
}
None
}
fn validate_marker(pos: usize, text: &str) -> bool {
if text.as_bytes()[pos - 1].is_ascii_whitespace() {
false
} else if let Some(&post) = text.as_bytes().get(pos + 1) {
match post {
b' ' | b'-' | b'.' | b',' | b':' | b'!' | b'?' | b'\'' | b'\n' | b')' | b'}' => true,
_ => false,
}
} else {
true
}
}
#[test]
fn parse() {
assert_eq!(parse_emphasis("*bold*", b'*'), Some(("", "bold")));
assert_eq!(parse_emphasis("*bo*ld*", b'*'), Some(("", "bo*ld")));
assert_eq!(parse_emphasis("*bo\nld*", b'*'), Some(("", "bo\nld")));
assert_eq!(parse_emphasis("*bold*a", b'*'), None);
assert_eq!(parse_emphasis("*bold*", b'/'), None);
assert_eq!(parse_emphasis("*bold *", b'*'), None);
assert_eq!(parse_emphasis("* bold*", b'*'), None);
assert_eq!(parse_emphasis("*b\nol\nd*", b'*'), None);
}

View file

@ -1,92 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_while1},
sequence::delimited,
IResult,
};
use crate::parsers::line;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct FnDef<'a> {
pub label: Cow<'a, str>,
}
impl FnDef<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, (FnDef<'_>, &str)> {
let (input, label) = delimited(
tag("[fn:"),
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
tag("]"),
)(input)?;
let (input, content) = line(input)?;
Ok((
input,
(
FnDef {
label: label.into(),
},
content,
),
))
}
pub fn into_owned(self) -> FnDef<'static> {
FnDef {
label: self.label.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
FnDef::parse("[fn:1] https://orgmode.org"),
Ok(("", (FnDef { label: "1".into() }, " https://orgmode.org")))
);
assert_eq!(
FnDef::parse("[fn:word_1] https://orgmode.org"),
Ok((
"",
(
FnDef {
label: "word_1".into()
},
" https://orgmode.org"
)
))
);
assert_eq!(
FnDef::parse("[fn:WORD-1] https://orgmode.org"),
Ok((
"",
(
FnDef {
label: "WORD-1".into()
},
" https://orgmode.org"
)
))
);
assert_eq!(
FnDef::parse("[fn:WORD]"),
Ok((
"",
(
FnDef {
label: "WORD".into()
},
""
)
))
);
assert!(FnDef::parse("[fn:] https://orgmode.org").is_err());
assert!(FnDef::parse("[fn:wor d] https://orgmode.org").is_err());
assert!(FnDef::parse("[fn:WORD https://orgmode.org").is_err());
}

View file

@ -1,106 +0,0 @@
use std::borrow::Cow;
use memchr::memchr2_iter;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
error::ErrorKind,
error_position,
sequence::preceded,
Err, IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct FnRef<'a> {
pub label: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub definition: Option<Cow<'a, str>>,
}
fn balanced_brackets(input: &str) -> IResult<&str, &str> {
let mut pairs = 1;
for i in memchr2_iter(b'[', b']', input.as_bytes()) {
if input.as_bytes()[i] == b'[' {
pairs += 1;
} else if pairs != 1 {
pairs -= 1;
} else {
return Ok((&input[i..], &input[0..i]));
}
}
Err(Err::Error(error_position!(input, ErrorKind::Tag)))
}
impl FnRef<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, FnRef<'_>> {
let (input, _) = tag("[fn:")(input)?;
let (input, label) =
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_')(input)?;
let (input, definition) = opt(preceded(tag(":"), balanced_brackets))(input)?;
let (input, _) = tag("]")(input)?;
Ok((
input,
FnRef {
label: label.into(),
definition: definition.map(Into::into),
},
))
}
pub fn into_owned(self) -> FnRef<'static> {
FnRef {
label: self.label.into_owned().into(),
definition: self.definition.map(Into::into).map(Cow::Owned),
}
}
}
#[test]
fn parse() {
assert_eq!(
FnRef::parse("[fn:1]"),
Ok((
"",
FnRef {
label: "1".into(),
definition: None
},
))
);
assert_eq!(
FnRef::parse("[fn:1:2]"),
Ok((
"",
FnRef {
label: "1".into(),
definition: Some("2".into())
},
))
);
assert_eq!(
FnRef::parse("[fn::2]"),
Ok((
"",
FnRef {
label: "".into(),
definition: Some("2".into())
},
))
);
assert_eq!(
FnRef::parse("[fn::[]]"),
Ok((
"",
FnRef {
label: "".into(),
definition: Some("[]".into())
},
))
);
assert!(FnRef::parse("[fn::[]").is_err());
}

View file

@ -1,113 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till},
combinator::opt,
sequence::{delimited, preceded},
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct InlineCall<'a> {
pub name: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub inside_header: Option<Cow<'a, str>>,
pub arguments: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub end_header: Option<Cow<'a, str>>,
}
impl InlineCall<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, InlineCall<'_>> {
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(delimited(
tag("["),
take_till(|c| c == ']' || c == '\n'),
tag("]"),
))(input)?;
Ok((
input,
InlineCall {
name: name.into(),
arguments: arguments.into(),
inside_header: inside_header.map(Into::into),
end_header: end_header.map(Into::into),
},
))
}
pub fn into_owned(self) -> InlineCall<'static> {
InlineCall {
name: self.name.into_owned().into(),
arguments: self.arguments.into_owned().into(),
inside_header: self.inside_header.map(Into::into).map(Cow::Owned),
end_header: self.end_header.map(Into::into).map(Cow::Owned),
}
}
}
#[test]
fn parse() {
assert_eq!(
InlineCall::parse("call_square(4)"),
Ok((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: None,
end_header: None,
}
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)"),
Ok((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: Some(":results output".into()),
end_header: None,
},
))
);
assert_eq!(
InlineCall::parse("call_square(4)[:results html]"),
Ok((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: None,
end_header: Some(":results html".into()),
},
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)[:results html]"),
Ok((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: Some(":results output".into()),
end_header: Some(":results html".into()),
},
))
);
}

View file

@ -1,81 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till, take_while1},
combinator::opt,
sequence::delimited,
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct InlineSrc<'a> {
pub lang: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub options: Option<Cow<'a, str>>,
pub body: Cow<'a, str>,
}
impl InlineSrc<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, InlineSrc<'_>> {
let (input, _) = tag("src_")(input)?;
let (input, lang) =
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?;
let (input, options) = opt(delimited(
tag("["),
take_till(|c| c == '\n' || c == ']'),
tag("]"),
))(input)?;
let (input, body) =
delimited(tag("{"), take_till(|c| c == '\n' || c == '}'), tag("}"))(input)?;
Ok((
input,
InlineSrc {
lang: lang.into(),
options: options.map(Into::into),
body: body.into(),
},
))
}
pub fn into_owned(self) -> InlineSrc<'static> {
InlineSrc {
lang: self.lang.into_owned().into(),
options: self.options.map(Into::into).map(Cow::Owned),
body: self.body.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
InlineSrc::parse("src_C{int a = 0;}"),
Ok((
"",
InlineSrc {
lang: "C".into(),
options: None,
body: "int a = 0;".into()
},
))
);
assert_eq!(
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
Ok((
"",
InlineSrc {
lang: "xml".into(),
options: Some(":exports code".into()),
body: "<tag>text</tag>".into(),
},
))
);
assert!(InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>").is_err());
assert!(InlineSrc::parse("src_[:exports code]{<tag>text</tag>}").is_err());
assert!(InlineSrc::parse("src_xml[:exports code]").is_err());
}

View file

@ -1,95 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till},
combinator::opt,
sequence::delimited,
IResult,
};
use crate::parsers::line;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Keyword<'a> {
pub key: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub optional: Option<Cow<'a, str>>,
pub value: Cow<'a, str>,
}
impl Keyword<'_> {
pub fn into_owned(self) -> Keyword<'static> {
Keyword {
key: self.key.into_owned().into(),
optional: self.optional.map(Into::into).map(Cow::Owned),
value: self.value.into_owned().into(),
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct BabelCall<'a> {
pub value: Cow<'a, str>,
}
impl BabelCall<'_> {
pub fn into_owned(self) -> BabelCall<'static> {
BabelCall {
value: self.value.into_owned().into(),
}
}
}
pub(crate) fn parse_keyword(input: &str) -> IResult<&str, (&str, Option<&str>, &str)> {
let (input, _) = tag("#+")(input)?;
let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
let (input, optional) = opt(delimited(
tag("["),
take_till(|c| c == ']' || c == '\n'),
tag("]"),
))(input)?;
let (input, _) = tag(":")(input)?;
let (input, value) = line(input)?;
Ok((input, (key, optional, value.trim())))
}
#[test]
fn parse() {
assert_eq!(parse_keyword("#+KEY:"), Ok(("", ("KEY", None, ""))));
assert_eq!(
parse_keyword("#+KEY: VALUE"),
Ok(("", ("KEY", None, "VALUE")))
);
assert_eq!(
parse_keyword("#+K_E_Y: VALUE"),
Ok(("", ("K_E_Y", None, "VALUE")))
);
assert_eq!(
parse_keyword("#+KEY:VALUE\n"),
Ok(("", ("KEY", None, "VALUE")))
);
assert!(parse_keyword("#+KE Y: VALUE").is_err());
assert!(parse_keyword("#+ KEY: VALUE").is_err());
assert_eq!(parse_keyword("#+RESULTS:"), Ok(("", ("RESULTS", None, ""))));
assert_eq!(
parse_keyword("#+ATTR_LATEX: :width 5cm\n"),
Ok(("", ("ATTR_LATEX", None, ":width 5cm")))
);
assert_eq!(
parse_keyword("#+CALL: double(n=4)"),
Ok(("", ("CALL", None, "double(n=4)")))
);
assert_eq!(
parse_keyword("#+CAPTION[Short caption]: Longer caption."),
Ok(("", ("CAPTION", Some("Short caption"), "Longer caption.",)))
);
}

View file

@ -1,73 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
sequence::delimited,
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Link<'a> {
pub path: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub desc: Option<Cow<'a, str>>,
}
impl Link<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Link<'_>> {
let (input, path) = delimited(
tag("[["),
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
tag("]"),
)(input)?;
let (input, desc) = opt(delimited(
tag("["),
take_while(|c: char| c != '[' && c != ']'),
tag("]"),
))(input)?;
let (input, _) = tag("]")(input)?;
Ok((
input,
Link {
path: path.into(),
desc: desc.map(Into::into),
},
))
}
pub fn into_owned(self) -> Link<'static> {
Link {
path: self.path.into_owned().into(),
desc: self.desc.map(Into::into).map(Cow::Owned),
}
}
}
#[test]
fn parse() {
assert_eq!(
Link::parse("[[#id]]"),
Ok((
"",
Link {
path: "#id".into(),
desc: None
}
))
);
assert_eq!(
Link::parse("[[#id][desc]]"),
Ok((
"",
Link {
path: "#id".into(),
desc: Some("desc".into())
}
))
);
assert!(Link::parse("[[#id][desc]").is_err());
}

View file

@ -1,252 +0,0 @@
use std::borrow::Cow;
use memchr::memchr_iter;
use std::iter::once;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct List {
pub indent: usize,
pub ordered: bool,
}
impl List {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, List, &str)> {
let (indent, tail) = text
.find(|c| c != ' ')
.map(|off| (off, &text[off..]))
.unwrap_or((0, text));
let ordered = is_item(tail)?;
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())
{
return Some((
&text[start..],
List { indent, ordered },
&text[0..start - 1],
));
} else {
last_end = 0;
start = i;
continue;
}
} else {
// 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;
}
}
}
if last_end != 0 {
Some(("", List { indent, ordered }, &text[0..last_end]))
} else {
Some(("", List { indent, ordered }, text))
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct ListItem<'a> {
pub bullet: Cow<'a, str>,
}
impl ListItem<'_> {
#[inline]
pub(crate) fn parse(text: &str, indent: usize) -> (&str, ListItem<'_>, &str) {
debug_assert!(&text[0..indent].trim().is_empty());
let off = &text[indent..].find(' ').unwrap() + 1 + indent;
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().unwrap();
for i in lines {
let line = &text[pos..i];
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
if line_indent == indent {
return (
&text[pos..],
ListItem {
bullet: text[indent..off].into(),
},
&text[off..pos],
);
}
}
pos = i;
}
(
"",
ListItem {
bullet: text[indent..off].into(),
},
&text[off..],
)
}
pub fn into_owned(self) -> ListItem<'static> {
ListItem {
bullet: self.bullet.into_owned().into(),
}
}
}
#[inline]
pub fn is_item(text: &str) -> Option<bool> {
let bytes = text.as_bytes();
match bytes.get(0)? {
b'*' | b'-' | b'+' => {
if text.len() > 1 && (bytes[1] == b' ' || bytes[1] == b'\n') {
Some(false)
} else {
None
}
}
b'0'..=b'9' => {
let i = bytes
.iter()
.position(|&c| !c.is_ascii_digit())
.unwrap_or_else(|| text.len() - 1);
if (bytes[i] == b'.' || bytes[i] == b')')
&& text.len() > i + 1
&& (bytes[i + 1] == b' ' || bytes[i + 1] == b'\n')
{
Some(true)
} else {
None
}
}
_ => None,
}
}
#[test]
fn test_is_item() {
assert_eq!(is_item("+ item"), Some(false));
assert_eq!(is_item("- item"), Some(false));
assert_eq!(is_item("10. item"), Some(true));
assert_eq!(is_item("10) item"), Some(true));
assert_eq!(is_item("1. item"), Some(true));
assert_eq!(is_item("1) item"), Some(true));
assert_eq!(is_item("10. "), Some(true));
assert_eq!(is_item("10.\n"), Some(true));
assert_eq!(is_item("10."), None);
assert_eq!(is_item("+"), None);
assert_eq!(is_item("-item"), None);
assert_eq!(is_item("+item"), None);
}
#[test]
fn list_parse() {
assert_eq!(
List::parse("+ item1\n+ item2"),
Some((
"",
List {
indent: 0,
ordered: false,
},
"+ item1\n+ item2"
))
);
assert_eq!(
List::parse("* item1\n \n* item2"),
Some((
"",
List {
indent: 0,
ordered: false,
},
"* item1\n \n* item2"
))
);
assert_eq!(
List::parse("* item1\n \n \n* item2"),
Some((
"* item2",
List {
indent: 0,
ordered: false,
},
"* item1\n"
))
);
assert_eq!(
List::parse("* item1\n \n "),
Some((
"",
List {
indent: 0,
ordered: false,
},
"* item1\n"
))
);
assert_eq!(
List::parse("+ item1\n + item2\n "),
Some((
"",
List {
indent: 0,
ordered: false,
},
"+ item1\n + item2\n"
))
);
assert_eq!(
List::parse("+ item1\n \n + item2\n \n+ item 3"),
Some((
"",
List {
indent: 0,
ordered: false,
},
"+ item1\n \n + item2\n \n+ item 3"
))
);
assert_eq!(
List::parse(" + item1\n \n + item2"),
Some((
"",
List {
indent: 2,
ordered: false,
},
" + item1\n \n + item2"
))
);
assert_eq!(
List::parse("+ 1\n\n - 2\n\n - 3\n\n+ 4"),
Some((
"",
List {
indent: 0,
ordered: false,
},
"+ 1\n\n - 2\n\n - 3\n\n+ 4"
))
);
}

View file

@ -1,83 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take, take_until, take_while1},
combinator::{opt, verify},
sequence::delimited,
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Macros<'a> {
pub name: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub arguments: Option<Cow<'a, str>>,
}
impl Macros<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Macros<'_>> {
let (input, _) = tag("{{{")(input)?;
let (input, name) = verify(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|s: &str| s.starts_with(|c: char| c.is_ascii_alphabetic()),
)(input)?;
let (input, arguments) = opt(delimited(tag("("), take_until(")}}}"), take(1usize)))(input)?;
let (input, _) = tag("}}}")(input)?;
Ok((
input,
Macros {
name: name.into(),
arguments: arguments.map(Into::into),
},
))
}
pub fn into_owned(self) -> Macros<'static> {
Macros {
name: self.name.into_owned().into(),
arguments: self.arguments.map(Into::into).map(Cow::Owned),
}
}
}
#[test]
fn parse() {
assert_eq!(
Macros::parse("{{{poem(red,blue)}}}"),
Ok((
"",
Macros {
name: "poem".into(),
arguments: Some("red,blue".into())
}
))
);
assert_eq!(
Macros::parse("{{{poem())}}}"),
Ok((
"",
Macros {
name: "poem".into(),
arguments: Some(")".into())
}
))
);
assert_eq!(
Macros::parse("{{{author}}}"),
Ok((
"",
Macros {
name: "author".into(),
arguments: None
}
))
);
assert!(Macros::parse("{{{0uthor}}}").is_err());
assert!(Macros::parse("{{{author}}").is_err());
assert!(Macros::parse("{{{poem(}}}").is_err());
assert!(Macros::parse("{{{poem)}}}").is_err());
}

View file

@ -1,248 +0,0 @@
//! Org-mode elements
mod block;
mod clock;
mod cookie;
mod drawer;
mod dyn_block;
mod emphasis;
mod fn_def;
mod fn_ref;
mod inline_call;
mod inline_src;
mod keyword;
mod link;
mod list;
mod macros;
mod planning;
mod radio_target;
mod rule;
mod snippet;
mod table;
mod target;
mod timestamp;
mod title;
pub(crate) use self::{
block::parse_block_element, emphasis::parse_emphasis, keyword::parse_keyword,
radio_target::parse_radio_target, rule::parse_rule, table::parse_table_el,
};
pub use self::{
block::{
CenterBlock, CommentBlock, ExampleBlock, ExportBlock, QuoteBlock, SourceBlock,
SpecialBlock, VerseBlock,
},
clock::Clock,
cookie::Cookie,
drawer::Drawer,
dyn_block::DynBlock,
fn_def::FnDef,
fn_ref::FnRef,
inline_call::InlineCall,
inline_src::InlineSrc,
keyword::{BabelCall, Keyword},
link::Link,
list::{List, ListItem},
macros::Macros,
planning::Planning,
snippet::Snippet,
table::{Table, TableRow},
target::Target,
timestamp::{Datetime, Timestamp},
title::Title,
};
use std::borrow::Cow;
/// Org-mode 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> {
SpecialBlock(SpecialBlock<'a>),
QuoteBlock(QuoteBlock<'a>),
CenterBlock(CenterBlock<'a>),
VerseBlock(VerseBlock<'a>),
CommentBlock(CommentBlock<'a>),
ExampleBlock(ExampleBlock<'a>),
ExportBlock(ExportBlock<'a>),
SourceBlock(SourceBlock<'a>),
BabelCall(BabelCall<'a>),
Section,
Clock(Clock<'a>),
Cookie(Cookie<'a>),
RadioTarget,
Drawer(Drawer<'a>),
Document,
DynBlock(DynBlock<'a>),
FnDef(FnDef<'a>),
FnRef(FnRef<'a>),
Headline { level: usize },
InlineCall(InlineCall<'a>),
InlineSrc(InlineSrc<'a>),
Keyword(Keyword<'a>),
Link(Link<'a>),
List(List),
ListItem(ListItem<'a>),
Macros(Macros<'a>),
Snippet(Snippet<'a>),
Text { value: Cow<'a, str> },
Paragraph,
Rule,
Timestamp(Timestamp<'a>),
Target(Target<'a>),
Bold,
Strike,
Italic,
Underline,
Verbatim { value: Cow<'a, str> },
Code { value: Cow<'a, str> },
Comment { value: Cow<'a, str> },
FixedWidth { value: Cow<'a, str> },
Title(Title<'a>),
Table(Table<'a>),
TableRow(TableRow),
TableCell,
}
impl Element<'_> {
pub fn is_container(&self) -> bool {
use Element::*;
match self {
SpecialBlock(_)
| QuoteBlock(_)
| CenterBlock(_)
| VerseBlock(_)
| Bold
| Document
| DynBlock(_)
| Headline { .. }
| Italic
| List(_)
| ListItem(_)
| Paragraph
| Section
| Strike
| Underline
| Title(_)
| Table(_)
| TableRow(_)
| TableCell => true,
_ => false,
}
}
pub fn into_owned(self) -> Element<'static> {
use Element::*;
match self {
SpecialBlock(e) => SpecialBlock(e.into_owned()),
QuoteBlock(e) => QuoteBlock(e.into_owned()),
CenterBlock(e) => CenterBlock(e.into_owned()),
VerseBlock(e) => VerseBlock(e.into_owned()),
CommentBlock(e) => CommentBlock(e.into_owned()),
ExampleBlock(e) => ExampleBlock(e.into_owned()),
ExportBlock(e) => ExportBlock(e.into_owned()),
SourceBlock(e) => SourceBlock(e.into_owned()),
BabelCall(e) => BabelCall(e.into_owned()),
Section => Section,
Clock(e) => Clock(e.into_onwed()),
Cookie(e) => Cookie(e.into_owned()),
RadioTarget => RadioTarget,
Drawer(e) => Drawer(e.into_owned()),
Document => Document,
DynBlock(e) => DynBlock(e.into_owned()),
FnDef(e) => FnDef(e.into_owned()),
FnRef(e) => FnRef(e.into_owned()),
Headline { level } => Headline { level },
InlineCall(e) => InlineCall(e.into_owned()),
InlineSrc(e) => InlineSrc(e.into_owned()),
Keyword(e) => Keyword(e.into_owned()),
Link(e) => Link(e.into_owned()),
List(e) => List(e),
ListItem(e) => ListItem(e.into_owned()),
Macros(e) => Macros(e.into_owned()),
Snippet(e) => Snippet(e.into_owned()),
Text { value } => Text {
value: value.into_owned().into(),
},
Paragraph => Paragraph,
Rule => Rule,
Timestamp(e) => Timestamp(e.into_owned()),
Target(e) => Target(e.into_owned()),
Bold => Bold,
Strike => Strike,
Italic => Italic,
Underline => Underline,
Verbatim { value } => Verbatim {
value: value.into_owned().into(),
},
Code { value } => Code {
value: value.into_owned().into(),
},
Comment { value } => Comment {
value: value.into_owned().into(),
},
FixedWidth { value } => FixedWidth {
value: value.into_owned().into(),
},
Title(e) => Title(e.into_owned()),
Table(e) => Table(e.into_owned()),
TableRow(e) => TableRow(e),
TableCell => TableCell,
}
}
}
macro_rules! impl_from {
($($ele0:ident),*; $($ele1:ident),*) => {
$(
impl<'a> From<$ele0<'a>> for Element<'a> {
fn from(ele: $ele0<'a>) -> Element<'a> {
Element::$ele0(ele)
}
}
)*
$(
impl<'a> From<$ele1> for Element<'a> {
fn from(ele: $ele1) -> Element<'a> {
Element::$ele1(ele)
}
}
)*
};
}
impl_from!(
BabelCall,
CenterBlock,
Clock,
CommentBlock,
Cookie,
Drawer,
DynBlock,
ExampleBlock,
ExportBlock,
FnDef,
FnRef,
InlineCall,
InlineSrc,
Keyword,
Link,
ListItem,
Macros,
QuoteBlock,
Snippet,
SourceBlock,
SpecialBlock,
Target,
Timestamp,
Table,
Title,
VerseBlock;
List,
TableRow
);

View file

@ -1,103 +0,0 @@
use memchr::memchr;
use crate::elements::Timestamp;
/// palnning elements
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Planning<'a> {
/// the date when the task should be done
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub deadline: Option<Timestamp<'a>>,
/// the date when you should start working on the task
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub scheduled: Option<Timestamp<'a>>,
/// the date when the task is closed
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub closed: Option<Timestamp<'a>>,
}
impl Planning<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, Planning<'_>)> {
let (mut deadline, mut scheduled, mut closed) = (None, None, None);
let (mut tail, off) = memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len()));
while let Some(i) = memchr(b' ', tail.as_bytes()) {
let next = &tail[i + 1..].trim_start();
macro_rules! set_timestamp {
($timestamp:expr) => {
if $timestamp.is_none() {
let (new_tail, timestamp) = Timestamp::parse_active(next)
.or_else(|_| Timestamp::parse_inactive(next))
.ok()?;
$timestamp = Some(timestamp);
tail = new_tail.trim_start();
} else {
return None;
}
};
}
match &tail[..i] {
"DEADLINE:" => set_timestamp!(deadline),
"SCHEDULED:" => set_timestamp!(scheduled),
"CLOSED:" => set_timestamp!(closed),
_ => return None,
}
}
if deadline.is_none() && scheduled.is_none() && closed.is_none() {
None
} else {
Some((
&text[off..],
Planning {
deadline,
scheduled,
closed,
},
))
}
}
pub fn into_owned(self) -> Planning<'static> {
Planning {
deadline: self.deadline.map(|x| x.into_owned()),
scheduled: self.scheduled.map(|x| x.into_owned()),
closed: self.closed.map(|x| x.into_owned()),
}
}
}
#[test]
fn prase() {
use crate::elements::Datetime;
assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
Some((
"",
Planning {
scheduled: Some(Timestamp::Active {
start: Datetime {
year: 2019,
month: 4,
day: 8,
dayname: "Mon".into(),
hour: None,
minute: None
},
repeater: None,
delay: None
}),
deadline: None,
closed: None,
}
))
)
}

View file

@ -1,34 +0,0 @@
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
sequence::delimited,
IResult,
};
// TODO: text-markup, entities, latex-fragments, subscript and superscript
#[inline]
pub(crate) fn parse_radio_target(input: &str) -> IResult<&str, &str> {
let (input, contents) = delimited(
tag("<<<"),
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
),
tag(">>>"),
)(input)?;
Ok((input, contents))
}
#[test]
fn parse() {
assert_eq!(parse_radio_target("<<<target>>>"), Ok(("", "target")));
assert_eq!(parse_radio_target("<<<tar get>>>"), Ok(("", "tar get")));
assert!(parse_radio_target("<<<target >>>").is_err());
assert!(parse_radio_target("<<< target>>>").is_err());
assert!(parse_radio_target("<<<ta<get>>>").is_err());
assert!(parse_radio_target("<<<ta>get>>>").is_err());
assert!(parse_radio_target("<<<ta\nget>>>").is_err());
assert!(parse_radio_target("<<<target>>").is_err());
}

View file

@ -1,28 +0,0 @@
use nom::{bytes::complete::take_while_m_n, IResult};
use std::usize;
use crate::parsers::eol;
pub(crate) fn parse_rule(input: &str) -> IResult<&str, ()> {
let (input, _) = take_while_m_n(5, usize::MAX, |c| c == '-')(input)?;
let (input, _) = eol(input)?;
Ok((input, ()))
}
#[test]
fn parse() {
assert_eq!(parse_rule("-----"), Ok(("", ())));
assert_eq!(parse_rule("--------"), Ok(("", ())));
assert_eq!(parse_rule("-----\n"), Ok(("", ())));
assert_eq!(parse_rule("----- \n"), Ok(("", ())));
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());
}

View file

@ -1,92 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_until, take_while1},
sequence::{delimited, separated_pair},
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Snippet<'a> {
pub name: Cow<'a, str>,
pub value: Cow<'a, str>,
}
impl Snippet<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Snippet<'_>> {
let (input, (name, value)) = delimited(
tag("@@"),
separated_pair(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
tag(":"),
take_until("@@"),
),
tag("@@"),
)(input)?;
Ok((
input,
Snippet {
name: name.into(),
value: value.into(),
},
))
}
pub fn into_owned(self) -> Snippet<'static> {
Snippet {
name: self.name.into_owned().into(),
value: self.value.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
Snippet::parse("@@html:<b>@@"),
Ok((
"",
Snippet {
name: "html".into(),
value: "<b>".into()
}
))
);
assert_eq!(
Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
Ok((
"",
Snippet {
name: "latex".into(),
value: "any arbitrary LaTeX code".into(),
}
))
);
assert_eq!(
Snippet::parse("@@html:@@"),
Ok((
"",
Snippet {
name: "html".into(),
value: "".into(),
}
))
);
assert_eq!(
Snippet::parse("@@html:<p>@</p>@@"),
Ok((
"",
Snippet {
name: "html".into(),
value: "<p>@</p>".into(),
}
))
);
assert!(Snippet::parse("@@html:<b>@").is_err());
assert!(Snippet::parse("@@html<b>@@").is_err());
assert!(Snippet::parse("@@:<b>@@").is_err());
}

View file

@ -1,88 +0,0 @@
use std::borrow::Cow;
use nom::{
combinator::{peek, verify},
IResult,
};
use crate::parsers::{line, take_lines_while};
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_type"))]
pub enum Table<'a> {
#[cfg_attr(feature = "ser", serde(rename = "org"))]
Org { tblfm: Option<Cow<'a, str>> },
#[cfg_attr(feature = "ser", serde(rename = "table.el"))]
TableEl { value: Cow<'a, str> },
}
impl Table<'_> {
pub fn into_owned(self) -> Table<'static> {
match self {
Table::Org { tblfm } => Table::Org {
tblfm: tblfm.map(Into::into).map(Cow::Owned),
},
Table::TableEl { value } => Table::TableEl {
value: value.into_owned().into(),
},
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(
feature = "ser",
serde(tag = "table_row_type", rename_all = "kebab-case")
)]
pub enum TableRow {
Standard,
Rule,
}
impl TableRow {
pub(crate) fn parse(input: &str) -> Option<TableRow> {
if input.starts_with("|-") {
Some(TableRow::Rule)
} else if input.starts_with('|') {
Some(TableRow::Standard)
} else {
None
}
}
}
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

@ -1,70 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
sequence::delimited,
IResult,
};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Target<'a> {
pub target: Cow<'a, str>,
}
impl Target<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> IResult<&str, Target<'_>> {
let (input, target) = delimited(
tag("<<"),
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
),
tag(">>"),
)(input)?;
Ok((
input,
Target {
target: target.into(),
},
))
}
pub fn into_owned(self) -> Target<'static> {
Target {
target: self.target.into_owned().into(),
}
}
}
#[test]
fn parse() {
assert_eq!(
Target::parse("<<target>>"),
Ok((
"",
Target {
target: "target".into()
}
))
);
assert_eq!(
Target::parse("<<tar get>>"),
Ok((
"",
Target {
target: "tar get".into()
}
))
);
assert!(Target::parse("<<target >>").is_err());
assert!(Target::parse("<< target>>").is_err());
assert!(Target::parse("<<ta<get>>").is_err());
assert!(Target::parse("<<ta>get>>").is_err());
assert!(Target::parse("<<ta\nget>>").is_err());
assert!(Target::parse("<<target>").is_err());
}

View file

@ -1,466 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take, take_till, take_while, take_while_m_n},
character::complete::{space0, space1},
combinator::{map, map_res, opt},
sequence::preceded,
IResult,
};
/// Datetime
///
/// # Syntax
///
/// ```text
/// YYYY-MM-DD DAYNAME
/// ```
///
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Datetime<'a> {
pub year: u16,
pub month: u8,
pub day: u8,
pub dayname: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub hour: Option<u8>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub minute: Option<u8>,
}
impl Datetime<'_> {
pub fn into_owned(self) -> Datetime<'static> {
Datetime {
year: self.year,
month: self.month,
day: self.day,
dayname: self.dayname.into_owned().into(),
hour: self.hour,
minute: self.minute,
}
}
}
#[cfg(feature = "chrono")]
mod chrono {
use super::Datetime;
use chrono::*;
impl Into<NaiveDate> for Datetime<'_> {
fn into(self) -> NaiveDate {
(&self).into()
}
}
impl Into<NaiveTime> for Datetime<'_> {
fn into(self) -> NaiveTime {
(&self).into()
}
}
impl Into<NaiveDateTime> for Datetime<'_> {
fn into(self) -> NaiveDateTime {
(&self).into()
}
}
impl Into<NaiveDate> for &Datetime<'_> {
fn into(self) -> NaiveDate {
NaiveDate::from_ymd(self.year.into(), self.month.into(), self.day.into())
}
}
impl Into<NaiveTime> for &Datetime<'_> {
fn into(self) -> NaiveTime {
NaiveTime::from_hms(
self.hour.unwrap_or_default().into(),
self.minute.unwrap_or_default().into(),
0,
)
}
}
impl Into<NaiveDateTime> for &Datetime<'_> {
fn into(self) -> NaiveDateTime {
NaiveDateTime::new(self.into(), self.into())
}
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(
feature = "ser",
serde(tag = "timestamp_type", rename_all = "kebab-case")
)]
#[derive(Debug)]
pub enum Timestamp<'a> {
Active {
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
Inactive {
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
ActiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
InactiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
Diary {
value: Cow<'a, str>,
},
}
impl Timestamp<'_> {
pub(crate) fn parse_active(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("<")(input)?;
let (input, start) = parse_datetime(input)?;
if input.starts_with('-') {
let (input, (hour, minute)) = parse_time(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
let mut end = start.clone();
end.hour = Some(hour);
end.minute = Some(minute);
return Ok((
input,
Timestamp::ActiveRange {
start,
end,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
if input.starts_with("--<") {
let (input, end) = parse_datetime(&input["--<".len()..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
Ok((
input,
Timestamp::ActiveRange {
start,
end,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Active {
start,
repeater: None,
delay: None,
},
))
}
}
pub(crate) fn parse_inactive(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("[")(input)?;
let (input, start) = parse_datetime(input)?;
if input.starts_with('-') {
let (input, (hour, minute)) = parse_time(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
let mut end = start.clone();
end.hour = Some(hour);
end.minute = Some(minute);
return Ok((
input,
Timestamp::InactiveRange {
start,
end,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
if input.starts_with("--[") {
let (input, end) = parse_datetime(&input["--[".len()..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
Ok((
input,
Timestamp::InactiveRange {
start,
end,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Inactive {
start,
repeater: None,
delay: None,
},
))
}
}
pub(crate) fn parse_diary(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("<%%(")(input)?;
let (input, value) = take_till(|c| c == ')' || c == '>' || c == '\n')(input)?;
let (input, _) = tag(")>")(input)?;
Ok((
input,
Timestamp::Diary {
value: value.into(),
},
))
}
pub fn into_owned(self) -> Timestamp<'static> {
match self {
Timestamp::Active {
start,
repeater,
delay,
} => Timestamp::Active {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::Inactive {
start,
repeater,
delay,
} => Timestamp::Inactive {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::ActiveRange {
start,
end,
repeater,
delay,
} => Timestamp::ActiveRange {
start: start.into_owned(),
end: end.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
} => Timestamp::InactiveRange {
start: start.into_owned(),
end: end.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::Diary { value } => Timestamp::Diary {
value: value.into_owned().into(),
},
}
}
}
fn parse_time(input: &str) -> IResult<&str, (u8, u8)> {
let (input, hour) = map_res(take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), |num| {
u8::from_str_radix(num, 10)
})(input)?;
let (input, _) = tag(":")(input)?;
let (input, minute) = map_res(take(2usize), |num| u8::from_str_radix(num, 10))(input)?;
Ok((input, (hour, minute)))
}
fn parse_datetime(input: &str) -> IResult<&str, Datetime<'_>> {
let parse_u8 = |num| u8::from_str_radix(num, 10);
let (input, year) = map_res(take(4usize), |num| u16::from_str_radix(num, 10))(input)?;
let (input, _) = tag("-")(input)?;
let (input, month) = map_res(take(2usize), parse_u8)(input)?;
let (input, _) = tag("-")(input)?;
let (input, day) = map_res(take(2usize), parse_u8)(input)?;
let (input, _) = space1(input)?;
let (input, dayname) = take_while(|c: char| {
!c.is_ascii_whitespace()
&& !c.is_ascii_digit()
&& c != '+'
&& c != '-'
&& c != ']'
&& c != '>'
})(input)?;
let (input, (hour, minute)) = map(opt(preceded(space1, parse_time)), |time| {
(time.map(|t| t.0), time.map(|t| t.1))
})(input)?;
Ok((
input,
Datetime {
year,
month,
day,
dayname: dayname.into(),
hour,
minute,
},
))
}
// TODO
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum RepeaterType {
// Cumulate,
// CatchUp,
// Restart,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum DelayType {
// All,
// First,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum TimeUnit {
// Hour,
// Day,
// Week,
// Month,
// Year,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub struct Repeater {
// pub ty: RepeaterType,
// pub value: usize,
// pub unit: TimeUnit,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub struct Delay {
// pub ty: DelayType,
// pub value: usize,
// pub unit: TimeUnit,
// }
#[test]
fn parse() {
assert_eq!(
Timestamp::parse_inactive("[2003-09-16 Tue]"),
Ok((
"",
Timestamp::Inactive {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: None,
minute: None
},
repeater: None,
delay: None,
},
))
);
assert_eq!(
Timestamp::parse_inactive("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]"),
Ok((
"",
Timestamp::InactiveRange {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39),
},
repeater: None,
delay: None
},
))
);
assert_eq!(
Timestamp::parse_active("<2003-09-16 Tue 09:39-10:39>"),
Ok((
"",
Timestamp::ActiveRange {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39),
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39),
},
repeater: None,
delay: None
},
))
);
}

View file

@ -1,291 +0,0 @@
//! Headline Title
use std::borrow::Cow;
use memchr::memrchr;
use nom::{
bytes::complete::{tag, take_until, take_while},
character::complete::{anychar, space1},
combinator::{map, map_parser, opt, verify},
error::ErrorKind,
error_position,
multi::fold_many0,
sequence::{delimited, preceded},
Err, IResult,
};
use std::collections::HashMap;
use crate::config::ParseConfig;
use crate::elements::{Drawer, Planning};
use crate::parsers::{line, skip_empty_lines, take_one_word};
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Title<'a> {
/// headline level, number of stars
pub level: usize,
/// priority cookie
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub priority: Option<char>,
/// headline tags, including the sparated colons
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))]
pub tags: Vec<Cow<'a, str>>,
/// headline keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub keyword: Option<Cow<'a, str>>,
pub raw: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub planning: Option<Box<Planning<'a>>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))]
pub properties: HashMap<Cow<'a, str>, Cow<'a, str>>,
}
impl Title<'_> {
#[inline]
pub(crate) fn parse<'a>(
input: &'a str,
config: &ParseConfig,
) -> IResult<&'a str, (Title<'a>, &'a str)> {
let (input, (level, keyword, priority, raw, tags)) = parse_headline(input, config)?;
let (input, planning) = Planning::parse(input)
.map(|(input, planning)| (input, Some(Box::new(planning))))
.unwrap_or((input, None));
let (input, properties) = opt(parse_properties_drawer)(input)?;
Ok((
input,
(
Title {
properties: properties.unwrap_or_default(),
level,
keyword: keyword.map(Into::into),
priority,
tags,
raw: raw.into(),
planning,
},
raw,
),
))
}
pub fn into_owned(self) -> Title<'static> {
Title {
level: self.level,
priority: self.priority,
tags: self
.tags
.into_iter()
.map(|s| s.into_owned().into())
.collect(),
keyword: self.keyword.map(Into::into).map(Cow::Owned),
raw: self.raw.into_owned().into(),
planning: self.planning.map(|p| Box::new(p.into_owned())),
properties: self
.properties
.into_iter()
.map(|(k, v)| (k.into_owned().into(), v.into_owned().into()))
.collect(),
}
}
}
impl Default for Title<'_> {
fn default() -> Title<'static> {
Title {
level: 1,
priority: None,
tags: Vec::new(),
keyword: None,
raw: Cow::Borrowed(""),
planning: None,
properties: HashMap::new(),
}
}
}
fn parse_headline<'a>(
input: &'a str,
config: &ParseConfig,
) -> IResult<
&'a str,
(
usize,
Option<&'a str>,
Option<char>,
&'a str,
Vec<Cow<'a, str>>,
),
> {
let (input, level) = map(take_while(|c: char| c == '*'), |s: &str| s.len())(input)?;
debug_assert!(level > 0);
let (input, keyword) = opt(preceded(
space1,
verify(take_one_word, |s: &str| {
config.todo_keywords.iter().any(|x| x == s)
|| config.done_keywords.iter().any(|x| x == s)
}),
))(input)?;
let (input, priority) = opt(preceded(
space1,
map_parser(
take_one_word,
delimited(
tag("[#"),
verify(anychar, |c: &char| c.is_ascii_uppercase()),
tag("]"),
),
),
))(input)?;
let (input, tail) = line(input)?;
let tail = tail.trim();
let (raw, tags) = memrchr(b' ', tail.as_bytes())
.map(|i| (tail[0..i].trim(), &tail[i + 1..]))
.filter(|(_, x)| x.len() > 2 && x.starts_with(':') && x.ends_with(':'))
.unwrap_or((tail, ""));
Ok((
input,
(
level,
keyword,
priority,
raw,
tags.split(':')
.filter(|s| !s.is_empty())
.map(Into::into)
.collect(),
),
))
}
fn parse_properties_drawer(input: &str) -> IResult<&str, HashMap<Cow<'_, str>, Cow<'_, str>>> {
let (input, (drawer, content)) = Drawer::parse(input.trim_start())?;
if drawer.name != "PROPERTIES" {
return Err(Err::Error(error_position!(input, ErrorKind::Tag)));
}
let (_, map) = fold_many0(
parse_node_property,
HashMap::new(),
|mut acc: HashMap<_, _>, (name, value)| {
acc.insert(name.into(), value.into());
acc
},
)(content)?;
Ok((input, map))
}
fn parse_node_property(input: &str) -> IResult<&str, (&str, &str)> {
let input = skip_empty_lines(input).trim_start();
let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| {
s.trim_end_matches('+')
})(input)?;
let (input, value) = line(input)?;
Ok((input, (name, value.trim())))
}
impl Title<'_> {
/// checks if this headline is "archived"
pub fn is_archived(&self) -> bool {
self.tags.iter().any(|tag| tag == "ARCHIVE")
}
}
#[cfg(test)]
lazy_static::lazy_static! {
static ref CONFIG: ParseConfig = ParseConfig::default();
}
#[test]
fn parse_headline_() {
assert_eq!(
parse_headline("**** DONE [#A] COMMENT Title :tag:a2%:", &CONFIG),
Ok((
"",
(
4,
Some("DONE"),
Some('A'),
"COMMENT Title",
vec!["tag".into(), "a2%".into()]
)
))
);
assert_eq!(
parse_headline("**** ToDO [#A] COMMENT Title", &CONFIG),
Ok(("", (4, None, None, "ToDO [#A] COMMENT Title", vec![])))
);
assert_eq!(
parse_headline("**** T0DO [#A] COMMENT Title", &CONFIG),
Ok(("", (4, None, None, "T0DO [#A] COMMENT Title", vec![])))
);
assert_eq!(
parse_headline("**** DONE [#1] COMMENT Title", &CONFIG),
Ok(("", (4, Some("DONE"), None, "[#1] COMMENT Title", vec![],)))
);
assert_eq!(
parse_headline("**** DONE [#a] COMMENT Title", &CONFIG),
Ok(("", (4, Some("DONE"), None, "[#a] COMMENT Title", vec![],)))
);
assert_eq!(
parse_headline("**** Title :tag:a2%", &CONFIG),
Ok(("", (4, None, None, "Title :tag:a2%", vec![],)))
);
assert_eq!(
parse_headline("**** Title tag:a2%:", &CONFIG),
Ok(("", (4, None, None, "Title tag:a2%:", vec![],)))
);
assert_eq!(
parse_headline(
"**** DONE Title",
&ParseConfig {
done_keywords: vec![],
..Default::default()
}
),
Ok(("", (4, None, None, "DONE Title", vec![])))
);
assert_eq!(
parse_headline(
"**** TASK [#A] Title",
&ParseConfig {
todo_keywords: vec!["TASK".to_string()],
..Default::default()
}
),
Ok(("", (4, Some("TASK"), Some('A'), "Title", vec![],)))
);
}
#[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]
// fn is_commented() {
// assert!(Title::parse("* COMMENT Title", &CONFIG)
// .1
// .is_commented());
// assert!(!Title::parse("* Title", &CONFIG).1.is_commented());
// assert!(!Title::parse("* C0MMENT Title", &CONFIG)
// .1
// .is_commented());
// assert!(!Title::parse("* comment Title", &CONFIG)
// .1
// .is_commented());
// }