chore: remove orgize-sync package
This commit is contained in:
parent
5d26466e07
commit
b15900b100
59 changed files with 62 additions and 647 deletions
23
src/config.rs
Normal file
23
src/config.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! Parse configuration module
|
||||
|
||||
/// Parse configuration
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParseConfig {
|
||||
/// Headline's todo keywords, todo type
|
||||
pub todo_keywords: Vec<String>,
|
||||
/// Headline's todo keywords, done type
|
||||
pub done_keywords: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ParseConfig {
|
||||
fn default() -> Self {
|
||||
ParseConfig {
|
||||
todo_keywords: vec![String::from("TODO")],
|
||||
done_keywords: vec![String::from("DONE")],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DEFAULT_CONFIG: ParseConfig = ParseConfig::default();
|
||||
}
|
||||
231
src/elements/block.rs
Normal file
231
src/elements/block.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag_no_case, character::complete::alpha1, error::ParseError,
|
||||
sequence::preceded, IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::{line, take_lines_while};
|
||||
|
||||
/// Special Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct SpecialBlock<'a> {
|
||||
/// Optional block parameters
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
/// Block name
|
||||
pub name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl SpecialBlock<'_> {
|
||||
pub fn into_owned(self) -> SpecialBlock<'static> {
|
||||
SpecialBlock {
|
||||
name: self.name.into_owned().into(),
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quote Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct QuoteBlock<'a> {
|
||||
/// Optional block parameters
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl QuoteBlock<'_> {
|
||||
pub fn into_owned(self) -> QuoteBlock<'static> {
|
||||
QuoteBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Center Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct CenterBlock<'a> {
|
||||
/// Optional block parameters
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl CenterBlock<'_> {
|
||||
pub fn into_owned(self) -> CenterBlock<'static> {
|
||||
CenterBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verse Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct VerseBlock<'a> {
|
||||
/// Optional block parameters
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl VerseBlock<'_> {
|
||||
pub fn into_owned(self) -> VerseBlock<'static> {
|
||||
VerseBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Comment Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct CommentBlock<'a> {
|
||||
pub data: Option<Cow<'a, str>>,
|
||||
/// Comment, without block's boundaries
|
||||
pub contents: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl CommentBlock<'_> {
|
||||
pub fn into_owned(self) -> CommentBlock<'static> {
|
||||
CommentBlock {
|
||||
data: self.data.map(Into::into).map(Cow::Owned),
|
||||
contents: self.contents.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct ExampleBlock<'a> {
|
||||
pub data: Option<Cow<'a, str>>,
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl ExampleBlock<'_> {
|
||||
pub fn into_owned(self) -> ExampleBlock<'static> {
|
||||
ExampleBlock {
|
||||
data: self.data.map(Into::into).map(Cow::Owned),
|
||||
contents: self.contents.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Export Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct ExportBlock<'a> {
|
||||
pub data: Cow<'a, str>,
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl ExportBlock<'_> {
|
||||
pub fn into_owned(self) -> ExportBlock<'static> {
|
||||
ExportBlock {
|
||||
data: self.data.into_owned().into(),
|
||||
contents: self.contents.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Src Block Element
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct SourceBlock<'a> {
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
/// Language of the code in the block
|
||||
pub language: Cow<'a, str>,
|
||||
pub arguments: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl SourceBlock<'_> {
|
||||
pub fn into_owned(self) -> SourceBlock<'static> {
|
||||
SourceBlock {
|
||||
language: self.language.into_owned().into(),
|
||||
arguments: self.arguments.into_owned().into(),
|
||||
contents: self.contents.into_owned().into(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fn number_lines() -> Some(New) | Some(Continued) | None { }
|
||||
// TODO: fn preserve_indent() -> bool { }
|
||||
// TODO: fn use_labels() -> bool { }
|
||||
// TODO: fn label_fmt() -> Option<String> { }
|
||||
// TODO: fn retain_labels() -> bool { }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parse_block_element(input: &str) -> Option<(&str, (&str, Option<&str>, &str))> {
|
||||
parse_block_element_internal::<()>(input).ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_block_element_internal<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, (&str, Option<&str>, &str), E> {
|
||||
let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?;
|
||||
let (input, args) = line(input)?;
|
||||
let end_line = format!("#+END_{}", name);
|
||||
let (input, contents) =
|
||||
take_lines_while(|line| !line.trim().eq_ignore_ascii_case(&end_line))(input);
|
||||
let (input, _) = line(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
name,
|
||||
if args.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(args.trim())
|
||||
},
|
||||
contents,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_block_element_internal::<VerboseError<&str>>(
|
||||
r#"#+BEGIN_SRC
|
||||
#+END_SRC"#
|
||||
),
|
||||
Ok(("", ("SRC".into(), None, "")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_block_element_internal::<VerboseError<&str>>(
|
||||
r#"#+begin_src
|
||||
#+end_src"#
|
||||
),
|
||||
Ok(("", ("src".into(), None, "")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_block_element_internal::<VerboseError<&str>>(
|
||||
r#"#+BEGIN_SRC javascript
|
||||
console.log('Hello World!');
|
||||
#+END_SRC
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
"SRC".into(),
|
||||
Some("javascript".into()),
|
||||
"console.log('Hello World!');\n"
|
||||
)
|
||||
))
|
||||
);
|
||||
// TODO: more testing
|
||||
}
|
||||
230
src/elements/clock.rs
Normal file
230
src/elements/clock.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::{char, digit1, space0},
|
||||
combinator::recognize,
|
||||
error::ParseError,
|
||||
sequence::separated_pair,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::elements::timestamp::{parse_inactive, Datetime, Timestamp};
|
||||
|
||||
use crate::parsers::eol;
|
||||
|
||||
/// Clock Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(untagged))]
|
||||
#[derive(Debug)]
|
||||
pub enum Clock<'a> {
|
||||
/// Closed Clock
|
||||
Closed {
|
||||
/// Time start
|
||||
start: Datetime<'a>,
|
||||
/// Time end
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
/// Clock duration
|
||||
duration: Cow<'a, str>,
|
||||
},
|
||||
/// Running Clock
|
||||
Running {
|
||||
/// Time start
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clock<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Clock)> {
|
||||
parse_clock::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_onwed(self) -> Clock<'static> {
|
||||
match self {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
duration,
|
||||
} => Clock::Closed {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
duration: duration.into_owned().into(),
|
||||
},
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Clock::Running {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock is running.
|
||||
pub fn is_running(&self) -> bool {
|
||||
match self {
|
||||
Clock::Closed { .. } => false,
|
||||
Clock::Running { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock is closed.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
match self {
|
||||
Clock::Closed { .. } => true,
|
||||
Clock::Running { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns clock duration, or `None` if it's running.
|
||||
pub fn duration(&self) -> Option<&str> {
|
||||
match self {
|
||||
Clock::Closed { duration, .. } => Some(duration),
|
||||
Clock::Running { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a timestamp from the clock.
|
||||
pub fn value(&self) -> Timestamp {
|
||||
match &*self {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
..
|
||||
} => Timestamp::InactiveRange {
|
||||
start: start.clone(),
|
||||
end: end.clone(),
|
||||
repeater: repeater.clone(),
|
||||
delay: delay.clone(),
|
||||
},
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::Inactive {
|
||||
start: start.clone(),
|
||||
repeater: repeater.clone(),
|
||||
delay: delay.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_clock<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Clock, E> {
|
||||
let (input, _) = tag("CLOCK:")(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, timestamp) = parse_inactive(input)?;
|
||||
|
||||
match timestamp {
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = tag("=>")(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, duration) = recognize(separated_pair(digit1, char(':'), digit1))(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
duration: duration.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => {
|
||||
let (input, _) = eol(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
},
|
||||
))
|
||||
}
|
||||
_ => unreachable!(
|
||||
"`parse_inactive` only returns `Timestamp::InactiveRange` or `Timestamp::Inactive`."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_clock::<VerboseError<&str>>("CLOCK: [2003-09-16 Tue 09:39]"),
|
||||
Ok((
|
||||
"",
|
||||
Clock::Running {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_clock::<VerboseError<&str>>(
|
||||
"CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
Clock::Closed {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39)
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
duration: "1:00".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
125
src/elements/cookie.rs
Normal file
125
src/elements/cookie.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::digit0,
|
||||
combinator::recognize,
|
||||
error::ParseError,
|
||||
sequence::{delimited, pair, separated_pair},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Statistics Cookie Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Cookie<'a> {
|
||||
/// Full cookie value
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Cookie<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Cookie)> {
|
||||
parse_cookie::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Cookie<'static> {
|
||||
Cookie {
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_cookie<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Cookie, E> {
|
||||
let (input, value) = recognize(delimited(
|
||||
tag("["),
|
||||
alt((
|
||||
separated_pair(digit0, tag("/"), digit0),
|
||||
pair(digit0, tag("%")),
|
||||
)),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Cookie {
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[1/10]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[1/10]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[1/1000]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[1/1000]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[10%]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[10%]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[%]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[%]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[/]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[/]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[100/]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[100/]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cookie::<VerboseError<&str>>("[/100]"),
|
||||
Ok((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[/100]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert!(parse_cookie::<VerboseError<&str>>("[10% ]").is_err());
|
||||
assert!(parse_cookie::<VerboseError<&str>>("[1//100]").is_err());
|
||||
assert!(parse_cookie::<VerboseError<&str>>("[1\\100]").is_err());
|
||||
assert!(parse_cookie::<VerboseError<&str>>("[10%%]").is_err());
|
||||
}
|
||||
78
src/elements/drawer.rs
Normal file
78
src/elements/drawer.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while1},
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::{eol, line, take_lines_while};
|
||||
|
||||
/// Drawer Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Drawer<'a> {
|
||||
/// Drawer name
|
||||
pub name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Drawer<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (Drawer, &str))> {
|
||||
parse_drawer::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Drawer<'static> {
|
||||
Drawer {
|
||||
name: self.name.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parse_drawer<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, (Drawer, &str), E> {
|
||||
let (input, name) = delimited(
|
||||
tag(":"),
|
||||
take_while1(|c: char| c.is_ascii_alphabetic() || c == '-' || c == '_'),
|
||||
tag(":"),
|
||||
)(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
let (input, contents) =
|
||||
take_lines_while(|line| !line.trim().eq_ignore_ascii_case(":END:"))(input);
|
||||
let (input, _) = line(input)?;
|
||||
|
||||
Ok((input, (Drawer { name: name.into() }, contents)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_drawer::<VerboseError<&str>>(":PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Drawer {
|
||||
name: "PROPERTIES".into()
|
||||
},
|
||||
" :CUSTOM_ID: id\n"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_drawer::<VerboseError<&str>>(":PROPERTIES:\n :END:"),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Drawer {
|
||||
name: "PROPERTIES".into()
|
||||
},
|
||||
""
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
88
src/elements/dyn_block.rs
Normal file
88
src/elements/dyn_block.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag_no_case,
|
||||
character::complete::{alpha1, space1},
|
||||
error::ParseError,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::{line, take_lines_while};
|
||||
|
||||
/// Dynamic Block Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct DynBlock<'a> {
|
||||
/// Block name
|
||||
pub block_name: Cow<'a, str>,
|
||||
/// Block argument
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub arguments: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl DynBlock<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (DynBlock, &str))> {
|
||||
parse_dyn_block::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> DynBlock<'static> {
|
||||
DynBlock {
|
||||
block_name: self.block_name.into_owned().into(),
|
||||
arguments: self.arguments.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_dyn_block<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, (DynBlock, &str), E> {
|
||||
let (input, _) = tag_no_case("#+BEGIN:")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, name) = alpha1(input)?;
|
||||
let (input, args) = line(input)?;
|
||||
let (input, contents) =
|
||||
take_lines_while(|line| !line.trim().eq_ignore_ascii_case("#+END:"))(input);
|
||||
let (input, _) = line(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
DynBlock {
|
||||
block_name: name.into(),
|
||||
arguments: if args.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(args.trim().into())
|
||||
},
|
||||
},
|
||||
contents,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
// TODO: testing
|
||||
assert_eq!(
|
||||
parse_dyn_block::<VerboseError<&str>>(
|
||||
r#"#+BEGIN: clocktable :scope file
|
||||
CONTENTS
|
||||
#+END:
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
DynBlock {
|
||||
block_name: "clocktable".into(),
|
||||
arguments: Some(":scope file".into()),
|
||||
},
|
||||
"CONTENTS\n"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
48
src/elements/emphasis.rs
Normal file
48
src/elements/emphasis.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use bytecount::count;
|
||||
use memchr::memchr_iter;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn parse_emphasis(text: &str, marker: u8) -> Option<(&str, &str)> {
|
||||
debug_assert!(text.len() >= 3);
|
||||
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
if bytes[1].is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in memchr_iter(marker, bytes).skip(1) {
|
||||
if count(&bytes[1..i], b'\n') >= 2 {
|
||||
break;
|
||||
} else if validate_marker(i, text) {
|
||||
return Some((&text[i + 1..], &text[1..i]));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn validate_marker(pos: usize, text: &str) -> bool {
|
||||
if text.as_bytes()[pos - 1].is_ascii_whitespace() {
|
||||
false
|
||||
} else if let Some(&post) = text.as_bytes().get(pos + 1) {
|
||||
match post {
|
||||
b' ' | b'-' | b'.' | b',' | b':' | b'!' | b'?' | b'\'' | b'\n' | b')' | b'}' => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(parse_emphasis("*bold*", b'*'), Some(("", "bold")));
|
||||
assert_eq!(parse_emphasis("*bo*ld*", b'*'), Some(("", "bo*ld")));
|
||||
assert_eq!(parse_emphasis("*bo\nld*", b'*'), Some(("", "bo\nld")));
|
||||
assert_eq!(parse_emphasis("*bold*a", b'*'), None);
|
||||
assert_eq!(parse_emphasis("*bold*", b'/'), None);
|
||||
assert_eq!(parse_emphasis("*bold *", b'*'), None);
|
||||
assert_eq!(parse_emphasis("* bold*", b'*'), None);
|
||||
assert_eq!(parse_emphasis("*b\nol\nd*", b'*'), None);
|
||||
}
|
||||
101
src/elements/fn_def.rs
Normal file
101
src/elements/fn_def.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while1},
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::line;
|
||||
|
||||
/// Footnote Definition Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct FnDef<'a> {
|
||||
/// Footnote label, used for refrence
|
||||
pub label: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl FnDef<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (FnDef, &str))> {
|
||||
parse_fn_def::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> FnDef<'static> {
|
||||
FnDef {
|
||||
label: self.label.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_fn_def<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, (FnDef, &str), E> {
|
||||
let (input, label) = delimited(
|
||||
tag("[fn:"),
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
tag("]"),
|
||||
)(input)?;
|
||||
let (input, content) = line(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
FnDef {
|
||||
label: label.into(),
|
||||
},
|
||||
content,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_fn_def::<VerboseError<&str>>("[fn:1] https://orgmode.org"),
|
||||
Ok(("", (FnDef { label: "1".into() }, " https://orgmode.org")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_def::<VerboseError<&str>>("[fn:word_1] https://orgmode.org"),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "word_1".into()
|
||||
},
|
||||
" https://orgmode.org"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_def::<VerboseError<&str>>("[fn:WORD-1] https://orgmode.org"),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "WORD-1".into()
|
||||
},
|
||||
" https://orgmode.org"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_def::<VerboseError<&str>>("[fn:WORD]"),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "WORD".into()
|
||||
},
|
||||
""
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
assert!(parse_fn_def::<VerboseError<&str>>("[fn:] https://orgmode.org").is_err());
|
||||
assert!(parse_fn_def::<VerboseError<&str>>("[fn:wor d] https://orgmode.org").is_err());
|
||||
assert!(parse_fn_def::<VerboseError<&str>>("[fn:WORD https://orgmode.org").is_err());
|
||||
}
|
||||
113
src/elements/fn_ref.rs
Normal file
113
src/elements/fn_ref.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use memchr::memchr2_iter;
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::opt,
|
||||
error::{ErrorKind, ParseError},
|
||||
sequence::preceded,
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
/// Footnote Reference Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct FnRef<'a> {
|
||||
/// Footnote label
|
||||
pub label: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub definition: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl FnRef<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, FnRef)> {
|
||||
parse_fn_ref::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> FnRef<'static> {
|
||||
FnRef {
|
||||
label: self.label.into_owned().into(),
|
||||
definition: self.definition.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_fn_ref<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, FnRef, E> {
|
||||
let (input, _) = tag("[fn:")(input)?;
|
||||
let (input, label) =
|
||||
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_')(input)?;
|
||||
let (input, definition) = opt(preceded(tag(":"), balanced_brackets))(input)?;
|
||||
let (input, _) = tag("]")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
FnRef {
|
||||
label: label.into(),
|
||||
definition: definition.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn balanced_brackets<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> {
|
||||
let mut pairs = 1;
|
||||
for i in memchr2_iter(b'[', b']', input.as_bytes()) {
|
||||
if input.as_bytes()[i] == b'[' {
|
||||
pairs += 1;
|
||||
} else if pairs != 1 {
|
||||
pairs -= 1;
|
||||
} else {
|
||||
return Ok((&input[i..], &input[0..i]));
|
||||
}
|
||||
}
|
||||
Err(Err::Error(E::from_error_kind(input, ErrorKind::Tag)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_fn_ref::<VerboseError<&str>>("[fn:1]"),
|
||||
Ok((
|
||||
"",
|
||||
FnRef {
|
||||
label: "1".into(),
|
||||
definition: None
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_ref::<VerboseError<&str>>("[fn:1:2]"),
|
||||
Ok((
|
||||
"",
|
||||
FnRef {
|
||||
label: "1".into(),
|
||||
definition: Some("2".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_ref::<VerboseError<&str>>("[fn::2]"),
|
||||
Ok((
|
||||
"",
|
||||
FnRef {
|
||||
label: "".into(),
|
||||
definition: Some("2".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_fn_ref::<VerboseError<&str>>("[fn::[]]"),
|
||||
Ok((
|
||||
"",
|
||||
FnRef {
|
||||
label: "".into(),
|
||||
definition: Some("[]".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
|
||||
assert!(parse_fn_ref::<VerboseError<&str>>("[fn::[]").is_err());
|
||||
}
|
||||
125
src/elements/inline_call.rs
Normal file
125
src/elements/inline_call.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till},
|
||||
combinator::opt,
|
||||
error::ParseError,
|
||||
sequence::{delimited, preceded},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Inline Babel Call Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InlineCall<'a> {
|
||||
/// Called code block name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Header arguments applied to the code block
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub inside_header: Option<Cow<'a, str>>,
|
||||
/// Arugment passed to the code block
|
||||
pub arguments: Cow<'a, str>,
|
||||
/// Header arguments applied to the calling instance
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub end_header: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl InlineCall<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, InlineCall)> {
|
||||
parse_inline_call::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> InlineCall<'static> {
|
||||
InlineCall {
|
||||
name: self.name.into_owned().into(),
|
||||
arguments: self.arguments.into_owned().into(),
|
||||
inside_header: self.inside_header.map(Into::into).map(Cow::Owned),
|
||||
end_header: self.end_header.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_inline_call<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, InlineCall, E> {
|
||||
let (input, name) = preceded(
|
||||
tag("call_"),
|
||||
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
|
||||
)(input)?;
|
||||
let (input, inside_header) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, arguments) =
|
||||
delimited(tag("("), take_till(|c| c == ')' || c == '\n'), tag(")"))(input)?;
|
||||
let (input, end_header) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
InlineCall {
|
||||
name: name.into(),
|
||||
arguments: arguments.into(),
|
||||
inside_header: inside_header.map(Into::into),
|
||||
end_header: end_header.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_inline_call::<VerboseError<&str>>("call_square(4)"),
|
||||
Ok((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: None,
|
||||
end_header: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inline_call::<VerboseError<&str>>("call_square[:results output](4)"),
|
||||
Ok((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: Some(":results output".into()),
|
||||
end_header: None,
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inline_call::<VerboseError<&str>>("call_square(4)[:results html]"),
|
||||
Ok((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: None,
|
||||
end_header: Some(":results html".into()),
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inline_call::<VerboseError<&str>>("call_square[:results output](4)[:results html]"),
|
||||
Ok((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: Some(":results output".into()),
|
||||
end_header: Some(":results html".into()),
|
||||
},
|
||||
))
|
||||
);
|
||||
}
|
||||
95
src/elements/inline_src.rs
Normal file
95
src/elements/inline_src.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till, take_while1},
|
||||
combinator::opt,
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Inline Src Block Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct InlineSrc<'a> {
|
||||
/// Language of the code
|
||||
pub lang: Cow<'a, str>,
|
||||
/// Optional header arguments
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<Cow<'a, str>>,
|
||||
/// Source code
|
||||
pub body: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl InlineSrc<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, InlineSrc)> {
|
||||
parse_inline_src::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> InlineSrc<'static> {
|
||||
InlineSrc {
|
||||
lang: self.lang.into_owned().into(),
|
||||
options: self.options.map(Into::into).map(Cow::Owned),
|
||||
body: self.body.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_inline_src<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, InlineSrc, E> {
|
||||
let (input, _) = tag("src_")(input)?;
|
||||
let (input, lang) =
|
||||
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?;
|
||||
let (input, options) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == '\n' || c == ']'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, body) = delimited(tag("{"), take_till(|c| c == '\n' || c == '}'), tag("}"))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
InlineSrc {
|
||||
lang: lang.into(),
|
||||
options: options.map(Into::into),
|
||||
body: body.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_inline_src::<VerboseError<&str>>("src_C{int a = 0;}"),
|
||||
Ok((
|
||||
"",
|
||||
InlineSrc {
|
||||
lang: "C".into(),
|
||||
options: None,
|
||||
body: "int a = 0;".into()
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inline_src::<VerboseError<&str>>("src_xml[:exports code]{<tag>text</tag>}"),
|
||||
Ok((
|
||||
"",
|
||||
InlineSrc {
|
||||
lang: "xml".into(),
|
||||
options: Some(":exports code".into()),
|
||||
body: "<tag>text</tag>".into(),
|
||||
},
|
||||
))
|
||||
);
|
||||
|
||||
assert!(
|
||||
parse_inline_src::<VerboseError<&str>>("src_xml[:exports code]{<tag>text</tag>").is_err()
|
||||
);
|
||||
assert!(
|
||||
parse_inline_src::<VerboseError<&str>>("src_[:exports code]{<tag>text</tag>}").is_err()
|
||||
);
|
||||
assert!(parse_inline_src::<VerboseError<&str>>("src_xml[:exports code]").is_err());
|
||||
}
|
||||
116
src/elements/keyword.rs
Normal file
116
src/elements/keyword.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till},
|
||||
combinator::opt,
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::line;
|
||||
|
||||
/// Keyword Elemenet
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Keyword<'a> {
|
||||
/// Keyword name
|
||||
pub key: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub optional: Option<Cow<'a, str>>,
|
||||
/// Keyword value
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Keyword<'_> {
|
||||
pub fn into_owned(self) -> Keyword<'static> {
|
||||
Keyword {
|
||||
key: self.key.into_owned().into(),
|
||||
optional: self.optional.map(Into::into).map(Cow::Owned),
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Babel Call Elemenet
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct BabelCall<'a> {
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl BabelCall<'_> {
|
||||
pub fn into_owned(self) -> BabelCall<'static> {
|
||||
BabelCall {
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parse_keyword(input: &str) -> Option<(&str, (&str, Option<&str>, &str))> {
|
||||
parse_keyword_internal::<()>(input).ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_keyword_internal<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, (&str, Option<&str>, &str), E> {
|
||||
let (input, _) = tag("#+")(input)?;
|
||||
let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
|
||||
let (input, optional) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, _) = tag(":")(input)?;
|
||||
let (input, value) = line(input)?;
|
||||
|
||||
Ok((input, (key, optional, value.trim())))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+KEY:"),
|
||||
Ok(("", ("KEY", None, "")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+KEY: VALUE"),
|
||||
Ok(("", ("KEY", None, "VALUE")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+K_E_Y: VALUE"),
|
||||
Ok(("", ("K_E_Y", None, "VALUE")))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+KEY:VALUE\n"),
|
||||
Ok(("", ("KEY", None, "VALUE")))
|
||||
);
|
||||
assert!(parse_keyword_internal::<VerboseError<&str>>("#+KE Y: VALUE").is_err());
|
||||
assert!(parse_keyword_internal::<VerboseError<&str>>("#+ KEY: VALUE").is_err());
|
||||
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+RESULTS:"),
|
||||
Ok(("", ("RESULTS", None, "")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+ATTR_LATEX: :width 5cm\n"),
|
||||
Ok(("", ("ATTR_LATEX", None, ":width 5cm")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+CALL: double(n=4)"),
|
||||
Ok(("", ("CALL", None, "double(n=4)")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_keyword_internal::<VerboseError<&str>>("#+CAPTION[Short caption]: Longer caption."),
|
||||
Ok(("", ("CAPTION", Some("Short caption"), "Longer caption.",)))
|
||||
);
|
||||
}
|
||||
83
src/elements/link.rs
Normal file
83
src/elements/link.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::opt,
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Link Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Link<'a> {
|
||||
/// Link destination
|
||||
pub path: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub desc: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl Link<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Link)> {
|
||||
parse_link::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Link<'static> {
|
||||
Link {
|
||||
path: self.path.into_owned().into(),
|
||||
desc: self.desc.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_link<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Link, E> {
|
||||
let (input, path) = delimited(
|
||||
tag("[["),
|
||||
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
|
||||
tag("]"),
|
||||
)(input)?;
|
||||
let (input, desc) = opt(delimited(
|
||||
tag("["),
|
||||
take_while(|c: char| c != '[' && c != ']'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, _) = tag("]")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Link {
|
||||
path: path.into(),
|
||||
desc: desc.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_link::<VerboseError<&str>>("[[#id]]"),
|
||||
Ok((
|
||||
"",
|
||||
Link {
|
||||
path: "#id".into(),
|
||||
desc: None
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_link::<VerboseError<&str>>("[[#id][desc]]"),
|
||||
Ok((
|
||||
"",
|
||||
Link {
|
||||
path: "#id".into(),
|
||||
desc: Some("desc".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(parse_link::<VerboseError<&str>>("[[#id][desc]").is_err());
|
||||
}
|
||||
255
src/elements/list.rs
Normal file
255
src/elements/list.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
use std::borrow::Cow;
|
||||
use std::iter::once;
|
||||
|
||||
use memchr::memchr_iter;
|
||||
|
||||
/// Plain List Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct List {
|
||||
pub indent: usize,
|
||||
pub ordered: bool,
|
||||
}
|
||||
|
||||
impl List {
|
||||
#[inline]
|
||||
pub(crate) fn parse(text: &str) -> Option<(&str, List, &str)> {
|
||||
let (indent, tail) = text
|
||||
.find(|c| c != ' ')
|
||||
.map(|off| (off, &text[off..]))
|
||||
.unwrap_or((0, text));
|
||||
|
||||
let ordered = is_item(tail)?;
|
||||
|
||||
let mut last_end = 0;
|
||||
let mut start = 0;
|
||||
|
||||
for i in memchr_iter(b'\n', text.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.chain(once(text.len()))
|
||||
{
|
||||
let line = &text[start..i];
|
||||
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
|
||||
if line_indent < indent
|
||||
|| (line_indent == indent && is_item(&line[line_indent..]).is_none())
|
||||
{
|
||||
return Some((
|
||||
&text[start..],
|
||||
List { indent, ordered },
|
||||
&text[0..start - 1],
|
||||
));
|
||||
} else {
|
||||
last_end = 0;
|
||||
start = i;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// this line is empty
|
||||
if last_end != 0 {
|
||||
return Some((&text[i..], List { indent, ordered }, &text[0..last_end]));
|
||||
} else {
|
||||
last_end = start;
|
||||
start = i;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_end != 0 {
|
||||
Some(("", List { indent, ordered }, &text[0..last_end]))
|
||||
} else {
|
||||
Some(("", List { indent, ordered }, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List Item Elemenet
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct ListItem<'a> {
|
||||
/// List item bullet
|
||||
pub bullet: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl ListItem<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(text: &str, indent: usize) -> (&str, ListItem, &str) {
|
||||
debug_assert!(&text[0..indent].trim().is_empty());
|
||||
let off = &text[indent..].find(' ').unwrap() + 1 + indent;
|
||||
|
||||
let bytes = text.as_bytes();
|
||||
let mut lines = memchr_iter(b'\n', bytes)
|
||||
.map(|i| i + 1)
|
||||
.chain(once(text.len()));
|
||||
let mut pos = lines.next().unwrap();
|
||||
|
||||
for i in lines {
|
||||
let line = &text[pos..i];
|
||||
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
|
||||
if line_indent == indent {
|
||||
return (
|
||||
&text[pos..],
|
||||
ListItem {
|
||||
bullet: text[indent..off].into(),
|
||||
},
|
||||
&text[off..pos],
|
||||
);
|
||||
}
|
||||
}
|
||||
pos = i;
|
||||
}
|
||||
|
||||
(
|
||||
"",
|
||||
ListItem {
|
||||
bullet: text[indent..off].into(),
|
||||
},
|
||||
&text[off..],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> ListItem<'static> {
|
||||
ListItem {
|
||||
bullet: self.bullet.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_item(text: &str) -> Option<bool> {
|
||||
let bytes = text.as_bytes();
|
||||
match bytes.get(0)? {
|
||||
b'*' | b'-' | b'+' => {
|
||||
if text.len() > 1 && (bytes[1] == b' ' || bytes[1] == b'\n') {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
b'0'..=b'9' => {
|
||||
let i = bytes
|
||||
.iter()
|
||||
.position(|&c| !c.is_ascii_digit())
|
||||
.unwrap_or_else(|| text.len() - 1);
|
||||
if (bytes[i] == b'.' || bytes[i] == b')')
|
||||
&& text.len() > i + 1
|
||||
&& (bytes[i + 1] == b' ' || bytes[i + 1] == b'\n')
|
||||
{
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_item() {
|
||||
assert_eq!(is_item("+ item"), Some(false));
|
||||
assert_eq!(is_item("- item"), Some(false));
|
||||
assert_eq!(is_item("10. item"), Some(true));
|
||||
assert_eq!(is_item("10) item"), Some(true));
|
||||
assert_eq!(is_item("1. item"), Some(true));
|
||||
assert_eq!(is_item("1) item"), Some(true));
|
||||
assert_eq!(is_item("10. "), Some(true));
|
||||
assert_eq!(is_item("10.\n"), Some(true));
|
||||
assert_eq!(is_item("10."), None);
|
||||
assert_eq!(is_item("+"), None);
|
||||
assert_eq!(is_item("-item"), None);
|
||||
assert_eq!(is_item("+item"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_parse() {
|
||||
assert_eq!(
|
||||
List::parse("+ item1\n+ item2"),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"+ item1\n+ item2"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("* item1\n \n* item2"),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"* item1\n \n* item2"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("* item1\n \n \n* item2"),
|
||||
Some((
|
||||
"* item2",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"* item1\n"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("* item1\n \n "),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"* item1\n"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("+ item1\n + item2\n "),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"+ item1\n + item2\n"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("+ item1\n \n + item2\n \n+ item 3"),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"+ item1\n \n + item2\n \n+ item 3"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse(" + item1\n \n + item2"),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 2,
|
||||
ordered: false,
|
||||
},
|
||||
" + item1\n \n + item2"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
List::parse("+ 1\n\n - 2\n\n - 3\n\n+ 4"),
|
||||
Some((
|
||||
"",
|
||||
List {
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
"+ 1\n\n - 2\n\n - 3\n\n+ 4"
|
||||
))
|
||||
);
|
||||
}
|
||||
93
src/elements/macros.rs
Normal file
93
src/elements/macros.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_until, take_while1},
|
||||
combinator::{opt, verify},
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Macro Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Macros<'a> {
|
||||
/// Macro name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Arguments passed to the macro
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub arguments: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl Macros<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Macros)> {
|
||||
parse_macros::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Macros<'static> {
|
||||
Macros {
|
||||
name: self.name.into_owned().into(),
|
||||
arguments: self.arguments.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_macros<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Macros, E> {
|
||||
let (input, _) = tag("{{{")(input)?;
|
||||
let (input, name) = verify(
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
|s: &str| s.starts_with(|c: char| c.is_ascii_alphabetic()),
|
||||
)(input)?;
|
||||
let (input, arguments) = opt(delimited(tag("("), take_until(")}}}"), take(1usize)))(input)?;
|
||||
let (input, _) = tag("}}}")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Macros {
|
||||
name: name.into(),
|
||||
arguments: arguments.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_macros::<VerboseError<&str>>("{{{poem(red,blue)}}}"),
|
||||
Ok((
|
||||
"",
|
||||
Macros {
|
||||
name: "poem".into(),
|
||||
arguments: Some("red,blue".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_macros::<VerboseError<&str>>("{{{poem())}}}"),
|
||||
Ok((
|
||||
"",
|
||||
Macros {
|
||||
name: "poem".into(),
|
||||
arguments: Some(")".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_macros::<VerboseError<&str>>("{{{author}}}"),
|
||||
Ok((
|
||||
"",
|
||||
Macros {
|
||||
name: "author".into(),
|
||||
arguments: None
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(parse_macros::<VerboseError<&str>>("{{{0uthor}}}").is_err());
|
||||
assert!(parse_macros::<VerboseError<&str>>("{{{author}}").is_err());
|
||||
assert!(parse_macros::<VerboseError<&str>>("{{{poem(}}}").is_err());
|
||||
assert!(parse_macros::<VerboseError<&str>>("{{{poem)}}}").is_err());
|
||||
}
|
||||
243
src/elements/mod.rs
Normal file
243
src/elements/mod.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
//! Org-mode elements
|
||||
|
||||
pub(crate) mod block;
|
||||
pub(crate) mod clock;
|
||||
pub(crate) mod cookie;
|
||||
pub(crate) mod drawer;
|
||||
pub(crate) mod dyn_block;
|
||||
pub(crate) mod emphasis;
|
||||
pub(crate) mod fn_def;
|
||||
pub(crate) mod fn_ref;
|
||||
pub(crate) mod inline_call;
|
||||
pub(crate) mod inline_src;
|
||||
pub(crate) mod keyword;
|
||||
pub(crate) mod link;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod macros;
|
||||
pub(crate) mod planning;
|
||||
pub(crate) mod radio_target;
|
||||
pub(crate) mod rule;
|
||||
pub(crate) mod snippet;
|
||||
pub(crate) mod table;
|
||||
pub(crate) mod target;
|
||||
pub(crate) mod timestamp;
|
||||
pub(crate) mod title;
|
||||
|
||||
pub use self::{
|
||||
block::{
|
||||
CenterBlock, CommentBlock, ExampleBlock, ExportBlock, QuoteBlock, SourceBlock,
|
||||
SpecialBlock, VerseBlock,
|
||||
},
|
||||
clock::Clock,
|
||||
cookie::Cookie,
|
||||
drawer::Drawer,
|
||||
dyn_block::DynBlock,
|
||||
fn_def::FnDef,
|
||||
fn_ref::FnRef,
|
||||
inline_call::InlineCall,
|
||||
inline_src::InlineSrc,
|
||||
keyword::{BabelCall, Keyword},
|
||||
link::Link,
|
||||
list::{List, ListItem},
|
||||
macros::Macros,
|
||||
planning::Planning,
|
||||
snippet::Snippet,
|
||||
table::{Table, TableRow},
|
||||
target::Target,
|
||||
timestamp::{Datetime, Timestamp},
|
||||
title::Title,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Orgize Element Enum
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "type", rename_all = "kebab-case"))]
|
||||
pub enum Element<'a> {
|
||||
SpecialBlock(SpecialBlock<'a>),
|
||||
QuoteBlock(QuoteBlock<'a>),
|
||||
CenterBlock(CenterBlock<'a>),
|
||||
VerseBlock(VerseBlock<'a>),
|
||||
CommentBlock(CommentBlock<'a>),
|
||||
ExampleBlock(ExampleBlock<'a>),
|
||||
ExportBlock(ExportBlock<'a>),
|
||||
SourceBlock(SourceBlock<'a>),
|
||||
BabelCall(BabelCall<'a>),
|
||||
Section,
|
||||
Clock(Clock<'a>),
|
||||
Cookie(Cookie<'a>),
|
||||
RadioTarget,
|
||||
Drawer(Drawer<'a>),
|
||||
Document,
|
||||
DynBlock(DynBlock<'a>),
|
||||
FnDef(FnDef<'a>),
|
||||
FnRef(FnRef<'a>),
|
||||
Headline { level: usize },
|
||||
InlineCall(InlineCall<'a>),
|
||||
InlineSrc(InlineSrc<'a>),
|
||||
Keyword(Keyword<'a>),
|
||||
Link(Link<'a>),
|
||||
List(List),
|
||||
ListItem(ListItem<'a>),
|
||||
Macros(Macros<'a>),
|
||||
Snippet(Snippet<'a>),
|
||||
Text { value: Cow<'a, str> },
|
||||
Paragraph,
|
||||
Rule,
|
||||
Timestamp(Timestamp<'a>),
|
||||
Target(Target<'a>),
|
||||
Bold,
|
||||
Strike,
|
||||
Italic,
|
||||
Underline,
|
||||
Verbatim { value: Cow<'a, str> },
|
||||
Code { value: Cow<'a, str> },
|
||||
Comment { value: Cow<'a, str> },
|
||||
FixedWidth { value: Cow<'a, str> },
|
||||
Title(Title<'a>),
|
||||
Table(Table<'a>),
|
||||
TableRow(TableRow),
|
||||
TableCell,
|
||||
}
|
||||
|
||||
impl Element<'_> {
|
||||
pub fn is_container(&self) -> bool {
|
||||
use Element::*;
|
||||
|
||||
match self {
|
||||
SpecialBlock(_)
|
||||
| QuoteBlock(_)
|
||||
| CenterBlock(_)
|
||||
| VerseBlock(_)
|
||||
| Bold
|
||||
| Document
|
||||
| DynBlock(_)
|
||||
| Headline { .. }
|
||||
| Italic
|
||||
| List(_)
|
||||
| ListItem(_)
|
||||
| Paragraph
|
||||
| Section
|
||||
| Strike
|
||||
| Underline
|
||||
| Title(_)
|
||||
| Table(_)
|
||||
| TableRow(_)
|
||||
| TableCell => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Element<'static> {
|
||||
use Element::*;
|
||||
|
||||
match self {
|
||||
SpecialBlock(e) => SpecialBlock(e.into_owned()),
|
||||
QuoteBlock(e) => QuoteBlock(e.into_owned()),
|
||||
CenterBlock(e) => CenterBlock(e.into_owned()),
|
||||
VerseBlock(e) => VerseBlock(e.into_owned()),
|
||||
CommentBlock(e) => CommentBlock(e.into_owned()),
|
||||
ExampleBlock(e) => ExampleBlock(e.into_owned()),
|
||||
ExportBlock(e) => ExportBlock(e.into_owned()),
|
||||
SourceBlock(e) => SourceBlock(e.into_owned()),
|
||||
BabelCall(e) => BabelCall(e.into_owned()),
|
||||
Section => Section,
|
||||
Clock(e) => Clock(e.into_onwed()),
|
||||
Cookie(e) => Cookie(e.into_owned()),
|
||||
RadioTarget => RadioTarget,
|
||||
Drawer(e) => Drawer(e.into_owned()),
|
||||
Document => Document,
|
||||
DynBlock(e) => DynBlock(e.into_owned()),
|
||||
FnDef(e) => FnDef(e.into_owned()),
|
||||
FnRef(e) => FnRef(e.into_owned()),
|
||||
Headline { level } => Headline { level },
|
||||
InlineCall(e) => InlineCall(e.into_owned()),
|
||||
InlineSrc(e) => InlineSrc(e.into_owned()),
|
||||
Keyword(e) => Keyword(e.into_owned()),
|
||||
Link(e) => Link(e.into_owned()),
|
||||
List(e) => List(e),
|
||||
ListItem(e) => ListItem(e.into_owned()),
|
||||
Macros(e) => Macros(e.into_owned()),
|
||||
Snippet(e) => Snippet(e.into_owned()),
|
||||
Text { value } => Text {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Paragraph => Paragraph,
|
||||
Rule => Rule,
|
||||
Timestamp(e) => Timestamp(e.into_owned()),
|
||||
Target(e) => Target(e.into_owned()),
|
||||
Bold => Bold,
|
||||
Strike => Strike,
|
||||
Italic => Italic,
|
||||
Underline => Underline,
|
||||
Verbatim { value } => Verbatim {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Code { value } => Code {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Comment { value } => Comment {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
FixedWidth { value } => FixedWidth {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Title(e) => Title(e.into_owned()),
|
||||
Table(e) => Table(e.into_owned()),
|
||||
TableRow(e) => TableRow(e),
|
||||
TableCell => TableCell,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($($ele0:ident),*; $($ele1:ident),*) => {
|
||||
$(
|
||||
impl<'a> From<$ele0<'a>> for Element<'a> {
|
||||
fn from(ele: $ele0<'a>) -> Element<'a> {
|
||||
Element::$ele0(ele)
|
||||
}
|
||||
}
|
||||
)*
|
||||
$(
|
||||
impl<'a> From<$ele1> for Element<'a> {
|
||||
fn from(ele: $ele1) -> Element<'a> {
|
||||
Element::$ele1(ele)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(
|
||||
BabelCall,
|
||||
CenterBlock,
|
||||
Clock,
|
||||
CommentBlock,
|
||||
Cookie,
|
||||
Drawer,
|
||||
DynBlock,
|
||||
ExampleBlock,
|
||||
ExportBlock,
|
||||
FnDef,
|
||||
FnRef,
|
||||
InlineCall,
|
||||
InlineSrc,
|
||||
Keyword,
|
||||
Link,
|
||||
ListItem,
|
||||
Macros,
|
||||
QuoteBlock,
|
||||
Snippet,
|
||||
SourceBlock,
|
||||
SpecialBlock,
|
||||
Target,
|
||||
Timestamp,
|
||||
Table,
|
||||
Title,
|
||||
VerseBlock;
|
||||
List,
|
||||
TableRow
|
||||
);
|
||||
98
src/elements/planning.rs
Normal file
98
src/elements/planning.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use memchr::memchr;
|
||||
|
||||
use crate::elements::Timestamp;
|
||||
|
||||
/// Palnning element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Planning<'a> {
|
||||
/// Timestamp associated to deadline keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub deadline: Option<Timestamp<'a>>,
|
||||
/// Timestamp associated to scheduled keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub scheduled: Option<Timestamp<'a>>,
|
||||
/// Timestamp associated to closed keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub closed: Option<Timestamp<'a>>,
|
||||
}
|
||||
|
||||
impl Planning<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(text: &str) -> Option<(&str, Planning)> {
|
||||
let (mut deadline, mut scheduled, mut closed) = (None, None, None);
|
||||
let (mut tail, off) = memchr(b'\n', text.as_bytes())
|
||||
.map(|i| (text[..i].trim(), i + 1))
|
||||
.unwrap_or_else(|| (text.trim(), text.len()));
|
||||
|
||||
while let Some(i) = memchr(b' ', tail.as_bytes()) {
|
||||
let next = &tail[i + 1..].trim_start();
|
||||
|
||||
macro_rules! set_timestamp {
|
||||
($timestamp:expr) => {{
|
||||
let (new_tail, timestamp) =
|
||||
Timestamp::parse_active(next).or(Timestamp::parse_inactive(next))?;
|
||||
$timestamp = Some(timestamp);
|
||||
tail = new_tail.trim_start();
|
||||
}};
|
||||
}
|
||||
|
||||
match &tail[..i] {
|
||||
"DEADLINE:" if deadline.is_none() => set_timestamp!(deadline),
|
||||
"SCHEDULED:" if scheduled.is_none() => set_timestamp!(scheduled),
|
||||
"CLOSED:" if closed.is_none() => set_timestamp!(closed),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
if deadline.is_none() && scheduled.is_none() && closed.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&text[off..],
|
||||
Planning {
|
||||
deadline,
|
||||
scheduled,
|
||||
closed,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Planning<'static> {
|
||||
Planning {
|
||||
deadline: self.deadline.map(|x| x.into_owned()),
|
||||
scheduled: self.scheduled.map(|x| x.into_owned()),
|
||||
closed: self.closed.map(|x| x.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prase() {
|
||||
use crate::elements::Datetime;
|
||||
|
||||
assert_eq!(
|
||||
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
|
||||
Some((
|
||||
"",
|
||||
Planning {
|
||||
scheduled: Some(Timestamp::Active {
|
||||
start: Datetime {
|
||||
year: 2019,
|
||||
month: 4,
|
||||
day: 8,
|
||||
dayname: "Mon".into(),
|
||||
hour: None,
|
||||
minute: None
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
}),
|
||||
deadline: None,
|
||||
closed: None,
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
50
src/elements/radio_target.rs
Normal file
50
src/elements/radio_target.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::verify,
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
// TODO: text-markup, entities, latex-fragments, subscript and superscript
|
||||
|
||||
#[inline]
|
||||
pub fn parse_radio_target(input: &str) -> Option<(&str, &str)> {
|
||||
parse_radio_target_internal::<()>(input).ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_radio_target_internal<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, &str, E> {
|
||||
let (input, contents) = delimited(
|
||||
tag("<<<"),
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
|
||||
),
|
||||
tag(">>>"),
|
||||
)(input)?;
|
||||
|
||||
Ok((input, contents))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_radio_target_internal::<VerboseError<&str>>("<<<target>>>"),
|
||||
Ok(("", "target"))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_radio_target_internal::<VerboseError<&str>>("<<<tar get>>>"),
|
||||
Ok(("", "tar get"))
|
||||
);
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<<target >>>").is_err());
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<< target>>>").is_err());
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<<ta<get>>>").is_err());
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<<ta>get>>>").is_err());
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<<ta\nget>>>").is_err());
|
||||
assert!(parse_radio_target_internal::<VerboseError<&str>>("<<<target>>").is_err());
|
||||
}
|
||||
49
src/elements/rule.rs
Normal file
49
src/elements/rule.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use std::usize;
|
||||
|
||||
use nom::{bytes::complete::take_while_m_n, error::ParseError, IResult};
|
||||
|
||||
use crate::parsers::eol;
|
||||
|
||||
pub fn parse_rule(input: &str) -> Option<&str> {
|
||||
parse_rule_internal::<()>(input)
|
||||
.ok()
|
||||
.map(|(input, _)| input)
|
||||
}
|
||||
|
||||
fn parse_rule_internal<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, (), E> {
|
||||
let (input, _) = take_while_m_n(5, usize::MAX, |c| c == '-')(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
Ok((input, ()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_rule_internal::<VerboseError<&str>>("-----"),
|
||||
Ok(("", ()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rule_internal::<VerboseError<&str>>("--------"),
|
||||
Ok(("", ()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rule_internal::<VerboseError<&str>>("-----\n"),
|
||||
Ok(("", ()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rule_internal::<VerboseError<&str>>("----- \n"),
|
||||
Ok(("", ()))
|
||||
);
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("None----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("None ----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("None------").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("----None----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("\t\t----").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("------None").is_err());
|
||||
assert!(parse_rule_internal::<VerboseError<&str>>("----- None").is_err());
|
||||
}
|
||||
102
src/elements/snippet.rs
Normal file
102
src/elements/snippet.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_until, take_while1},
|
||||
error::ParseError,
|
||||
sequence::{delimited, separated_pair},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Export Snippet Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Snippet<'a> {
|
||||
/// Back-end name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Export code
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Snippet<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Snippet)> {
|
||||
parse_snippet::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Snippet<'static> {
|
||||
Snippet {
|
||||
name: self.name.into_owned().into(),
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_snippet<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Snippet, E> {
|
||||
let (input, (name, value)) = delimited(
|
||||
tag("@@"),
|
||||
separated_pair(
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
|
||||
tag(":"),
|
||||
take_until("@@"),
|
||||
),
|
||||
take(2usize),
|
||||
)(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Snippet {
|
||||
name: name.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_snippet::<VerboseError<&str>>("@@html:<b>@@"),
|
||||
Ok((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "<b>".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_snippet::<VerboseError<&str>>("@@latex:any arbitrary LaTeX code@@"),
|
||||
Ok((
|
||||
"",
|
||||
Snippet {
|
||||
name: "latex".into(),
|
||||
value: "any arbitrary LaTeX code".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_snippet::<VerboseError<&str>>("@@html:@@"),
|
||||
Ok((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_snippet::<VerboseError<&str>>("@@html:<p>@</p>@@"),
|
||||
Ok((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "<p>@</p>".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(parse_snippet::<VerboseError<&str>>("@@html:<b>@").is_err());
|
||||
assert!(parse_snippet::<VerboseError<&str>>("@@html<b>@@").is_err());
|
||||
assert!(parse_snippet::<VerboseError<&str>>("@@:<b>@@").is_err());
|
||||
}
|
||||
102
src/elements/table.rs
Normal file
102
src/elements/table.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
combinator::{peek, verify},
|
||||
error::ParseError,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parsers::{line, take_lines_while};
|
||||
|
||||
/// Table Elemenet
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "table_type"))]
|
||||
pub enum Table<'a> {
|
||||
/// "org" type table
|
||||
#[cfg_attr(feature = "ser", serde(rename = "org"))]
|
||||
Org { tblfm: Option<Cow<'a, str>> },
|
||||
/// "table.el" type table
|
||||
#[cfg_attr(feature = "ser", serde(rename = "table.el"))]
|
||||
TableEl { value: Cow<'a, str> },
|
||||
}
|
||||
|
||||
impl Table<'_> {
|
||||
pub fn into_owned(self) -> Table<'static> {
|
||||
match self {
|
||||
Table::Org { tblfm } => Table::Org {
|
||||
tblfm: tblfm.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Table::TableEl { value } => Table::TableEl {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table Row Elemenet
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "table_row_type"))]
|
||||
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
|
||||
pub enum TableRow {
|
||||
Standard,
|
||||
Rule,
|
||||
}
|
||||
|
||||
impl TableRow {
|
||||
pub(crate) fn parse(input: &str) -> Option<TableRow> {
|
||||
if input.starts_with("|-") {
|
||||
Some(TableRow::Rule)
|
||||
} else if input.starts_with('|') {
|
||||
Some(TableRow::Standard)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_table_el(input: &str) -> Option<(&str, &str)> {
|
||||
parse_table_el_internal::<()>(input).ok()
|
||||
}
|
||||
|
||||
fn parse_table_el_internal<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&'a str, &'a str, E> {
|
||||
let (input, _) = peek(verify(line, |s: &str| {
|
||||
let s = s.trim();
|
||||
s.starts_with("+-") && s.as_bytes().iter().all(|&c| c == b'+' || c == b'-')
|
||||
}))(input)?;
|
||||
|
||||
let (input, content) =
|
||||
take_lines_while(|line| line.starts_with('|') || line.starts_with('+'))(input);
|
||||
|
||||
Ok((input, content))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_table_el_() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_table_el_internal::<VerboseError<&str>>(
|
||||
r#"+---+
|
||||
| |
|
||||
+---+
|
||||
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
r#"
|
||||
"#,
|
||||
r#"+---+
|
||||
| |
|
||||
+---+
|
||||
"#
|
||||
))
|
||||
);
|
||||
assert!(parse_table_el_internal::<VerboseError<&str>>("").is_err());
|
||||
assert!(parse_table_el_internal::<VerboseError<&str>>("+----|---").is_err());
|
||||
}
|
||||
80
src/elements/target.rs
Normal file
80
src/elements/target.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::verify,
|
||||
error::ParseError,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Target Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Target<'a> {
|
||||
/// Target ID
|
||||
pub target: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Target<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Target)> {
|
||||
parse_target::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Target<'static> {
|
||||
Target {
|
||||
target: self.target.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_target<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Target, E> {
|
||||
let (input, target) = delimited(
|
||||
tag("<<"),
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
|
||||
),
|
||||
tag(">>"),
|
||||
)(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Target {
|
||||
target: target.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_target::<VerboseError<&str>>("<<target>>"),
|
||||
Ok((
|
||||
"",
|
||||
Target {
|
||||
target: "target".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_target::<VerboseError<&str>>("<<tar get>>"),
|
||||
Ok((
|
||||
"",
|
||||
Target {
|
||||
target: "tar get".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(parse_target::<VerboseError<&str>>("<<target >>").is_err());
|
||||
assert!(parse_target::<VerboseError<&str>>("<< target>>").is_err());
|
||||
assert!(parse_target::<VerboseError<&str>>("<<ta<get>>").is_err());
|
||||
assert!(parse_target::<VerboseError<&str>>("<<ta>get>>").is_err());
|
||||
assert!(parse_target::<VerboseError<&str>>("<<ta\nget>>").is_err());
|
||||
assert!(parse_target::<VerboseError<&str>>("<<target>").is_err());
|
||||
}
|
||||
485
src/elements/timestamp.rs
Normal file
485
src/elements/timestamp.rs
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_till, take_while, take_while_m_n},
|
||||
character::complete::{space0, space1},
|
||||
combinator::{map, map_res, opt},
|
||||
error::ParseError,
|
||||
sequence::preceded,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Orgize Datetime Struct
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Datetime<'a> {
|
||||
pub year: u16,
|
||||
pub month: u8,
|
||||
pub day: u8,
|
||||
pub dayname: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub hour: Option<u8>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub minute: Option<u8>,
|
||||
}
|
||||
|
||||
impl Datetime<'_> {
|
||||
pub fn into_owned(self) -> Datetime<'static> {
|
||||
Datetime {
|
||||
year: self.year,
|
||||
month: self.month,
|
||||
day: self.day,
|
||||
dayname: self.dayname.into_owned().into(),
|
||||
hour: self.hour,
|
||||
minute: self.minute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod chrono {
|
||||
use super::Datetime;
|
||||
use chrono::*;
|
||||
|
||||
impl Into<NaiveDate> for Datetime<'_> {
|
||||
fn into(self) -> NaiveDate {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveTime> for Datetime<'_> {
|
||||
fn into(self) -> NaiveTime {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDateTime> for Datetime<'_> {
|
||||
fn into(self) -> NaiveDateTime {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<DateTime<Utc>> for Datetime<'_> {
|
||||
fn into(self) -> DateTime<Utc> {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDate> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveDate {
|
||||
NaiveDate::from_ymd(self.year.into(), self.month.into(), self.day.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveTime> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveTime {
|
||||
NaiveTime::from_hms(
|
||||
self.hour.unwrap_or_default().into(),
|
||||
self.minute.unwrap_or_default().into(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDateTime> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveDateTime {
|
||||
NaiveDateTime::new(self.into(), self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<DateTime<Utc>> for &Datetime<'_> {
|
||||
fn into(self) -> DateTime<Utc> {
|
||||
DateTime::from_utc(self.into(), Utc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamp Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "timestamp_type"))]
|
||||
#[derive(Debug)]
|
||||
pub enum Timestamp<'a> {
|
||||
Active {
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
Inactive {
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
ActiveRange {
|
||||
start: Datetime<'a>,
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
InactiveRange {
|
||||
start: Datetime<'a>,
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
Diary {
|
||||
value: Cow<'a, str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Timestamp<'_> {
|
||||
pub(crate) fn parse_active(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_active::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_inactive(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_inactive::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_diary(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_diary::<()>(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Timestamp<'static> {
|
||||
match self {
|
||||
Timestamp::Active {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::Active {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::Inactive {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::ActiveRange {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::InactiveRange {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::Diary { value } => Timestamp::Diary {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_active<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Timestamp, E> {
|
||||
let (input, _) = tag("<")(input)?;
|
||||
let (input, start) = parse_datetime(input)?;
|
||||
|
||||
if input.starts_with('-') {
|
||||
let (input, (hour, minute)) = parse_time(&input[1..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
let mut end = start.clone();
|
||||
end.hour = Some(hour);
|
||||
end.minute = Some(minute);
|
||||
return Ok((
|
||||
input,
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
|
||||
if input.starts_with("--<") {
|
||||
let (input, end) = parse_datetime(&input["--<".len()..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Active {
|
||||
start,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inactive<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Timestamp, E> {
|
||||
let (input, _) = tag("[")(input)?;
|
||||
let (input, start) = parse_datetime(input)?;
|
||||
|
||||
if input.starts_with('-') {
|
||||
let (input, (hour, minute)) = parse_time(&input[1..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
let mut end = start.clone();
|
||||
end.hour = Some(hour);
|
||||
end.minute = Some(minute);
|
||||
return Ok((
|
||||
input,
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
|
||||
if input.starts_with("--[") {
|
||||
let (input, end) = parse_datetime(&input["--[".len()..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_diary<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Timestamp, E> {
|
||||
let (input, _) = tag("<%%(")(input)?;
|
||||
let (input, value) = take_till(|c| c == ')' || c == '>' || c == '\n')(input)?;
|
||||
let (input, _) = tag(")>")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Diary {
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_time<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, (u8, u8), E> {
|
||||
let (input, hour) = map_res(take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), |num| {
|
||||
u8::from_str_radix(num, 10)
|
||||
})(input)?;
|
||||
let (input, _) = tag(":")(input)?;
|
||||
let (input, minute) = map_res(take(2usize), |num| u8::from_str_radix(num, 10))(input)?;
|
||||
Ok((input, (hour, minute)))
|
||||
}
|
||||
|
||||
fn parse_datetime<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, Datetime, E> {
|
||||
let parse_u8 = |num| u8::from_str_radix(num, 10);
|
||||
|
||||
let (input, year) = map_res(take(4usize), |num| u16::from_str_radix(num, 10))(input)?;
|
||||
let (input, _) = tag("-")(input)?;
|
||||
let (input, month) = map_res(take(2usize), parse_u8)(input)?;
|
||||
let (input, _) = tag("-")(input)?;
|
||||
let (input, day) = map_res(take(2usize), parse_u8)(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, dayname) = take_while(|c: char| {
|
||||
!c.is_ascii_whitespace()
|
||||
&& !c.is_ascii_digit()
|
||||
&& c != '+'
|
||||
&& c != '-'
|
||||
&& c != ']'
|
||||
&& c != '>'
|
||||
})(input)?;
|
||||
let (input, (hour, minute)) = map(opt(preceded(space1, parse_time)), |time| {
|
||||
(time.map(|t| t.0), time.map(|t| t.1))
|
||||
})(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Datetime {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
dayname: dayname.into(),
|
||||
hour,
|
||||
minute,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// TODO
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum RepeaterType {
|
||||
// Cumulate,
|
||||
// CatchUp,
|
||||
// Restart,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum DelayType {
|
||||
// All,
|
||||
// First,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum TimeUnit {
|
||||
// Hour,
|
||||
// Day,
|
||||
// Week,
|
||||
// Month,
|
||||
// Year,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub struct Repeater {
|
||||
// pub ty: RepeaterType,
|
||||
// pub value: usize,
|
||||
// pub unit: TimeUnit,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub struct Delay {
|
||||
// pub ty: DelayType,
|
||||
// pub value: usize,
|
||||
// pub unit: TimeUnit,
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_inactive::<VerboseError<&str>>("[2003-09-16 Tue]"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::Inactive {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: None,
|
||||
minute: None
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inactive::<VerboseError<&str>>("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::InactiveRange {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39),
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_active::<VerboseError<&str>>("<2003-09-16 Tue 09:39-10:39>"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::ActiveRange {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39),
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39),
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
},
|
||||
))
|
||||
);
|
||||
}
|
||||
431
src/elements/title.rs
Normal file
431
src/elements/title.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
//! Headline Title
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use memchr::memrchr;
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_until, take_while},
|
||||
character::complete::{anychar, space1},
|
||||
combinator::{map, map_parser, opt, verify},
|
||||
error::{ErrorKind, ParseError},
|
||||
multi::fold_many0,
|
||||
sequence::{delimited, preceded},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
use crate::config::ParseConfig;
|
||||
use crate::elements::{drawer::parse_drawer, Planning, Timestamp};
|
||||
use crate::parsers::{line, skip_empty_lines, take_one_word};
|
||||
|
||||
/// Title Elemenet
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Title<'a> {
|
||||
/// Headline level, number of stars
|
||||
pub level: usize,
|
||||
/// Headline priority cookie
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub priority: Option<char>,
|
||||
/// Headline title tags, including the sparated colons
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))]
|
||||
pub tags: Vec<Cow<'a, str>>,
|
||||
/// Headline title keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub keyword: Option<Cow<'a, str>>,
|
||||
/// Raw headline's text, without the stars and the tags
|
||||
pub raw: Cow<'a, str>,
|
||||
/// Planning elemenet associated to this headline
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub planning: Option<Box<Planning<'a>>>,
|
||||
/// Property drawer associated to this headline
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))]
|
||||
pub properties: HashMap<Cow<'a, str>, Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl Title<'_> {
|
||||
pub(crate) fn parse<'a>(
|
||||
input: &'a str,
|
||||
config: &ParseConfig,
|
||||
) -> Option<(&'a str, (Title<'a>, &'a str))> {
|
||||
parse_title::<()>(input, config).ok()
|
||||
}
|
||||
|
||||
// TODO: fn is_archived(&self) -> bool { }
|
||||
// TODO: fn is_commented(&self) -> bool { }
|
||||
// TODO: fn is_quoted(&self) -> bool { }
|
||||
// TODO: fn is_footnote_section(&self) -> bool { }
|
||||
|
||||
/// Returns this headline's closed timestamp, or `None` if not set.
|
||||
pub fn closed(&self) -> Option<&Timestamp> {
|
||||
self.planning
|
||||
.as_ref()
|
||||
.and_then(|planning| planning.closed.as_ref())
|
||||
}
|
||||
|
||||
/// Returns this headline's scheduled timestamp, or `None` if not set.
|
||||
pub fn scheduled(&self) -> Option<&Timestamp> {
|
||||
self.planning
|
||||
.as_ref()
|
||||
.and_then(|planning| planning.scheduled.as_ref())
|
||||
}
|
||||
|
||||
/// Returns this headline's deadline timestamp, or `None` if not set.
|
||||
pub fn deadline(&self) -> Option<&Timestamp> {
|
||||
self.planning
|
||||
.as_ref()
|
||||
.and_then(|planning| planning.deadline.as_ref())
|
||||
}
|
||||
|
||||
/// checks if this headline is "archived"
|
||||
pub fn is_archived(&self) -> bool {
|
||||
self.tags.iter().any(|tag| tag == "ARCHIVE")
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Title<'static> {
|
||||
Title {
|
||||
level: self.level,
|
||||
priority: self.priority,
|
||||
tags: self
|
||||
.tags
|
||||
.into_iter()
|
||||
.map(|s| s.into_owned().into())
|
||||
.collect(),
|
||||
keyword: self.keyword.map(Into::into).map(Cow::Owned),
|
||||
raw: self.raw.into_owned().into(),
|
||||
planning: self.planning.map(|p| Box::new(p.into_owned())),
|
||||
properties: self
|
||||
.properties
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into_owned().into(), v.into_owned().into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Title<'_> {
|
||||
fn default() -> Title<'static> {
|
||||
Title {
|
||||
level: 1,
|
||||
priority: None,
|
||||
tags: Vec::new(),
|
||||
keyword: None,
|
||||
raw: Cow::Borrowed(""),
|
||||
planning: None,
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_title<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
config: &ParseConfig,
|
||||
) -> IResult<&'a str, (Title<'a>, &'a str), E> {
|
||||
let (input, level) = map(take_while(|c: char| c == '*'), |s: &str| s.len())(input)?;
|
||||
|
||||
debug_assert!(level > 0);
|
||||
|
||||
let (input, keyword) = opt(preceded(
|
||||
space1,
|
||||
verify(take_one_word, |s: &str| {
|
||||
config.todo_keywords.iter().any(|x| x == s)
|
||||
|| config.done_keywords.iter().any(|x| x == s)
|
||||
}),
|
||||
))(input)?;
|
||||
|
||||
let (input, priority) = opt(preceded(
|
||||
space1,
|
||||
map_parser(
|
||||
take_one_word,
|
||||
delimited(
|
||||
tag("[#"),
|
||||
verify(anychar, |c: &char| c.is_ascii_uppercase()),
|
||||
tag("]"),
|
||||
),
|
||||
),
|
||||
))(input)?;
|
||||
let (input, tail) = line(input)?;
|
||||
let tail = tail.trim();
|
||||
let (raw, tags) = memrchr(b' ', tail.as_bytes())
|
||||
.map(|i| (tail[0..i].trim(), &tail[i + 1..]))
|
||||
.filter(|(_, x)| x.len() > 2 && x.starts_with(':') && x.ends_with(':'))
|
||||
.unwrap_or((tail, ""));
|
||||
|
||||
let tags = tags
|
||||
.split(':')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
let (input, planning) = Planning::parse(input)
|
||||
.map(|(input, planning)| (input, Some(Box::new(planning))))
|
||||
.unwrap_or((input, None));
|
||||
|
||||
let (input, properties) = opt(parse_properties_drawer)(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
Title {
|
||||
properties: properties.unwrap_or_default(),
|
||||
level,
|
||||
keyword: keyword.map(Into::into),
|
||||
priority,
|
||||
tags,
|
||||
raw: raw.into(),
|
||||
planning,
|
||||
},
|
||||
raw,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_properties_drawer<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, HashMap<Cow<'_, str>, Cow<'_, str>>, E> {
|
||||
let (input, (drawer, content)) = parse_drawer(input.trim_start())?;
|
||||
if drawer.name != "PROPERTIES" {
|
||||
return Err(Err::Error(E::from_error_kind(input, ErrorKind::Tag)));
|
||||
}
|
||||
let (_, map) = fold_many0(
|
||||
parse_node_property,
|
||||
HashMap::new(),
|
||||
|mut acc: HashMap<_, _>, (name, value)| {
|
||||
acc.insert(name.into(), value.into());
|
||||
acc
|
||||
},
|
||||
)(content)?;
|
||||
Ok((input, map))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_node_property<'a, E: ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&str, (&str, &str), E> {
|
||||
let input = skip_empty_lines(input).trim_start();
|
||||
let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| {
|
||||
s.trim_end_matches('+')
|
||||
})(input)?;
|
||||
let (input, value) = line(input)?;
|
||||
Ok((input, (name, value.trim())))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_title_() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
use crate::config::DEFAULT_CONFIG;
|
||||
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>(
|
||||
"**** DONE [#A] COMMENT Title :tag:a2%:",
|
||||
&DEFAULT_CONFIG
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: Some('A'),
|
||||
raw: "COMMENT Title".into(),
|
||||
tags: vec!["tag".into(), "a2%".into()],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** ToDO [#A] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "ToDO [#A] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"ToDO [#A] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** T0DO [#A] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "T0DO [#A] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"T0DO [#A] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** DONE [#1] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: None,
|
||||
raw: "[#1] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"[#1] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** DONE [#a] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: None,
|
||||
raw: "[#a] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"[#a] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** Title :tag:a2%", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "Title :tag:a2%".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"Title :tag:a2%"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>("**** Title tag:a2%:", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "Title tag:a2%:".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"Title tag:a2%:"
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>(
|
||||
"**** DONE Title",
|
||||
&ParseConfig {
|
||||
done_keywords: vec![],
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "DONE Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"DONE Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title::<VerboseError<&str>>(
|
||||
"**** TASK [#A] Title",
|
||||
&ParseConfig {
|
||||
todo_keywords: vec!["TASK".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("TASK".into()),
|
||||
priority: Some('A'),
|
||||
raw: "Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: HashMap::new()
|
||||
},
|
||||
"Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_properties_drawer_() {
|
||||
use nom::error::VerboseError;
|
||||
|
||||
assert_eq!(
|
||||
parse_properties_drawer::<VerboseError<&str>>(
|
||||
" :PROPERTIES:\n :CUSTOM_ID: id\n :END:"
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
vec![("CUSTOM_ID".into(), "id".into())]
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>()
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn is_commented() {
|
||||
// assert!(parse_title::<VerboseError<&str>>("* COMMENT Title", &DEFAULT_CONFIG)
|
||||
// .1
|
||||
// .is_commented());
|
||||
// assert!(!parse_title::<VerboseError<&str>>("* Title", &DEFAULT_CONFIG).1.is_commented());
|
||||
// assert!(!parse_title::<VerboseError<&str>>("* C0MMENT Title", &DEFAULT_CONFIG)
|
||||
// .1
|
||||
// .is_commented());
|
||||
// assert!(!parse_title::<VerboseError<&str>>("* comment Title", &DEFAULT_CONFIG)
|
||||
// .1
|
||||
// .is_commented());
|
||||
// }
|
||||
159
src/error.rs
Normal file
159
src/error.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
use indextree::NodeId;
|
||||
|
||||
use crate::elements::*;
|
||||
use crate::Org;
|
||||
|
||||
/// Orgize Validation Error
|
||||
#[derive(Debug)]
|
||||
pub enum OrgizeError {
|
||||
/// Expect this node has children
|
||||
Children { at: NodeId },
|
||||
/// Expect this node has no children
|
||||
NoChildren { at: NodeId },
|
||||
/// Expect this node contains a headline or section element
|
||||
HeadlineOrSection { at: NodeId },
|
||||
/// Expect this node contains a title element
|
||||
Title { at: NodeId },
|
||||
/// Expect this node contains a headline element
|
||||
Headline { at: NodeId },
|
||||
/// Expect a detached headline
|
||||
Detached { at: NodeId },
|
||||
/// Expect a headline where its level >= max and <= min
|
||||
HeadlineLevel {
|
||||
max: Option<usize>,
|
||||
min: Option<usize>,
|
||||
at: NodeId,
|
||||
},
|
||||
}
|
||||
|
||||
impl OrgizeError {
|
||||
pub fn element<'a, 'b>(&self, org: &'a Org<'b>) -> &'a Element<'b> {
|
||||
match &self {
|
||||
OrgizeError::Children { at }
|
||||
| OrgizeError::NoChildren { at }
|
||||
| OrgizeError::HeadlineOrSection { at }
|
||||
| OrgizeError::Title { at }
|
||||
| OrgizeError::Headline { at }
|
||||
| OrgizeError::Detached { at }
|
||||
| OrgizeError::HeadlineLevel { at, .. } => org.arena[*at].get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Org<'_> {
|
||||
/// Validate an `Org` struct.
|
||||
pub fn validate(&self) -> Result<(), OrgizeError> {
|
||||
for node_id in self.root.descendants(&self.arena) {
|
||||
let node = &self.arena[node_id];
|
||||
match node.get() {
|
||||
Element::Document => {
|
||||
for child_id in node_id.children(&self.arena) {
|
||||
match self.arena[child_id].get() {
|
||||
Element::Headline { .. } | Element::Section => (),
|
||||
_ => return Err(OrgizeError::HeadlineOrSection { at: child_id }),
|
||||
}
|
||||
}
|
||||
}
|
||||
Element::Headline { .. } => {
|
||||
if node.first_child().is_none() {
|
||||
return Err(OrgizeError::Children { at: node_id });
|
||||
}
|
||||
let title = node.first_child().unwrap();
|
||||
match self.arena[title].get() {
|
||||
Element::Title(Title { .. }) => (),
|
||||
_ => return Err(OrgizeError::Title { at: title }),
|
||||
}
|
||||
if let Some(next) = self.arena[title].next_sibling() {
|
||||
match self.arena[next].get() {
|
||||
Element::Headline { .. } | Element::Section => (),
|
||||
_ => return Err(OrgizeError::HeadlineOrSection { at: next }),
|
||||
}
|
||||
|
||||
for sibling in next.following_siblings(&self.arena).skip(1) {
|
||||
match self.arena[sibling].get() {
|
||||
Element::Headline { .. } => (),
|
||||
_ => return Err(OrgizeError::Headline { at: sibling }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Element::Title(Title { raw, .. }) => {
|
||||
if !raw.is_empty() && node.first_child().is_none() {
|
||||
return Err(OrgizeError::Children { at: node_id });
|
||||
}
|
||||
}
|
||||
Element::CommentBlock(_)
|
||||
| Element::ExampleBlock(_)
|
||||
| Element::ExportBlock(_)
|
||||
| Element::SourceBlock(_)
|
||||
| Element::BabelCall(_)
|
||||
| Element::InlineSrc(_)
|
||||
| Element::Code { .. }
|
||||
| Element::FnRef(_)
|
||||
| Element::InlineCall(_)
|
||||
| Element::Link(_)
|
||||
| Element::Macros(_)
|
||||
| Element::RadioTarget
|
||||
| Element::Snippet(_)
|
||||
| Element::Target(_)
|
||||
| Element::Text { .. }
|
||||
| Element::Timestamp(_)
|
||||
| Element::Verbatim { .. }
|
||||
| Element::FnDef(_)
|
||||
| Element::Clock(_)
|
||||
| Element::Comment { .. }
|
||||
| Element::FixedWidth { .. }
|
||||
| Element::Keyword(_)
|
||||
| Element::Rule
|
||||
| Element::Cookie(_)
|
||||
| Element::Table(Table::TableEl { .. })
|
||||
| Element::TableRow(TableRow::Rule) => {
|
||||
if node.first_child().is_some() {
|
||||
return Err(OrgizeError::NoChildren { at: node_id });
|
||||
}
|
||||
}
|
||||
Element::SpecialBlock(_)
|
||||
| Element::QuoteBlock(_)
|
||||
| Element::CenterBlock(_)
|
||||
| Element::VerseBlock(_)
|
||||
| Element::Paragraph
|
||||
| Element::Section
|
||||
| Element::Table(Table::Org { .. })
|
||||
| Element::TableRow(TableRow::Standard)
|
||||
| Element::Bold
|
||||
| Element::Italic
|
||||
| Element::Underline
|
||||
| Element::Strike
|
||||
| Element::DynBlock(_)
|
||||
| Element::List(_)
|
||||
| Element::ListItem(_) => {
|
||||
if node.first_child().is_none() {
|
||||
return Err(OrgizeError::Children { at: node_id });
|
||||
}
|
||||
}
|
||||
// TableCell is a container but it might
|
||||
// not contains anything, e.g. `||||||`
|
||||
Element::Drawer(_) | Element::TableCell => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[deprecated(since = "0.3.1", note = "rename to validate")]
|
||||
/// Validate an `Org` struct.
|
||||
pub fn check(&self) -> Result<(), OrgizeError> {
|
||||
self.validate()
|
||||
}
|
||||
|
||||
pub(crate) fn debug_validate(&self) {
|
||||
if cfg!(debug_assertions) {
|
||||
if let Err(err) = self.validate() {
|
||||
panic!(
|
||||
"Validation error: {:?} at element: {:?}",
|
||||
err,
|
||||
err.element(self)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
352
src/export/html.rs
Normal file
352
src/export/html.rs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
use std::fmt;
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use jetscii::{bytes, BytesConst};
|
||||
|
||||
use crate::elements::Element;
|
||||
use crate::export::write_datetime;
|
||||
|
||||
pub struct Escape<S: AsRef<str>>(pub S);
|
||||
|
||||
impl<S: AsRef<str>> fmt::Display for Escape<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut pos = 0;
|
||||
let bytes = self.0.as_ref().as_bytes();
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"');
|
||||
}
|
||||
|
||||
while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) {
|
||||
write!(f, "{}", &self.0.as_ref()[pos..pos + off])?;
|
||||
|
||||
pos += off + 1;
|
||||
|
||||
match bytes[pos - 1] {
|
||||
b'<' => write!(f, "<")?,
|
||||
b'>' => write!(f, ">")?,
|
||||
b'&' => write!(f, "&")?,
|
||||
b'\'' => write!(f, "'")?,
|
||||
b'"' => write!(f, """)?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
write!(f, "{}", &self.0.as_ref()[pos..])
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HtmlHandler<E: From<Error>>: Default {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
use Element::*;
|
||||
|
||||
match element {
|
||||
// container elements
|
||||
SpecialBlock(_) => (),
|
||||
QuoteBlock(_) => write!(w, "<blockquote>")?,
|
||||
CenterBlock(_) => write!(w, "<div class=\"center\">")?,
|
||||
VerseBlock(_) => write!(w, "<p class=\"verse\">")?,
|
||||
Bold => write!(w, "<b>")?,
|
||||
Document => write!(w, "<main>")?,
|
||||
DynBlock(_dyn_block) => (),
|
||||
Headline { .. } => (),
|
||||
List(list) => {
|
||||
if list.ordered {
|
||||
write!(w, "<ol>")?;
|
||||
} else {
|
||||
write!(w, "<ul>")?;
|
||||
}
|
||||
}
|
||||
Italic => write!(w, "<i>")?,
|
||||
ListItem(_) => write!(w, "<li>")?,
|
||||
Paragraph => write!(w, "<p>")?,
|
||||
Section => write!(w, "<section>")?,
|
||||
Strike => write!(w, "<s>")?,
|
||||
Underline => write!(w, "<u>")?,
|
||||
// non-container elements
|
||||
CommentBlock(_) => (),
|
||||
ExampleBlock(block) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
Escape(&block.contents)
|
||||
)?,
|
||||
ExportBlock(block) => {
|
||||
if block.data.eq_ignore_ascii_case("HTML") {
|
||||
write!(w, "{}", block.contents)?
|
||||
}
|
||||
}
|
||||
SourceBlock(block) => {
|
||||
if block.language.is_empty() {
|
||||
write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
Escape(&block.contents)
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
|
||||
block.language,
|
||||
Escape(&block.contents)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
BabelCall(_) => (),
|
||||
InlineSrc(inline_src) => write!(
|
||||
w,
|
||||
"<code class=\"src src-{}\">{}</code>",
|
||||
inline_src.lang,
|
||||
Escape(&inline_src.body)
|
||||
)?,
|
||||
Code { value } => write!(w, "<code>{}</code>", Escape(value))?,
|
||||
FnRef(_fn_ref) => (),
|
||||
InlineCall(_) => (),
|
||||
Link(link) => write!(
|
||||
w,
|
||||
"<a href=\"{}\">{}</a>",
|
||||
Escape(&link.path),
|
||||
Escape(link.desc.as_ref().unwrap_or(&link.path)),
|
||||
)?,
|
||||
Macros(_macros) => (),
|
||||
RadioTarget => (),
|
||||
Snippet(snippet) => {
|
||||
if snippet.name.eq_ignore_ascii_case("HTML") {
|
||||
write!(w, "{}", snippet.value)?;
|
||||
}
|
||||
}
|
||||
Target(_target) => (),
|
||||
Text { value } => write!(w, "{}", Escape(value))?,
|
||||
Timestamp(timestamp) => {
|
||||
use crate::elements::Timestamp;
|
||||
|
||||
write!(
|
||||
&mut w,
|
||||
"<span class=\"timestamp-wrapper\"><span class=\"timestamp\">"
|
||||
)?;
|
||||
|
||||
match timestamp {
|
||||
Timestamp::Active { start, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">")?;
|
||||
}
|
||||
Timestamp::Inactive { start, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]")?;
|
||||
}
|
||||
Timestamp::ActiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">–")?;
|
||||
write_datetime(&mut w, "<", end, ">")?;
|
||||
}
|
||||
Timestamp::InactiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]–")?;
|
||||
write_datetime(&mut w, "[", end, "]")?;
|
||||
}
|
||||
Timestamp::Diary { value } => write!(&mut w, "<%%({})>", Escape(value))?,
|
||||
}
|
||||
|
||||
write!(&mut w, "</span></span>")?;
|
||||
}
|
||||
Verbatim { value } => write!(&mut w, "<code>{}</code>", Escape(value))?,
|
||||
FnDef(_fn_def) => (),
|
||||
Clock(_clock) => (),
|
||||
Comment { .. } => (),
|
||||
FixedWidth { value } => write!(w, "<pre class=\"example\">{}</pre>", Escape(value))?,
|
||||
Keyword(_keyword) => (),
|
||||
Drawer(_drawer) => (),
|
||||
Rule => write!(w, "<hr>")?,
|
||||
Cookie(cookie) => write!(w, "<code>{}</code>", cookie.value)?,
|
||||
Title(title) => write!(w, "<h{}>", if title.level <= 6 { title.level } else { 6 })?,
|
||||
Table(_) => (),
|
||||
TableRow(_) => (),
|
||||
TableCell => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
use Element::*;
|
||||
|
||||
match element {
|
||||
// container elements
|
||||
SpecialBlock(_) => (),
|
||||
QuoteBlock(_) => write!(w, "</blockquote>")?,
|
||||
CenterBlock(_) => write!(w, "</div>")?,
|
||||
VerseBlock(_) => write!(w, "</p>")?,
|
||||
Bold => write!(w, "</b>")?,
|
||||
Document => write!(w, "</main>")?,
|
||||
DynBlock(_dyn_block) => (),
|
||||
Headline { .. } => (),
|
||||
List(list) => {
|
||||
if list.ordered {
|
||||
write!(w, "</ol>")?;
|
||||
} else {
|
||||
write!(w, "</ul>")?;
|
||||
}
|
||||
}
|
||||
Italic => write!(w, "</i>")?,
|
||||
ListItem(_) => write!(w, "</li>")?,
|
||||
Paragraph => write!(w, "</p>")?,
|
||||
Section => write!(w, "</section>")?,
|
||||
Strike => write!(w, "</s>")?,
|
||||
Underline => write!(w, "</u>")?,
|
||||
Title(title) => write!(w, "</h{}>", if title.level <= 6 { title.level } else { 6 })?,
|
||||
Table(_) => (),
|
||||
TableRow(_) => (),
|
||||
TableCell => (),
|
||||
// non-container elements
|
||||
_ => debug_assert!(!element.is_container()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Default Html Handler
|
||||
#[derive(Default)]
|
||||
pub struct DefaultHtmlHandler;
|
||||
|
||||
impl HtmlHandler<Error> for DefaultHtmlHandler {}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
mod syntect_handler {
|
||||
use super::*;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::ThemeSet,
|
||||
html::{styled_line_to_highlighted_html, IncludeBackground},
|
||||
parsing::SyntaxSet,
|
||||
};
|
||||
|
||||
/// Syntect Html Handler
|
||||
///
|
||||
/// Simple Usage:
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::Org;
|
||||
/// use orgize::export::html::{DefaultHtmlHandler, SyntectHtmlHandler};
|
||||
///
|
||||
/// let mut handler = SyntectHtmlHandler::new(DefaultHtmlHandler);
|
||||
/// let org = Org::parse("src_rust{println!(\"Hello\")}");
|
||||
///
|
||||
/// let mut vec = vec![];
|
||||
///
|
||||
/// org.html_with_handler(&mut vec, &mut handler).unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// Customize:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// // orgize has re-exported the whole syntect crate
|
||||
/// use orgize::syntect::parsing::SyntaxSet;
|
||||
/// use orgize::export::html::{DefaultHtmlHandler, SyntectHtmlHandler};
|
||||
///
|
||||
/// let mut handler = SyntectHtmlHandler {
|
||||
/// syntax_set: {
|
||||
/// let set = SyntaxSet::load_defaults_newlines();
|
||||
/// let mut builder = set.into_builder();
|
||||
/// // add extra language syntax
|
||||
/// builder.add_from_folder("path/to/syntax/dir", true).unwrap();
|
||||
/// builder.build()
|
||||
/// },
|
||||
/// // specify theme
|
||||
/// theme: String::from("Solarized (dark)"),
|
||||
/// inner: DefaultHtmlHandler,
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// // Make sure to check if theme presents or it will painc at runtime
|
||||
/// if handler.theme_set.themes.contains_key("dont-exists") {
|
||||
///
|
||||
/// }
|
||||
/// ```
|
||||
pub struct SyntectHtmlHandler<E: From<Error>, H: HtmlHandler<E>> {
|
||||
/// syntax set, default is `SyntaxSet::load_defaults_newlines()`
|
||||
pub syntax_set: SyntaxSet,
|
||||
/// theme set, default is `ThemeSet::load_defaults()`
|
||||
pub theme_set: ThemeSet,
|
||||
/// theme used for highlighting, default is `"InspiredGitHub"`
|
||||
pub theme: String,
|
||||
/// inner html handler
|
||||
pub inner: H,
|
||||
/// background color, default is `IncludeBackground::No`
|
||||
pub background: IncludeBackground,
|
||||
/// handler error type
|
||||
pub error_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> SyntectHtmlHandler<E, H> {
|
||||
pub fn new(inner: H) -> Self {
|
||||
SyntectHtmlHandler {
|
||||
inner,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight(&self, language: Option<&str>, content: &str) -> String {
|
||||
let mut highlighter = HighlightLines::new(
|
||||
language
|
||||
.and_then(|lang| self.syntax_set.find_syntax_by_token(lang))
|
||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()),
|
||||
&self.theme_set.themes[&self.theme],
|
||||
);
|
||||
let regions = highlighter.highlight(content, &self.syntax_set);
|
||||
styled_line_to_highlighted_html(®ions[..], self.background)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> Default for SyntectHtmlHandler<E, H> {
|
||||
fn default() -> Self {
|
||||
SyntectHtmlHandler {
|
||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
theme: String::from("InspiredGitHub"),
|
||||
inner: H::default(),
|
||||
background: IncludeBackground::No,
|
||||
error_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> HtmlHandler<E> for SyntectHtmlHandler<E, H> {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
match element {
|
||||
Element::InlineSrc(inline_src) => write!(
|
||||
w,
|
||||
"<code>{}</code>",
|
||||
self.highlight(Some(&inline_src.lang), &inline_src.body)
|
||||
)?,
|
||||
Element::SourceBlock(block) => {
|
||||
if block.language.is_empty() {
|
||||
write!(w, "<pre class=\"example\">{}</pre>", block.contents)?;
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
|
||||
block.language,
|
||||
self.highlight(Some(&block.language), &block.contents)
|
||||
)?
|
||||
}
|
||||
}
|
||||
Element::FixedWidth { value } => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
self.highlight(None, value)
|
||||
)?,
|
||||
Element::ExampleBlock(block) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
self.highlight(None, &block.contents)
|
||||
)?,
|
||||
_ => self.inner.start(w, element)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E> {
|
||||
self.inner.end(w, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use syntect_handler::SyntectHtmlHandler;
|
||||
29
src/export/mod.rs
Normal file
29
src/export/mod.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! Export `Org` struct to various formats.
|
||||
|
||||
pub mod html;
|
||||
pub mod org;
|
||||
|
||||
pub use html::*;
|
||||
pub use org::*;
|
||||
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use crate::elements::Datetime;
|
||||
|
||||
pub(crate) fn write_datetime<W: Write>(
|
||||
mut w: W,
|
||||
start: &str,
|
||||
datetime: &Datetime,
|
||||
end: &str,
|
||||
) -> Result<(), Error> {
|
||||
write!(w, "{}", start)?;
|
||||
write!(
|
||||
w,
|
||||
"{}-{:02}-{:02} {}",
|
||||
datetime.year, datetime.month, datetime.day, datetime.dayname
|
||||
)?;
|
||||
if let (Some(hour), Some(minute)) = (datetime.hour, datetime.minute) {
|
||||
write!(w, " {:02}:{:02}", hour, minute)?;
|
||||
}
|
||||
write!(w, "{}", end)
|
||||
}
|
||||
239
src/export/org.rs
Normal file
239
src/export/org.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use std::io::{Error, Write};
|
||||
|
||||
use crate::elements::{Element, Timestamp};
|
||||
use crate::export::write_datetime;
|
||||
|
||||
pub trait OrgHandler<E: From<Error>>: Default {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
use Element::*;
|
||||
|
||||
match element {
|
||||
// container elements
|
||||
SpecialBlock(block) => writeln!(w, "#+BEGIN_{}", block.name)?,
|
||||
QuoteBlock(_) => write!(w, "#+BEGIN_QUOTE")?,
|
||||
CenterBlock(_) => write!(w, "#+BEGIN_CENTER")?,
|
||||
VerseBlock(_) => write!(w, "#+BEGIN_VERSE")?,
|
||||
Bold => write!(w, "*")?,
|
||||
Document => (),
|
||||
DynBlock(dyn_block) => {
|
||||
write!(&mut w, "#+BEGIN: {}", dyn_block.block_name)?;
|
||||
if let Some(parameters) = &dyn_block.arguments {
|
||||
write!(&mut w, " {}", parameters)?;
|
||||
}
|
||||
writeln!(&mut w)?;
|
||||
}
|
||||
Headline { .. } => (),
|
||||
List(_list) => (),
|
||||
Italic => write!(w, "/")?,
|
||||
ListItem(list_item) => write!(w, "{}", list_item.bullet)?,
|
||||
Paragraph => (),
|
||||
Section => (),
|
||||
Strike => write!(w, "+")?,
|
||||
Underline => write!(w, "_")?,
|
||||
Drawer(drawer) => writeln!(w, ":{}:", drawer.name)?,
|
||||
// non-container elements
|
||||
CommentBlock(block) => {
|
||||
writeln!(w, "#+BEGIN_COMMENT\n{}\n#+END_COMMENT", block.contents)?
|
||||
}
|
||||
ExampleBlock(block) => {
|
||||
writeln!(w, "#+BEGIN_EXAMPLE\n{}\n#+END_EXAMPLE", block.contents)?
|
||||
}
|
||||
ExportBlock(block) => writeln!(
|
||||
w,
|
||||
"#+BEGIN_EXPORT {}\n{}\n#+END_EXPORT",
|
||||
block.data, block.contents
|
||||
)?,
|
||||
SourceBlock(block) => writeln!(
|
||||
w,
|
||||
"#+BEGIN_SRC {}\n{}\n#+END_SRC",
|
||||
block.language, block.contents
|
||||
)?,
|
||||
BabelCall(_babel_call) => (),
|
||||
InlineSrc(inline_src) => {
|
||||
write!(&mut w, "src_{}", inline_src.lang)?;
|
||||
if let Some(options) = &inline_src.options {
|
||||
write!(&mut w, "[{}]", options)?;
|
||||
}
|
||||
write!(&mut w, "{{{}}}", inline_src.body)?;
|
||||
}
|
||||
Code { value } => write!(w, "~{}~", value)?,
|
||||
FnRef(fn_ref) => {
|
||||
write!(&mut w, "[fn:{}", fn_ref.label)?;
|
||||
if let Some(definition) = &fn_ref.definition {
|
||||
write!(&mut w, ":{}", definition)?;
|
||||
}
|
||||
write!(&mut w, "]")?;
|
||||
}
|
||||
InlineCall(inline_call) => {
|
||||
write!(&mut w, "call_{}", inline_call.name)?;
|
||||
if let Some(header) = &inline_call.inside_header {
|
||||
write!(&mut w, "[{}]", header)?;
|
||||
}
|
||||
write!(&mut w, "({})", inline_call.arguments)?;
|
||||
if let Some(header) = &inline_call.end_header {
|
||||
write!(&mut w, "[{}]", header)?;
|
||||
}
|
||||
}
|
||||
Link(link) => {
|
||||
write!(&mut w, "[[{}]", link.path)?;
|
||||
if let Some(desc) = &link.desc {
|
||||
write!(&mut w, "[{}]", desc)?;
|
||||
}
|
||||
write!(&mut w, "]")?;
|
||||
}
|
||||
Macros(_macros) => (),
|
||||
RadioTarget => (),
|
||||
Snippet(snippet) => write!(w, "@@{}:{}@@", snippet.name, snippet.value)?,
|
||||
Target(_target) => (),
|
||||
Text { value } => write!(w, "{}", value)?,
|
||||
Timestamp(timestamp) => {
|
||||
write_timestamp(&mut w, ×tamp)?;
|
||||
}
|
||||
Verbatim { value } => write!(w, "={}=", value)?,
|
||||
FnDef(_fn_def) => (),
|
||||
Clock(clock) => {
|
||||
use crate::elements::Clock;
|
||||
|
||||
write!(w, "CLOCK: ")?;
|
||||
|
||||
match clock {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
..
|
||||
} => {
|
||||
write_datetime(&mut w, "[", start, "]--")?;
|
||||
write_datetime(&mut w, "[", end, "]")?;
|
||||
writeln!(w, " => {}", duration)?;
|
||||
}
|
||||
Clock::Running { start, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]\n")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Comment { value } => write!(w, "{}", value)?,
|
||||
FixedWidth { value } => write!(w, "{}", value)?,
|
||||
Keyword(keyword) => {
|
||||
write!(&mut w, "#+{}", keyword.key)?;
|
||||
if let Some(optional) = &keyword.optional {
|
||||
write!(&mut w, "[{}]", optional)?;
|
||||
}
|
||||
writeln!(&mut w, ": {}", keyword.value)?;
|
||||
}
|
||||
Rule => writeln!(w, "-----")?,
|
||||
Cookie(_cookie) => (),
|
||||
Title(title) => {
|
||||
for _ in 0..title.level {
|
||||
write!(&mut w, "*")?;
|
||||
}
|
||||
if let Some(keyword) = &title.keyword {
|
||||
write!(&mut w, " {}", keyword)?;
|
||||
}
|
||||
if let Some(priority) = title.priority {
|
||||
write!(&mut w, " [#{}]", priority)?;
|
||||
}
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
Table(_) => (),
|
||||
TableRow(_) => (),
|
||||
TableCell => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
use Element::*;
|
||||
|
||||
match element {
|
||||
// container elements
|
||||
SpecialBlock(block) => writeln!(w, "#+END_{}", block.name)?,
|
||||
QuoteBlock(_) => writeln!(w, "#+END_QUOTE")?,
|
||||
CenterBlock(_) => writeln!(w, "#+END_CENTER")?,
|
||||
VerseBlock(_) => writeln!(w, "#+END_VERSE")?,
|
||||
Bold => write!(w, "*")?,
|
||||
Document => (),
|
||||
DynBlock(_dyn_block) => writeln!(w, "#+END:")?,
|
||||
Headline { .. } => (),
|
||||
List(_list) => (),
|
||||
Italic => write!(w, "/")?,
|
||||
ListItem(_) => (),
|
||||
Paragraph => write!(w, "\n\n")?,
|
||||
Section => (),
|
||||
Strike => write!(w, "+")?,
|
||||
Underline => write!(w, "_")?,
|
||||
Drawer(_) => writeln!(w, ":END:")?,
|
||||
Title(title) => {
|
||||
if !title.tags.is_empty() {
|
||||
write!(&mut w, " :")?;
|
||||
}
|
||||
for tag in &title.tags {
|
||||
write!(&mut w, "{}:", tag)?;
|
||||
}
|
||||
writeln!(&mut w)?;
|
||||
if let Some(planning) = &title.planning {
|
||||
if let Some(scheduled) = &planning.scheduled {
|
||||
write!(&mut w, "SCHEDULED: ")?;
|
||||
write_timestamp(&mut w, &scheduled)?;
|
||||
}
|
||||
if let Some(deadline) = &planning.deadline {
|
||||
if planning.scheduled.is_some() {
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
write!(&mut w, "DEADLINE: ")?;
|
||||
write_timestamp(&mut w, &deadline)?;
|
||||
}
|
||||
if let Some(closed) = &planning.closed {
|
||||
if planning.deadline.is_some() {
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
write!(&mut w, "CLOSED: ")?;
|
||||
write_timestamp(&mut w, &closed)?;
|
||||
}
|
||||
writeln!(&mut w)?;
|
||||
}
|
||||
if !title.properties.is_empty() {
|
||||
writeln!(&mut w, ":PROPERTIES:")?;
|
||||
for (key, value) in &title.properties {
|
||||
writeln!(&mut w, ":{}: {}", key, value)?;
|
||||
}
|
||||
writeln!(&mut w, ":END:")?;
|
||||
}
|
||||
}
|
||||
Table(_) => (),
|
||||
TableRow(_) => (),
|
||||
TableCell => (),
|
||||
// non-container elements
|
||||
_ => debug_assert!(!element.is_container()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_timestamp<W: Write>(mut w: W, timestamp: &Timestamp) -> std::io::Result<()> {
|
||||
match timestamp {
|
||||
Timestamp::Active { start, .. } => {
|
||||
write_datetime(w, "<", start, ">")?;
|
||||
}
|
||||
Timestamp::Inactive { start, .. } => {
|
||||
write_datetime(w, "[", start, "]")?;
|
||||
}
|
||||
Timestamp::ActiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">--")?;
|
||||
write_datetime(&mut w, "<", end, ">")?;
|
||||
}
|
||||
Timestamp::InactiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]--")?;
|
||||
write_datetime(&mut w, "[", end, "]")?;
|
||||
}
|
||||
Timestamp::Diary { value } => write!(w, "<%%({})>", value)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultOrgHandler;
|
||||
|
||||
impl OrgHandler<Error> for DefaultOrgHandler {}
|
||||
240
src/lib.rs
Normal file
240
src/lib.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
//! A Rust library for parsing orgmode files.
|
||||
//!
|
||||
//! Live demo: https://orgize.herokuapp.com/
|
||||
//!
|
||||
//! # Parse
|
||||
//!
|
||||
//! To parse a orgmode string, simply invoking the [`Org::parse`] function:
|
||||
//!
|
||||
//! [`Org::parse`]: org/struct.Org.html#method.parse
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//!
|
||||
//! Org::parse("* DONE Title :tag:");
|
||||
//! ```
|
||||
//!
|
||||
//! or [`Org::parse_with_config`]:
|
||||
//!
|
||||
//! [`Org::parse_with_config`]: org/struct.Org.html#method.parse_with_config
|
||||
//!
|
||||
//! ``` rust
|
||||
//! use orgize::{Org, ParseConfig};
|
||||
//!
|
||||
//! Org::parse_with_config(
|
||||
//! "* TASK Title 1",
|
||||
//! &ParseConfig {
|
||||
//! // custom todo keywords
|
||||
//! todo_keywords: vec!["TASK".to_string()],
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! # Iter
|
||||
//!
|
||||
//! [`Org::iter`] function will returns an iteractor of [`Event`]s, which is
|
||||
//! a simple wrapper of [`Element`].
|
||||
//!
|
||||
//! [`Org::iter`]: org/struct.Org.html#method.iter
|
||||
//! [`Event`]: iter/enum.Event.html
|
||||
//! [`Element`]: elements/enum.Element.html
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//!
|
||||
//! for event in Org::parse("* DONE Title :tag:").iter() {
|
||||
//! // handling the event
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: whether an element is container or not, it will appears twice in one loop.
|
||||
//! One as [`Event::Start(element)`], one as [`Event::End(element)`].
|
||||
//!
|
||||
//! [`Event::Start(element)`]: iter/enum.Event.html#variant.Start
|
||||
//! [`Event::End(element)`]: iter/enum.Event.html#variant.End
|
||||
//!
|
||||
//! # Render html
|
||||
//!
|
||||
//! You can call the [`Org::html`] function to generate html directly, which
|
||||
//! uses the [`DefaultHtmlHandler`] internally:
|
||||
//!
|
||||
//! [`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(&mut writer).unwrap();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! String::from_utf8(writer).unwrap(),
|
||||
//! "<main><h1>title</h1><section><p><b>section</b></p></section></main>"
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! # Render html with custom `HtmlHandler`
|
||||
//!
|
||||
//! To customize html rendering, simply implementing [`HtmlHandler`] trait and passing
|
||||
//! it to the [`Org::html_with_handler`] function.
|
||||
//!
|
||||
//! [`HtmlHandler`]: export/html/trait.HtmlHandler.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.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::convert::From;
|
||||
//! use std::io::{Error as IOError, Write};
|
||||
//! use std::string::FromUtf8Error;
|
||||
//!
|
||||
//! use orgize::export::{DefaultHtmlHandler, HtmlHandler};
|
||||
//! use orgize::{Element, Org};
|
||||
//! use slugify::slugify;
|
||||
//!
|
||||
//! #[derive(Debug)]
|
||||
//! enum MyError {
|
||||
//! IO(IOError),
|
||||
//! Heading,
|
||||
//! Utf8(FromUtf8Error),
|
||||
//! }
|
||||
//!
|
||||
//! // From<std::io::Error> trait is required for custom error type
|
||||
//! impl From<IOError> for MyError {
|
||||
//! fn from(err: IOError) -> Self {
|
||||
//! MyError::IO(err)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl From<FromUtf8Error> for MyError {
|
||||
//! fn from(err: FromUtf8Error) -> Self {
|
||||
//! MyError::Utf8(err)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Default)]
|
||||
//! struct MyHtmlHandler(DefaultHtmlHandler);
|
||||
//!
|
||||
//! impl HtmlHandler<MyError> for MyHtmlHandler {
|
||||
//! fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
//! if let Element::Title(title) = element {
|
||||
//! if title.level > 6 {
|
||||
//! return Err(MyError::Heading);
|
||||
//! } else {
|
||||
//! write!(
|
||||
//! w,
|
||||
//! "<h{0}><a id=\"{1}\" href=\"#{1}\">",
|
||||
//! title.level,
|
||||
//! slugify!(&title.raw),
|
||||
//! )?;
|
||||
//! }
|
||||
//! } else {
|
||||
//! // fallthrough to default handler
|
||||
//! self.0.start(w, element)?;
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
//! if let Element::Title(title) = element {
|
||||
//! write!(w, "</a></h{}>", title.level)?;
|
||||
//! } else {
|
||||
//! self.0.end(w, element)?;
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), MyError> {
|
||||
//! let mut writer = Vec::new();
|
||||
//! let mut handler = MyHtmlHandler::default();
|
||||
//! Org::parse("* title\n*section*").html_with_handler(&mut writer, &mut handler)?;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! String::from_utf8(writer)?,
|
||||
//! "<main><h1><a id=\"title\" href=\"#title\">title</a></h1>\
|
||||
//! <section><p><b>section</b></p></section></main>"
|
||||
//! );
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: as I mentioned above, each element will appears two times while iterating.
|
||||
//! And handler will silently ignores all end events from non-container elements.
|
||||
//!
|
||||
//! So if you want to change how a non-container element renders, just redefine the `start`
|
||||
//! function and leave the `end` function unchanged.
|
||||
//!
|
||||
//! # Serde
|
||||
//!
|
||||
//! `Org` struct have already implemented serde's `Serialize` trait. It means you can
|
||||
//! serialize it into any format supported by serde, such as json:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//! use serde_json::{json, to_string};
|
||||
//!
|
||||
//! let org = Org::parse("I 'm *bold*.");
|
||||
//! println!("{}", to_string(&org).unwrap());
|
||||
//!
|
||||
//! // {
|
||||
//! // "type": "document",
|
||||
//! // "children": [{
|
||||
//! // "type": "section",
|
||||
//! // "children": [{
|
||||
//! // "type": "paragraph",
|
||||
//! // "children":[{
|
||||
//! // "type": "text",
|
||||
//! // "value":"I 'm "
|
||||
//! // }, {
|
||||
//! // "type": "bold",
|
||||
//! // "children":[{
|
||||
//! // "type": "text",
|
||||
//! // "value": "bold"
|
||||
//! // }]
|
||||
//! // }, {
|
||||
//! // "type":"text",
|
||||
//! // "value":"."
|
||||
//! // }]
|
||||
//! // }]
|
||||
//! // }]
|
||||
//! // }
|
||||
//! ```
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! By now, orgize provides two features:
|
||||
//!
|
||||
//! + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
|
||||
//!
|
||||
//! + `chrono`: adds the ability to convert `Datetime` into `chrono` structs, disabled by default.
|
||||
//!
|
||||
//! + `syntect`: provides `SyntectHtmlHandler` for highlighting code block, disabled by default.
|
||||
//!
|
||||
//! # License
|
||||
//!
|
||||
//! MIT
|
||||
|
||||
#![allow(clippy::range_plus_one)]
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
pub mod elements;
|
||||
pub mod export;
|
||||
mod node;
|
||||
mod org;
|
||||
mod parsers;
|
||||
|
||||
// Re-export of the indextree crate.
|
||||
pub use indextree;
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use syntect;
|
||||
|
||||
pub use config::ParseConfig;
|
||||
pub use elements::Element;
|
||||
pub use error::OrgizeError;
|
||||
pub use node::{DocumentNode, HeadlineNode};
|
||||
pub use org::{Event, Org};
|
||||
374
src/node.rs
Normal file
374
src/node.rs
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
use indextree::NodeId;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::config::ParseConfig;
|
||||
use crate::elements::{Element, Title};
|
||||
use crate::parsers::{parse_container, Container, OwnedArena};
|
||||
use crate::{Org, OrgizeError};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct HeadlineNode {
|
||||
pub(crate) node: NodeId,
|
||||
pub(crate) level: usize,
|
||||
pub(crate) title_node: NodeId,
|
||||
pub(crate) section_node: Option<NodeId>,
|
||||
}
|
||||
|
||||
impl HeadlineNode {
|
||||
pub(crate) fn new(node: NodeId, level: usize, org: &Org) -> HeadlineNode {
|
||||
let title_node = org.arena[node].first_child().unwrap();
|
||||
let section_node = if let Some(node) = org.arena[title_node].next_sibling() {
|
||||
if let Element::Section = org.arena[node].get() {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
HeadlineNode {
|
||||
node,
|
||||
level,
|
||||
title_node,
|
||||
section_node,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node(self) -> NodeId {
|
||||
self.node
|
||||
}
|
||||
|
||||
pub fn title_node(self) -> NodeId {
|
||||
self.title_node
|
||||
}
|
||||
|
||||
pub fn section_node(self) -> Option<NodeId> {
|
||||
self.section_node
|
||||
}
|
||||
|
||||
pub fn level(self) -> usize {
|
||||
self.level
|
||||
}
|
||||
|
||||
pub fn title<'a: 'b, 'b>(self, org: &'b Org<'a>) -> &'b Title<'a> {
|
||||
if let Element::Title(title) = org.arena[self.title_node].get() {
|
||||
title
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title_mut<'a: 'b, 'b>(self, org: &'b mut Org<'a>) -> &'b mut Title<'a> {
|
||||
if let Element::Title(title) = org.arena[self.title_node].get_mut() {
|
||||
title
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title_content<'a, S: Into<Cow<'a, str>>>(self, content: S, org: &mut Org<'a>) {
|
||||
let content = content.into();
|
||||
|
||||
let children: Vec<_> = self.title_node.children(&org.arena).collect();
|
||||
for child in children {
|
||||
child.detach(&mut org.arena);
|
||||
}
|
||||
|
||||
match &content {
|
||||
Cow::Borrowed(content) => parse_container(
|
||||
&mut org.arena,
|
||||
Container::Inline {
|
||||
node: self.title_node,
|
||||
content,
|
||||
},
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
Cow::Owned(ref content) => parse_container(
|
||||
&mut OwnedArena::new(&mut org.arena),
|
||||
Container::Inline {
|
||||
node: self.title_node,
|
||||
content,
|
||||
},
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
}
|
||||
|
||||
self.title_mut(org).raw = content;
|
||||
|
||||
org.debug_validate();
|
||||
}
|
||||
|
||||
pub fn set_section_content<'a, S: Into<Cow<'a, str>>>(self, content: S, org: &mut Org<'a>) {
|
||||
let node = if let Some(node) = self.section_node {
|
||||
let children: Vec<_> = node.children(&org.arena).collect();
|
||||
for child in children {
|
||||
child.detach(&mut org.arena);
|
||||
}
|
||||
node
|
||||
} else {
|
||||
let node = org.arena.new_node(Element::Section);
|
||||
self.node.append(node, &mut org.arena);
|
||||
node
|
||||
};
|
||||
|
||||
match content.into() {
|
||||
Cow::Borrowed(content) => parse_container(
|
||||
&mut org.arena,
|
||||
Container::Block { node, content },
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
Cow::Owned(ref content) => parse_container(
|
||||
&mut OwnedArena::new(&mut org.arena),
|
||||
Container::Block { node, content },
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
}
|
||||
|
||||
org.debug_validate();
|
||||
}
|
||||
|
||||
pub fn parent(self, org: &Org) -> Option<HeadlineNode> {
|
||||
org.arena[self.node].parent().map(|node| {
|
||||
if let Element::Headline { level } = *org.arena[node].get() {
|
||||
HeadlineNode::new(node, level, org)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn children<'a>(self, org: &'a Org) -> impl Iterator<Item = HeadlineNode> + 'a {
|
||||
self.node.children(&org.arena).filter_map(move |node| {
|
||||
if let Element::Headline { level } = *org.arena[node].get() {
|
||||
Some(HeadlineNode::new(node, level, org))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn previous_headline(self, org: &Org) -> Option<HeadlineNode> {
|
||||
if let Some(node) = org.arena[self.node].previous_sibling() {
|
||||
if let Element::Headline { level } = *org.arena[node].get() {
|
||||
Some(HeadlineNode::new(node, level, org))
|
||||
} else {
|
||||
debug_assert_eq!(node, self.section_node.unwrap());
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_headline(self, org: &Org) -> Option<HeadlineNode> {
|
||||
if let Some(node) = org.arena[self.node].next_sibling() {
|
||||
if let Element::Headline { level } = *org.arena[node].get() {
|
||||
Some(HeadlineNode::new(node, level, org))
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detach(self, org: &mut Org) {
|
||||
self.node.detach(&mut org.arena);
|
||||
|
||||
org.debug_validate();
|
||||
}
|
||||
|
||||
pub fn is_detached(self, org: &Org) -> bool {
|
||||
self.parent(&org).is_none()
|
||||
}
|
||||
|
||||
fn check_level(self, min: usize, max: Option<usize>) -> Result<(), OrgizeError> {
|
||||
match max {
|
||||
Some(max) if self.level > max || self.level < min => Err(OrgizeError::HeadlineLevel {
|
||||
min: Some(min),
|
||||
max: Some(max),
|
||||
at: self.node,
|
||||
}),
|
||||
None if self.level < min => Err(OrgizeError::HeadlineLevel {
|
||||
min: Some(min),
|
||||
max: None,
|
||||
at: self.node,
|
||||
}),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(last_headline) = org.headlines().last() {
|
||||
headline.check_level(self.level + 1, Some(last_headline.level))?;
|
||||
} else {
|
||||
headline.check_level(self.level + 1, None)?;
|
||||
}
|
||||
|
||||
self.node.append(headline.node, &mut org.arena);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepend(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(first_headline) = self.children(org).next() {
|
||||
headline.check_level(first_headline.level, None)?;
|
||||
} else {
|
||||
headline.check_level(self.level + 1, None)?;
|
||||
}
|
||||
|
||||
if let Some(node) = self.section_node {
|
||||
node.insert_after(headline.node, &mut org.arena);
|
||||
} else {
|
||||
self.title_node.insert_after(headline.node, &mut org.arena);
|
||||
}
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_before(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(previous) = self.previous_headline(org) {
|
||||
headline.check_level(self.level, Some(previous.level))?;
|
||||
} else {
|
||||
headline.check_level(self.level, None)?;
|
||||
}
|
||||
|
||||
self.node.insert_before(headline.node, &mut org.arena);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_after(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(next) = self.next_headline(org) {
|
||||
headline.check_level(next.level, Some(self.level))?;
|
||||
} else if let Some(parent) = self.parent(org) {
|
||||
headline.check_level(parent.level + 1, Some(self.level))?;
|
||||
} else {
|
||||
headline.check_level(1, Some(self.level))?;
|
||||
}
|
||||
|
||||
self.node.insert_after(headline.node, &mut org.arena);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DocumentNode {
|
||||
section_node: Option<NodeId>,
|
||||
}
|
||||
|
||||
impl DocumentNode {
|
||||
pub(crate) fn new(org: &Org) -> DocumentNode {
|
||||
if let Some(node) = org.arena[org.root].first_child() {
|
||||
if let Element::Section = org.arena[node].get() {
|
||||
DocumentNode {
|
||||
section_node: Some(node),
|
||||
}
|
||||
} else {
|
||||
DocumentNode { section_node: None }
|
||||
}
|
||||
} else {
|
||||
DocumentNode { section_node: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn children<'a>(self, org: &'a Org) -> impl Iterator<Item = HeadlineNode> + 'a {
|
||||
org.root.children(&org.arena).filter_map(move |node| {
|
||||
if let Element::Headline { level } = *org.arena[node].get() {
|
||||
Some(HeadlineNode::new(node, level, org))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_section_content<'a, S: Into<Cow<'a, str>>>(self, content: S, org: &mut Org<'a>) {
|
||||
let node = if let Some(node) = self.section_node {
|
||||
let children: Vec<_> = node.children(&org.arena).collect();
|
||||
for child in children {
|
||||
child.detach(&mut org.arena);
|
||||
}
|
||||
node
|
||||
} else {
|
||||
let node = org.arena.new_node(Element::Section);
|
||||
org.root.append(node, &mut org.arena);
|
||||
node
|
||||
};
|
||||
|
||||
match content.into() {
|
||||
Cow::Borrowed(content) => parse_container(
|
||||
&mut org.arena,
|
||||
Container::Block { node, content },
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
Cow::Owned(ref content) => parse_container(
|
||||
&mut OwnedArena::new(&mut org.arena),
|
||||
Container::Block { node, content },
|
||||
&ParseConfig::default(),
|
||||
),
|
||||
}
|
||||
|
||||
org.debug_validate();
|
||||
}
|
||||
|
||||
pub fn append(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(last_headline) = org.headlines().last() {
|
||||
headline.check_level(1, Some(last_headline.level))?;
|
||||
}
|
||||
|
||||
org.root.append(headline.node, &mut org.arena);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepend(self, headline: HeadlineNode, org: &mut Org) -> Result<(), OrgizeError> {
|
||||
if !headline.is_detached(org) {
|
||||
return Err(OrgizeError::Detached { at: headline.node });
|
||||
}
|
||||
|
||||
if let Some(first_headline) = self.children(org).next() {
|
||||
headline.check_level(first_headline.level, None)?;
|
||||
}
|
||||
|
||||
if let Some(node) = self.section_node {
|
||||
node.insert_after(headline.node, &mut org.arena);
|
||||
} else {
|
||||
org.root.prepend(headline.node, &mut org.arena);
|
||||
}
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
161
src/org.rs
Normal file
161
src/org.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
use indextree::{Arena, NodeEdge, NodeId};
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use crate::config::{ParseConfig, DEFAULT_CONFIG};
|
||||
use crate::elements::{Element, Title};
|
||||
use crate::export::*;
|
||||
use crate::node::{DocumentNode, HeadlineNode};
|
||||
use crate::parsers::{parse_container, Container};
|
||||
|
||||
pub struct Org<'a> {
|
||||
pub(crate) arena: Arena<Element<'a>>,
|
||||
pub(crate) root: NodeId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event<'a, 'b> {
|
||||
Start(&'b Element<'a>),
|
||||
End(&'b Element<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Org<'a> {
|
||||
/// Create a new empty Org struct
|
||||
pub fn new() -> Org<'static> {
|
||||
let mut arena = Arena::new();
|
||||
let root = arena.new_node(Element::Document);
|
||||
|
||||
Org { arena, root }
|
||||
}
|
||||
|
||||
/// Create a new Org struct from parsing `text`, using the default ParseConfig
|
||||
pub fn parse(text: &'a str) -> Org<'a> {
|
||||
Org::parse_with_config(text, &DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
/// Create a new Org struct from parsing `text`, using a custom ParseConfig
|
||||
pub fn parse_with_config(content: &'a str, config: &ParseConfig) -> Org<'a> {
|
||||
let mut org = Org::new();
|
||||
|
||||
parse_container(
|
||||
&mut org.arena,
|
||||
Container::Document {
|
||||
content,
|
||||
node: org.root,
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
org
|
||||
}
|
||||
|
||||
/// Return a DocumentNode
|
||||
pub fn document(&self) -> DocumentNode {
|
||||
DocumentNode::new(self)
|
||||
}
|
||||
|
||||
/// Return an iterator of HeadlineNode
|
||||
pub fn headlines<'b>(&'b self) -> impl Iterator<Item = HeadlineNode> + 'b {
|
||||
self.root
|
||||
.descendants(&self.arena)
|
||||
.skip(1)
|
||||
.filter_map(move |node| match &self.arena[node].get() {
|
||||
Element::Headline { level } => Some(HeadlineNode::new(node, *level, self)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a refrence to underlay arena
|
||||
pub fn arena(&self) -> &Arena<Element<'a>> {
|
||||
&self.arena
|
||||
}
|
||||
|
||||
/// Return a mutual reference to underlay arena
|
||||
pub fn arena_mut(&mut self) -> &mut Arena<Element<'a>> {
|
||||
&mut self.arena
|
||||
}
|
||||
|
||||
/// Create a new headline and return it's HeadlineNode
|
||||
pub fn new_headline(&mut self, title: Title<'a>) -> HeadlineNode {
|
||||
let level = title.level;
|
||||
let title_raw = title.raw.clone();
|
||||
let headline_node = self.arena.new_node(Element::Headline { level });
|
||||
let title_node = self.arena.new_node(Element::Title(title));
|
||||
headline_node.append(title_node, &mut self.arena);
|
||||
let headline_node = HeadlineNode {
|
||||
node: headline_node,
|
||||
level,
|
||||
title_node,
|
||||
section_node: None,
|
||||
};
|
||||
headline_node.set_title_content(title_raw, self);
|
||||
headline_node
|
||||
}
|
||||
|
||||
/// Return an iterator of Event
|
||||
pub fn iter<'b>(&'b self) -> impl Iterator<Item = Event<'a, 'b>> + 'b {
|
||||
self.root.traverse(&self.arena).map(move |edge| match edge {
|
||||
NodeEdge::Start(node) => Event::Start(self.arena[node].get()),
|
||||
NodeEdge::End(node) => Event::End(self.arena[node].get()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn html<W: Write>(&self, wrtier: W) -> Result<(), Error> {
|
||||
self.html_with_handler(wrtier, &mut DefaultHtmlHandler)
|
||||
}
|
||||
|
||||
pub fn html_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
|
||||
where
|
||||
W: Write,
|
||||
E: From<Error>,
|
||||
H: HtmlHandler<E>,
|
||||
{
|
||||
for event in self.iter() {
|
||||
match event {
|
||||
Event::Start(element) => handler.start(&mut writer, element)?,
|
||||
Event::End(element) => handler.end(&mut writer, element)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn org<W: Write>(&self, wrtier: W) -> Result<(), Error> {
|
||||
self.org_with_handler(wrtier, &mut DefaultOrgHandler)
|
||||
}
|
||||
|
||||
pub fn org_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
|
||||
where
|
||||
W: Write,
|
||||
E: From<Error>,
|
||||
H: OrgHandler<E>,
|
||||
{
|
||||
for event in self.iter() {
|
||||
match event {
|
||||
Event::Start(element) => handler.start(&mut writer, element)?,
|
||||
Event::End(element) => handler.end(&mut writer, element)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Org<'static> {
|
||||
fn default() -> Self {
|
||||
Org::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ser")]
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
#[cfg(feature = "ser")]
|
||||
impl Serialize for Org<'_> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
use serde_indextree::Node;
|
||||
|
||||
serializer.serialize_newtype_struct("Org", &Node::new(self.root, &self.arena))
|
||||
}
|
||||
}
|
||||
766
src/parsers.rs
Normal file
766
src/parsers.rs
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
// parser related functions
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::iter::once;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use indextree::{Arena, NodeId};
|
||||
use jetscii::{bytes, BytesConst};
|
||||
use memchr::{memchr, memchr_iter};
|
||||
use nom::{bytes::complete::take_while1, combinator::verify, error::ParseError, IResult};
|
||||
|
||||
use crate::config::ParseConfig;
|
||||
use crate::elements::{
|
||||
block::parse_block_element, emphasis::parse_emphasis, keyword::parse_keyword,
|
||||
radio_target::parse_radio_target, rule::parse_rule, table::parse_table_el, BabelCall,
|
||||
CenterBlock, Clock, CommentBlock, Cookie, Drawer, DynBlock, Element, ExampleBlock, ExportBlock,
|
||||
FnDef, FnRef, InlineCall, InlineSrc, Keyword, Link, List, ListItem, Macros, QuoteBlock,
|
||||
Snippet, SourceBlock, SpecialBlock, Table, TableRow, Target, Timestamp, Title, VerseBlock,
|
||||
};
|
||||
|
||||
pub trait ElementArena<'a> {
|
||||
fn append_element<T: Into<Element<'a>>>(&mut self, element: T, parent: NodeId) -> NodeId;
|
||||
fn insert_before_last_child<T: Into<Element<'a>>>(
|
||||
&mut self,
|
||||
element: T,
|
||||
parent: NodeId,
|
||||
) -> NodeId;
|
||||
}
|
||||
|
||||
impl<'a> ElementArena<'a> for Arena<Element<'a>> {
|
||||
fn append_element<T: Into<Element<'a>>>(&mut self, element: T, parent: NodeId) -> NodeId {
|
||||
let node = self.new_node(element.into());
|
||||
parent.append(node, self);
|
||||
node
|
||||
}
|
||||
|
||||
fn insert_before_last_child<T: Into<Element<'a>>>(
|
||||
&mut self,
|
||||
element: T,
|
||||
parent: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some(child) = self[parent].last_child() {
|
||||
let node = self.new_node(element.into());
|
||||
child.insert_before(node, self);
|
||||
node
|
||||
} else {
|
||||
self.append_element(element, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OwnedArena<'a, 'b, 'c> {
|
||||
arena: &'b mut Arena<Element<'c>>,
|
||||
phantom: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c> OwnedArena<'a, 'b, 'c> {
|
||||
pub fn new(arena: &'b mut Arena<Element<'c>>) -> OwnedArena<'a, 'b, 'c> {
|
||||
OwnedArena {
|
||||
arena,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ElementArena<'a> for OwnedArena<'a, '_, '_> {
|
||||
fn append_element<T: Into<Element<'a>>>(&mut self, element: T, parent: NodeId) -> NodeId {
|
||||
let node = self.arena.new_node(element.into().into_owned());
|
||||
parent.append(node, self.arena);
|
||||
node
|
||||
}
|
||||
|
||||
fn insert_before_last_child<T: Into<Element<'a>>>(
|
||||
&mut self,
|
||||
element: T,
|
||||
parent: NodeId,
|
||||
) -> NodeId {
|
||||
self.arena
|
||||
.insert_before_last_child(element.into().into_owned(), parent)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Container<'a> {
|
||||
// List
|
||||
List {
|
||||
content: &'a str,
|
||||
node: NodeId,
|
||||
indent: usize,
|
||||
},
|
||||
// Block, List Item
|
||||
Block {
|
||||
content: &'a str,
|
||||
node: NodeId,
|
||||
},
|
||||
// Pargraph, Inline Markup
|
||||
Inline {
|
||||
content: &'a str,
|
||||
node: NodeId,
|
||||
},
|
||||
// Headline
|
||||
Headline {
|
||||
content: &'a str,
|
||||
node: NodeId,
|
||||
},
|
||||
// Document
|
||||
Document {
|
||||
content: &'a str,
|
||||
node: NodeId,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse_container<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
container: Container<'a>,
|
||||
config: &ParseConfig,
|
||||
) {
|
||||
let containers = &mut vec![container];
|
||||
|
||||
while let Some(container) = containers.pop() {
|
||||
match container {
|
||||
Container::Document { content, node } => {
|
||||
parse_section_and_headlines(arena, content, node, containers);
|
||||
}
|
||||
Container::Headline { content, node } => {
|
||||
parse_headline_content(arena, content, node, containers, config);
|
||||
}
|
||||
Container::Block { content, node } => {
|
||||
parse_blocks(arena, content, node, containers);
|
||||
}
|
||||
Container::Inline { content, node } => {
|
||||
parse_inlines(arena, content, node, containers);
|
||||
}
|
||||
Container::List {
|
||||
content,
|
||||
node,
|
||||
indent,
|
||||
} => {
|
||||
parse_list_items(arena, content, indent, node, containers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_headline_content<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
config: &ParseConfig,
|
||||
) {
|
||||
let (tail, (title, content)) = Title::parse(content, config).unwrap();
|
||||
let node = arena.append_element(title, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
parse_section_and_headlines(arena, tail, parent, containers);
|
||||
}
|
||||
|
||||
pub fn parse_section_and_headlines<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let content = skip_empty_lines(content);
|
||||
if content.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut last_end = 0;
|
||||
for i in memchr_iter(b'\n', content.as_bytes()).chain(once(content.len())) {
|
||||
if let Some((mut tail, (headline_content, level))) = parse_headline(&content[last_end..]) {
|
||||
if last_end != 0 {
|
||||
let node = arena.append_element(Element::Section, parent);
|
||||
let content = &content[0..last_end];
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
|
||||
let node = arena.append_element(Element::Headline { level }, parent);
|
||||
containers.push(Container::Headline {
|
||||
content: headline_content,
|
||||
node,
|
||||
});
|
||||
|
||||
while let Some((new_tail, (content, level))) = parse_headline(tail) {
|
||||
debug_assert_ne!(tail, new_tail);
|
||||
let node = arena.append_element(Element::Headline { level }, parent);
|
||||
containers.push(Container::Headline { content, node });
|
||||
tail = new_tail;
|
||||
}
|
||||
return;
|
||||
}
|
||||
last_end = i + 1;
|
||||
}
|
||||
|
||||
let node = arena.append_element(Element::Section, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
|
||||
pub fn parse_blocks<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let mut tail = skip_empty_lines(content);
|
||||
|
||||
if let Some(new_tail) = parse_block(content, arena, parent, containers) {
|
||||
tail = skip_empty_lines(new_tail);
|
||||
}
|
||||
|
||||
let mut text = tail;
|
||||
let mut pos = 0;
|
||||
|
||||
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) {
|
||||
let node = arena.append_element(Element::Paragraph, parent);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end_matches('\n'),
|
||||
node,
|
||||
});
|
||||
|
||||
pos = 0;
|
||||
debug_assert_ne!(tail, skip_empty_lines(&tail[i..]));
|
||||
tail = skip_empty_lines(&tail[i..]);
|
||||
text = tail;
|
||||
} else if let Some(new_tail) = parse_block(tail, arena, parent, containers) {
|
||||
if pos != 0 {
|
||||
let node = arena.insert_before_last_child(Element::Paragraph, parent);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end_matches('\n'),
|
||||
node,
|
||||
});
|
||||
|
||||
pos = 0;
|
||||
}
|
||||
debug_assert_ne!(tail, skip_empty_lines(new_tail));
|
||||
tail = skip_empty_lines(new_tail);
|
||||
text = tail;
|
||||
} else {
|
||||
debug_assert_ne!(tail, &tail[i..]);
|
||||
tail = &tail[i..];
|
||||
pos += i;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
let node = arena.append_element(Element::Paragraph, parent);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end_matches('\n'),
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_block<'a, T: ElementArena<'a>>(
|
||||
contents: &'a str,
|
||||
arena: &mut T,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) -> Option<&'a str> {
|
||||
if let Some((tail, (fn_def, content))) = FnDef::parse(contents) {
|
||||
let node = arena.append_element(fn_def, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
return Some(tail);
|
||||
} else if let Some((tail, list, content)) = List::parse(contents) {
|
||||
let indent = list.indent;
|
||||
let node = arena.append_element(list, parent);
|
||||
containers.push(Container::List {
|
||||
content,
|
||||
node,
|
||||
indent,
|
||||
});
|
||||
return Some(tail);
|
||||
}
|
||||
|
||||
let contents = contents.trim_start();
|
||||
|
||||
match contents.as_bytes().get(0)? {
|
||||
b'C' => {
|
||||
let (tail, clock) = Clock::parse(contents)?;
|
||||
arena.append_element(clock, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'\'' => {
|
||||
// TODO: LaTeX environment
|
||||
None
|
||||
}
|
||||
b'-' => {
|
||||
let tail = parse_rule(contents)?;
|
||||
arena.append_element(Element::Rule, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b':' => {
|
||||
if let Some((tail, (drawer, content))) = Drawer::parse(contents) {
|
||||
let node = arena.append_element(drawer, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, value) = parse_fixed_width(contents)?;
|
||||
let value = value.into();
|
||||
arena.append_element(Element::FixedWidth { value }, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'|' => {
|
||||
let tail = parse_table(arena, contents, containers, parent)?;
|
||||
Some(tail)
|
||||
}
|
||||
b'#' => {
|
||||
if let Some((tail, (name, args, content))) = parse_block_element(contents) {
|
||||
match_block(
|
||||
arena,
|
||||
parent,
|
||||
containers,
|
||||
name.into(),
|
||||
args.map(Into::into),
|
||||
content,
|
||||
);
|
||||
Some(tail)
|
||||
} else if let Some((tail, (dyn_block, content))) = DynBlock::parse(contents) {
|
||||
let node = arena.append_element(dyn_block, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
Some(tail)
|
||||
} else if let Some((tail, (key, optional, value))) = parse_keyword(contents) {
|
||||
if (&*key).eq_ignore_ascii_case("CALL") {
|
||||
arena.append_element(
|
||||
BabelCall {
|
||||
value: value.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
} else {
|
||||
arena.append_element(
|
||||
Keyword {
|
||||
key: key.into(),
|
||||
optional: optional.map(Into::into),
|
||||
value: value.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, value) = parse_comment(contents)?;
|
||||
let value = value.into();
|
||||
arena.append_element(Element::Comment { value }, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_block<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
name: Cow<'a, str>,
|
||||
args: Option<Cow<'a, str>>,
|
||||
content: &'a str,
|
||||
) {
|
||||
match &*name.to_uppercase() {
|
||||
"CENTER" => {
|
||||
let node = arena.append_element(CenterBlock { parameters: args }, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
"QUOTE" => {
|
||||
let node = arena.append_element(QuoteBlock { parameters: args }, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
"COMMENT" => {
|
||||
arena.append_element(
|
||||
CommentBlock {
|
||||
data: args,
|
||||
contents: content.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
"EXAMPLE" => {
|
||||
arena.append_element(
|
||||
ExampleBlock {
|
||||
data: args,
|
||||
contents: content.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
"EXPORT" => {
|
||||
arena.append_element(
|
||||
ExportBlock {
|
||||
data: args.unwrap_or_default(),
|
||||
contents: content.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
"SRC" => {
|
||||
let (language, arguments) = match &args {
|
||||
Some(Cow::Borrowed(args)) => {
|
||||
let (language, arguments) =
|
||||
args.split_at(args.find(' ').unwrap_or_else(|| args.len()));
|
||||
(language.into(), arguments.into())
|
||||
}
|
||||
None => (Cow::Borrowed(""), Cow::Borrowed("")),
|
||||
_ => unreachable!("`parse_block_element` returns `Some(Cow::Borrowed)` or `None`"),
|
||||
};
|
||||
arena.append_element(
|
||||
SourceBlock {
|
||||
arguments,
|
||||
language,
|
||||
contents: content.into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
"VERSE" => {
|
||||
let node = arena.append_element(VerseBlock { parameters: args }, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
_ => {
|
||||
let node = arena.append_element(
|
||||
SpecialBlock {
|
||||
parameters: args,
|
||||
name,
|
||||
},
|
||||
parent,
|
||||
);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InlinePositions<'a> {
|
||||
bytes: &'a [u8],
|
||||
position: usize,
|
||||
next: Option<usize>,
|
||||
}
|
||||
|
||||
impl InlinePositions<'_> {
|
||||
fn new(bytes: &[u8]) -> InlinePositions {
|
||||
InlinePositions {
|
||||
bytes,
|
||||
position: 0,
|
||||
next: Some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for InlinePositions<'_> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref PRE_BYTES: BytesConst =
|
||||
bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n');
|
||||
}
|
||||
|
||||
self.next.take().or_else(|| {
|
||||
PRE_BYTES.find(&self.bytes[self.position..]).map(|i| {
|
||||
self.position += i + 1;
|
||||
|
||||
match self.bytes[self.position - 1] {
|
||||
b'{' => {
|
||||
self.next = Some(self.position);
|
||||
self.position - 1
|
||||
}
|
||||
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.position,
|
||||
_ => self.position - 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inlines<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let mut tail = content;
|
||||
|
||||
if let Some(tail_) = parse_inline(tail, arena, containers, parent) {
|
||||
tail = tail_;
|
||||
}
|
||||
|
||||
while let Some((tail_, i)) = InlinePositions::new(tail.as_bytes())
|
||||
.filter_map(|i| parse_inline(&tail[i..], arena, containers, parent).map(|tail| (tail, i)))
|
||||
.next()
|
||||
{
|
||||
if i != 0 {
|
||||
arena.insert_before_last_child(
|
||||
Element::Text {
|
||||
value: tail[0..i].into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
tail = tail_;
|
||||
}
|
||||
|
||||
if !tail.is_empty() {
|
||||
arena.append_element(Element::Text { value: tail.into() }, parent);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inline<'a, T: ElementArena<'a>>(
|
||||
contents: &'a str,
|
||||
arena: &mut T,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
parent: NodeId,
|
||||
) -> Option<&'a str> {
|
||||
if contents.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match contents.as_bytes()[0] {
|
||||
b'@' => {
|
||||
let (tail, snippet) = Snippet::parse(contents)?;
|
||||
arena.append_element(snippet, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'{' => {
|
||||
let (tail, macros) = Macros::parse(contents)?;
|
||||
arena.append_element(macros, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'<' => {
|
||||
if let Some((tail, _content)) = parse_radio_target(contents) {
|
||||
arena.append_element(Element::RadioTarget, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, target)) = Target::parse(contents) {
|
||||
arena.append_element(target, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, timestamp)) = Timestamp::parse_active(contents) {
|
||||
arena.append_element(timestamp, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, timestamp) = Timestamp::parse_diary(contents)?;
|
||||
arena.append_element(timestamp, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'[' => {
|
||||
if let Some((tail, fn_ref)) = FnRef::parse(contents) {
|
||||
arena.append_element(fn_ref, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, link)) = Link::parse(contents) {
|
||||
arena.append_element(link, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, cookie)) = Cookie::parse(contents) {
|
||||
arena.append_element(cookie, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, timestamp) = Timestamp::parse_inactive(contents)?;
|
||||
arena.append_element(timestamp, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'*' => {
|
||||
let (tail, content) = parse_emphasis(contents, b'*')?;
|
||||
let node = arena.append_element(Element::Bold, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
Some(tail)
|
||||
}
|
||||
b'+' => {
|
||||
let (tail, content) = parse_emphasis(contents, b'+')?;
|
||||
let node = arena.append_element(Element::Strike, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
Some(tail)
|
||||
}
|
||||
b'/' => {
|
||||
let (tail, content) = parse_emphasis(contents, b'/')?;
|
||||
let node = arena.append_element(Element::Italic, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
Some(tail)
|
||||
}
|
||||
b'_' => {
|
||||
let (tail, content) = parse_emphasis(contents, b'_')?;
|
||||
let node = arena.append_element(Element::Underline, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
Some(tail)
|
||||
}
|
||||
b'=' => {
|
||||
let (tail, value) = parse_emphasis(contents, b'=')?;
|
||||
let value = value.into();
|
||||
arena.append_element(Element::Verbatim { value }, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'~' => {
|
||||
let (tail, value) = parse_emphasis(contents, b'~')?;
|
||||
let value = value.into();
|
||||
arena.append_element(Element::Code { value }, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b's' => {
|
||||
let (tail, inline_src) = InlineSrc::parse(contents)?;
|
||||
arena.append_element(inline_src, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'c' => {
|
||||
let (tail, inline_call) = InlineCall::parse(contents)?;
|
||||
arena.append_element(inline_call, parent);
|
||||
Some(tail)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_list_items<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
mut contents: &'a str,
|
||||
indent: usize,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
while !contents.is_empty() {
|
||||
let (tail, list_item, content) = ListItem::parse(contents, indent);
|
||||
let node = arena.append_element(list_item, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
contents = tail;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_table<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
contents: &'a str,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
parent: NodeId,
|
||||
) -> Option<&'a str> {
|
||||
if contents.trim_start().starts_with('|') {
|
||||
let table_node = arena.append_element(Table::Org { tblfm: None }, parent);
|
||||
|
||||
let mut last_end = 0;
|
||||
for start in memchr_iter(b'\n', contents.as_bytes()).chain(once(contents.len())) {
|
||||
let line = contents[last_end..start].trim();
|
||||
match TableRow::parse(line) {
|
||||
Some(TableRow::Standard) => {
|
||||
let row_node = arena.append_element(TableRow::Standard, table_node);
|
||||
for cell in line[1..].split_terminator('|') {
|
||||
let cell_node = arena.append_element(Element::TableCell, row_node);
|
||||
containers.push(Container::Inline {
|
||||
content: cell.trim(),
|
||||
node: cell_node,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(TableRow::Rule) => {
|
||||
arena.append_element(TableRow::Rule, table_node);
|
||||
}
|
||||
None => return Some(&contents[last_end..]),
|
||||
}
|
||||
last_end = start + 1;
|
||||
}
|
||||
|
||||
Some("")
|
||||
} else {
|
||||
let (tail, value) = parse_table_el(contents)?;
|
||||
let value = value.into();
|
||||
arena.append_element(Table::TableEl { value }, parent);
|
||||
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> {
|
||||
if let Some(i) = memchr(b'\n', input.as_bytes()) {
|
||||
if i > 0 && input.as_bytes()[i - 1] == b'\r' {
|
||||
Ok((&input[i + 1..], &input[0..i - 1]))
|
||||
} else {
|
||||
Ok((&input[i + 1..], &input[0..i]))
|
||||
}
|
||||
} else {
|
||||
Ok(("", input))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eol<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> {
|
||||
verify(line, |s: &str| s.trim().is_empty())(input)
|
||||
}
|
||||
|
||||
pub fn take_lines_while(predicate: impl Fn(&str) -> bool) -> impl Fn(&str) -> (&str, &str) {
|
||||
move |input| {
|
||||
let mut last_end = 0;
|
||||
for i in memchr_iter(b'\n', input.as_bytes()) {
|
||||
if i > 0 && input.as_bytes()[i - 1] == b'\r' {
|
||||
if !predicate(&input[last_end..i - 1]) {
|
||||
return (&input[last_end..], &input[0..last_end]);
|
||||
}
|
||||
} else if !predicate(&input[last_end..i]) {
|
||||
return (&input[last_end..], &input[0..last_end]);
|
||||
}
|
||||
last_end = i + 1;
|
||||
}
|
||||
if !predicate(&input[last_end..]) {
|
||||
(&input[last_end..], &input[0..last_end])
|
||||
} else {
|
||||
("", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skip_empty_lines(input: &str) -> &str {
|
||||
take_lines_while(|line| line.trim().is_empty())(input).0
|
||||
}
|
||||
|
||||
pub fn parse_headline(input: &str) -> Option<(&str, (&str, usize))> {
|
||||
let (input_, level) = parse_headline_level(input)?;
|
||||
let (input_, content) = take_lines_while(move |line| {
|
||||
parse_headline_level(line)
|
||||
.map(|(_, l)| l > level)
|
||||
.unwrap_or(true)
|
||||
})(input_);
|
||||
Some((input_, (&input[0..level + content.len()], level)))
|
||||
}
|
||||
|
||||
pub fn parse_headline_level(input: &str) -> Option<(&str, usize)> {
|
||||
let (input, stars) = take_while1::<_, _, ()>(|c: char| c == '*')(input).ok()?;
|
||||
|
||||
if input.starts_with(' ') || input.starts_with('\n') || input.is_empty() {
|
||||
Some((input, stars.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_fixed_width(input: &str) -> Option<(&str, &str)> {
|
||||
let (input, content) = take_lines_while(|line| line == ":" || line.starts_with(": "))(input);
|
||||
|
||||
if !content.is_empty() {
|
||||
Some((input, content))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_comment(input: &str) -> Option<(&str, &str)> {
|
||||
let (input, content) = take_lines_while(|line| line == "#" || line.starts_with("# "))(input);
|
||||
|
||||
if !content.is_empty() {
|
||||
Some((input, content))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_one_word<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> {
|
||||
take_while1(|c: char| !c.is_ascii_whitespace())(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_skip_empty_lines() {
|
||||
assert_eq!(skip_empty_lines("foo"), "foo");
|
||||
assert_eq!(skip_empty_lines(" foo"), " foo");
|
||||
assert_eq!(skip_empty_lines(" \nfoo\n"), "foo\n");
|
||||
assert_eq!(skip_empty_lines(" \n\n\nfoo\n"), "foo\n");
|
||||
assert_eq!(skip_empty_lines(" \n \n\nfoo\n"), "foo\n");
|
||||
assert_eq!(skip_empty_lines(" \n \n\n foo\n"), " foo\n");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue