refactor: rewrite some parsers with nom
This commit is contained in:
parent
afcb5090ec
commit
50f6b9f52a
|
@ -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"
|
||||
|
|
10
README.md
10
README.md
|
@ -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)?,
|
||||
|
|
|
@ -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)?);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
})
|
||||
))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
}),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -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;
|
||||
|
|
785
src/org.rs
785
src/org.rs
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue