refactor: rewrite some parsers with nom

This commit is contained in:
PoiScript 2019-07-01 02:11:21 +08:00
parent afcb5090ec
commit 50f6b9f52a
29 changed files with 1415 additions and 2061 deletions

View file

@ -26,8 +26,10 @@ indextree = "3.2.0"
jetscii = "0.4.4"
memchr = "2.2.0"
serde = { version = "1.0.94", optional = true, features = ["derive"] }
nom = "5.0.0"
[dev-dependencies]
lazy_static = "1.3.0"
pretty_assertions = "0.6.1"
serde_json = "1.0.39"
slugify = "0.1.0"

View file

@ -49,14 +49,14 @@ One as `Event::Start(element)`, one as `Event::End(element)`.
## Render html
You can call the `Org::html_default` function to generate html directly, which
You can call the `Org::html` function to generate html directly, which
uses the `DefaultHtmlHandler` internally:
```rust
use orgize::Org;
let mut writer = Vec::new();
Org::parse("* title\n*section*").html_default(&mut writer).unwrap();
Org::parse("* title\n*section*").html(&mut writer).unwrap();
assert_eq!(
String::from_utf8(writer).unwrap(),
@ -67,7 +67,7 @@ assert_eq!(
## Render html with custom HtmlHandler
To customize html rendering, simply implementing `HtmlHandler` trait and passing
it to the `Org::html` function.
it to the `Org::html_with_handler` function.
The following code demonstrates how to add a id for every headline and return
own error type while rendering.
@ -107,7 +107,7 @@ impl HtmlHandler<MyError> for MyHtmlHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element<'_>) -> Result<(), MyError> {
let mut default_handler = DefaultHtmlHandler;
match element {
Element::Headline { headline, .. } => {
Element::Headline(headline) => {
if headline.level > 6 {
return Err(MyError::Heading);
} else {
@ -130,7 +130,7 @@ impl HtmlHandler<MyError> for MyHtmlHandler {
fn main() -> Result<(), MyError> {
let mut writer = Vec::new();
Org::parse("* title\n*section*").html(&mut writer, MyHtmlHandler)?;
Org::parse("* title\n*section*").html_with_handler(&mut writer, MyHtmlHandler)?;
assert_eq!(
String::from_utf8(writer)?,

View file

@ -35,7 +35,7 @@ impl HtmlHandler<MyError> for MyHtmlHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element<'_>) -> Result<(), MyError> {
let mut default_handler = DefaultHtmlHandler;
match element {
Element::Headline { headline, .. } => {
Element::Headline(headline) => {
if headline.level > 6 {
return Err(MyError::Heading);
} else {
@ -65,7 +65,7 @@ fn main() -> Result<(), MyError> {
let contents = String::from_utf8(fs::read(&args[1])?)?;
let mut writer = Vec::new();
Org::parse(&contents).html(&mut writer, MyHtmlHandler)?;
Org::parse(&contents).html_with_handler(&mut writer, MyHtmlHandler)?;
println!("{}", String::from_utf8(writer)?);
}

View file

@ -1,5 +1,7 @@
use memchr::{memchr, memchr_iter};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
@ -7,12 +9,13 @@ pub struct Block<'a> {
pub name: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub args: Option<&'a str>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl Block<'_> {
#[inline]
// return (block, contents-begin, contents-end, end)
pub(crate) fn parse(text: &str) -> Option<(Block<'_>, usize, usize, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, Element<'_>)> {
debug_assert!(text.starts_with("#+"));
if text.len() <= 8 || text[2..8].to_uppercase() != "BEGIN_" {
@ -35,14 +38,28 @@ impl Block<'_> {
for i in lines {
if text[pos..i].trim().eq_ignore_ascii_case(&end) {
return Some((Block { name, args }, off, pos, i + 1));
return Some((
&text[i + 1..],
Element::Block(Block {
name,
args,
contents: &text[off..pos],
}),
));
}
pos = i + 1;
}
if text[pos..].trim().eq_ignore_ascii_case(&end) {
Some((Block { name, args }, off, pos, text.len()))
Some((
"",
Element::Block(Block {
name,
args,
contents: &text[off..pos],
}),
))
} else {
None
}
@ -54,25 +71,23 @@ fn parse() {
assert_eq!(
Block::parse("#+BEGIN_SRC\n#+END_SRC"),
Some((
Block {
"",
Element::Block(Block {
name: "SRC",
args: None,
},
"#+BEGIN_SRC\n".len(),
"#+BEGIN_SRC\n".len(),
"#+BEGIN_SRC\n#+END_SRC".len()
contents: ""
}),
))
);
assert_eq!(
Block::parse("#+BEGIN_SRC javascript \nconsole.log('Hello World!');\n#+END_SRC\n"),
Some((
Block {
"",
Element::Block(Block {
name: "SRC",
args: Some("javascript"),
},
"#+BEGIN_SRC javascript \n".len(),
"#+BEGIN_SRC javascript \nconsole.log('Hello World!');\n".len(),
"#+BEGIN_SRC javascript \nconsole.log('Hello World!');\n#+END_SRC\n".len()
contents: "console.log('Hello World!');\n"
}),
))
);
// TODO: more testing

View file

@ -1,4 +1,4 @@
use crate::elements::{Datetime, Timestamp};
use crate::elements::{Date, Element, Time, Timestamp};
use memchr::memchr;
/// clock elements
@ -10,26 +10,25 @@ use memchr::memchr;
pub enum Clock<'a> {
/// closed Clock
Closed {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
end_date: Date<'a>,
end_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
duration: &'a str,
},
/// running Clock
Running {
start: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
},
}
impl Clock<'_> {
pub(crate) fn parse(text: &str) -> Option<(Clock<'_>, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, Element<'_>)> {
let (text, eol) = memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len()));
@ -44,14 +43,16 @@ impl Clock<'_> {
return None;
}
let (timestamp, off) = Timestamp::parse_inactive(tail)?;
let (tail, timestamp) = Timestamp::parse_inactive(tail).ok()?;
let tail = tail[off..].trim();
let tail = tail.trim();
match timestamp {
Timestamp::InactiveRange {
start,
end,
start_date,
start_time,
end_date,
end_time,
repeater,
delay,
} if tail.starts_with("=>") => {
@ -63,30 +64,34 @@ impl Clock<'_> {
&& duration.as_bytes()[colon + 2].is_ascii_digit()
{
Some((
Clock::Closed {
start,
end,
&text[eol..],
Element::Clock(Clock::Closed {
start_date,
start_time,
end_date,
end_time,
repeater,
delay,
duration,
},
eol,
}),
))
} else {
None
}
}
Timestamp::Inactive {
start,
start_date,
start_time,
repeater,
delay,
} if tail.is_empty() => Some((
Clock::Running {
start,
&text[eol..],
Element::Clock(Clock::Running {
start_date,
start_time,
repeater,
delay,
},
eol,
}),
)),
_ => None,
}
@ -120,24 +125,29 @@ impl Clock<'_> {
pub fn value(&self) -> Timestamp<'_> {
match *self {
Clock::Closed {
start,
end,
start_date,
start_time,
end_date,
end_time,
repeater,
delay,
..
} => Timestamp::InactiveRange {
start,
end,
start_date,
start_time,
end_date,
end_time,
repeater,
delay,
},
Clock::Running {
start,
start_date,
start_time,
repeater,
delay,
..
} => Timestamp::Inactive {
start,
start_date,
start_time,
repeater,
delay,
},
@ -150,37 +160,52 @@ fn parse() {
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Some((
Clock::Running {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
"",
Element::Clock(Clock::Running {
start_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
start_time: Some(Time {
hour: 9,
minute: 39
}),
repeater: None,
delay: None,
},
"CLOCK: [2003-09-16 Tue 09:39]".len()
})
))
);
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"),
Some((
Clock::Closed {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
"",
Element::Clock(Clock::Closed {
start_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end: Datetime {
date: "2003-09-16",
time: Some("10:39"),
start_time: Some(Time {
hour: 9,
minute: 39
}),
end_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end_time: Some(Time {
hour: 10,
minute: 39
}),
repeater: None,
delay: None,
duration: "1:00",
},
"CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00".len()
})
))
);
}

View file

@ -10,8 +10,7 @@ pub enum Cookie<'a> {
impl Cookie<'_> {
#[inline]
// return (clock, offset)
pub(crate) fn parse(src: &str) -> Option<(Cookie<'_>, usize)> {
pub(crate) fn parse(src: &str) -> Option<(&str, Cookie<'_>)> {
debug_assert!(src.starts_with('['));
let bytes = src.as_bytes();
@ -19,12 +18,15 @@ impl Cookie<'_> {
memchr2(b'%', b'/', bytes).filter(|&i| bytes[1..i].iter().all(u8::is_ascii_digit))?;
if bytes[num1] == b'%' && *bytes.get(num1 + 1)? == b']' {
Some((Cookie::Percent(&src[1..num1]), num1 + 2))
Some((&src[num1 + 2..], Cookie::Percent(&src[1..num1])))
} else {
let num2 = memchr(b']', bytes)
.filter(|&i| bytes[num1 + 1..i].iter().all(u8::is_ascii_digit))?;
Some((Cookie::Slash(&src[1..num1], &src[num1 + 1..num2]), num2 + 1))
Some((
&src[num2 + 1..],
Cookie::Slash(&src[1..num1], &src[num1 + 1..num2]),
))
}
}
}
@ -33,31 +35,22 @@ impl Cookie<'_> {
fn parse() {
assert_eq!(
Cookie::parse("[1/10]"),
Some((Cookie::Slash("1", "10"), "[1/10]".len()))
Some(("", Cookie::Slash("1", "10")))
);
assert_eq!(
Cookie::parse("[1/1000]"),
Some((Cookie::Slash("1", "1000"), "[1/1000]".len()))
);
assert_eq!(
Cookie::parse("[10%]"),
Some((Cookie::Percent("10"), "[10%]".len()))
);
assert_eq!(
Cookie::parse("[%]"),
Some((Cookie::Percent(""), "[%]".len()))
);
assert_eq!(
Cookie::parse("[/]"),
Some((Cookie::Slash("", ""), "[/]".len()))
Some(("", Cookie::Slash("1", "1000")))
);
assert_eq!(Cookie::parse("[10%]"), Some(("", Cookie::Percent("10"))));
assert_eq!(Cookie::parse("[%]"), Some(("", Cookie::Percent(""))));
assert_eq!(Cookie::parse("[/]"), Some(("", Cookie::Slash("", ""))));
assert_eq!(
Cookie::parse("[100/]"),
Some((Cookie::Slash("100", ""), "[100/]".len()))
Some(("", Cookie::Slash("100", "")))
);
assert_eq!(
Cookie::parse("[/100]"),
Some((Cookie::Slash("", "100"), "[/100]".len()))
Some(("", Cookie::Slash("", "100")))
);
assert_eq!(Cookie::parse("[10% ]"), None);

View file

@ -1,16 +1,19 @@
use memchr::memchr_iter;
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Drawer<'a> {
pub name: &'a str,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl<'a> Drawer<'a> {
impl Drawer<'_> {
#[inline]
// return (drawer, contents-begin, contents-end , end)
pub(crate) fn parse(text: &'a str) -> Option<(Drawer<'a>, usize, usize, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, Element<'_>)> {
debug_assert!(text.starts_with(':'));
let mut lines = memchr_iter(b'\n', text.as_bytes());
@ -30,12 +33,11 @@ impl<'a> Drawer<'a> {
for i in lines {
if text[pos..i].trim().eq_ignore_ascii_case(":END:") {
return Some((
Drawer {
&text[i + 1..],
Element::Drawer(Drawer {
name: &name[0..name.len() - 1],
},
off,
pos,
i + 1,
contents: &text[off..pos],
}),
));
}
pos = i + 1;
@ -43,12 +45,11 @@ impl<'a> Drawer<'a> {
if text[pos..].trim().eq_ignore_ascii_case(":END:") {
Some((
Drawer {
"",
Element::Drawer(Drawer {
name: &name[0..name.len() - 1],
},
off,
pos,
text.len(),
contents: &text[off..pos],
}),
))
} else {
None
@ -61,10 +62,11 @@ fn parse() {
assert_eq!(
Drawer::parse(":PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
Some((
Drawer { name: "PROPERTIES" },
":PROPERTIES:\n".len(),
":PROPERTIES:\n :CUSTOM_ID: id\n".len(),
":PROPERTIES:\n :CUSTOM_ID: id\n :END:".len()
"",
Element::Drawer(Drawer {
name: "PROPERTIES",
contents: " :CUSTOM_ID: id\n"
})
))
)
}

View file

@ -1,3 +1,5 @@
use crate::elements::Element;
use memchr::{memchr, memchr_iter};
#[cfg_attr(test, derive(PartialEq))]
@ -7,12 +9,14 @@ pub struct DynBlock<'a> {
pub block_name: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub arguments: Option<&'a str>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl DynBlock<'_> {
#[inline]
// return (dyn_block, contents-begin, contents-end, end)
pub(crate) fn parse(text: &str) -> Option<(DynBlock<'_>, usize, usize, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, Element<'_>)> {
debug_assert!(text.starts_with("#+"));
if text.len() <= "#+BEGIN: ".len() || !text[2..9].eq_ignore_ascii_case("BEGIN: ") {
@ -42,13 +46,12 @@ impl DynBlock<'_> {
for i in lines {
if text[pos..i].trim().eq_ignore_ascii_case("#+END:") {
return Some((
DynBlock {
&text[i + 1..],
Element::DynBlock(DynBlock {
block_name: name,
arguments: para,
},
off,
pos,
i + 1,
contents: &text[off..pos],
}),
));
}
@ -57,13 +60,12 @@ impl DynBlock<'_> {
if text[pos..].trim().eq_ignore_ascii_case("#+END:") {
Some((
DynBlock {
"",
Element::DynBlock(DynBlock {
block_name: name,
arguments: para,
},
off,
pos,
text.len(),
contents: &text[off..pos],
}),
))
} else {
None
@ -77,13 +79,12 @@ fn parse() {
assert_eq!(
DynBlock::parse("#+BEGIN: clocktable :scope file\nCONTENTS\n#+END:\n"),
Some((
DynBlock {
"",
Element::DynBlock(DynBlock {
block_name: "clocktable",
arguments: Some(":scope file"),
},
"#+BEGIN: clocktable :scope file\n".len(),
"#+BEGIN: clocktable :scope file\nCONTENTS\n".len(),
"#+BEGIN: clocktable :scope file\nCONTENTS\n#+END:\n".len(),
contents: "CONTENTS\n"
},)
))
);
}

View file

@ -2,7 +2,7 @@ use bytecount::count;
use memchr::memchr;
#[inline]
pub(crate) fn parse(text: &str, marker: u8) -> Option<usize> {
pub(crate) fn parse(text: &str, marker: u8) -> Option<(&str, &str)> {
debug_assert!(text.len() >= 3);
let bytes = text.as_bytes();
@ -30,12 +30,12 @@ pub(crate) fn parse(text: &str, marker: u8) -> Option<usize> {
|| post == b')'
|| post == b'}'
{
Some(end + 2)
Some((&text[end + 2..], &text[1..end + 1]))
} else {
None
}
} else {
Some(end + 2)
Some((&text[end + 2..], &text[1..end + 1]))
}
}
@ -45,8 +45,8 @@ mod tests {
fn parse() {
use super::parse;
assert_eq!(parse("*bold*", b'*'), Some("*bold*".len()));
assert_eq!(parse("*bo\nld*", b'*'), Some("*bo\nld*".len()));
assert_eq!(parse("*bold*", b'*'), Some(("", "bold")));
assert_eq!(parse("*bo\nld*", b'*'), Some(("", "bo\nld")));
assert_eq!(parse("*bold*a", b'*'), None);
assert_eq!(parse("*bold*", b'/'), None);
assert_eq!(parse("*bold *", b'*'), None);

View file

@ -5,11 +5,13 @@ use memchr::memchr;
#[derive(Debug)]
pub struct FnDef<'a> {
pub label: &'a str,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl FnDef<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(FnDef<'_>, usize, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, FnDef<'_>)> {
if text.starts_with("[fn:") {
let (label, off) = memchr(b']', text.as_bytes())
.filter(|&i| {
@ -22,7 +24,13 @@ impl FnDef<'_> {
let end = memchr(b'\n', text.as_bytes()).unwrap_or_else(|| text.len());
Some((FnDef { label }, off, end))
Some((
&text[end..],
FnDef {
label,
contents: &text[off..end],
},
))
} else {
None
}
@ -34,33 +42,41 @@ fn parse() {
assert_eq!(
FnDef::parse("[fn:1] https://orgmode.org"),
Some((
FnDef { label: "1" },
"[fn:1]".len(),
"[fn:1] https://orgmode.org".len()
"",
FnDef {
label: "1",
contents: " https://orgmode.org"
},
))
);
assert_eq!(
FnDef::parse("[fn:word_1] https://orgmode.org"),
Some((
FnDef { label: "word_1" },
"[fn:word_1]".len(),
"[fn:word_1] https://orgmode.org".len()
"",
FnDef {
label: "word_1",
contents: " https://orgmode.org"
},
))
);
assert_eq!(
FnDef::parse("[fn:WORD-1] https://orgmode.org"),
Some((
FnDef { label: "WORD-1" },
"[fn:WORD-1]".len(),
"[fn:WORD-1] https://orgmode.org".len()
"",
FnDef {
label: "WORD-1",
contents: " https://orgmode.org"
},
))
);
assert_eq!(
FnDef::parse("[fn:WORD]"),
Some((
FnDef { label: "WORD" },
"[fn:WORD]".len(),
"[fn:WORD]".len()
"",
FnDef {
label: "WORD",
contents: ""
},
))
);
assert_eq!(FnDef::parse("[fn:] https://orgmode.org"), None);

View file

@ -12,8 +12,7 @@ pub struct FnRef<'a> {
impl FnRef<'_> {
#[inline]
// return (fn_ref, offset)
pub(crate) fn parse(text: &str) -> Option<(FnRef<'_>, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, FnRef<'_>)> {
debug_assert!(text.starts_with("[fn:"));
let bytes = text.as_bytes();
@ -50,7 +49,7 @@ impl FnRef<'_> {
(None, off + 1)
};
Some((FnRef { label, definition }, off))
Some((&text[off..], FnRef { label, definition }))
}
}
@ -59,41 +58,41 @@ fn parse() {
assert_eq!(
FnRef::parse("[fn:1]"),
Some((
"",
FnRef {
label: Some("1"),
definition: None
},
"[fn:1]".len()
))
);
assert_eq!(
FnRef::parse("[fn:1:2]"),
Some((
"",
FnRef {
label: Some("1"),
definition: Some("2")
},
"[fn:1:2]".len()
))
);
assert_eq!(
FnRef::parse("[fn::2]"),
Some((
"",
FnRef {
label: None,
definition: Some("2")
},
"[fn::2]".len()
))
);
assert_eq!(
FnRef::parse("[fn::[]]"),
Some((
"",
FnRef {
label: None,
definition: Some("[]")
},
"[fn::[]]".len()
))
);
assert_eq!(FnRef::parse("[fn::[]"), None);

View file

@ -22,13 +22,12 @@ pub struct Headline<'a> {
/// headline keyword
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub keyword: Option<&'a str>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl Headline<'_> {
pub(crate) fn parse<'a>(
text: &'a str,
config: &ParseConfig<'_>,
) -> (Headline<'a>, usize, usize) {
pub(crate) fn parse<'a>(text: &'a str, config: &ParseConfig<'_>) -> (&'a str, Headline<'a>) {
let level = memchr2(b'\n', b' ', text.as_bytes()).unwrap_or_else(|| text.len());
debug_assert!(level > 0);
@ -49,15 +48,15 @@ impl Headline<'_> {
if level == off {
return (
&text[end..],
Headline {
level,
keyword: None,
priority: None,
title: "",
tags: Vec::new(),
contents: &text[off..end],
},
off,
end,
);
}
@ -102,15 +101,15 @@ impl Headline<'_> {
};
(
&text[end..],
Headline {
level,
keyword,
priority,
title,
tags: tags.split(':').filter(|s| !s.is_empty()).collect(),
contents: &text[off..end],
},
off,
end,
)
}
@ -149,115 +148,124 @@ impl Headline<'_> {
}
}
#[cfg(test)]
lazy_static::lazy_static! {
static ref CONFIG: ParseConfig<'static> = ParseConfig::default();
}
#[test]
fn parse() {
assert_eq!(
Headline::parse(
"**** DONE [#A] COMMENT Title :tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** DONE [#A] COMMENT Title :tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: Some('A'),
keyword: Some("DONE"),
title: "COMMENT Title",
tags: vec!["tag", "a2%"],
contents: ""
},
)
);
assert_eq!(
Headline::parse(
"**** ToDO [#A] COMMENT Title :tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** ToDO [#A] COMMENT Title :tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: None,
tags: vec!["tag", "a2%"],
title: "ToDO [#A] COMMENT Title",
keyword: None,
contents: ""
},
)
);
assert_eq!(
Headline::parse(
"**** T0DO [#A] COMMENT Title :tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** T0DO [#A] COMMENT Title :tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: None,
tags: vec!["tag", "a2%"],
title: "T0DO [#A] COMMENT Title",
keyword: None,
contents: ""
},
)
);
assert_eq!(
Headline::parse(
"**** DONE [#1] COMMENT Title :tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** DONE [#1] COMMENT Title :tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: None,
tags: vec!["tag", "a2%"],
title: "[#1] COMMENT Title",
keyword: Some("DONE")
keyword: Some("DONE"),
contents: "",
},
)
);
assert_eq!(
Headline::parse(
"**** DONE [#a] COMMENT Title :tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** DONE [#a] COMMENT Title :tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: None,
tags: vec!["tag", "a2%"],
title: "[#a] COMMENT Title",
keyword: Some("DONE")
keyword: Some("DONE"),
contents: "",
},
)
);
assert_eq!(
Headline::parse(
"**** DONE [#A] COMMENT Title :tag:a2%",
&ParseConfig::default()
)
.0,
Headline::parse("**** DONE [#A] COMMENT Title :tag:a2%", &CONFIG),
(
"",
Headline {
level: 4,
priority: Some('A'),
tags: Vec::new(),
title: "COMMENT Title :tag:a2%",
keyword: Some("DONE"),
contents: ""
},
)
);
assert_eq!(
Headline::parse(
"**** DONE [#A] COMMENT Title tag:a2%:",
&ParseConfig::default()
)
.0,
Headline::parse("**** DONE [#A] COMMENT Title tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: Some('A'),
tags: Vec::new(),
title: "COMMENT Title tag:a2%:",
keyword: Some("DONE"),
contents: ""
},
)
);
assert_eq!(
Headline::parse("**** COMMENT Title tag:a2%:", &ParseConfig::default()).0,
Headline::parse("**** COMMENT Title tag:a2%:", &CONFIG),
(
"",
Headline {
level: 4,
priority: None,
tags: Vec::new(),
title: "COMMENT Title tag:a2%:",
keyword: None,
contents: ""
},
)
);
}
@ -270,15 +278,18 @@ fn parse_todo_keywords() {
default_todo_keywords: &[],
..Default::default()
}
)
.0,
),
(
"",
Headline {
level: 4,
priority: None,
keyword: None,
title: "DONE [#A] COMMENT Title",
tags: vec!["tag", "a2%"],
contents: ""
},
)
);
assert_eq!(
Headline::parse(
@ -287,69 +298,50 @@ fn parse_todo_keywords() {
todo_keywords: &["TASK"],
..Default::default()
}
)
.0,
),
(
"",
Headline {
level: 4,
priority: Some('A'),
keyword: Some("TASK"),
title: "COMMENT Title",
tags: vec!["tag", "a2%"],
contents: ""
},
)
);
}
#[test]
fn is_commented() {
assert!(Headline::parse("* COMMENT Title", &ParseConfig::default())
.0
.is_commented());
assert!(!Headline::parse("* Title", &ParseConfig::default())
.0
.is_commented());
assert!(!Headline::parse("* C0MMENT Title", &ParseConfig::default())
.0
.is_commented());
assert!(!Headline::parse("* comment Title", &ParseConfig::default())
.0
.is_commented());
assert!(Headline::parse("* COMMENT Title", &CONFIG).1.is_commented());
assert!(!Headline::parse("* Title", &CONFIG).1.is_commented());
assert!(!Headline::parse("* C0MMENT Title", &CONFIG).1.is_commented());
assert!(!Headline::parse("* comment Title", &CONFIG).1.is_commented());
}
#[test]
fn is_archived() {
assert!(
Headline::parse("* Title :ARCHIVE:", &ParseConfig::default())
.0
.is_archived()
);
assert!(
Headline::parse("* Title :t:ARCHIVE:", &ParseConfig::default())
.0
.is_archived()
);
assert!(
Headline::parse("* Title :ARCHIVE:t:", &ParseConfig::default())
.0
.is_archived()
);
assert!(!Headline::parse("* Title", &ParseConfig::default())
.0
.is_commented());
assert!(
!Headline::parse("* Title :ARCHIVED:", &ParseConfig::default())
.0
.is_archived()
);
assert!(
!Headline::parse("* Title :ARCHIVES:", &ParseConfig::default())
.0
.is_archived()
);
assert!(
!Headline::parse("* Title :archive:", &ParseConfig::default())
.0
.is_archived()
);
assert!(Headline::parse("* Title :ARCHIVE:", &CONFIG)
.1
.is_archived());
assert!(Headline::parse("* Title :t:ARCHIVE:", &CONFIG)
.1
.is_archived());
assert!(Headline::parse("* Title :ARCHIVE:t:", &CONFIG)
.1
.is_archived());
assert!(!Headline::parse("* Title", &CONFIG).1.is_commented());
assert!(!Headline::parse("* Title :ARCHIVED:", &CONFIG)
.1
.is_archived());
assert!(!Headline::parse("* Title :ARCHIVES:", &CONFIG)
.1
.is_archived());
assert!(!Headline::parse("* Title :archive:", &CONFIG)
.1
.is_archived());
}
#[test]

View file

@ -1,4 +1,11 @@
use memchr::{memchr, memchr2};
use nom::{
bytes::complete::{tag, take_till},
combinator::opt,
sequence::delimited,
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -7,53 +14,34 @@ pub struct InlineCall<'a> {
pub name: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub inside_header: Option<&'a str>,
pub args: &'a str,
pub arguments: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub end_header: Option<&'a str>,
}
fn header(input: &str) -> IResult<&str, &str> {
delimited(tag("["), take_till(|c| c == ']' || c == '\n'), tag("]"))(input)
}
impl<'a> InlineCall<'a> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(InlineCall<'_>, usize)> {
debug_assert!(text.starts_with("call_"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("call_")(input)?;
let (input, name) = take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')')(input)?;
let (input, inside_header) = opt(header)(input)?;
let (input, _) = tag("(")(input)?;
let (input, arguments) = take_till(|c| c == ')' || c == '\n')(input)?;
let (input, _) = tag(")")(input)?;
let (input, end_header) = opt(header)(input)?;
let bytes = text.as_bytes();
let (name, off) = memchr2(b'[', b'(', bytes)
.map(|i| (&text["call_".len()..i], i))
.filter(|(name, _)| name.as_bytes().iter().all(u8::is_ascii_graphic))?;
let (inside_header, off) = if bytes[off] == b'[' {
memchr(b']', &bytes[off..])
.filter(|&i| {
bytes[off + i + 1] == b'('
&& bytes[off + 1..off + i].iter().all(|&c| c != b'\n')
})
.map(|i| (Some(&text[off + 1..off + i]), off + i + 1))?
} else {
(None, off)
};
let (args, off) = memchr(b')', &bytes[off..])
.map(|i| (&text[off + 1..off + i], off + i + 1))
.filter(|(args, _)| args.as_bytes().iter().all(|&c| c != b'\n'))?;
let (end_header, off) = if text.len() > off && text.as_bytes()[off] == b'[' {
memchr(b']', &bytes[off..])
.filter(|&i| bytes[off + 1..off + i].iter().all(|&c| c != b'\n'))
.map(|i| (Some(&text[off + 1..off + i]), off + i + 1))?
} else {
(None, off)
};
Some((
InlineCall {
Ok((
input,
Element::InlineCall(InlineCall {
name,
args,
arguments,
inside_header,
end_header,
},
off,
}),
))
}
}
@ -62,50 +50,50 @@ impl<'a> InlineCall<'a> {
fn parse() {
assert_eq!(
InlineCall::parse("call_square(4)"),
Some((
InlineCall {
Ok((
"",
Element::InlineCall(InlineCall {
name: "square",
args: "4",
arguments: "4",
inside_header: None,
end_header: None,
},
"call_square(4)".len()
}),
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)"),
Some((
InlineCall {
Ok((
"",
Element::InlineCall(InlineCall {
name: "square",
args: "4",
arguments: "4",
inside_header: Some(":results output"),
end_header: None,
},
"call_square[:results output](4)".len()
}),
))
);
assert_eq!(
InlineCall::parse("call_square(4)[:results html]"),
Some((
InlineCall {
Ok((
"",
Element::InlineCall(InlineCall {
name: "square",
args: "4",
arguments: "4",
inside_header: None,
end_header: Some(":results html"),
},
"call_square(4)[:results html]".len()
}),
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)[:results html]"),
Some((
InlineCall {
Ok((
"",
Element::InlineCall(InlineCall {
name: "square",
args: "4",
arguments: "4",
inside_header: Some(":results output"),
end_header: Some(":results html"),
},
"call_square[:results output](4)[:results html]".len()
}),
))
);
}

View file

@ -1,4 +1,11 @@
use memchr::{memchr, memchr2};
use nom::{
bytes::complete::{tag, take_till, take_while1},
combinator::opt,
sequence::delimited,
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -6,34 +13,33 @@ use memchr::{memchr, memchr2};
pub struct InlineSrc<'a> {
pub lang: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub option: Option<&'a str>,
pub options: Option<&'a str>,
pub body: &'a str,
}
impl<'a> InlineSrc<'a> {
impl InlineSrc<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(InlineSrc<'_>, usize)> {
debug_assert!(text.starts_with("src_"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
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, _) = tag("{")(input)?;
let (input, body) = take_till(|c| c == '\n' || c == '}')(input)?;
let (input, _) = tag("}")(input)?;
let (lang, off) = memchr2(b'[', b'{', text.as_bytes())
.map(|i| (&text["src_".len()..i], i))
.filter(|(lang, off)| {
*off != 4 && lang.as_bytes().iter().all(|c| !c.is_ascii_whitespace())
})?;
let (option, off) = if text.as_bytes()[off] == b'[' {
memchr(b']', text[off..].as_bytes())
.filter(|&i| text[off..off + i].as_bytes().iter().all(|c| *c != b'\n'))
.map(|i| (Some(&text[off + 1..off + i]), off + i + 1))?
} else {
(None, off)
};
let (body, off) = memchr(b'}', text[off..].as_bytes())
.map(|i| (&text[off + 1..off + i], off + i + 1))
.filter(|(body, _)| body.as_bytes().iter().all(|c| *c != b'\n'))?;
Some((InlineSrc { lang, option, body }, off))
Ok((
input,
Element::InlineSrc(InlineSrc {
lang,
options,
body,
}),
))
}
}
@ -41,33 +47,27 @@ impl<'a> InlineSrc<'a> {
fn parse() {
assert_eq!(
InlineSrc::parse("src_C{int a = 0;}"),
Some((
InlineSrc {
Ok((
"",
Element::InlineSrc(InlineSrc {
lang: "C",
option: None,
options: None,
body: "int a = 0;"
},
"src_C{int a = 0;}".len()
}),
))
);
assert_eq!(
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
Some((
InlineSrc {
Ok((
"",
Element::InlineSrc(InlineSrc {
lang: "xml",
option: Some(":exports code"),
options: Some(":exports code"),
body: "<tag>text</tag>",
},
"src_xml[:exports code]{<tag>text</tag>}".len()
}),
))
);
assert_eq!(
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>"),
None
);
assert_eq!(
InlineSrc::parse("src_[:exports code]{<tag>text</tag>}"),
None
);
assert!(InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>").is_err(),);
assert!(InlineSrc::parse("src_[:exports code]{<tag>text</tag>}").is_err(),);
// assert_eq!(parse("src_xml[:exports code]"), None);
}

View file

@ -1,4 +1,11 @@
use memchr::{memchr, memchr2};
use nom::{
bytes::complete::{tag, take_till, take_while},
combinator::{map, opt},
sequence::delimited,
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -6,7 +13,7 @@ use memchr::{memchr, memchr2};
pub struct Keyword<'a> {
pub key: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub option: Option<&'a str>,
pub optional: Option<&'a str>,
pub value: &'a str,
}
@ -14,43 +21,36 @@ pub struct Keyword<'a> {
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
pub struct BabelCall<'a> {
pub key: &'a str,
pub value: &'a str,
}
fn optional(input: &str) -> IResult<&str, &str> {
delimited(tag("["), take_till(|c| c == ']' || c == '\n'), tag("]"))(input)
}
impl Keyword<'_> {
#[inline]
// return (key, option, value, offset)
pub(crate) fn parse(text: &str) -> Option<(&str, Option<&str>, &str, usize)> {
debug_assert!(text.starts_with("#+"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("#+")(input)?;
let (input, key) =
take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
let (input, optional) = opt(optional)(input)?;
let (input, _) = tag(":")(input)?;
let (input, value) = map(take_while(|c| c != '\n'), str::trim)(input)?;
let (input, _) = opt(tag("\n"))(input)?;
let bytes = text.as_bytes();
let (key, off) = memchr2(b':', b'[', bytes)
.filter(|&i| {
bytes[2..i]
.iter()
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
})
.map(|i| (&text[2..i], i + 1))?;
let (option, off) = if bytes[off - 1] == b'[' {
memchr(b']', bytes)
.filter(|&i| {
bytes[off..i].iter().all(|&c| c != b'\n')
&& i < text.len()
&& bytes[i + 1] == b':'
})
.map(|i| (Some(&text[off..i]), i + "]:".len()))?
if key.eq_ignore_ascii_case("CALL") {
Ok((input, Element::BabelCall(BabelCall { value })))
} else {
(None, off)
};
let end = memchr(b'\n', bytes)
.map(|i| i + 1)
.unwrap_or_else(|| text.len());
Some((key, option, &text[off..end].trim(), end))
Ok((
input,
Element::Keyword(Keyword {
key,
optional,
value,
}),
))
}
}
}
@ -58,50 +58,94 @@ impl Keyword<'_> {
fn parse() {
assert_eq!(
Keyword::parse("#+KEY:"),
Some(("KEY", None, "", "#+KEY:".len()))
Ok((
"",
Element::Keyword(Keyword {
key: "KEY",
optional: None,
value: "",
})
))
);
assert_eq!(
Keyword::parse("#+KEY: VALUE"),
Some(("KEY", None, "VALUE", "#+KEY: VALUE".len()))
Ok((
"",
Element::Keyword(Keyword {
key: "KEY",
optional: None,
value: "VALUE",
})
))
);
assert_eq!(
Keyword::parse("#+K_E_Y: VALUE"),
Some(("K_E_Y", None, "VALUE", "#+K_E_Y: VALUE".len()))
Ok((
"",
Element::Keyword(Keyword {
key: "K_E_Y",
optional: None,
value: "VALUE",
})
))
);
assert_eq!(
Keyword::parse("#+KEY:VALUE\n"),
Some(("KEY", None, "VALUE", "#+KEY:VALUE\n".len()))
Ok((
"",
Element::Keyword(Keyword {
key: "KEY",
optional: None,
value: "VALUE",
})
))
);
assert_eq!(Keyword::parse("#+KE Y: VALUE"), None);
assert_eq!(Keyword::parse("#+ KEY: VALUE"), None);
assert!(Keyword::parse("#+KE Y: VALUE").is_err());
assert!(Keyword::parse("#+ KEY: VALUE").is_err());
assert_eq!(
Keyword::parse("#+RESULTS:"),
Some(("RESULTS", None, "", "#+RESULTS:".len()))
Ok((
"",
Element::Keyword(Keyword {
key: "RESULTS",
optional: None,
value: "",
})
))
);
assert_eq!(
Keyword::parse("#+ATTR_LATEX: :width 5cm\n"),
Some((
"ATTR_LATEX",
None,
":width 5cm",
"#+ATTR_LATEX: :width 5cm\n".len()
Ok((
"",
Element::Keyword(Keyword {
key: "ATTR_LATEX",
optional: None,
value: ":width 5cm",
})
))
);
assert_eq!(
Keyword::parse("#+CALL: double(n=4)"),
Some(("CALL", None, "double(n=4)", "#+CALL: double(n=4)".len()))
Ok((
"",
Element::BabelCall(BabelCall {
value: "double(n=4)",
})
))
);
assert_eq!(
Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
Some((
"CAPTION",
Some("Short caption"),
"Longer caption.",
"#+CAPTION[Short caption]: Longer caption.".len()
Ok((
"",
Element::Keyword(Keyword {
key: "CAPTION",
optional: Some("Short caption"),
value: "Longer caption.",
})
))
);
}

View file

@ -1,5 +1,10 @@
use jetscii::Substring;
use memchr::memchr;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -12,35 +17,19 @@ pub struct Link<'a> {
impl Link<'_> {
#[inline]
// return (link, offset)
pub(crate) fn parse(text: &str) -> Option<(Link<'_>, usize)> {
debug_assert!(text.starts_with("[["));
let (path, off) = memchr(b']', text.as_bytes())
.map(|i| (&text["[[".len()..i], i))
.filter(|(path, _)| {
path.as_bytes()
.iter()
.all(|&c| c != b'<' && c != b'>' && c != b'\n')
})?;
if *text.as_bytes().get(off + 1)? == b']' {
Some((Link { path, desc: None }, off + 2))
} else if text.as_bytes()[off + 1] == b'[' {
let (desc, off) = Substring::new("]]")
.find(&text[off + 1..])
.map(|i| (&text[off + 2..off + 1 + i], off + 1 + i + "]]".len()))
.filter(|(desc, _)| desc.as_bytes().iter().all(|&c| c != b'[' && c != b']'))?;
Some((
Link {
path,
desc: Some(desc),
},
off,
))
} else {
None
}
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("[[")(input)?;
let (input, path) =
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']')(input)?;
let (input, _) = tag("]")(input)?;
let (input, desc) = opt(|input| {
let (input, _) = tag("[")(input)?;
let (input, desc) = take_while(|c: char| c != '[' && c != ']')(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, desc))
})(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, Element::Link(Link { path, desc })))
}
}
@ -48,23 +37,23 @@ impl Link<'_> {
fn parse() {
assert_eq!(
Link::parse("[[#id]]"),
Some((
Link {
Ok((
"",
Element::Link(Link {
path: "#id",
desc: None
},
"[[#id]]".len()
},)
))
);
assert_eq!(
Link::parse("[[#id][desc]]"),
Some((
Link {
Ok((
"",
Element::Link(Link {
path: "#id",
desc: Some("desc")
},
"[[#id][desc]]".len()
})
))
);
assert_eq!(Link::parse("[[#id][desc]"), None);
assert!(Link::parse("[[#id][desc]").is_err());
}

View file

@ -4,15 +4,16 @@ use std::iter::once;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
pub struct List {
pub struct List<'a> {
pub indent: usize,
pub ordered: bool,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl List {
impl List<'_> {
#[inline]
// return (list, begin, end)
pub(crate) fn parse(text: &str) -> Option<(List, usize, usize)> {
pub(crate) fn parse(text: &str) -> Option<(&str, List<'_>)> {
let (indent, tail) = text
.find(|c| c != ' ')
.map(|off| (off, &text[off..]))
@ -32,7 +33,14 @@ impl List {
if line_indent < indent
|| (line_indent == indent && is_item(&line[line_indent..]).is_none())
{
Some((List { indent, ordered }, pos, pos))
Some((
&text[pos..],
List {
indent,
ordered,
contents: &text[0..pos],
},
))
} else {
pos = i;
continue;
@ -44,20 +52,48 @@ impl List {
if line_indent < indent
|| (line_indent == indent && is_item(&line[line_indent..]).is_none())
{
Some((List { indent, ordered }, pos, pos))
Some((
&text[pos..],
List {
indent,
ordered,
contents: &text[0..pos],
},
))
} else {
pos = next_i;
continue;
}
} else {
Some((List { indent, ordered }, pos, next_i))
Some((
&text[next_i..],
List {
indent,
ordered,
contents: &text[0..pos],
},
))
}
} else {
Some((List { indent, ordered }, pos, i))
Some((
&text[i..],
List {
indent,
ordered,
contents: &text[0..pos],
},
))
};
}
Some((List { indent, ordered }, pos, pos))
Some((
&text[pos..],
List {
indent,
ordered,
contents: &text[0..pos],
},
))
}
}
@ -66,10 +102,12 @@ impl List {
#[derive(Debug)]
pub struct ListItem<'a> {
pub bullet: &'a str,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
pub contents: &'a str,
}
impl ListItem<'_> {
pub(crate) fn parse(text: &str, indent: usize) -> (ListItem<'_>, usize, usize) {
pub(crate) fn parse(text: &str, indent: usize) -> (&str, ListItem<'_>) {
debug_assert!(&text[0..indent].trim().is_empty());
let off = &text[indent..].find(' ').unwrap() + 1 + indent;
@ -84,11 +122,11 @@ impl ListItem<'_> {
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
if line_indent == indent {
return (
&text[pos..],
ListItem {
bullet: &text[indent..off],
contents: &text[off..pos],
},
off,
pos,
);
}
}
@ -96,11 +134,11 @@ impl ListItem<'_> {
}
(
"",
ListItem {
bullet: &text[indent..off],
contents: &text[off..],
},
off,
text.len(),
)
}
}
@ -116,7 +154,7 @@ pub fn is_item(text: &str) -> Option<bool> {
None
}
}
b'0'...b'9' => {
b'0'..=b'9' => {
let i = bytes
.iter()
.position(|&c| !c.is_ascii_digit())
@ -155,89 +193,89 @@ fn list_parse() {
assert_eq!(
List::parse("+ item1\n+ item2"),
Some((
"",
List {
indent: 0,
ordered: false,
contents: "+ item1\n+ item2"
},
"+ item1\n+ item2".len(),
"+ item1\n+ item2".len()
))
);
assert_eq!(
List::parse("* item1\n \n* item2"),
Some((
"",
List {
indent: 0,
ordered: false
ordered: false,
contents: "* item1\n \n* item2"
},
"* item1\n \n* item2".len(),
"* item1\n \n* item2".len()
))
);
assert_eq!(
List::parse("* item1\n \n \n* item2"),
Some((
"* item2",
List {
indent: 0,
ordered: false,
contents: "* item1\n"
},
"* item1\n".len(),
"* item1\n \n \n".len()
))
);
assert_eq!(
List::parse("* item1\n \n "),
Some((
"",
List {
indent: 0,
ordered: false,
contents: "* item1\n"
},
"+ item1\n".len(),
"* item1\n \n ".len()
))
);
assert_eq!(
List::parse("+ item1\n + item2\n "),
Some((
"",
List {
indent: 0,
ordered: false,
contents: "+ item1\n + item2\n"
},
"+ item1\n + item2\n".len(),
"+ item1\n + item2\n ".len()
))
);
assert_eq!(
List::parse("+ item1\n \n + item2\n \n+ item 3"),
Some((
"",
List {
indent: 0,
ordered: false,
contents: "+ item1\n \n + item2\n \n+ item 3"
},
"+ item1\n \n + item2\n \n+ item 3".len(),
"+ item1\n \n + item2\n \n+ item 3".len()
))
);
assert_eq!(
List::parse(" + item1\n \n + item2"),
Some((
"",
List {
indent: 2,
ordered: false,
contents: " + item1\n \n + item2"
},
" + item1\n \n + item2".len(),
" + item1\n \n + item2".len()
))
);
assert_eq!(
List::parse("+ 1\n\n - 2\n\n - 3\n\n+ 4"),
Some((
"",
List {
indent: 0,
ordered: false,
contents: "+ 1\n\n - 2\n\n - 3\n\n+ 4"
},
"+ 1\n\n - 2\n\n - 3\n\n+ 4".len(),
"+ 1\n\n - 2\n\n - 3\n\n+ 4".len()
))
);
}

View file

@ -1,5 +1,10 @@
use jetscii::Substring;
use memchr::memchr2;
use nom::{
bytes::complete::{tag, take, take_until, take_while1},
combinator::{opt, verify},
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -12,34 +17,21 @@ pub struct Macros<'a> {
impl Macros<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(Macros<'_>, usize)> {
debug_assert!(text.starts_with("{{{"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
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(|input| {
let (input, _) = tag("(")(input)?;
let (input, args) = take_until(")}}}")(input)?;
let (input, _) = take(1usize)(input)?;
Ok((input, args))
})(input)?;
let (input, _) = tag("}}}")(input)?;
let bytes = text.as_bytes();
if text.len() <= 3 || !bytes[3].is_ascii_alphabetic() {
return None;
}
let (name, off) = memchr2(b'}', b'(', bytes)
.filter(|&i| {
bytes[3..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
})
.map(|i| (&text[3..i], i))?;
let (arguments, off) = if bytes[off] == b'}' {
if text.len() <= off + 2 || bytes[off + 1] != b'}' || bytes[off + 2] != b'}' {
return None;
}
(None, off + "}}}".len())
} else {
Substring::new(")}}}")
.find(&text[off..])
.map(|i| (Some(&text[off + 1..off + i]), off + i + ")}}}".len()))?
};
Some((Macros { name, arguments }, off))
Ok((input, Element::Macros(Macros { name, arguments })))
}
}
@ -47,36 +39,36 @@ impl Macros<'_> {
fn parse() {
assert_eq!(
Macros::parse("{{{poem(red,blue)}}}"),
Some((
Macros {
Ok((
"",
Element::Macros(Macros {
name: "poem",
arguments: Some("red,blue")
},
"{{{poem(red,blue)}}}".len()
},)
))
);
assert_eq!(
Macros::parse("{{{poem())}}}"),
Some((
Macros {
Ok((
"",
Element::Macros(Macros {
name: "poem",
arguments: Some(")")
},
"{{{poem())}}}".len()
},)
))
);
assert_eq!(
Macros::parse("{{{author}}}"),
Some((
Macros {
Ok((
"",
Element::Macros(Macros {
name: "author",
arguments: None
},
"{{{author}}}".len()
},)
))
);
assert_eq!(Macros::parse("{{{0uthor}}}"), None);
assert_eq!(Macros::parse("{{{author}}"), None);
assert_eq!(Macros::parse("{{{poem(}}}"), None);
assert_eq!(Macros::parse("{{{poem)}}}"), 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

@ -44,11 +44,9 @@ pub use self::{
rule::Rule,
snippet::Snippet,
target::Target,
timestamp::*,
timestamp::{Date, Time, Timestamp},
};
use indextree::NodeId;
/// Org-mode element enum
///
/// Generally, each variant contains a element struct and
@ -56,318 +54,106 @@ use indextree::NodeId;
/// element in the original string.
///
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Element<'a> {
Block {
#[cfg_attr(feature = "serde", serde(flatten))]
block: Block<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
BabelCall {
#[cfg_attr(feature = "serde", serde(flatten))]
call: BabelCall<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Block(Block<'a>),
BabelCall(BabelCall<'a>),
Section {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
Clock {
#[cfg_attr(feature = "serde", serde(flatten))]
clock: Clock<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Cookie {
#[cfg_attr(feature = "serde", serde(flatten))]
cookie: Cookie<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
RadioTarget {
#[cfg_attr(feature = "serde", serde(flatten))]
radio_target: RadioTarget<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Drawer {
#[cfg_attr(feature = "serde", serde(flatten))]
drawer: Drawer<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
contents: &'a str,
},
Clock(Clock<'a>),
Cookie(Cookie<'a>),
RadioTarget(RadioTarget<'a>),
Drawer(Drawer<'a>),
Document {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
DynBlock {
#[cfg_attr(feature = "serde", serde(flatten))]
dyn_block: DynBlock<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
FnDef {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
fn_def: FnDef<'a>,
},
FnRef {
#[cfg_attr(feature = "serde", serde(flatten))]
fn_ref: FnRef<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Headline {
#[cfg_attr(feature = "serde", serde(flatten))]
headline: Headline<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
InlineCall {
#[cfg_attr(feature = "serde", serde(flatten))]
inline_call: InlineCall<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
InlineSrc {
#[cfg_attr(feature = "serde", serde(flatten))]
inline_src: InlineSrc<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Keyword {
#[cfg_attr(feature = "serde", serde(flatten))]
keyword: Keyword<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Link {
#[cfg_attr(feature = "serde", serde(flatten))]
link: Link<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
List {
#[cfg_attr(feature = "serde", serde(flatten))]
list: List,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
ListItem {
#[cfg_attr(feature = "serde", serde(flatten))]
list_item: ListItem<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
Macros {
#[cfg_attr(feature = "serde", serde(flatten))]
macros: Macros<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Planning {
#[cfg_attr(feature = "serde", serde(skip))]
deadline: Option<NodeId>,
#[cfg_attr(feature = "serde", serde(skip))]
scheduled: Option<NodeId>,
#[cfg_attr(feature = "serde", serde(skip))]
closed: Option<NodeId>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Snippet {
#[cfg_attr(feature = "serde", serde(flatten))]
snippet: Snippet<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
contents: &'a str,
},
DynBlock(DynBlock<'a>),
FnDef(FnDef<'a>),
FnRef(FnRef<'a>),
Headline(Headline<'a>),
InlineCall(InlineCall<'a>),
InlineSrc(InlineSrc<'a>),
Keyword(Keyword<'a>),
Link(Link<'a>),
List(List<'a>),
ListItem(ListItem<'a>),
Macros(Macros<'a>),
Planning(Planning<'a>),
Snippet(Snippet<'a>),
Text {
value: &'a str,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Paragraph {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
},
Rule {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Timestamp {
#[cfg_attr(feature = "serde", serde(flatten))]
timestamp: Timestamp<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
},
Target {
#[cfg_attr(feature = "serde", serde(flatten))]
target: Target<'a>,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
contents: &'a str,
},
Rule,
Timestamp(Timestamp<'a>),
Target(Target<'a>),
Bold {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
contents: &'a str,
},
Strike {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
contents: &'a str,
},
Italic {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
contents: &'a str,
},
Underline {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents_end: usize,
contents: &'a str,
},
Verbatim {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
value: &'a str,
},
Code {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
value: &'a str,
},
Comment {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
value: &'a str,
},
FixedWidth {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
begin: usize,
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
end: usize,
value: &'a str,
},
}
macro_rules! impl_from {
($ident:ident) => {
impl<'a> From<$ident<'a>> for Element<'a> {
fn from(ele: $ident<'a>) -> Element<'a> {
Element::$ident(ele)
}
}
};
}
impl_from!(Block);
impl_from!(BabelCall);
impl_from!(Clock);
impl_from!(Cookie);
impl_from!(RadioTarget);
impl_from!(Drawer);
impl_from!(DynBlock);
impl_from!(FnDef);
impl_from!(FnRef);
impl_from!(Headline);
impl_from!(InlineCall);
impl_from!(InlineSrc);
impl_from!(Keyword);
impl_from!(Link);
impl_from!(List);
impl_from!(ListItem);
impl_from!(Macros);
impl_from!(Planning);
impl_from!(Snippet);
impl_from!(Timestamp);
impl_from!(Target);

View file

@ -1,6 +1,7 @@
use crate::elements::Timestamp;
use memchr::memchr;
use crate::elements::Timestamp;
/// palnning elements
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -8,26 +9,18 @@ use memchr::memchr;
pub struct Planning<'a> {
/// the date when the task should be done
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub deadline: Option<&'a Timestamp<'a>>,
pub deadline: Option<Box<Timestamp<'a>>>,
/// the date when you should start working on the task
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub scheduled: Option<&'a Timestamp<'a>>,
pub scheduled: Option<Box<Timestamp<'a>>>,
/// the date when the task is closed
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub closed: Option<&'a Timestamp<'a>>,
pub closed: Option<Box<Timestamp<'a>>>,
}
impl Planning<'_> {
#[inline]
pub(crate) fn parse(
text: &str,
) -> Option<(
// TODO: timestamp position
Option<(Timestamp<'_>, usize, usize)>,
Option<(Timestamp<'_>, usize, usize)>,
Option<(Timestamp<'_>, usize, usize)>,
usize,
)> {
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))
@ -39,9 +32,11 @@ impl Planning<'_> {
macro_rules! set_timestamp {
($timestamp:expr) => {
if $timestamp.is_none() {
let (timestamp, off) = Timestamp::parse(next)?;
$timestamp = Some((timestamp, 0, 0));
tail = &next[off..].trim_start();
let (new_tail, timestamp) = Timestamp::parse_active(next)
.or_else(|_| Timestamp::parse_inactive(next))
.ok()?;
$timestamp = Some(Box::new(timestamp));
tail = new_tail.trim_start();
} else {
return None;
}
@ -59,34 +54,41 @@ impl Planning<'_> {
if deadline.is_none() && scheduled.is_none() && closed.is_none() {
None
} else {
Some((deadline, scheduled, closed, off))
Some((
&text[off..],
Planning {
deadline,
scheduled,
closed,
},
))
}
}
}
#[test]
fn prase() {
use crate::elements::Datetime;
use crate::elements::Date;
assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
Some((
None,
Some((
Timestamp::Active {
start: Datetime {
date: "2019-04-08",
time: None,
"",
Planning {
scheduled: Some(Box::new(Timestamp::Active {
start_date: Date {
year: 2019,
month: 4,
day: 8,
dayname: "Mon"
},
start_time: None,
repeater: None,
delay: None
},
0,
0
)),
None,
"SCHEDULED: <2019-04-08 Mon>\n".len()
})),
deadline: None,
closed: None,
}
))
)
}

View file

@ -1,32 +1,31 @@
use jetscii::Substring;
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
IResult,
};
use crate::elements::Element;
// TODO: text-markup, entities, latex-fragments, subscript and superscript
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
pub struct RadioTarget<'a> {
#[cfg_attr(all(feature = "serde", not(feature = "extra-serde-info")), serde(skip))]
contents: &'a str,
}
impl RadioTarget<'_> {
#[inline]
// return (radio_target, offset)
pub(crate) fn parse(src: &str) -> Option<(RadioTarget<'_>, usize)> {
debug_assert!(src.starts_with("<<<"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("<<<")(input)?;
let (input, contents) = verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
)(input)?;
let (input, _) = tag(">>>")(input)?;
let bytes = src.as_bytes();
let (contents, off) = Substring::new(">>>")
.find(src)
.filter(|&i| {
bytes[3] != b' '
&& bytes[i - 1] != b' '
&& bytes[3..i]
.iter()
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
})
.map(|i| (&src[3..i], i + ">>>".len()))?;
Some((RadioTarget { contents }, off))
Ok((input, Element::RadioTarget(RadioTarget { contents })))
}
}
@ -34,21 +33,21 @@ impl RadioTarget<'_> {
fn parse() {
assert_eq!(
RadioTarget::parse("<<<target>>>"),
Some((RadioTarget { contents: "target" }, "<<<target>>>".len()))
Ok(("", Element::RadioTarget(RadioTarget { contents: "target" })))
);
assert_eq!(
RadioTarget::parse("<<<tar get>>>"),
Some((
RadioTarget {
Ok((
"",
Element::RadioTarget(RadioTarget {
contents: "tar get"
},
"<<<tar get>>>".len()
},)
))
);
assert_eq!(RadioTarget::parse("<<<target >>>"), None);
assert_eq!(RadioTarget::parse("<<< target>>>"), None);
assert_eq!(RadioTarget::parse("<<<ta<get>>>"), None);
assert_eq!(RadioTarget::parse("<<<ta>get>>>"), None);
assert_eq!(RadioTarget::parse("<<<ta\nget>>>"), None);
assert_eq!(RadioTarget::parse("<<<target>>"), None);
assert!(RadioTarget::parse("<<<target >>>").is_err());
assert!(RadioTarget::parse("<<< target>>>").is_err());
assert!(RadioTarget::parse("<<<ta<get>>>").is_err());
assert!(RadioTarget::parse("<<<ta>get>>>").is_err());
assert!(RadioTarget::parse("<<<ta\nget>>>").is_err());
assert!(RadioTarget::parse("<<<target>>").is_err());
}

View file

@ -1,37 +1,43 @@
use nom::{
branch::alt,
bytes::complete::{tag, take, take_while_m_n},
character::complete::space0,
combinator::{map, not},
IResult,
};
use std::usize;
use crate::elements::Element;
pub struct Rule;
impl Rule {
#[inline]
// return offset
pub(crate) fn parse(text: &str) -> Option<usize> {
let (text, off) = memchr::memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len()));
if text.len() >= 5 && text.as_bytes().iter().all(|&c| c == b'-') {
Some(off)
} else {
None
}
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, _) = space0(input)?;
let (input, _) = alt((tag("\n"), map(not(take(1usize)), |_| "")))(input)?;
Ok((input, Element::Rule))
}
}
#[test]
fn parse() {
assert_eq!(Rule::parse("-----"), Some("-----".len()));
assert_eq!(Rule::parse("--------"), Some("--------".len()));
assert_eq!(Rule::parse(" -----"), Some(" -----".len()));
assert_eq!(Rule::parse("\t\t-----"), Some("\t\t-----".len()));
assert_eq!(Rule::parse("\t\t-----\n"), Some("\t\t-----\n".len()));
assert_eq!(Rule::parse("\t\t----- \n"), Some("\t\t----- \n".len()));
assert_eq!(Rule::parse(""), None);
assert_eq!(Rule::parse("----"), None);
assert_eq!(Rule::parse(" ----"), None);
assert_eq!(Rule::parse(" None----"), None);
assert_eq!(Rule::parse("None ----"), None);
assert_eq!(Rule::parse("None------"), None);
assert_eq!(Rule::parse("----None----"), None);
assert_eq!(Rule::parse("\t\t----"), None);
assert_eq!(Rule::parse("------None"), None);
assert_eq!(Rule::parse("----- None"), None);
assert_eq!(Rule::parse("-----"), Ok(("", Element::Rule)));
assert_eq!(Rule::parse("--------"), Ok(("", Element::Rule)));
assert_eq!(Rule::parse(" -----"), Ok(("", Element::Rule)));
assert_eq!(Rule::parse("\t\t-----"), Ok(("", Element::Rule)));
assert_eq!(Rule::parse("\t\t-----\n"), Ok(("", Element::Rule)));
assert_eq!(Rule::parse("\t\t----- \n"), Ok(("", Element::Rule)));
assert!(Rule::parse("").is_err());
assert!(Rule::parse("----").is_err());
assert!(Rule::parse(" ----").is_err());
assert!(Rule::parse(" None----").is_err());
assert!(Rule::parse("None ----").is_err());
assert!(Rule::parse("None------").is_err());
assert!(Rule::parse("----None----").is_err());
assert!(Rule::parse("\t\t----").is_err());
assert!(Rule::parse("------None").is_err());
assert!(Rule::parse("----- None").is_err());
}

View file

@ -1,5 +1,9 @@
use jetscii::Substring;
use memchr::memchr;
use nom::{
bytes::complete::{tag, take, take_until, take_while1},
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -11,24 +15,14 @@ pub struct Snippet<'a> {
impl Snippet<'_> {
#[inline]
// return (snippet offset)
pub(crate) fn parse(text: &str) -> Option<(Snippet<'_>, usize)> {
debug_assert!(text.starts_with("@@"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("@@")(input)?;
let (input, name) = take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-')(input)?;
let (input, _) = tag(":")(input)?;
let (input, value) = take_until("@@")(input)?;
let (input, _) = take(2usize)(input)?;
let (name, off) = memchr(b':', text.as_bytes())
.filter(|&i| {
i != 2
&& text.as_bytes()[2..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-')
})
.map(|i| (&text[2..i], i + 1))?;
let (value, off) = Substring::new("@@")
.find(&text[off..])
.map(|i| (&text[off..off + i], off + i + "@@".len()))?;
Some((Snippet { name, value }, off))
Ok((input, Element::Snippet(Snippet { name, value })))
}
}
@ -36,35 +30,45 @@ impl Snippet<'_> {
fn parse() {
assert_eq!(
Snippet::parse("@@html:<b>@@"),
Some((
Snippet {
Ok((
"",
Element::Snippet(Snippet {
name: "html",
value: "<b>"
},
"@@html:<b>@@".len()
},)
))
);
assert_eq!(
Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
Some((
Snippet {
Ok((
"",
Element::Snippet(Snippet {
name: "latex",
value: "any arbitrary LaTeX code",
},
"@@latex:any arbitrary LaTeX code@@".len()
},)
))
);
assert_eq!(
Snippet::parse("@@html:@@"),
Some((
Snippet {
Ok((
"",
Element::Snippet(Snippet {
name: "html",
value: "",
},
"@@html:@@".len()
},)
))
);
assert_eq!(Snippet::parse("@@html:<b>@"), None);
assert_eq!(Snippet::parse("@@html<b>@@"), None);
assert_eq!(Snippet::parse("@@:<b>@@"), None);
assert_eq!(
Snippet::parse("@@html:<p>@</p>@@"),
Ok((
"",
Element::Snippet(Snippet {
name: "html",
value: "<p>@</p>",
},)
))
);
assert!(Snippet::parse("@@html:<b>@").is_err());
assert!(Snippet::parse("@@html<b>@@").is_err());
assert!(Snippet::parse("@@:<b>@@").is_err());
}

View file

@ -1,4 +1,10 @@
use jetscii::Substring;
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
IResult,
};
use crate::elements::Element;
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@ -9,29 +15,15 @@ pub struct Target<'a> {
impl Target<'_> {
#[inline]
// return (target, offset)
pub(crate) fn parse(text: &str) -> Option<(Target<'_>, usize)> {
debug_assert!(text.starts_with("<<"));
pub(crate) fn parse(input: &str) -> IResult<&str, Element<'_>> {
let (input, _) = tag("<<")(input)?;
let (input, target) = verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
)(input)?;
let (input, _) = tag(">>")(input)?;
let bytes = text.as_bytes();
Substring::new(">>")
.find(text)
.filter(|&i| {
bytes[2] != b' '
&& bytes[i - 1] != b' '
&& bytes[2..i]
.iter()
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
})
.map(|i| {
(
Target {
target: &text[2..i],
},
i + ">>".len(),
)
})
Ok((input, Element::Target(Target { target })))
}
}
@ -39,16 +31,16 @@ impl Target<'_> {
fn parse() {
assert_eq!(
Target::parse("<<target>>"),
Some((Target { target: "target" }, "<<target>>".len()))
Ok(("", Element::Target(Target { target: "target" })))
);
assert_eq!(
Target::parse("<<tar get>>"),
Some((Target { target: "tar get" }, "<<tar get>>".len()))
Ok(("", Element::Target(Target { target: "tar get" })))
);
assert_eq!(Target::parse("<<target >>"), None);
assert_eq!(Target::parse("<< target>>"), None);
assert_eq!(Target::parse("<<ta<get>>"), None);
assert_eq!(Target::parse("<<ta>get>>"), None);
assert_eq!(Target::parse("<<ta\nget>>"), None);
assert_eq!(Target::parse("<<target>"), None);
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,542 +1,366 @@
#[cfg(feature = "chrono")]
use chrono::*;
use memchr::memchr;
use std::str::FromStr;
use nom::{
bytes::complete::{tag, take, take_till, take_while, take_while_m_n},
character::complete::{space0, space1},
combinator::{map_res, opt},
IResult,
};
/// Date
///
/// # Syntax
///
/// ```text
/// YYYY-MM-DD DAYNAME
/// ```
///
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy)]
pub struct Date<'a> {
pub year: u16,
pub month: u8,
pub day: u8,
pub dayname: &'a str,
}
impl Date<'_> {
fn parse(input: &str) -> IResult<&str, Date<'_>> {
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), |num| u8::from_str_radix(num, 10))(input)?;
let (input, _) = tag("-")(input)?;
let (input, day) = map_res(take(2usize), |num| u8::from_str_radix(num, 10))(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)?;
Ok((
input,
Date {
year,
month,
day,
dayname,
},
))
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy)]
pub struct Datetime<'a> {
pub(crate) date: &'a str,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub(crate) time: Option<&'a str>,
pub(crate) dayname: &'a str,
pub struct Time {
pub hour: u8,
pub minute: u8,
}
impl Datetime<'_> {
pub fn year(&self) -> u32 {
u32::from_str(&self.date[0..4]).unwrap()
}
impl Time {
fn parse(input: &str) -> IResult<&str, Time> {
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)?;
pub fn month(&self) -> u32 {
u32::from_str(&self.date[5..7]).unwrap()
}
pub fn day(&self) -> u32 {
u32::from_str(&self.date[8..10]).unwrap()
}
pub fn hour(&self) -> Option<u32> {
self.time.map(|time| {
if time.len() == 4 {
u32::from_str(&time[0..1]).unwrap()
} else {
u32::from_str(&time[0..2]).unwrap()
}
})
}
pub fn minute(&self) -> Option<u32> {
self.time.map(|time| {
if time.len() == 4 {
u32::from_str(&time[2..4]).unwrap()
} else {
u32::from_str(&time[3..5]).unwrap()
}
})
}
pub fn dayname(&self) -> &str {
self.dayname
}
#[cfg(feature = "chrono")]
pub fn naive_date(&self) -> NaiveDate {
NaiveDate::from_ymd(self.year() as i32, self.month(), self.day())
}
#[cfg(feature = "chrono")]
pub fn naive_time(&self) -> NaiveTime {
NaiveTime::from_hms(self.hour().unwrap_or(0), self.minute().unwrap_or(0), 0)
}
#[cfg(feature = "chrono")]
pub fn naive_date_time(&self) -> NaiveDateTime {
NaiveDateTime::new(self.naive_date(), self.naive_time())
}
#[cfg(feature = "chrono")]
pub fn date_time<Tz: TimeZone>(&self, offset: Tz::Offset) -> DateTime<Tz> {
DateTime::from_utc(self.naive_date_time(), offset)
}
#[cfg(feature = "chrono")]
pub fn date<Tz: TimeZone>(&self, offset: Tz::Offset) -> Date<Tz> {
Date::from_utc(self.naive_date(), offset)
Ok((input, Time { hour, minute }))
}
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Copy, Clone)]
pub enum RepeaterType {
Cumulate,
CatchUp,
Restart,
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Copy, Clone)]
pub enum DelayType {
All,
First,
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Copy, Clone)]
pub enum TimeUnit {
Hour,
Day,
Week,
Month,
Year,
}
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", 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 = "serde", derive(serde::Serialize))]
#[derive(Debug, Copy, Clone)]
pub struct Delay {
pub ty: DelayType,
pub value: usize,
pub unit: TimeUnit,
}
/// timestamp obejcts
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug)]
pub enum Timestamp<'a> {
Active {
start: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
},
Inactive {
start: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
},
ActiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
end_date: Date<'a>,
end_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
},
InactiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
start_date: Date<'a>,
start_time: Option<Time>,
end_date: Date<'a>,
end_time: Option<Time>,
repeater: Option<&'a str>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<&'a str>,
},
Diary(&'a str),
}
impl Timestamp<'_> {
pub(crate) fn parse(text: &str) -> Option<(Timestamp<'_>, usize)> {
if text.starts_with('<') {
Timestamp::parse_active(text).or_else(|| Timestamp::parse_diary(text))
} else if text.starts_with('[') {
Timestamp::parse_inactive(text)
} else {
None
}
}
pub(crate) fn parse_active(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("<")(input)?;
let (input, start_date) = Date::parse(input)?;
let (input, _) = space0(input)?;
let (input, start_time) = opt(Time::parse)(input)?;
pub(crate) fn parse_active(text: &str) -> Option<(Timestamp<'_>, usize)> {
debug_assert!(text.starts_with('<'));
let bytes = text.as_bytes();
let mut off = memchr(b'>', bytes)?;
let (start, mut end) = Self::parse_datetime(&text[1..off])?;
if end.is_none()
&& off + "--<YYYY-MM-DD >".len() <= text.len()
&& text[off + 1..].starts_with("--<")
{
if let Some(new_off) = memchr(b'>', &bytes[off + 1..]) {
if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) {
end = Some(start);
off += new_off + 1;
}
}
}
Some((
if let Some(end) = end {
if input.starts_with('-') {
let (input, end_time) = opt(Time::parse)(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
return Ok((
input,
Timestamp::ActiveRange {
start,
end,
start_date,
start_time,
end_date: start_date,
end_time,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
if input.starts_with("--<") {
let (input, end_date) = Date::parse(&input["--<".len()..])?;
let (input, _) = space0(input)?;
let (input, end_time) = opt(Time::parse)(input)?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
Ok((
input,
Timestamp::ActiveRange {
start_date,
start_time,
end_date,
end_time,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Active {
start,
start_date,
start_time,
repeater: None,
delay: None,
}
},
off + 1,
))
}
pub(crate) fn parse_inactive(text: &str) -> Option<(Timestamp<'_>, usize)> {
debug_assert!(text.starts_with('['));
let bytes = text.as_bytes();
let mut off = memchr(b']', bytes)?;
let (start, mut end) = Self::parse_datetime(&text[1..off])?;
if end.is_none()
&& off + "--[YYYY-MM-DD ]".len() <= text.len()
&& text[off + 1..].starts_with("--[")
{
if let Some(new_off) = memchr(b']', &bytes[off + 1..]) {
if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) {
end = Some(start);
off += new_off + 1;
}
}
}
Some((
if let Some(end) = end {
pub(crate) fn parse_inactive(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("[")(input)?;
let (input, start_date) = Date::parse(input)?;
let (input, _) = space0(input)?;
let (input, start_time) = opt(Time::parse)(input)?;
if input.starts_with('-') {
let (input, end_time) = opt(Time::parse)(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
return Ok((
input,
Timestamp::InactiveRange {
start,
end,
start_date,
start_time,
end_date: start_date,
end_time,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
if input.starts_with("--[") {
let (input, end_date) = Date::parse(&input["--[".len()..])?;
let (input, _) = space0(input)?;
let (input, end_time) = opt(Time::parse)(input)?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
Ok((
input,
Timestamp::InactiveRange {
start_date,
start_time,
end_date,
end_time,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Inactive {
start_date,
start_time,
repeater: None,
delay: None,
}
} else {
Timestamp::Inactive {
start,
repeater: None,
delay: None,
}
},
off + 1,
))
}
fn parse_datetime(text: &str) -> Option<(Datetime<'_>, Option<Datetime<'_>>)> {
if text.is_empty()
|| !text.starts_with(|c: char| c.is_ascii_digit())
|| !text.ends_with(|c: char| c.is_ascii_alphanumeric())
{
return None;
}
let mut words = text.split_ascii_whitespace();
pub(crate) fn parse_diary(input: &str) -> IResult<&str, Timestamp<'_>> {
let (input, _) = tag("<%%(")(input)?;
let (input, sexp) = take_till(|c| c == ')' || c == '>' || c == '\n')(input)?;
let (input, _) = tag(")>")(input)?;
let date = words.next().filter(|word| {
let word = word.as_bytes();
// YYYY-MM-DD
word.len() == 10
&& word[0..4].iter().all(u8::is_ascii_digit)
&& word[4] == b'-'
&& word[5..7].iter().all(u8::is_ascii_digit)
&& word[7] == b'-'
&& word[8..10].iter().all(u8::is_ascii_digit)
})?;
let dayname = words.next().filter(|word| {
word.as_bytes().iter().all(|&c| {
!(c == b'+'
|| c == b'-'
|| c == b']'
|| c == b'>'
|| c.is_ascii_digit()
|| c == b'\n')
})
})?;
let (start, end) = if let Some(word) = words.next() {
let time = word.as_bytes();
if (time.len() == "H:MM".len()
&& time[0].is_ascii_digit()
&& time[1] == b':'
&& time[2..4].iter().all(u8::is_ascii_digit))
|| (time.len() == "HH:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit))
{
(
Datetime {
date,
dayname,
time: Some(word),
},
None,
)
} else if time.len() == "H:MM-H:MM".len()
&& time[0].is_ascii_digit()
&& time[1] == b':'
&& time[2..4].iter().all(u8::is_ascii_digit)
&& time[4] == b'-'
&& time[5].is_ascii_digit()
&& time[6] == b':'
&& time[7..9].iter().all(u8::is_ascii_digit)
{
(
Datetime {
date,
dayname,
time: Some(&word[0..4]),
},
Some(Datetime {
date,
dayname,
time: Some(&word[5..9]),
}),
)
} else if time.len() == "H:MM-HH:MM".len()
&& time[0].is_ascii_digit()
&& time[1] == b':'
&& time[2..4].iter().all(u8::is_ascii_digit)
&& time[4] == b'-'
&& time[5..7].iter().all(u8::is_ascii_digit)
&& time[7] == b':'
&& time[8..10].iter().all(u8::is_ascii_digit)
{
(
Datetime {
date,
dayname,
time: Some(&word[0..4]),
},
Some(Datetime {
date,
dayname,
time: Some(&word[5..10]),
}),
)
} else if time.len() == "HH:MM-H:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit)
&& time[5] == b'-'
&& time[6].is_ascii_digit()
&& time[7] == b':'
&& time[8..10].iter().all(u8::is_ascii_digit)
{
(
Datetime {
date,
dayname,
time: Some(&word[0..5]),
},
Some(Datetime {
date,
dayname,
time: Some(&word[6..10]),
}),
)
} else if time.len() == "HH:MM-HH:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit)
&& time[5] == b'-'
&& time[6..8].iter().all(u8::is_ascii_digit)
&& time[8] == b':'
&& time[9..11].iter().all(u8::is_ascii_digit)
{
(
Datetime {
date,
dayname,
time: Some(&word[0..5]),
},
Some(Datetime {
date,
dayname,
time: Some(&word[6..11]),
}),
)
} else {
return None;
}
} else {
(
Datetime {
date,
dayname,
time: None,
},
None,
)
};
// TODO: repeater and delay
if words.next().is_some() {
None
} else {
Some((start, end))
}
}
pub(crate) fn parse_diary(text: &str) -> Option<(Timestamp<'_>, usize)> {
debug_assert!(text.starts_with('<'));
if text.len() <= "<%%()>".len() || &text[1..4] != "%%(" {
return None;
}
let bytes = text.as_bytes();
memchr(b'>', bytes)
.filter(|i| {
bytes[i - 1] == b')' && bytes["<%%(".len()..i - 1].iter().all(|&c| c != b'\n')
})
.map(|i| (Timestamp::Diary(&text["<%%(".len()..i - 1]), i))
Ok((input, Timestamp::Diary(sexp)))
}
}
#[test]
fn parse_range() {
use super::*;
// TODO
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum RepeaterType {
// Cumulate,
// CatchUp,
// Restart,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum DelayType {
// All,
// First,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum TimeUnit {
// Hour,
// Day,
// Week,
// Month,
// Year,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "serde", 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 = "serde", 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]"),
Some((
Ok((
"",
Timestamp::Inactive {
start: Datetime {
date: "2003-09-16",
time: None,
start_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
start_time: None,
repeater: None,
delay: None,
},
"[2003-09-16 Tue]".len()
))
);
assert_eq!(
Timestamp::parse_inactive("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]"),
Some((
Ok((
"",
Timestamp::InactiveRange {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
start_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end: Datetime {
date: "2003-09-16",
time: Some("10:39"),
start_time: Some(Time {
hour: 9,
minute: 39
}),
end_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end_time: Some(Time {
hour: 10,
minute: 39
}),
repeater: None,
delay: None
},
"[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]".len()
))
);
assert_eq!(
Timestamp::parse_active("<2003-09-16 Tue 09:39-10:39>"),
Some((
Ok((
"",
Timestamp::ActiveRange {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
start_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end: Datetime {
date: "2003-09-16",
time: Some("10:39"),
start_time: Some(Time {
hour: 9,
minute: 39
}),
end_date: Date {
year: 2003,
month: 9,
day: 16,
dayname: "Tue"
},
end_time: Some(Time {
hour: 10,
minute: 39
}),
repeater: None,
delay: None
},
"<2003-09-16 Tue 09:39-10:39>".len()
))
);
}
#[test]
fn parse_datetime() {
use super::*;
assert_eq!(
Timestamp::parse_datetime("2003-09-16 Tue"),
Some((
Datetime {
date: "2003-09-16",
time: None,
dayname: "Tue"
},
None
))
);
assert_eq!(
Timestamp::parse_datetime("2003-09-16 Tue 9:39"),
Some((
Datetime {
date: "2003-09-16",
time: Some("9:39"),
dayname: "Tue"
},
None
))
);
assert_eq!(
Timestamp::parse_datetime("2003-09-16 Tue 09:39"),
Some((
Datetime {
date: "2003-09-16",
time: Some("09:39"),
dayname: "Tue"
},
None
))
);
assert_eq!(
Timestamp::parse_datetime("2003-09-16 Tue 9:39-10:39"),
Some((
Datetime {
date: "2003-09-16",
time: Some("9:39"),
dayname: "Tue"
},
Some(Datetime {
date: "2003-09-16",
time: Some("10:39"),
dayname: "Tue"
}),
))
);
assert_eq!(Timestamp::parse_datetime("2003-9-16 Tue"), None);
assert_eq!(Timestamp::parse_datetime("2003-09-16"), None);
assert_eq!(Timestamp::parse_datetime("2003-09-16 09:39"), None);
assert_eq!(Timestamp::parse_datetime("2003-09-16 Tue 0939"), None);
}

View file

@ -34,11 +34,11 @@ pub trait HtmlHandler<E: From<Error>> {
match element {
// container elements
Block { .. } => write!(w, "<div>")?,
Block(_block) => write!(w, "<div>")?,
Bold { .. } => write!(w, "<b>")?,
Document { .. } => write!(w, "<main>")?,
DynBlock { .. } => (),
Headline { headline, .. } => {
DynBlock(_dyn_block) => (),
Headline(headline) => {
let level = if headline.level <= 6 {
headline.level
} else {
@ -46,7 +46,7 @@ pub trait HtmlHandler<E: From<Error>> {
};
write!(w, "<h{0}>{1}</h{0}>", level, Escape(headline.title))?;
}
List { list, .. } => {
List(list) => {
if list.ordered {
write!(w, "<ol>")?;
} else {
@ -60,37 +60,37 @@ pub trait HtmlHandler<E: From<Error>> {
Strike { .. } => write!(w, "<s>")?,
Underline { .. } => write!(w, "<u>")?,
// non-container elements
BabelCall { .. } => (),
InlineSrc { inline_src, .. } => write!(w, "<code>{}</code>", Escape(inline_src.body))?,
Code { value, .. } => write!(w, "<code>{}</code>", Escape(value))?,
FnRef { .. } => (),
InlineCall { .. } => (),
Link { link, .. } => write!(
BabelCall(_babel_call) => (),
InlineSrc(inline_src) => write!(w, "<code>{}</code>", Escape(inline_src.body))?,
Code { value } => write!(w, "<code>{}</code>", Escape(value))?,
FnRef(_fn_ref) => (),
InlineCall(_inline_call) => (),
Link(link) => write!(
w,
"<a href=\"{}\">{}</a>",
Escape(link.path),
Escape(link.desc.unwrap_or(link.path)),
)?,
Macros { .. } => (),
Planning { .. } => (),
RadioTarget { .. } => (),
Snippet { snippet, .. } => {
Macros(_macros) => (),
Planning(_planning) => (),
RadioTarget(_radio_target) => (),
Snippet(snippet) => {
if snippet.name.eq_ignore_ascii_case("HTML") {
write!(w, "{}", snippet.value)?;
}
}
Target { .. } => (),
Text { value, .. } => write!(w, "{}", Escape(value))?,
Timestamp { .. } => (),
Verbatim { value, .. } => write!(&mut w, "<code>{}</code>", Escape(value))?,
FnDef { .. } => (),
Clock { .. } => (),
Comment { value, .. } => write!(w, "<!--\n{}\n-->", Escape(value))?,
FixedWidth { value, .. } => write!(w, "<pre>{}</pre>", Escape(value))?,
Keyword { .. } => (),
Drawer { .. } => (),
Rule { .. } => write!(w, "<hr>")?,
Cookie { .. } => (),
Target(_target) => (),
Text { value } => write!(w, "{}", Escape(value))?,
Timestamp(_timestamp) => (),
Verbatim { value } => write!(&mut w, "<code>{}</code>", Escape(value))?,
FnDef(_fn_def) => (),
Clock(_clock) => (),
Comment { value } => write!(w, "<!--\n{}\n-->", Escape(value))?,
FixedWidth { value } => write!(w, "<pre>{}</pre>", Escape(value))?,
Keyword(_keyword) => (),
Drawer(_drawer) => (),
Rule => write!(w, "<hr>")?,
Cookie(_cookie) => (),
}
Ok(())
@ -100,12 +100,12 @@ pub trait HtmlHandler<E: From<Error>> {
match element {
// container elements
Block { .. } => write!(w, "</div>")?,
Block(_block) => write!(w, "</div>")?,
Bold { .. } => write!(w, "</b>")?,
Document { .. } => write!(w, "</main>")?,
DynBlock { .. } => (),
Headline { .. } => (),
List { list, .. } => {
DynBlock(_dyn_block) => (),
Headline(_headline) => (),
List(list) => {
if list.ordered {
write!(w, "</ol>")?;
} else {

View file

@ -54,17 +54,17 @@
//!
//! # Render html
//!
//! You can call the [`Org::html_default`] function to generate html directly, which
//! You can call the [`Org::html`] function to generate html directly, which
//! uses the [`DefaultHtmlHandler`] internally:
//!
//! [`Org::html_default`]: org/struct.Org.html#method.html_default
//! [`Org::html`]: org/struct.Org.html#method.html
//! [`DefaultHtmlHandler`]: export/html/struct.DefaultHtmlHandler.html
//!
//! ```rust
//! use orgize::Org;
//!
//! let mut writer = Vec::new();
//! Org::parse("* title\n*section*").html_default(&mut writer).unwrap();
//! Org::parse("* title\n*section*").html(&mut writer).unwrap();
//!
//! assert_eq!(
//! String::from_utf8(writer).unwrap(),
@ -75,10 +75,10 @@
//! # Render html with custom HtmlHandler
//!
//! To customize html rendering, simply implementing [`HtmlHandler`] trait and passing
//! it to the [`Org::html`] function.
//! it to the [`Org::html_with_handler`] function.
//!
//! [`HtmlHandler`]: export/html/trait.HtmlHandler.html
//! [`Org::html`]: org/struct.Org.html#method.html
//! [`Org::html_with_handler`]: org/struct.Org.html#method.html_with_handler
//!
//! The following code demonstrates how to add a id for every headline and return
//! own error type while rendering.
@ -118,7 +118,7 @@
//! fn start<W: Write>(&mut self, mut w: W, element: &Element<'_>) -> Result<(), MyError> {
//! let mut default_handler = DefaultHtmlHandler;
//! match element {
//! Element::Headline { headline, .. } => {
//! Element::Headline(headline) => {
//! if headline.level > 6 {
//! return Err(MyError::Heading);
//! } else {
@ -141,7 +141,7 @@
//!
//! fn main() -> Result<(), MyError> {
//! let mut writer = Vec::new();
//! Org::parse("* title\n*section*").html(&mut writer, MyHtmlHandler)?;
//! Org::parse("* title\n*section*").html_with_handler(&mut writer, MyHtmlHandler)?;
//!
//! assert_eq!(
//! String::from_utf8(writer)?,
@ -209,6 +209,8 @@
//!
//! MIT
#![allow(clippy::range_plus_one)]
pub mod config;
pub mod elements;
pub mod export;

View file

@ -11,44 +11,18 @@ use crate::iter::Iter;
pub struct Org<'a> {
pub(crate) arena: Arena<Element<'a>>,
pub(crate) document: NodeId,
text: &'a str,
}
impl<'a> Org<'a> {
pub fn parse(text: &'a str) -> Self {
let mut arena = Arena::new();
let document = arena.new_node(Element::Document {
begin: 0,
end: text.len(),
contents_begin: 0,
contents_end: text.len(),
});
let mut org = Org {
arena,
document,
text,
};
org.parse_internal(ParseConfig::default());
org
Org::parse_with_config(text, ParseConfig::default())
}
pub fn parse_with_config(text: &'a str, config: ParseConfig<'_>) -> Self {
let mut arena = Arena::new();
let document = arena.new_node(Element::Document {
begin: 0,
end: text.len(),
contents_begin: 0,
contents_end: text.len(),
});
let document = arena.new_node(Element::Document { contents: text });
let mut org = Org {
arena,
document,
text,
};
let mut org = Org { arena, document };
org.parse_internal(config);
org
@ -61,7 +35,11 @@ impl<'a> Org<'a> {
}
}
pub fn html<W, H, E>(&self, mut writer: W, mut handler: H) -> Result<(), E>
pub fn html<W: Write>(&self, wrtier: W) -> Result<(), Error> {
self.html_with_handler(wrtier, DefaultHtmlHandler)
}
pub fn html_with_handler<W, H, E>(&self, mut writer: W, mut handler: H) -> Result<(), E>
where
W: Write,
E: From<Error>,
@ -79,147 +57,54 @@ impl<'a> Org<'a> {
Ok(())
}
pub fn html_default<W: Write>(&self, wrtier: W) -> Result<(), Error> {
self.html(wrtier, DefaultHtmlHandler)
}
fn parse_internal(&mut self, config: ParseConfig<'_>) {
let mut node = self.document;
loop {
match self.arena[node].data {
Element::Document {
contents_begin: begin,
contents_end: end,
..
}
| Element::Headline {
contents_begin: begin,
contents_end: end,
..
} => {
let mut begin = begin;
if begin < end {
let off = Headline::find_level(&self.text[begin..end], std::usize::MAX);
Element::Document { mut contents }
| Element::Headline(Headline { mut contents, .. }) => {
if !contents.is_empty() {
let off = Headline::find_level(contents, std::usize::MAX);
if off != 0 {
let section = Element::Section {
begin,
end: begin + off,
contents_begin: begin,
contents_end: begin + off,
contents: &contents[0..off],
};
let new_node = self.arena.new_node(section);
node.append(new_node, &mut self.arena).unwrap();
begin += off;
contents = &contents[off..];
}
}
while begin < end {
let (headline, off, end) = Headline::parse(&self.text[begin..end], &config);
let headline = Element::Headline {
headline,
begin,
end: begin + end,
contents_begin: begin + off,
contents_end: begin + end,
};
while !contents.is_empty() {
let (tail, headline) = Headline::parse(contents, &config);
let headline = Element::Headline(headline);
let new_node = self.arena.new_node(headline);
node.append(new_node, &mut self.arena).unwrap();
begin += end;
contents = tail;
}
}
Element::Section {
contents_begin,
contents_end,
..
} => {
let (mut deadline_node, mut scheduled_node, mut closed_node) =
(None, None, None);
if let Some((deadline, scheduled, closed, off)) =
Planning::parse(&self.text[contents_begin..contents_end])
{
if let Some((deadline, off, end)) = deadline {
let timestamp = Element::Timestamp {
timestamp: deadline,
begin: contents_begin + off,
end: contents_end + end,
};
deadline_node = Some(self.arena.new_node(timestamp));
}
if let Some((scheduled, off, end)) = scheduled {
let timestamp = Element::Timestamp {
timestamp: scheduled,
begin: contents_begin + off,
end: contents_end + end,
};
scheduled_node = Some(self.arena.new_node(timestamp));
}
if let Some((closed, off, end)) = closed {
let timestamp = Element::Timestamp {
timestamp: closed,
begin: contents_begin + off,
end: contents_end + end,
};
closed_node = Some(self.arena.new_node(timestamp));
}
let planning = Element::Planning {
deadline: deadline_node,
scheduled: scheduled_node,
closed: closed_node,
begin: contents_begin,
end: contents_begin + off,
};
let new_node = self.arena.new_node(planning);
node.append(new_node, &mut self.arena).unwrap();
self.parse_elements_children(contents_begin + off, contents_end, node);
Element::Section { contents } => {
// TODO
if let Some((tail, _planning)) = Planning::parse(contents) {
self.parse_elements_children(tail, node);
} else {
self.parse_elements_children(contents_begin, contents_end, node);
self.parse_elements_children(contents, node);
}
}
Element::Block {
contents_begin,
contents_end,
..
Element::Block(Block { contents, .. })
| Element::ListItem(ListItem { contents, .. }) => {
self.parse_elements_children(contents, node);
}
| Element::ListItem {
contents_begin,
contents_end,
..
} => {
self.parse_elements_children(contents_begin, contents_end, node);
Element::Paragraph { contents }
| Element::Bold { contents }
| Element::Underline { contents }
| Element::Italic { contents }
| Element::Strike { contents } => {
self.parse_objects_children(contents, node);
}
Element::Paragraph {
contents_begin,
contents_end,
..
}
| Element::Bold {
contents_begin,
contents_end,
..
}
| Element::Underline {
contents_begin,
contents_end,
..
}
| Element::Italic {
contents_begin,
contents_end,
..
}
| Element::Strike {
contents_begin,
contents_end,
..
} => {
self.parse_objects_children(contents_begin, contents_end, node);
}
Element::List {
list: List { indent, .. },
contents_begin,
contents_end,
..
} => {
self.parse_list_items(contents_begin, contents_end, indent, node);
Element::List(List {
contents, indent, ..
}) => {
self.parse_list_items(contents, indent, node);
}
_ => (),
}
@ -248,560 +133,316 @@ impl<'a> Org<'a> {
}
}
fn parse_elements_children(&mut self, begin: usize, end: usize, node: NodeId) {
let text = &self.text[begin..end];
let mut pos = skip_empty_lines(text);
fn parse_elements_children(&mut self, input: &'a str, node: NodeId) {
let mut tail = skip_empty_lines(input);
if let Some((ty, off)) = self.parse_element(begin + pos, end) {
let new_node = self.arena.new_node(ty);
if let Some((new_tail, element)) = self.parse_element(input) {
let new_node = self.arena.new_node(element);
node.append(new_node, &mut self.arena).unwrap();
pos += off + skip_empty_lines(&text[off..]);
tail = skip_empty_lines(new_tail);
}
let mut last_end = pos;
let mut text = tail;
let mut pos = 0;
while pos < text.len() {
let i = memchr(b'\n', &text.as_bytes()[pos..]).unwrap_or(text.len() - pos);
if text.as_bytes()[pos..pos + i]
.iter()
.all(u8::is_ascii_whitespace)
{
let end = skip_empty_lines(&text[pos + i..]);
while !tail.is_empty() {
let i = memchr(b'\n', tail.as_bytes())
.map(|i| i + 1)
.unwrap_or_else(|| tail.len());
if tail.as_bytes()[0..i].iter().all(u8::is_ascii_whitespace) {
tail = skip_empty_lines(&tail[i..]);
let new_node = self.arena.new_node(Element::Paragraph {
begin: begin + last_end,
end: begin + pos + i + end,
contents_begin: begin + last_end,
contents_end: begin
+ if text.as_bytes()[pos - 1] == b'\n' {
pos - 1
contents: if text.as_bytes()[pos - 1] == b'\n' {
&text[0..pos - 1]
} else {
pos
&text[0..pos]
},
});
node.append(new_node, &mut self.arena).unwrap();
pos += i + end;
last_end = pos;
} else if let Some((ty, off)) = self.parse_element(begin + pos, end) {
if last_end != pos {
text = tail;
pos = 0;
} else if let Some((new_tail, element)) = self.parse_element(tail) {
if pos != 0 {
let new_node = self.arena.new_node(Element::Paragraph {
begin: begin + last_end,
end: begin + pos,
contents_begin: begin + last_end,
contents_end: begin
+ if text.as_bytes()[pos - 1] == b'\n' {
pos - 1
contents: if text.as_bytes()[pos - 1] == b'\n' {
&text[0..pos - 1]
} else {
pos
&text[0..pos]
},
});
node.append(new_node, &mut self.arena).unwrap();
pos = 0;
}
let new_node = self.arena.new_node(element);
node.append(new_node, &mut self.arena).unwrap();
tail = skip_empty_lines(new_tail);
text = tail;
} else {
tail = &tail[i..];
pos += i;
}
}
if !text.is_empty() {
let new_node = self.arena.new_node(Element::Paragraph {
contents: if text.as_bytes()[pos - 1] == b'\n' {
&text[0..pos - 1]
} else {
&text[0..pos]
},
});
node.append(new_node, &mut self.arena).unwrap();
}
let new_node = self.arena.new_node(ty);
node.append(new_node, &mut self.arena).unwrap();
pos += off + skip_empty_lines(&text[pos + off..]);
last_end = pos;
} else {
pos += i + 1;
}
}
if begin + last_end < end {
let new_node = self.arena.new_node(Element::Paragraph {
begin: begin + last_end,
end,
contents_begin: begin + last_end,
contents_end: if text.ends_with('\n') { end - 1 } else { end },
});
node.append(new_node, &mut self.arena).unwrap();
}
fn parse_element(&self, contents: &'a str) -> Option<(&'a str, Element<'a>)> {
if let Some((tail, fn_def)) = FnDef::parse(contents) {
let fn_def = Element::FnDef(fn_def);
return Some((tail, fn_def));
} else if let Some((tail, list)) = List::parse(contents) {
let list = Element::List(list);
return Some((tail, list));
}
fn parse_element(&self, begin: usize, end: usize) -> Option<(Element<'a>, usize)> {
let text = &self.text[begin..end];
let tail = contents.trim_start();
if let Some((fn_def, off, end)) = FnDef::parse(text) {
let fn_def = Element::FnDef {
begin,
end: begin + end,
contents_begin: begin + off,
contents_end: begin + end,
fn_def,
};
return Some((fn_def, end));
} else if let Some((list, limit, end)) = List::parse(text) {
let list = Element::List {
list,
begin,
end: begin + end,
contents_begin: begin,
contents_end: begin + limit,
};
return Some((list, end));
}
let line_begin = text.find(|c: char| !c.is_ascii_whitespace()).unwrap_or(0);
let tail = &text[line_begin..];
if let Some((clock, end)) = Clock::parse(tail) {
let clock = Element::Clock {
clock,
begin,
end: begin + line_begin + end,
};
return Some((clock, line_begin + end));
if let Some((tail, clock)) = Clock::parse(tail) {
return Some((tail, clock));
}
// TODO: LaTeX environment
if tail.starts_with("\\begin{") {}
if tail.starts_with('-') {
if let Some(end) = Rule::parse(tail) {
let rule = Element::Rule {
begin,
end: begin + line_begin + end,
};
return Some((rule, line_begin + end));
if let Ok((tail, rule)) = Rule::parse(tail) {
return Some((tail, rule));
}
}
if tail.starts_with(':') {
if let Some((drawer, off, limit, end)) = Drawer::parse(tail) {
let drawer = Element::Drawer {
drawer,
begin,
end: begin + line_begin + end,
contents_begin: begin + line_begin + off,
contents_end: begin + line_begin + limit,
};
return Some((drawer, line_begin + end));
if let Some((tail, drawer)) = Drawer::parse(tail) {
return Some((tail, drawer));
}
}
if tail == ":" || tail.starts_with(": ") || tail.starts_with(":\n") {
let mut last_end = 1; // ":"
for i in memchr_iter(b'\n', text.as_bytes()) {
for i in memchr_iter(b'\n', contents.as_bytes()) {
last_end = i + 1;
let line = &text[last_end..];
let line = &contents[last_end..];
if !(line == ":" || line.starts_with(": ") || line.starts_with(":\n")) {
let fixed_width = Element::FixedWidth {
value: &text[0..i + 1],
begin,
end: begin + i + 1,
value: &contents[0..i + 1],
};
return Some((fixed_width, i + 1));
return Some((&contents[i + 1..], fixed_width));
}
}
let fixed_width = Element::FixedWidth {
value: &text[0..last_end],
begin,
end: begin + last_end,
value: &contents[0..last_end],
};
return Some((fixed_width, last_end));
return Some((&contents[last_end..], fixed_width));
}
if tail == "#" || tail.starts_with("# ") || tail.starts_with("#\n") {
let mut last_end = 1; // "#"
for i in memchr_iter(b'\n', text.as_bytes()) {
for i in memchr_iter(b'\n', contents.as_bytes()) {
last_end = i + 1;
let line = &text[last_end..];
let line = &contents[last_end..];
if !(line == "#" || line.starts_with("# ") || line.starts_with("#\n")) {
let fixed_width = Element::Comment {
value: &text[0..i + 1],
begin,
end: begin + i + 1,
value: &contents[0..i + 1],
};
return Some((fixed_width, i + 1));
return Some((&contents[i + 1..], fixed_width));
}
}
let fixed_width = Element::Comment {
value: &text[0..last_end],
begin,
end: begin + last_end,
value: &contents[0..last_end],
};
return Some((fixed_width, last_end));
return Some((&contents[last_end..], fixed_width));
}
if tail.starts_with("#+") {
if let Some((block, off, limit, end)) = Block::parse(tail) {
let block = Element::Block {
block,
begin,
end: begin + line_begin + end,
contents_begin: begin + line_begin + off,
contents_end: begin + line_begin + limit,
};
return Some((block, line_begin + end));
} else if let Some((dyn_block, off, limit, end)) = DynBlock::parse(tail) {
let dyn_block = Element::DynBlock {
dyn_block,
begin,
end: begin + line_begin + end,
contents_begin: begin + line_begin + off,
contents_end: begin + line_begin + limit,
};
return Some((dyn_block, line_begin + end));
} else if let Some((key, option, value, end)) = Keyword::parse(tail) {
if key.eq_ignore_ascii_case("CALL") {
let call = Element::BabelCall {
call: BabelCall { key, value },
begin,
end: begin + line_begin + end,
};
return Some((call, line_begin + end));
Block::parse(tail)
.or_else(|| DynBlock::parse(tail))
.or_else(|| Keyword::parse(tail).ok())
} else {
let kw = Element::Keyword {
keyword: Keyword { key, option, value },
begin,
end: begin + line_begin + end,
};
return Some((kw, line_begin + end));
}
}
}
None
}
fn parse_objects_children(&mut self, begin: usize, end: usize, node: NodeId) {
if begin >= end {
return;
}
fn parse_objects_children(&mut self, contents: &'a str, node: NodeId) {
let mut tail = contents;
if let Some((new_tail, obj)) = self.parse_object(tail) {
let new_node = self.arena.new_node(obj);
node.append(new_node, &mut self.arena).unwrap();
tail = new_tail;
}
let mut text = tail;
let mut pos = 0;
if let Some((ty, off)) = self.parse_object(begin, end) {
let new_node = self.arena.new_node(ty);
node.append(new_node, &mut self.arena).unwrap();
pos += off;
}
let bs = bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n');
let mut last_end = pos;
let text = &self.text[begin..end];
while let Some(off) = bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n')
.find(&text[pos..].as_bytes())
{
pos += off;
match text.as_bytes()[pos] {
while let Some(off) = bs.find(tail.as_bytes()) {
match tail.as_bytes()[off] {
b'{' => {
if let Some((ty, off)) = self.parse_object(begin + pos, end) {
if last_end != pos {
if let Some((new_tail, obj)) = self.parse_object(&tail[off..]) {
if pos != 0 {
let new_node = self.arena.new_node(Element::Text {
value: &text[last_end..pos],
begin: begin + last_end,
end: begin + pos,
value: &text[0..pos + off],
});
node.append(new_node, &mut self.arena).unwrap();
pos = 0;
}
let new_node = self.arena.new_node(ty);
let new_node = self.arena.new_node(obj);
node.append(new_node, &mut self.arena).unwrap();
pos += off;
last_end = pos;
} else if let Some((ty, off)) = self.parse_object(begin + pos + 1, end) {
tail = new_tail;
text = new_tail;
} else if let Some((new_tail, obj)) = self.parse_object(&tail[off + 1..]) {
let new_node = self.arena.new_node(Element::Text {
value: &text[last_end..=pos],
begin: begin + last_end,
end: begin + pos + 1,
value: &text[0..pos + off + 1],
});
node.append(new_node, &mut self.arena).unwrap();
let new_node = self.arena.new_node(ty);
pos = 0;
let new_node = self.arena.new_node(obj);
node.append(new_node, &mut self.arena).unwrap();
pos += off + 1;
last_end = pos;
tail = new_tail;
text = new_tail;
} else {
pos += 1;
tail = &tail[off + 1..];
pos += off + 1;
}
}
b' ' | b'(' | b'\'' | b'"' | b'\n' => {
if let Some((ty, off)) = self.parse_object(begin + pos + 1, end) {
if let Some((new_tail, obj)) = self.parse_object(&tail[off + 1..]) {
let new_node = self.arena.new_node(Element::Text {
value: &text[last_end..=pos],
begin: begin + last_end,
end: begin + pos + 1,
value: &text[0..pos + off + 1],
});
node.append(new_node, &mut self.arena).unwrap();
let new_node = self.arena.new_node(ty);
pos = 0;
let new_node = self.arena.new_node(obj);
node.append(new_node, &mut self.arena).unwrap();
pos += off + 1;
last_end = pos;
tail = new_tail;
text = new_tail;
} else {
pos += 1;
tail = &tail[off + 1..];
pos += off + 1;
}
}
_ => {
if let Some((ty, off)) = self.parse_object(begin + pos, end) {
if last_end != pos {
if let Some((new_tail, obj)) = self.parse_object(&tail[off..]) {
if pos != 0 {
let new_node = self.arena.new_node(Element::Text {
value: &text[last_end..pos],
begin: begin + last_end,
end: begin + pos,
value: &text[0..pos + off],
});
node.append(new_node, &mut self.arena).unwrap();
pos = 0;
}
let new_node = self.arena.new_node(ty);
let new_node = self.arena.new_node(obj);
node.append(new_node, &mut self.arena).unwrap();
pos += off;
last_end = pos;
tail = new_tail;
text = new_tail;
} else {
pos += 1;
tail = &tail[off + 1..];
pos += off + 1;
}
}
}
}
if begin + last_end < end {
let new_node = self.arena.new_node(Element::Text {
value: &text[last_end..],
begin: begin + last_end,
end,
});
if !text.is_empty() {
let new_node = self.arena.new_node(Element::Text { value: text });
node.append(new_node, &mut self.arena).unwrap();
}
}
fn parse_object(&self, begin: usize, end: usize) -> Option<(Element<'a>, usize)> {
let text = &self.text[begin..end];
if text.len() < 3 {
None
} else {
let bytes = text.as_bytes();
fn parse_object(&self, contents: &'a str) -> Option<(&'a str, Element<'a>)> {
if contents.len() < 3 {
return None;
}
let bytes = contents.as_bytes();
match bytes[0] {
b'@' if bytes[1] == b'@' => Snippet::parse(text).map(|(snippet, off)| {
(
Element::Snippet {
snippet,
begin,
end: begin + off,
},
off,
)
}),
b'{' if bytes[1] == b'{' && bytes[2] == b'{' => {
Macros::parse(text).map(|(macros, off)| {
(
Element::Macros {
macros,
begin,
end: begin + off,
},
off,
)
b'@' => Snippet::parse(contents).ok(),
b'{' => Macros::parse(contents).ok(),
b'<' => RadioTarget::parse(contents)
.or_else(|_| Target::parse(contents))
.or_else(|_| {
Timestamp::parse_active(contents)
.map(|(tail, timestamp)| (tail, timestamp.into()))
})
}
b'<' if bytes[1] == b'<' => {
if bytes[2] == b'<' {
RadioTarget::parse(text).map(|(radio_target, off)| {
(
Element::RadioTarget {
radio_target,
begin,
end: begin + off,
},
off,
)
.or_else(|_| {
Timestamp::parse_diary(contents)
.map(|(tail, timestamp)| (tail, timestamp.into()))
})
} else {
Target::parse(text).map(|(target, off)| {
(
Element::Target {
target,
begin,
end: begin + off,
},
off,
)
})
}
}
b'<' => Timestamp::parse_active(text)
.or_else(|| (Timestamp::parse_diary(text)))
.map(|(timestamp, off)| {
(
Element::Timestamp {
timestamp,
begin,
end: begin + off,
},
off,
)
}),
.ok(),
b'[' => {
if text[1..].starts_with("fn:") {
FnRef::parse(text).map(|(fn_ref, off)| {
(
Element::FnRef {
fn_ref,
begin,
end: begin + off,
},
off,
)
})
if contents[1..].starts_with("fn:") {
FnRef::parse(contents).map(|(tail, fn_ref)| (tail, fn_ref.into()))
} else if bytes[1] == b'[' {
Link::parse(text).map(|(link, off)| {
(
Element::Link {
link,
begin,
end: begin + off,
},
off,
)
})
Link::parse(contents).ok()
} else {
Cookie::parse(text)
.map(|(cookie, off)| {
(
Element::Cookie {
cookie,
begin,
end: begin + off,
},
off,
)
})
Cookie::parse(contents)
.map(|(tail, cookie)| (tail, cookie.into()))
.or_else(|| {
Timestamp::parse_inactive(text).map(|(timestamp, off)| {
(
Element::Timestamp {
timestamp,
begin,
end: begin + off,
},
off,
)
})
Timestamp::parse_inactive(contents)
.map(|(tail, timestamp)| (tail, timestamp.into()))
.ok()
})
}
}
b'*' => parse_emphasis(text, b'*').map(|off| {
(
Element::Bold {
begin,
contents_begin: begin + 1,
contents_end: begin + off - 1,
end: begin + off,
},
off,
)
}),
b'+' => parse_emphasis(text, b'+').map(|off| {
(
Element::Strike {
begin,
contents_begin: begin + 1,
contents_end: begin + off - 1,
end: begin + off,
},
off,
)
}),
b'/' => parse_emphasis(text, b'/').map(|off| {
(
Element::Italic {
begin,
contents_begin: begin + 1,
contents_end: begin + off - 1,
end: begin + off,
},
off,
)
}),
b'_' => parse_emphasis(text, b'_').map(|off| {
(
Element::Underline {
begin,
contents_begin: begin + 1,
contents_end: begin + off - 1,
end: begin + off,
},
off,
)
}),
b'=' => parse_emphasis(text, b'=').map(|off| {
(
Element::Verbatim {
begin,
end: begin + off,
value: &text[1..off - 1],
},
off,
)
}),
b'~' => parse_emphasis(text, b'~').map(|off| {
(
Element::Code {
begin,
end: begin + off,
value: &text[1..off - 1],
},
off,
)
}),
b's' if text.starts_with("src_") => {
InlineSrc::parse(text).map(|(inline_src, off)| {
(
Element::InlineSrc {
inline_src,
begin,
end: begin + off,
},
off,
)
})
}
b'c' if text.starts_with("call_") => {
InlineCall::parse(text).map(|(inline_call, off)| {
(
Element::InlineCall {
inline_call,
begin,
end: begin + off,
},
off,
)
})
b'*' => parse_emphasis(contents, b'*')
.map(|(tail, contents)| (tail, Element::Bold { contents })),
b'+' => parse_emphasis(contents, b'+')
.map(|(tail, contents)| (tail, Element::Strike { contents })),
b'/' => parse_emphasis(contents, b'/')
.map(|(tail, contents)| (tail, Element::Italic { contents })),
b'_' => parse_emphasis(contents, b'_')
.map(|(tail, contents)| (tail, Element::Underline { contents })),
b'=' => parse_emphasis(contents, b'=')
.map(|(tail, value)| (tail, Element::Verbatim { value })),
b'~' => {
parse_emphasis(contents, b'~').map(|(tail, value)| (tail, Element::Code { value }))
}
b's' if contents.starts_with("src_") => InlineSrc::parse(contents).ok(),
b'c' if contents.starts_with("call_") => InlineCall::parse(contents).ok(),
_ => None,
}
}
}
fn parse_list_items(&mut self, mut begin: usize, end: usize, indent: usize, node: NodeId) {
while begin < end {
let text = &self.text[begin..end];
let (list_item, off, end) = ListItem::parse(text, indent);
let list_item = Element::ListItem {
list_item,
begin,
end: begin + end,
contents_begin: begin + off,
contents_end: begin + end,
};
fn parse_list_items(&mut self, mut contents: &'a str, indent: usize, node: NodeId) {
while !contents.is_empty() {
let (tail, list_item) = ListItem::parse(contents, indent);
let list_item = Element::ListItem(list_item);
let new_node = self.arena.new_node(list_item);
node.append(new_node, &mut self.arena).unwrap();
begin += end;
contents = tail;
}
}
}
fn skip_empty_lines(text: &str) -> usize {
fn skip_empty_lines(contents: &str) -> &str {
let mut i = 0;
for pos in memchr_iter(b'\n', text.as_bytes()) {
if text.as_bytes()[i..pos].iter().all(u8::is_ascii_whitespace) {
for pos in memchr_iter(b'\n', contents.as_bytes()) {
if contents.as_bytes()[i..pos]
.iter()
.all(u8::is_ascii_whitespace)
{
i = pos + 1;
} else {
break;
}
}
i
&contents[i..]
}
#[test]
fn test_skip_empty_lines() {
assert_eq!(skip_empty_lines("foo"), 0);
assert_eq!(skip_empty_lines(" foo"), 0);
assert_eq!(skip_empty_lines(" \nfoo\n"), " \n".len());
assert_eq!(skip_empty_lines(" \n\n\nfoo\n"), " \n\n\n".len());
assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), " \n \n\n".len());
assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " \n \n\n".len());
assert_eq!(skip_empty_lines("foo"), "foo");
assert_eq!(skip_empty_lines(" foo"), " foo");
assert_eq!(skip_empty_lines(" \nfoo\n"), "foo\n");
assert_eq!(skip_empty_lines(" \n\n\nfoo\n"), "foo\n");
assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), "foo\n");
assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " foo\n");
}

View file

@ -1,15 +1,17 @@
use orgize::Org;
use pretty_assertions::assert_eq;
use std::io::Result;
macro_rules! test_suite {
($name:ident, $content:expr, $expected:expr) => {
#[test]
fn $name() {
fn $name() -> Result<()> {
let mut writer = Vec::new();
let org = Org::parse($content);
org.html_default(&mut writer).unwrap();
org.html(&mut writer).unwrap();
let string = String::from_utf8(writer).unwrap();
assert_eq!(string, $expected);
Ok(())
}
};
}