refactor(parser): wrap some objects and elements with struct

This commit is contained in:
PoiScript 2019-04-24 17:42:21 +08:00
parent 69534576f1
commit 56e289fb48
22 changed files with 1055 additions and 875 deletions

View file

@ -1,4 +1,4 @@
use crate::objects::timestamp::{Datetime, Delay, Repeater, Timestamp}; use crate::objects::timestamp::{Datetime, Timestamp};
use memchr::memchr; use memchr::memchr;
/// clock elements /// clock elements
@ -11,15 +11,15 @@ pub enum Clock<'a> {
Closed { Closed {
start: Datetime<'a>, start: Datetime<'a>,
end: Datetime<'a>, end: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
duration: &'a str, duration: &'a str,
}, },
/// running Clock /// running Clock
Running { Running {
start: Datetime<'a>, start: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
}, },
} }
@ -37,17 +37,16 @@ impl<'a> Clock<'a> {
return None; return None;
} }
match Timestamp::parse_inactive(tail).map(|(t, off)| (t, tail[off..].trim_start())) { let (timestamp, tail) =
Some(( Timestamp::parse_inactive(tail).map(|(t, off)| (t, tail[off..].trim_start()))?;
match timestamp {
Timestamp::InactiveRange { Timestamp::InactiveRange {
start, start,
end, end,
repeater, repeater,
delay, delay,
}, } if tail.starts_with("=>") => {
tail,
)) => {
if tail.starts_with("=>") {
let duration = &tail[3..].trim(); let duration = &tail[3..].trim();
let colon = memchr(b':', duration.as_bytes())?; let colon = memchr(b':', duration.as_bytes())?;
if duration.as_bytes()[0..colon].iter().all(u8::is_ascii_digit) if duration.as_bytes()[0..colon].iter().all(u8::is_ascii_digit)
@ -55,7 +54,7 @@ impl<'a> Clock<'a> {
&& duration.as_bytes()[colon + 1].is_ascii_digit() && duration.as_bytes()[colon + 1].is_ascii_digit()
&& duration.as_bytes()[colon + 2].is_ascii_digit() && duration.as_bytes()[colon + 2].is_ascii_digit()
{ {
return Some(( Some((
Clock::Closed { Clock::Closed {
start, start,
end, end,
@ -64,34 +63,32 @@ impl<'a> Clock<'a> {
duration, duration,
}, },
off, off,
)); ))
} else {
None
} }
} }
}
Some((
Timestamp::Inactive { Timestamp::Inactive {
start, start,
repeater, repeater,
delay, delay,
}, } => {
tail,
)) => {
if tail.as_bytes().iter().all(u8::is_ascii_whitespace) { if tail.as_bytes().iter().all(u8::is_ascii_whitespace) {
return Some(( Some((
Clock::Running { Clock::Running {
start, start,
repeater, repeater,
delay, delay,
}, },
off, off,
)); ))
} } else {
}
_ => (),
}
None None
} }
}
_ => None,
}
}
/// returns `true` if the clock is running /// returns `true` if the clock is running
pub fn is_running(&self) -> bool { pub fn is_running(&self) -> bool {
@ -146,11 +143,6 @@ impl<'a> Clock<'a> {
} }
} }
#[cfg(test)]
mod tests {
use super::Clock;
use crate::objects::timestamp::Datetime;
#[test] #[test]
fn parse() { fn parse() {
assert_eq!( assert_eq!(
@ -158,8 +150,8 @@ mod tests {
Some(( Some((
Clock::Running { Clock::Running {
start: Datetime { start: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("09:39"),
dayname: "Tue" dayname: "Tue"
}, },
repeater: None, repeater: None,
@ -173,13 +165,13 @@ mod tests {
Some(( Some((
Clock::Closed { Clock::Closed {
start: Datetime { start: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("09:39"),
dayname: "Tue" dayname: "Tue"
}, },
end: Datetime { end: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((10, 39)), time: Some("10:39"),
dayname: "Tue" dayname: "Tue"
}, },
repeater: None, repeater: None,
@ -190,4 +182,3 @@ mod tests {
)) ))
); );
} }
}

View file

@ -5,7 +5,7 @@ use memchr::{memchr, memchr_iter};
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> { pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
debug_assert!(text.starts_with("#+")); debug_assert!(text.starts_with("#+"));
if text.len() <= 9 || !text[2..9].eq_ignore_ascii_case("BEGIN: ") { if text.len() <= "#+BEGIN: ".len() || !text[2..9].eq_ignore_ascii_case("BEGIN: ") {
return None; return None;
} }
@ -15,9 +15,15 @@ pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
let (name, para, off) = lines let (name, para, off) = lines
.next() .next()
.map(|i| { .map(|i| {
memchr(b' ', &bytes[9..i]) memchr(b' ', &bytes["#+BEGIN: ".len()..i])
.map(|x| (&text[9..9 + x], Some(text[9 + x..i].trim()), i + 1)) .map(|x| {
.unwrap_or((&text[9..i], None, i + 1)) (
&text["#+BEGIN: ".len().."#+BEGIN: ".len() + x],
Some(text["#+BEGIN: ".len() + x..i].trim()),
i + 1,
)
})
.unwrap_or((&text["#+BEGIN: ".len()..i], None, i + 1))
}) })
.filter(|(name, _, _)| name.as_bytes().iter().all(|&c| c.is_ascii_alphabetic()))?; .filter(|(name, _, _)| name.as_bytes().iter().all(|&c| c.is_ascii_alphabetic()))?;

View file

@ -7,11 +7,11 @@ pub fn parse(text: &str) -> Option<(&str, &str, usize)> {
let (label, off) = memchr(b']', text.as_bytes()) let (label, off) = memchr(b']', text.as_bytes())
.filter(|&i| { .filter(|&i| {
i != 4 i != 4
&& text.as_bytes()[4..i] && text.as_bytes()["[fn:".len()..i]
.iter() .iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_') .all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}) })
.map(|i| (&text[4..i], i + 1))?; .map(|i| (&text["[fn:".len()..i], i + 1))?;
let (content, off) = memchr(b'\n', text.as_bytes()) let (content, off) = memchr(b'\n', text.as_bytes())
.map(|i| (&text[off..i], i)) .map(|i| (&text[off..i], i))

View file

@ -17,12 +17,44 @@ pub enum Key<'a> {
Date, Date,
Title, Title,
Custom(&'a str), Custom(&'a str),
// Babel Call
Call,
} }
pub fn parse(text: &str) -> Option<(Key<'_>, &str, usize)> { #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Keyword<'a> {
pub key: Key<'a>,
pub value: &'a str,
}
impl<'a> Keyword<'a> {
#[inline]
pub(crate) fn new(key: &'a str, option: Option<&'a str>, value: &'a str) -> Keyword<'a> {
Keyword {
key: match &*key.to_uppercase() {
"AUTHOR" => Key::Author,
"DATE" => Key::Date,
"HEADER" => Key::Header,
"NAME" => Key::Name,
"PLOT" => Key::Plot,
"TITLE" => Key::Title,
"RESULTS" => Key::Results { option },
"CAPTION" => Key::Caption { option },
k => {
if k.starts_with("ATTR_") {
Key::Attr {
backend: &key["ATTR_".len()..],
}
} else {
Key::Custom(key)
}
}
},
value,
}
}
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, Option<&str>, &str, usize)> {
debug_assert!(text.starts_with("#+")); debug_assert!(text.starts_with("#+"));
let bytes = text.as_bytes(); let bytes = text.as_bytes();
@ -38,9 +70,11 @@ pub fn parse(text: &str) -> Option<(Key<'_>, &str, usize)> {
let (option, off) = if bytes[off - 1] == b'[' { let (option, off) = if bytes[off - 1] == b'[' {
memchr(b']', bytes) memchr(b']', bytes)
.filter(|&i| { .filter(|&i| {
bytes[off..i].iter().all(|&c| c != b'\n') && i < text.len() && bytes[i + 1] == b':' bytes[off..i].iter().all(|&c| c != b'\n')
&& i < text.len()
&& bytes[i + 1] == b':'
}) })
.map(|i| (Some(&text[off..i]), i + 2 /* ]: */))? .map(|i| (Some(&text[off..i]), i + "]:".len()))?
} else { } else {
(None, off) (None, off)
}; };
@ -49,80 +83,58 @@ pub fn parse(text: &str) -> Option<(Key<'_>, &str, usize)> {
.map(|i| (&text[off..i], i + 1)) .map(|i| (&text[off..i], i + 1))
.unwrap_or_else(|| (&text[off..], text.len())); .unwrap_or_else(|| (&text[off..], text.len()));
Some(( Some((key, option, value.trim(), off))
match &*key.to_uppercase() { }
"AUTHOR" => Key::Author,
"CALL" => Key::Call,
"DATE" => Key::Date,
"HEADER" => Key::Header,
"NAME" => Key::Name,
"PLOT" => Key::Plot,
"TITLE" => Key::Title,
"RESULTS" => Key::Results { option },
"CAPTION" => Key::Caption { option },
k if k.starts_with("ATTR_") => Key::Attr {
backend: &key["ATTR_".len()..],
},
_ => Key::Custom(key),
},
value.trim(),
off,
))
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::*; assert_eq!(
Keyword::parse("#+KEY:"),
Some(("KEY", None, "", "#+KEY:".len()))
);
assert_eq!(
Keyword::parse("#+KEY: VALUE"),
Some(("KEY", None, "VALUE", "#+KEY: VALUE".len()))
);
assert_eq!(
Keyword::parse("#+K_E_Y: VALUE"),
Some(("K_E_Y", None, "VALUE", "#+K_E_Y: VALUE".len()))
);
assert_eq!(
Keyword::parse("#+KEY:VALUE\n"),
Some(("KEY", None, "VALUE", "#+KEY:VALUE\n".len()))
);
assert_eq!(Keyword::parse("#+KE Y: VALUE"), None);
assert_eq!(Keyword::parse("#+ KEY: VALUE"), None);
assert_eq!( assert_eq!(
parse("#+KEY:"), Keyword::parse("#+RESULTS:"),
Some((Key::Custom("KEY"), "", "#+KEY:".len())) Some(("RESULTS", None, "", "#+RESULTS:".len()))
);
assert_eq!(
parse("#+KEY: VALUE"),
Some((Key::Custom("KEY"), "VALUE", "#+KEY: VALUE".len()))
);
assert_eq!(
parse("#+K_E_Y: VALUE"),
Some((Key::Custom("K_E_Y"), "VALUE", "#+K_E_Y: VALUE".len()))
);
assert_eq!(
parse("#+KEY:VALUE\n"),
Some((Key::Custom("KEY"), "VALUE", "#+KEY:VALUE\n".len()))
);
assert_eq!(parse("#+KE Y: VALUE"), None);
assert_eq!(parse("#+ KEY: VALUE"), None);
assert_eq!(
parse("#+RESULTS:"),
Some((Key::Results { option: None }, "", "#+RESULTS:".len()))
); );
assert_eq!( assert_eq!(
parse("#+ATTR_LATEX: :width 5cm"), Keyword::parse("#+ATTR_LATEX: :width 5cm"),
Some(( Some((
Key::Attr { backend: "LATEX" }, "ATTR_LATEX",
None,
":width 5cm", ":width 5cm",
"#+ATTR_LATEX: :width 5cm".len() "#+ATTR_LATEX: :width 5cm".len()
)) ))
); );
assert_eq!( assert_eq!(
parse("#+CALL: double(n=4)"), Keyword::parse("#+CALL: double(n=4)"),
Some((Key::Call, "double(n=4)", "#+CALL: double(n=4)".len())) Some(("CALL", None, "double(n=4)", "#+CALL: double(n=4)".len()))
); );
assert_eq!( assert_eq!(
parse("#+CAPTION[Short caption]: Longer caption."), Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
Some(( Some((
Key::Caption { "CAPTION",
option: Some("Short caption") Some("Short caption"),
},
"Longer caption.", "Longer caption.",
"#+CAPTION[Short caption]: Longer caption.".len() "#+CAPTION[Short caption]: Longer caption.".len()
)) ))
); );
} }
}

View file

@ -12,5 +12,5 @@ pub(crate) mod planning;
pub(crate) mod rule; pub(crate) mod rule;
pub use self::clock::Clock; pub use self::clock::Clock;
pub use self::keyword::Key; pub use self::keyword::{Key, Keyword};
pub use self::planning::Planning; pub use self::planning::Planning;

View file

@ -1,4 +1,4 @@
use crate::objects::timestamp::Timestamp; use crate::objects::Timestamp;
use memchr::memchr; use memchr::memchr;
/// palnning elements /// palnning elements
@ -58,12 +58,9 @@ impl<'a> Planning<'a> {
} }
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn prase() { fn prase() {
use super::Planning; use crate::objects::Datetime;
use crate::objects::timestamp::{Datetime, Timestamp};
assert_eq!( assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"), Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
@ -71,7 +68,7 @@ mod tests {
Planning { Planning {
scheduled: Some(Timestamp::Active { scheduled: Some(Timestamp::Active {
start: Datetime { start: Datetime {
date: (2019, 4, 8), date: "2019-04-08",
time: None, time: None,
dayname: "Mon" dayname: "Mon"
}, },
@ -85,4 +82,3 @@ mod tests {
)) ))
) )
} }
}

View file

@ -1,11 +1,11 @@
#[inline] #[inline]
pub fn parse(src: &str) -> usize { pub fn parse(text: &str) -> usize {
let end = memchr::memchr(b'\n', src.as_bytes()) let (text, off) = memchr::memchr(b'\n', text.as_bytes())
.map(|i| i + 1) .map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| src.len()); .unwrap_or_else(|| (text.trim(), text.len()));
let rules = &src[0..end].trim();
if rules.len() >= 5 && rules.chars().all(|c| c == '-') { if text.len() >= 5 && text.as_bytes().iter().all(|&c| c == b'-') {
end off
} else { } else {
0 0
} }

View file

@ -32,7 +32,7 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
fn headline_beg(&mut self, w: &mut W, hdl: Headline) -> Result<(), E> { fn headline_beg(&mut self, w: &mut W, hdl: Headline) -> Result<(), E> {
let level = if hdl.level <= 6 { hdl.level } else { 6 }; let level = if hdl.level <= 6 { hdl.level } else { 6 };
write!(w, "<h{}>", level)?; write!(w, "<h{}>", level)?;
self.escape(w, hdl.title)?; self.text(w, hdl.title)?;
write!(w, "</h{}>", level)?; write!(w, "</h{}>", level)?;
Ok(()) Ok(())
} }
@ -155,7 +155,7 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
fn fn_def(&mut self, w: &mut W, label: &str, cont: &str) -> Result<(), E> { fn fn_def(&mut self, w: &mut W, label: &str, cont: &str) -> Result<(), E> {
Ok(()) Ok(())
} }
fn keyword(&mut self, w: &mut W, key: Key<'_>, value: &str) -> Result<(), E> { fn keyword(&mut self, w: &mut W, keyword: Keyword<'_>) -> Result<(), E> {
Ok(()) Ok(())
} }
fn rule(&mut self, w: &mut W) -> Result<(), E> { fn rule(&mut self, w: &mut W) -> Result<(), E> {
@ -164,42 +164,35 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
fn cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<(), E> { fn cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<(), E> {
Ok(()) Ok(())
} }
fn fn_ref(&mut self, w: &mut W, label: Option<&str>, def: Option<&str>) -> Result<(), E> { fn fn_ref(&mut self, w: &mut W, fn_ref: FnRef<'_>) -> Result<(), E> {
Ok(()) Ok(())
} }
fn inline_call( fn inline_call(&mut self, w: &mut W, call: InlineCall<'_>) -> Result<(), E> {
&mut self,
w: &mut W,
name: &str,
args: &str,
inside_header: Option<&str>,
end_header: Option<&str>,
) -> Result<(), E> {
Ok(()) Ok(())
} }
fn inline_src(&mut self, w: &mut W, _: &str, _: Option<&str>, body: &str) -> Result<(), E> { fn inline_src(&mut self, w: &mut W, src: InlineSrc<'_>) -> Result<(), E> {
write!(w, "<code>")?; write!(w, "<code>")?;
self.escape(w, body)?; self.text(w, src.body)?;
write!(w, "</code>")?; write!(w, "</code>")?;
Ok(()) Ok(())
} }
fn link(&mut self, w: &mut W, path: &str, desc: Option<&str>) -> Result<(), E> { fn link(&mut self, w: &mut W, link: Link<'_>) -> Result<(), E> {
write!(w, r#"<a href=""#)?; write!(w, r#"<a href=""#)?;
self.escape(w, path)?; self.text(w, link.path)?;
write!(w, r#"">"#)?; write!(w, r#"">"#)?;
self.escape(w, desc.unwrap_or(path))?; self.text(w, link.desc.unwrap_or(link.path))?;
write!(w, "</a>")?; write!(w, "</a>")?;
Ok(()) Ok(())
} }
fn macros(&mut self, w: &mut W, name: &str, args: Option<&str>) -> Result<(), E> { fn macros(&mut self, w: &mut W, macros: Macros<'_>) -> Result<(), E> {
Ok(()) Ok(())
} }
fn radio_target(&mut self, w: &mut W, target: &str) -> Result<(), E> { fn radio_target(&mut self, w: &mut W, target: &str) -> Result<(), E> {
Ok(()) Ok(())
} }
fn snippet(&mut self, w: &mut W, name: &str, value: &str) -> Result<(), E> { fn snippet(&mut self, w: &mut W, snippet: Snippet<'_>) -> Result<(), E> {
if name.eq_ignore_ascii_case("HTML") { if snippet.name.eq_ignore_ascii_case("HTML") {
Ok(write!(w, "{}", value)?) Ok(write!(w, "{}", snippet.value)?)
} else { } else {
Ok(()) Ok(())
} }
@ -236,13 +229,13 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
} }
fn verbatim(&mut self, w: &mut W, cont: &str) -> Result<(), E> { fn verbatim(&mut self, w: &mut W, cont: &str) -> Result<(), E> {
write!(w, "<code>")?; write!(w, "<code>")?;
self.escape(w, cont)?; self.text(w, cont)?;
write!(w, "</code>")?; write!(w, "</code>")?;
Ok(()) Ok(())
} }
fn code(&mut self, w: &mut W, cont: &str) -> Result<(), E> { fn code(&mut self, w: &mut W, cont: &str) -> Result<(), E> {
write!(w, "<code>")?; write!(w, "<code>")?;
self.escape(w, cont)?; self.text(w, cont)?;
write!(w, "</code>")?; write!(w, "</code>")?;
Ok(()) Ok(())
} }

View file

@ -40,21 +40,16 @@ macro_rules! handle_event {
TableCell => $handler.table_cell($writer)?, TableCell => $handler.table_cell($writer)?,
LatexEnv => $handler.latex_env($writer)?, LatexEnv => $handler.latex_env($writer)?,
FnDef { label, cont } => $handler.fn_def($writer, label, cont)?, FnDef { label, cont } => $handler.fn_def($writer, label, cont)?,
Keyword { key, value } => $handler.keyword($writer, key, value)?, Keyword(keyword) => $handler.keyword($writer, keyword)?,
Rule => $handler.rule($writer)?, Rule => $handler.rule($writer)?,
Cookie(cookie) => $handler.cookie($writer, cookie)?, Cookie(cookie) => $handler.cookie($writer, cookie)?,
FnRef { label, def } => $handler.fn_ref($writer, label, def)?, FnRef(fn_ref) => $handler.fn_ref($writer, fn_ref)?,
InlineSrc { lang, option, body } => $handler.inline_src($writer, lang, option, body)?, InlineSrc(src) => $handler.inline_src($writer, src)?,
InlineCall { InlineCall(call) => $handler.inline_call($writer, call)?,
name, Link(link) => $handler.link($writer, link)?,
args, Macros(macros) => $handler.macros($writer, macros)?,
inside_header,
end_header,
} => $handler.inline_call($writer, name, args, inside_header, end_header)?,
Link { path, desc } => $handler.link($writer, path, desc)?,
Macros { name, args } => $handler.macros($writer, name, args)?,
RadioTarget { target } => $handler.radio_target($writer, target)?, RadioTarget { target } => $handler.radio_target($writer, target)?,
Snippet { name, value } => $handler.snippet($writer, name, value)?, Snippet(snippet) => $handler.snippet($writer, snippet)?,
Target { target } => $handler.target($writer, target)?, Target { target } => $handler.target($writer, target)?,
BoldBeg => $handler.bold_beg($writer)?, BoldBeg => $handler.bold_beg($writer)?,
BoldEnd => $handler.bold_end($writer)?, BoldEnd => $handler.bold_end($writer)?,

View file

@ -7,8 +7,9 @@ pub enum Cookie<'a> {
Slash(&'a str, &'a str), Slash(&'a str, &'a str),
} }
impl<'a> Cookie<'a> {
#[inline] #[inline]
pub fn parse(src: &str) -> Option<(Cookie<'_>, usize)> { pub(crate) fn parse(src: &str) -> Option<(Cookie<'_>, usize)> {
debug_assert!(src.starts_with('[')); debug_assert!(src.starts_with('['));
let bytes = src.as_bytes(); let bytes = src.as_bytes();
@ -18,34 +19,47 @@ pub fn parse(src: &str) -> Option<(Cookie<'_>, usize)> {
if bytes[num1] == b'%' && *bytes.get(num1 + 1)? == b']' { if bytes[num1] == b'%' && *bytes.get(num1 + 1)? == b']' {
Some((Cookie::Percent(&src[1..num1]), num1 + 2)) Some((Cookie::Percent(&src[1..num1]), num1 + 2))
} else { } else {
let num2 = let num2 = memchr(b']', bytes)
memchr(b']', bytes).filter(|&i| bytes[num1 + 1..i].iter().all(u8::is_ascii_digit))?; .filter(|&i| bytes[num1 + 1..i].iter().all(u8::is_ascii_digit))?;
Some((Cookie::Slash(&src[1..num1], &src[num1 + 1..num2]), num2 + 1)) Some((Cookie::Slash(&src[1..num1], &src[num1 + 1..num2]), num2 + 1))
} }
} }
}
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
use super::Cookie::*;
assert_eq!(parse("[1/10]"), Some((Slash("1", "10"), "[1/10]".len())));
assert_eq!( assert_eq!(
parse("[1/1000]"), Cookie::parse("[1/10]"),
Some((Slash("1", "1000"), "[1/1000]".len())) Some((Cookie::Slash("1", "10"), "[1/10]".len()))
);
assert_eq!(
Cookie::parse("[1/1000]"),
Some((Cookie::Slash("1", "1000"), "[1/1000]".len()))
);
assert_eq!(
Cookie::parse("[10%]"),
Some((Cookie::Percent("10"), "[10%]".len()))
);
assert_eq!(
Cookie::parse("[%]"),
Some((Cookie::Percent(""), "[%]".len()))
);
assert_eq!(
Cookie::parse("[/]"),
Some((Cookie::Slash("", ""), "[/]".len()))
);
assert_eq!(
Cookie::parse("[100/]"),
Some((Cookie::Slash("100", ""), "[100/]".len()))
);
assert_eq!(
Cookie::parse("[/100]"),
Some((Cookie::Slash("", "100"), "[/100]".len()))
); );
assert_eq!(parse("[10%]"), Some((Percent("10"), "[10%]".len())));
assert_eq!(parse("[%]"), Some((Percent(""), "[%]".len())));
assert_eq!(parse("[/]"), Some((Slash("", ""), "[/]".len())));
assert_eq!(parse("[100/]"), Some((Slash("100", ""), "[100/]".len())));
assert_eq!(parse("[/100]"), Some((Slash("", "100"), "[/100]".len())));
assert_eq!(parse("[10% ]"), None); assert_eq!(Cookie::parse("[10% ]"), None);
assert_eq!(parse("[1//100]"), None); assert_eq!(Cookie::parse("[1//100]"), None);
assert_eq!(parse("[1\\100]"), None); assert_eq!(Cookie::parse("[1\\100]"), None);
assert_eq!(parse("[10%%]"), None); assert_eq!(Cookie::parse("[10%%]"), None);
}
} }

View file

@ -1,20 +1,36 @@
use memchr::{memchr2, memchr2_iter}; use memchr::{memchr2, memchr2_iter};
/// returns (footnote reference label, footnote reference definition, offset) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct FnRef<'a> {
pub label: Option<&'a str>,
pub definition: Option<&'a str>,
}
impl<'a> FnRef<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(Option<&str>, Option<&str>, usize)> { pub fn parse(text: &str) -> Option<(FnRef<'_>, usize)> {
debug_assert!(text.starts_with("[fn:")); debug_assert!(text.starts_with("[fn:"));
let bytes = text.as_bytes(); let bytes = text.as_bytes();
let (label, off) = memchr2(b']', b':', &bytes[4..]) let (label, off) = memchr2(b']', b':', &bytes["[fn:".len()..])
.filter(|&i| { .filter(|&i| {
bytes[4..i + 4] bytes["[fn:".len().."[fn:".len() + i]
.iter() .iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_') .all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}) })
.map(|i| (if i == 0 { None } else { Some(&text[4..i + 4]) }, i + 4))?; .map(|i| {
(
if i == 0 {
None
} else {
Some(&text["[fn:".len().."[fn:".len() + i])
},
"[fn:".len() + i,
)
})?;
let (def, off) = if bytes[off] == b':' { let (definition, off) = if bytes[off] == b':' {
let mut pairs = 1; let mut pairs = 1;
memchr2_iter(b'[', b']', &bytes[off..]) memchr2_iter(b'[', b']', &bytes[off..])
.find(|&i| { .find(|&i| {
@ -30,25 +46,51 @@ pub fn parse(text: &str) -> Option<(Option<&str>, Option<&str>, usize)> {
(None, off + 1) (None, off + 1)
}; };
Some((label, def, off)) Some((FnRef { label, definition }, off))
}
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!(parse("[fn:1]"), Some((Some("1"), None, "[fn:1]".len())));
assert_eq!( assert_eq!(
parse("[fn:1:2]"), FnRef::parse("[fn:1]"),
Some((Some("1"), Some("2"), "[fn:1:2]".len())) Some((
FnRef {
label: Some("1"),
definition: None
},
"[fn:1]".len()
))
); );
assert_eq!(parse("[fn::2]"), Some((None, Some("2"), "[fn::2]".len())));
assert_eq!( assert_eq!(
parse("[fn::[]]"), FnRef::parse("[fn:1:2]"),
Some((None, Some("[]"), "[fn::[]]".len())) Some((
FnRef {
label: Some("1"),
definition: Some("2")
},
"[fn:1:2]".len()
))
); );
assert_eq!(parse("[fn::[]"), None); assert_eq!(
} FnRef::parse("[fn::2]"),
Some((
FnRef {
label: None,
definition: Some("2")
},
"[fn::2]".len()
))
);
assert_eq!(
FnRef::parse("[fn::[]]"),
Some((
FnRef {
label: None,
definition: Some("[]")
},
"[fn::[]]".len()
))
);
assert_eq!(FnRef::parse("[fn::[]"), None);
} }

View file

@ -1,20 +1,30 @@
use memchr::{memchr, memchr2}; use memchr::{memchr, memchr2};
// returns (name, args, inside_header, end_header) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct InlineCall<'a> {
pub name: &'a str,
pub inside_header: Option<&'a str>,
pub args: &'a str,
pub end_header: Option<&'a str>,
}
impl<'a> InlineCall<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, &str, Option<&str>, Option<&str>, usize)> { pub(crate) fn parse(text: &str) -> Option<(InlineCall<'_>, usize)> {
debug_assert!(text.starts_with("call_")); debug_assert!(text.starts_with("call_"));
let bytes = text.as_bytes(); let bytes = text.as_bytes();
let (name, off) = memchr2(b'[', b'(', bytes) let (name, off) = memchr2(b'[', b'(', bytes)
.map(|i| (&text[5..i], i)) .map(|i| (&text["call_".len()..i], i))
.filter(|(name, _)| name.as_bytes().iter().all(u8::is_ascii_graphic))?; .filter(|(name, _)| name.as_bytes().iter().all(u8::is_ascii_graphic))?;
let (inside_header, off) = if bytes[off] == b'[' { let (inside_header, off) = if bytes[off] == b'[' {
memchr(b']', &bytes[off..]) memchr(b']', &bytes[off..])
.filter(|&i| { .filter(|&i| {
bytes[off + i + 1] == b'(' && bytes[off + 1..off + i].iter().all(|&c| c != b'\n') bytes[off + i + 1] == b'('
&& bytes[off + 1..off + i].iter().all(|&c| c != b'\n')
}) })
.map(|i| (Some(&text[off + 1..off + i]), off + i + 1))? .map(|i| (Some(&text[off + 1..off + i]), off + i + 1))?
} else { } else {
@ -33,48 +43,66 @@ pub fn parse(text: &str) -> Option<(&str, &str, Option<&str>, Option<&str>, usiz
(None, off) (None, off)
}; };
Some((name, args, inside_header, end_header, off)) Some((
InlineCall {
name,
args,
inside_header,
end_header,
},
off,
))
}
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!( assert_eq!(
parse("call_square(4)"), InlineCall::parse("call_square(4)"),
Some(("square", "4", None, None, "call_square(4)".len())) Some((
InlineCall {
name: "square",
args: "4",
inside_header: None,
end_header: None,
},
"call_square(4)".len()
))
); );
assert_eq!( assert_eq!(
parse("call_square[:results output](4)"), InlineCall::parse("call_square[:results output](4)"),
Some(( Some((
"square", InlineCall {
"4", name: "square",
Some(":results output"), args: "4",
None, inside_header: Some(":results output"),
end_header: None,
},
"call_square[:results output](4)".len() "call_square[:results output](4)".len()
)) ))
); );
assert_eq!( assert_eq!(
parse("call_square(4)[:results html]"), InlineCall::parse("call_square(4)[:results html]"),
Some(( Some((
"square", InlineCall {
"4", name: "square",
None, args: "4",
Some(":results html"), inside_header: None,
end_header: Some(":results html"),
},
"call_square(4)[:results html]".len() "call_square(4)[:results html]".len()
)) ))
); );
assert_eq!( assert_eq!(
parse("call_square[:results output](4)[:results html]"), InlineCall::parse("call_square[:results output](4)[:results html]"),
Some(( Some((
"square", InlineCall {
"4", name: "square",
Some(":results output"), args: "4",
Some(":results html"), inside_header: Some(":results output"),
end_header: Some(":results html"),
},
"call_square[:results output](4)[:results html]".len() "call_square[:results output](4)[:results html]".len()
)) ))
); );
} }
}

View file

@ -1,12 +1,20 @@
use memchr::{memchr, memchr2}; use memchr::{memchr, memchr2};
/// returns (language, option, body, offset) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct InlineSrc<'a> {
pub lang: &'a str,
pub option: Option<&'a str>,
pub body: &'a str,
}
impl<'a> InlineSrc<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, Option<&str>, &str, usize)> { pub(crate) fn parse(text: &str) -> Option<(InlineSrc<'_>, usize)> {
debug_assert!(text.starts_with("src_")); debug_assert!(text.starts_with("src_"));
let (lang, off) = memchr2(b'[', b'{', text.as_bytes()) let (lang, off) = memchr2(b'[', b'{', text.as_bytes())
.map(|i| (&text[4..i], i)) .map(|i| (&text["src_".len()..i], i))
.filter(|(lang, off)| { .filter(|(lang, off)| {
*off != 4 && lang.as_bytes().iter().all(|c| !c.is_ascii_whitespace()) *off != 4 && lang.as_bytes().iter().all(|c| !c.is_ascii_whitespace())
})?; })?;
@ -23,30 +31,41 @@ pub fn parse(text: &str) -> Option<(&str, Option<&str>, &str, usize)> {
.map(|i| (&text[off + 1..off + i], off + i + 1)) .map(|i| (&text[off + 1..off + i], off + i + 1))
.filter(|(body, _)| body.as_bytes().iter().all(|c| *c != b'\n'))?; .filter(|(body, _)| body.as_bytes().iter().all(|c| *c != b'\n'))?;
Some((lang, option, body, off)) Some((InlineSrc { lang, option, body }, off))
}
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!( assert_eq!(
parse("src_C{int a = 0;}"), InlineSrc::parse("src_C{int a = 0;}"),
Some(("C", None, "int a = 0;", "src_C{int a = 0;}".len())) Some((
InlineSrc {
lang: "C",
option: None,
body: "int a = 0;"
},
"src_C{int a = 0;}".len()
))
); );
assert_eq!( assert_eq!(
parse("src_xml[:exports code]{<tag>text</tag>}"), InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
Some(( Some((
"xml", InlineSrc {
Some(":exports code"), lang: "xml",
"<tag>text</tag>", option: Some(":exports code"),
body: "<tag>text</tag>",
},
"src_xml[:exports code]{<tag>text</tag>}".len() "src_xml[:exports code]{<tag>text</tag>}".len()
)) ))
); );
assert_eq!(parse("src_xml[:exports code]{<tag>text</tag>"), None); assert_eq!(
assert_eq!(parse("src_[:exports code]{<tag>text</tag>}"), None); InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>"),
None
);
assert_eq!(
InlineSrc::parse("src_[:exports code]{<tag>text</tag>}"),
None
);
// assert_eq!(parse("src_xml[:exports code]"), None); // assert_eq!(parse("src_xml[:exports code]"), None);
} }
}

View file

@ -1,13 +1,20 @@
use jetscii::Substring; use jetscii::Substring;
use memchr::memchr; use memchr::memchr;
/// returns (link path, link description, offset) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Link<'a> {
pub path: &'a str,
pub desc: Option<&'a str>,
}
impl<'a> Link<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> { pub(crate) fn parse(text: &str) -> Option<(Link<'_>, usize)> {
debug_assert!(text.starts_with("[[")); debug_assert!(text.starts_with("[["));
let (path, off) = memchr(b']', text.as_bytes()) let (path, off) = memchr(b']', text.as_bytes())
.map(|i| (&text[2..i], i)) .map(|i| (&text["[[".len()..i], i))
.filter(|(path, _)| { .filter(|(path, _)| {
path.as_bytes() path.as_bytes()
.iter() .iter()
@ -15,29 +22,46 @@ pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> {
})?; })?;
if *text.as_bytes().get(off + 1)? == b']' { if *text.as_bytes().get(off + 1)? == b']' {
Some((path, None, off + 2)) Some((Link { path, desc: None }, off + 2))
} else if text.as_bytes()[off + 1] == b'[' { } else if text.as_bytes()[off + 1] == b'[' {
let (desc, off) = Substring::new("]]") let (desc, off) = Substring::new("]]")
.find(&text[off + 1..]) .find(&text[off + 1..])
.map(|i| (&text[off + 2..off + i + 1], off + i + 3)) .map(|i| (&text[off + 2..off + 1 + i], off + 1 + i + "]]".len()))
.filter(|(desc, _)| desc.as_bytes().iter().all(|&c| c != b'[' && c != b']'))?; .filter(|(desc, _)| desc.as_bytes().iter().all(|&c| c != b'[' && c != b']'))?;
Some((path, Some(desc), off)) Some((
Link {
path,
desc: Some(desc),
},
off,
))
} else { } else {
None None
} }
} }
}
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!(parse("[[#id]]"), Some(("#id", None, "[[#id]]".len())));
assert_eq!( assert_eq!(
parse("[[#id][desc]]"), Link::parse("[[#id]]"),
Some(("#id", Some("desc"), "[[#id][desc]]".len())) Some((
Link {
path: "#id",
desc: None
},
"[[#id]]".len()
))
); );
assert_eq!(parse("[[#id][desc]"), None); assert_eq!(
} Link::parse("[[#id][desc]]"),
Some((
Link {
path: "#id",
desc: Some("desc")
},
"[[#id][desc]]".len()
))
);
assert_eq!(Link::parse("[[#id][desc]"), None);
} }

View file

@ -1,9 +1,16 @@
use jetscii::Substring; use jetscii::Substring;
use memchr::memchr2; use memchr::memchr2;
/// returns (macros name, macros arguments, offset) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Macros<'a> {
pub name: &'a str,
pub arguments: Option<&'a str>,
}
impl<'a> Macros<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> { pub fn parse(text: &str) -> Option<(Macros<'_>, usize)> {
debug_assert!(text.starts_with("{{{")); debug_assert!(text.starts_with("{{{"));
let bytes = text.as_bytes(); let bytes = text.as_bytes();
@ -19,42 +26,55 @@ pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> {
}) })
.map(|i| (&text[3..i], i))?; .map(|i| (&text[3..i], i))?;
let (args, off) = if bytes[off] == b'}' { let (arguments, off) = if bytes[off] == b'}' {
if text.len() <= off + 2 || bytes[off + 1] != b'}' || bytes[off + 2] != b'}' { if text.len() <= off + 2 || bytes[off + 1] != b'}' || bytes[off + 2] != b'}' {
return None; return None;
} }
(None, off + 3 /* }}} */) (None, off + "}}}".len())
} else { } else {
Substring::new(")}}}") Substring::new(")}}}")
.find(&text[off..]) .find(&text[off..])
.map(|i| (Some(&text[off + 1..off + i]), off + i + 4 /* )}}} */))? .map(|i| (Some(&text[off + 1..off + i]), off + i + ")}}}".len()))?
}; };
Some((name, args, off)) Some((Macros { name, arguments }, off))
}
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!( assert_eq!(
parse("{{{poem(red,blue)}}}"), Macros::parse("{{{poem(red,blue)}}}"),
Some(("poem", Some("red,blue"), "{{{poem(red,blue)}}}".len())) Some((
Macros {
name: "poem",
arguments: Some("red,blue")
},
"{{{poem(red,blue)}}}".len()
))
); );
assert_eq!( assert_eq!(
parse("{{{poem())}}}"), Macros::parse("{{{poem())}}}"),
Some(("poem", Some(")"), "{{{poem())}}}".len())) Some((
Macros {
name: "poem",
arguments: Some(")")
},
"{{{poem())}}}".len()
))
); );
assert_eq!( assert_eq!(
parse("{{{author}}}"), Macros::parse("{{{author}}}"),
Some(("author", None, "{{{author}}}".len())) Some((
Macros {
name: "author",
arguments: None
},
"{{{author}}}".len()
))
); );
assert_eq!(Macros::parse("{{{0uthor}}}"), None);
assert_eq!(parse("{{{0uthor}}}"), None); assert_eq!(Macros::parse("{{{author}}"), None);
assert_eq!(parse("{{{author}}"), None); assert_eq!(Macros::parse("{{{poem(}}}"), None);
assert_eq!(parse("{{{poem(}}}"), None); assert_eq!(Macros::parse("{{{poem)}}}"), None);
assert_eq!(parse("{{{poem)}}}"), None);
}
} }

View file

@ -14,4 +14,10 @@ pub(crate) mod target;
pub(crate) mod timestamp; pub(crate) mod timestamp;
pub use self::cookie::Cookie; pub use self::cookie::Cookie;
pub use self::fn_ref::FnRef;
pub use self::inline_call::InlineCall;
pub use self::inline_src::InlineSrc;
pub use self::link::Link;
pub use self::macros::Macros;
pub use self::snippet::Snippet;
pub use self::timestamp::*; pub use self::timestamp::*;

View file

@ -15,7 +15,7 @@ pub fn parse(src: &str) -> Option<(&str, usize)> {
.iter() .iter()
.all(|&c| c != b'<' && c != b'\n' && c != b'>') .all(|&c| c != b'<' && c != b'\n' && c != b'>')
}) })
.map(|i| (&src[3..i], i + 3 /* >>> */))?; .map(|i| (&src[3..i], i + ">>>".len()))?;
Some((target, off)) Some((target, off))
} }

View file

@ -1,9 +1,16 @@
use jetscii::Substring; use jetscii::Substring;
use memchr::memchr; use memchr::memchr;
/// returns (snippet name, snippet value, offset) #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Snippet<'a> {
pub name: &'a str,
pub value: &'a str,
}
impl<'a> Snippet<'a> {
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, &str, usize)> { pub(crate) fn parse(text: &str) -> Option<(Snippet<'_>, usize)> {
debug_assert!(text.starts_with("@@")); debug_assert!(text.starts_with("@@"));
let (name, off) = memchr(b':', text.as_bytes()) let (name, off) = memchr(b':', text.as_bytes())
@ -17,32 +24,45 @@ pub fn parse(text: &str) -> Option<(&str, &str, usize)> {
let (value, off) = Substring::new("@@") let (value, off) = Substring::new("@@")
.find(&text[off..]) .find(&text[off..])
.map(|i| (&text[off..off + i], off + i + 2 /* @@ */))?; .map(|i| (&text[off..off + i], off + i + "@@".len()))?;
Some((name, value, off)) Some((Snippet { name, value }, off))
}
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse() { fn parse() {
use super::parse;
assert_eq!( assert_eq!(
parse("@@html:<b>@@"), Snippet::parse("@@html:<b>@@"),
Some(("html", "<b>", "@@html:<b>@@".len())) Some((
Snippet {
name: "html",
value: "<b>"
},
"@@html:<b>@@".len()
))
); );
assert_eq!( assert_eq!(
parse("@@latex:any arbitrary LaTeX code@@"), Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
Some(( Some((
"latex", Snippet {
"any arbitrary LaTeX code", name: "latex",
value: "any arbitrary LaTeX code",
},
"@@latex:any arbitrary LaTeX code@@".len() "@@latex:any arbitrary LaTeX code@@".len()
)) ))
); );
assert_eq!(parse("@@html:@@"), Some(("html", "", "@@html:@@".len()))); assert_eq!(
assert_eq!(parse("@@html:<b>@"), None); Snippet::parse("@@html:@@"),
assert_eq!(parse("@@html<b>@@"), None); Some((
assert_eq!(parse("@@:<b>@@"), None); Snippet {
} name: "html",
value: "",
},
"@@html:@@".len()
))
);
assert_eq!(Snippet::parse("@@html:<b>@"), None);
assert_eq!(Snippet::parse("@@html<b>@@"), None);
assert_eq!(Snippet::parse("@@:<b>@@"), None);
} }

View file

@ -15,7 +15,7 @@ pub fn parse(text: &str) -> Option<(&str, usize)> {
.iter() .iter()
.all(|&c| c != b'<' && c != b'\n' && c != b'>') .all(|&c| c != b'<' && c != b'\n' && c != b'>')
}) })
.map(|i| (&text[2..i], i + 2 /* >> */)) .map(|i| (&text[2..i], i + ">>".len()))
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,11 +1,50 @@
use memchr::memchr; use memchr::memchr;
use std::str::FromStr;
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Datetime<'a> { pub struct Datetime<'a> {
pub date: (u16, u8, u8), pub(crate) date: &'a str,
pub time: Option<(u8, u8)>, pub(crate) time: Option<&'a str>,
pub dayname: &'a str, pub(crate) dayname: &'a str,
}
impl<'a> Datetime<'a> {
pub fn year(&self) -> u32 {
u32::from_str(&self.date[0..4]).unwrap()
}
pub fn month(&self) -> u32 {
u32::from_str(&self.date[5..7]).unwrap()
}
pub fn day(&self) -> u32 {
u32::from_str(&self.date[8..10]).unwrap()
}
pub fn hour(&self) -> Option<u32> {
self.time.map(|time| {
if time.len() == 4 {
u32::from_str(&time[0..1]).unwrap()
} else {
u32::from_str(&time[0..2]).unwrap()
}
})
}
pub fn minute(&self) -> Option<u32> {
self.time.map(|time| {
if time.len() == 4 {
u32::from_str(&time[2..4]).unwrap()
} else {
u32::from_str(&time[3..5]).unwrap()
}
})
}
pub fn dayname(&self) -> &str {
self.dayname
}
} }
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
@ -15,16 +54,11 @@ mod chrono {
impl<'a> Datetime<'a> { impl<'a> Datetime<'a> {
pub fn naive_date(&self) -> NaiveDate { pub fn naive_date(&self) -> NaiveDate {
let (y, m, d) = self.date; NaiveDate::from_ymd(self.year() as i32, self.month(), self.day())
NaiveDate::from_ymd(y.into(), m.into(), d.into())
} }
pub fn naive_time(&self) -> NaiveTime { pub fn naive_time(&self) -> NaiveTime {
if let Some((h, m)) = self.time { NaiveTime::from_hms(self.hour().unwrap_or(0), self.minute().unwrap_or(0), 0)
NaiveTime::from_hms(h.into(), m.into(), 0)
} else {
NaiveTime::from_hms(0, 0, 0)
}
} }
pub fn naive_date_time(&self) -> NaiveDateTime { pub fn naive_date_time(&self) -> NaiveDateTime {
@ -88,25 +122,25 @@ pub struct Delay {
pub enum Timestamp<'a> { pub enum Timestamp<'a> {
Active { Active {
start: Datetime<'a>, start: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
}, },
Inactive { Inactive {
start: Datetime<'a>, start: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
}, },
ActiveRange { ActiveRange {
start: Datetime<'a>, start: Datetime<'a>,
end: Datetime<'a>, end: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
}, },
InactiveRange { InactiveRange {
start: Datetime<'a>, start: Datetime<'a>,
end: Datetime<'a>, end: Datetime<'a>,
repeater: Option<Repeater>, repeater: Option<&'a str>,
delay: Option<Delay>, delay: Option<&'a str>,
}, },
Diary(&'a str), Diary(&'a str),
} }
@ -129,7 +163,9 @@ impl<'a> Timestamp<'a> {
let mut off = memchr(b'>', bytes)?; let mut off = memchr(b'>', bytes)?;
let (start, mut end) = Self::parse_datetime(&text[1..off])?; let (start, mut end) = Self::parse_datetime(&text[1..off])?;
if end.is_none() && off <= text.len() - 14 /* --<YYYY-MM-DD> */ && text[off + 1..].starts_with("--<") if end.is_none()
&& off + "--<YYYY-MM-DD >".len() <= text.len()
&& text[off + 1..].starts_with("--<")
{ {
if let Some(new_off) = memchr(b'>', &bytes[off + 1..]) { if let Some(new_off) = memchr(b'>', &bytes[off + 1..]) {
if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) { if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) {
@ -164,7 +200,9 @@ impl<'a> Timestamp<'a> {
let bytes = text.as_bytes(); let bytes = text.as_bytes();
let mut off = memchr(b']', bytes)?; let mut off = memchr(b']', bytes)?;
let (start, mut end) = Self::parse_datetime(&text[1..off])?; let (start, mut end) = Self::parse_datetime(&text[1..off])?;
if end.is_none() && off <= text.len() - 14 /* --[YYYY-MM-DD] */ && text[off + 1..].starts_with("--[") if end.is_none()
&& off + "--[YYYY-MM-DD ]".len() <= text.len()
&& text[off + 1..].starts_with("--[")
{ {
if let Some(new_off) = memchr(b']', &bytes[off + 1..]) { if let Some(new_off) = memchr(b']', &bytes[off + 1..]) {
if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) { if let Some((start, _)) = Self::parse_datetime(&text[off + 4..off + 1 + new_off]) {
@ -203,9 +241,7 @@ impl<'a> Timestamp<'a> {
let mut words = text.split_ascii_whitespace(); let mut words = text.split_ascii_whitespace();
let date = words let date = words.next().filter(|word| {
.next()
.filter(|word| {
let word = word.as_bytes(); let word = word.as_bytes();
// YYYY-MM-DD // YYYY-MM-DD
word.len() == 10 word.len() == 10
@ -214,17 +250,6 @@ impl<'a> Timestamp<'a> {
&& word[5..7].iter().all(u8::is_ascii_digit) && word[5..7].iter().all(u8::is_ascii_digit)
&& word[7] == b'-' && word[7] == b'-'
&& word[8..10].iter().all(u8::is_ascii_digit) && word[8..10].iter().all(u8::is_ascii_digit)
})
.map(|word| {
let word = word.as_bytes();
(
(u16::from(word[0]) - u16::from(b'0')) * 1000
+ (u16::from(word[1]) - u16::from(b'0')) * 100
+ (u16::from(word[2]) - u16::from(b'0')) * 10
+ (u16::from(word[3]) - u16::from(b'0')),
(word[5] - b'0') * 10 + (word[6] - b'0'),
(word[8] - b'0') * 10 + (word[9] - b'0'),
)
})?; })?;
let dayname = words.next().filter(|word| { let dayname = words.next().filter(|word| {
@ -239,86 +264,109 @@ impl<'a> Timestamp<'a> {
})?; })?;
let (start, end) = if let Some(word) = words.next() { let (start, end) = if let Some(word) = words.next() {
let word = word.as_bytes(); let time = word.as_bytes();
macro_rules! datetime { if (time.len() == "H:MM".len()
($a:expr, $b:expr, $c:expr) => { && time[0].is_ascii_digit()
&& time[1] == b':'
&& time[2..4].iter().all(u8::is_ascii_digit))
|| (time.len() == "HH:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit))
{
(
Datetime { Datetime {
date, date,
dayname, dayname,
time: Some((word[$a] - b'0', (word[$b] - b'0') * 10 + (word[$c] - b'0'))), time: Some(word),
} },
}; None,
($a:expr, $b:expr, $c:expr, $d:expr) => { )
} else if time.len() == "H:MM-H:MM".len()
&& time[0].is_ascii_digit()
&& time[1] == b':'
&& time[2..4].iter().all(u8::is_ascii_digit)
&& time[4] == b'-'
&& time[5].is_ascii_digit()
&& time[6] == b':'
&& time[7..9].iter().all(u8::is_ascii_digit)
{
(
Datetime { Datetime {
date, date,
dayname, dayname,
time: Some(( time: Some(&word[0..4]),
(word[$a] - b'0') * 10 + (word[$b] - b'0'), },
(word[$c] - b'0') * 10 + (word[$d] - b'0'), Some(Datetime {
)), date,
} dayname,
}; time: Some(&word[5..9]),
} }),
)
if word.len() == 4 } else if time.len() == "H:MM-HH:MM".len()
&& word[0].is_ascii_digit() && time[0].is_ascii_digit()
&& word[1] == b':' && time[1] == b':'
&& word[2..4].iter().all(u8::is_ascii_digit) && time[2..4].iter().all(u8::is_ascii_digit)
&& time[4] == b'-'
&& time[5..7].iter().all(u8::is_ascii_digit)
&& time[7] == b':'
&& time[8..10].iter().all(u8::is_ascii_digit)
{ {
// H:MM (
(datetime!(0, 2, 3), None) Datetime {
} else if word.len() == 5 date,
&& word[0..2].iter().all(u8::is_ascii_digit) dayname,
&& word[2] == b':' time: Some(&word[0..4]),
&& word[3..5].iter().all(u8::is_ascii_digit) },
Some(Datetime {
date,
dayname,
time: Some(&word[5..10]),
}),
)
} else if time.len() == "HH:MM-H:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit)
&& time[5] == b'-'
&& time[6].is_ascii_digit()
&& time[7] == b':'
&& time[8..10].iter().all(u8::is_ascii_digit)
{ {
// HH:MM (
(datetime!(0, 1, 3, 4), None) Datetime {
} else if word.len() == 9 date,
&& word[0].is_ascii_digit() dayname,
&& word[1] == b':' time: Some(&word[0..5]),
&& word[2..4].iter().all(u8::is_ascii_digit) },
&& word[4] == b'-' Some(Datetime {
&& word[5].is_ascii_digit() date,
&& word[6] == b':' dayname,
&& word[7..9].iter().all(u8::is_ascii_digit) time: Some(&word[6..10]),
}),
)
} else if time.len() == "HH:MM-HH:MM".len()
&& time[0..2].iter().all(u8::is_ascii_digit)
&& time[2] == b':'
&& time[3..5].iter().all(u8::is_ascii_digit)
&& time[5] == b'-'
&& time[6..8].iter().all(u8::is_ascii_digit)
&& time[8] == b':'
&& time[9..11].iter().all(u8::is_ascii_digit)
{ {
// H:MM-H:MM (
(datetime!(0, 2, 3), Some(datetime!(5, 7, 8))) Datetime {
} else if word.len() == 10 date,
&& word[0].is_ascii_digit() dayname,
&& word[1] == b':' time: Some(&word[0..5]),
&& word[2..4].iter().all(u8::is_ascii_digit) },
&& word[4] == b'-' Some(Datetime {
&& word[5..7].iter().all(u8::is_ascii_digit) date,
&& word[7] == b':' dayname,
&& word[8..10].iter().all(u8::is_ascii_digit) time: Some(&word[6..11]),
{ }),
// H:MM-HH:MM )
(datetime!(0, 2, 3), Some(datetime!(5, 6, 8, 9)))
} else if word.len() == 10
&& word[0..2].iter().all(u8::is_ascii_digit)
&& word[2] == b':'
&& word[3..5].iter().all(u8::is_ascii_digit)
&& word[5] == b'-'
&& word[6].is_ascii_digit()
&& word[7] == b':'
&& word[8..10].iter().all(u8::is_ascii_digit)
{
// HH:MM-H:MM
(datetime!(0, 1, 3, 4), Some(datetime!(6, 8, 9)))
} else if word.len() == 11
&& word[0..2].iter().all(u8::is_ascii_digit)
&& word[2] == b':'
&& word[3..5].iter().all(u8::is_ascii_digit)
&& word[5] == b'-'
&& word[6..8].iter().all(u8::is_ascii_digit)
&& word[8] == b':'
&& word[9..11].iter().all(u8::is_ascii_digit)
{
// HH:MM-HH:MM
(datetime!(0, 1, 3, 4), Some(datetime!(6, 7, 9, 10)))
} else { } else {
return None; return None;
} }
@ -344,20 +392,20 @@ impl<'a> Timestamp<'a> {
pub(crate) fn parse_diary(text: &str) -> Option<(Timestamp<'_>, usize)> { pub(crate) fn parse_diary(text: &str) -> Option<(Timestamp<'_>, usize)> {
debug_assert!(text.starts_with('<')); debug_assert!(text.starts_with('<'));
if text.len() <= 6 /* <%%()> */ || &text[1..4] != "%%(" { if text.len() <= "<%%()>".len() || &text[1..4] != "%%(" {
return None; return None;
} }
let bytes = text.as_bytes(); let bytes = text.as_bytes();
memchr(b'>', bytes) memchr(b'>', bytes)
.filter(|i| bytes[i - 1] == b')' && bytes[4..i - 1].iter().all(|&c| c != b'\n')) .filter(|i| {
.map(|i| (Timestamp::Diary(&text[4..i - 1]), i)) bytes[i - 1] == b')' && bytes["<%%(".len()..i - 1].iter().all(|&c| c != b'\n')
})
.map(|i| (Timestamp::Diary(&text["<%%(".len()..i - 1]), i))
} }
} }
#[cfg(test)]
mod tests {
#[test] #[test]
fn parse_range() { fn parse_range() {
use super::*; use super::*;
@ -367,7 +415,7 @@ mod tests {
Some(( Some((
Timestamp::Inactive { Timestamp::Inactive {
start: Datetime { start: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: None, time: None,
dayname: "Tue" dayname: "Tue"
}, },
@ -382,13 +430,13 @@ mod tests {
Some(( Some((
Timestamp::InactiveRange { Timestamp::InactiveRange {
start: Datetime { start: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("09:39"),
dayname: "Tue" dayname: "Tue"
}, },
end: Datetime { end: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((10, 39)), time: Some("10:39"),
dayname: "Tue" dayname: "Tue"
}, },
repeater: None, repeater: None,
@ -402,13 +450,13 @@ mod tests {
Some(( Some((
Timestamp::ActiveRange { Timestamp::ActiveRange {
start: Datetime { start: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("09:39"),
dayname: "Tue" dayname: "Tue"
}, },
end: Datetime { end: Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((10, 39)), time: Some("10:39"),
dayname: "Tue" dayname: "Tue"
}, },
repeater: None, repeater: None,
@ -427,7 +475,7 @@ mod tests {
Timestamp::parse_datetime("2003-09-16 Tue"), Timestamp::parse_datetime("2003-09-16 Tue"),
Some(( Some((
Datetime { Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: None, time: None,
dayname: "Tue" dayname: "Tue"
}, },
@ -438,8 +486,8 @@ mod tests {
Timestamp::parse_datetime("2003-09-16 Tue 9:39"), Timestamp::parse_datetime("2003-09-16 Tue 9:39"),
Some(( Some((
Datetime { Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("9:39"),
dayname: "Tue" dayname: "Tue"
}, },
None None
@ -449,8 +497,8 @@ mod tests {
Timestamp::parse_datetime("2003-09-16 Tue 09:39"), Timestamp::parse_datetime("2003-09-16 Tue 09:39"),
Some(( Some((
Datetime { Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("09:39"),
dayname: "Tue" dayname: "Tue"
}, },
None None
@ -460,13 +508,13 @@ mod tests {
Timestamp::parse_datetime("2003-09-16 Tue 9:39-10:39"), Timestamp::parse_datetime("2003-09-16 Tue 9:39-10:39"),
Some(( Some((
Datetime { Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((9, 39)), time: Some("9:39"),
dayname: "Tue" dayname: "Tue"
}, },
Some(Datetime { Some(Datetime {
date: (2003, 9, 16), date: "2003-09-16",
time: Some((10, 39)), time: Some("10:39"),
dayname: "Tue" dayname: "Tue"
}), }),
)) ))
@ -477,4 +525,3 @@ mod tests {
assert_eq!(Timestamp::parse_datetime("2003-09-16 09:39"), None); assert_eq!(Timestamp::parse_datetime("2003-09-16 09:39"), None);
assert_eq!(Timestamp::parse_datetime("2003-09-16 Tue 0939"), None); assert_eq!(Timestamp::parse_datetime("2003-09-16 Tue 0939"), None);
} }
}

View file

@ -105,44 +105,20 @@ pub enum Event<'a> {
label: &'a str, label: &'a str,
cont: &'a str, cont: &'a str,
}, },
Keyword { Keyword(Keyword<'a>),
key: Key<'a>,
value: &'a str,
},
Rule, Rule,
Timestamp(Timestamp<'a>), Timestamp(Timestamp<'a>),
Cookie(Cookie<'a>), Cookie(Cookie<'a>),
FnRef { FnRef(FnRef<'a>),
label: Option<&'a str>, InlineCall(InlineCall<'a>),
def: Option<&'a str>, InlineSrc(InlineSrc<'a>),
}, Link(Link<'a>),
InlineCall { Macros(Macros<'a>),
name: &'a str,
args: &'a str,
inside_header: Option<&'a str>,
end_header: Option<&'a str>,
},
InlineSrc {
lang: &'a str,
option: Option<&'a str>,
body: &'a str,
},
Link {
path: &'a str,
desc: Option<&'a str>,
},
Macros {
name: &'a str,
args: Option<&'a str>,
},
RadioTarget { RadioTarget {
target: &'a str, target: &'a str,
}, },
Snippet { Snippet(Snippet<'a>),
name: &'a str,
value: &'a str,
},
Target { Target {
target: &'a str, target: &'a str,
}, },
@ -430,12 +406,17 @@ impl<'a> Parser<'a> {
}) })
}) })
.or_else(|| { .or_else(|| {
keyword::parse(tail).map(|(key, value, off)| { Keyword::parse(tail).map(|(key, option, value, off)| {
if let Key::Call = key { (
(Event::Call { value }, off + line_begin, 0, 0) if key.eq_ignore_ascii_case("CALL") {
Event::Call { value }
} else { } else {
(Event::Keyword { key, value }, off + line_begin, 0, 0) Event::Keyword(Keyword::new(key, option, value))
} },
off + line_begin,
0,
0,
)
}) })
}) })
} else { } else {
@ -494,10 +475,12 @@ impl<'a> Parser<'a> {
let bytes = text.as_bytes(); let bytes = text.as_bytes();
match bytes[0] { match bytes[0] {
b'@' if bytes[1] == b'@' => snippet::parse(text) b'@' if bytes[1] == b'@' => {
.map(|(name, value, off)| (Event::Snippet { name, value }, off, 0, 0)), Snippet::parse(text).map(|(snippet, off)| (Event::Snippet(snippet), off, 0, 0))
b'{' if bytes[1] == b'{' && bytes[2] == b'{' => macros::parse(text) }
.map(|(name, args, off)| (Event::Macros { name, args }, off, 0, 0)), b'{' if bytes[1] == b'{' && bytes[2] == b'{' => {
Macros::parse(text).map(|(macros, off)| (Event::Macros(macros), off, 0, 0))
}
b'<' if bytes[1] == b'<' => { b'<' if bytes[1] == b'<' => {
if bytes[2] == b'<' { if bytes[2] == b'<' {
radio_target::parse(text) radio_target::parse(text)
@ -514,13 +497,11 @@ impl<'a> Parser<'a> {
}), }),
b'[' => { b'[' => {
if text[1..].starts_with("fn:") { if text[1..].starts_with("fn:") {
fn_ref::parse(text) FnRef::parse(text).map(|(fn_ref, off)| (Event::FnRef(fn_ref), off, 0, 0))
.map(|(label, def, off)| (Event::FnRef { label, def }, off, 0, 0))
} else if bytes[1] == b'[' { } else if bytes[1] == b'[' {
link::parse(text) Link::parse(text).map(|(link, off)| (Event::Link(link), off, 0, 0))
.map(|(path, desc, off)| (Event::Link { path, desc }, off, 0, 0))
} else { } else {
cookie::parse(text) Cookie::parse(text)
.map(|(cookie, off)| (Event::Cookie(cookie), off, 0, 0)) .map(|(cookie, off)| (Event::Cookie(cookie), off, 0, 0))
.or_else(|| { .or_else(|| {
Timestamp::parse_inactive(text) Timestamp::parse_inactive(text)
@ -545,24 +526,10 @@ impl<'a> Parser<'a> {
emphasis::parse(text, b'~').map(|end| (Event::Code(&text[1..end]), end + 1, 0, 0)) emphasis::parse(text, b'~').map(|end| (Event::Code(&text[1..end]), end + 1, 0, 0))
} }
b's' if text.starts_with("src_") => { b's' if text.starts_with("src_") => {
inline_src::parse(text).map(|(lang, option, body, off)| { InlineSrc::parse(text).map(|(src, off)| (Event::InlineSrc(src), off, 0, 0))
(Event::InlineSrc { lang, option, body }, off, 0, 0)
})
} }
b'c' if text.starts_with("call_") => { b'c' if text.starts_with("call_") => {
inline_call::parse(text).map(|(name, args, inside_header, end_header, off)| { InlineCall::parse(text).map(|(call, off)| (Event::InlineCall(call), off, 0, 0))
(
Event::InlineCall {
name,
args,
inside_header,
end_header,
},
off,
0,
0,
)
})
} }
_ => None, _ => None,
} }

View file

@ -1,9 +1,9 @@
use crate::elements::{fn_def, keyword, Key}; use crate::elements::{fn_def, Keyword};
use crate::headline::{Headline, DEFAULT_KEYWORDS}; use crate::headline::{Headline, DEFAULT_KEYWORDS};
use memchr::memchr; use memchr::memchr;
type Headlines<'a> = Vec<Headline<'a>>; type Headlines<'a> = Vec<Headline<'a>>;
type Keywords<'a> = Vec<(Key<'a>, &'a str)>; type Keywords<'a> = Vec<(&'a str, &'a str)>;
type Footnotes<'a> = Vec<&'a str>; type Footnotes<'a> = Vec<&'a str>;
pub fn metadata(src: &str) -> (Headlines<'_>, Keywords<'_>, Footnotes<'_>) { pub fn metadata(src: &str) -> (Headlines<'_>, Keywords<'_>, Footnotes<'_>) {
@ -16,7 +16,7 @@ pub fn metadata(src: &str) -> (Headlines<'_>, Keywords<'_>, Footnotes<'_>) {
headlines.push(Headline::parse(line, DEFAULT_KEYWORDS).0) headlines.push(Headline::parse(line, DEFAULT_KEYWORDS).0)
} }
} else if line.starts_with("#+") { } else if line.starts_with("#+") {
if let Some((key, value, _)) = keyword::parse(line) { if let Some((key, _, value, _)) = Keyword::parse(line) {
keywords.push((key, value)) keywords.push((key, value))
} }
} else if line.starts_with("[fn:") { } else if line.starts_with("[fn:") {