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;
/// clock elements
@ -11,15 +11,15 @@ pub enum Clock<'a> {
Closed {
start: Datetime<'a>,
end: Datetime<'a>,
repeater: Option<Repeater>,
delay: Option<Delay>,
repeater: Option<&'a str>,
delay: Option<&'a str>,
duration: &'a str,
},
/// running Clock
Running {
start: Datetime<'a>,
repeater: Option<Repeater>,
delay: Option<Delay>,
repeater: Option<&'a str>,
delay: Option<&'a str>,
},
}
@ -37,60 +37,57 @@ impl<'a> Clock<'a> {
return None;
}
match Timestamp::parse_inactive(tail).map(|(t, off)| (t, tail[off..].trim_start())) {
Some((
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
},
tail,
)) => {
if tail.starts_with("=>") {
let duration = &tail[3..].trim();
let colon = memchr(b':', duration.as_bytes())?;
if duration.as_bytes()[0..colon].iter().all(u8::is_ascii_digit)
&& colon == duration.len() - 3
&& duration.as_bytes()[colon + 1].is_ascii_digit()
&& duration.as_bytes()[colon + 2].is_ascii_digit()
{
return Some((
Clock::Closed {
start,
end,
repeater,
delay,
duration,
},
off,
));
}
let (timestamp, tail) =
Timestamp::parse_inactive(tail).map(|(t, off)| (t, tail[off..].trim_start()))?;
match timestamp {
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
} if tail.starts_with("=>") => {
let duration = &tail[3..].trim();
let colon = memchr(b':', duration.as_bytes())?;
if duration.as_bytes()[0..colon].iter().all(u8::is_ascii_digit)
&& colon == duration.len() - 3
&& duration.as_bytes()[colon + 1].is_ascii_digit()
&& duration.as_bytes()[colon + 2].is_ascii_digit()
{
Some((
Clock::Closed {
start,
end,
repeater,
delay,
duration,
},
off,
))
} else {
None
}
}
Some((
Timestamp::Inactive {
start,
repeater,
delay,
},
tail,
)) => {
Timestamp::Inactive {
start,
repeater,
delay,
} => {
if tail.as_bytes().iter().all(u8::is_ascii_whitespace) {
return Some((
Some((
Clock::Running {
start,
repeater,
delay,
},
off,
));
))
} else {
None
}
}
_ => (),
_ => None,
}
None
}
/// returns `true` if the clock is running
@ -146,48 +143,42 @@ impl<'a> Clock<'a> {
}
}
#[cfg(test)]
mod tests {
use super::Clock;
use crate::objects::timestamp::Datetime;
#[test]
fn parse() {
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Some((
Clock::Running {
start: Datetime {
date: (2003, 9, 16),
time: Some((9, 39)),
dayname: "Tue"
},
repeater: None,
delay: None,
#[test]
fn parse() {
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Some((
Clock::Running {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
dayname: "Tue"
},
"CLOCK: [2003-09-16 Tue 09:39]".len()
))
);
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"),
Some((
Clock::Closed {
start: Datetime {
date: (2003, 9, 16),
time: Some((9, 39)),
dayname: "Tue"
},
end: Datetime {
date: (2003, 9, 16),
time: Some((10, 39)),
dayname: "Tue"
},
repeater: None,
delay: None,
duration: "1:00",
repeater: None,
delay: None,
},
"CLOCK: [2003-09-16 Tue 09:39]".len()
))
);
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00"),
Some((
Clock::Closed {
start: Datetime {
date: "2003-09-16",
time: Some("09:39"),
dayname: "Tue"
},
"CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00".len()
))
);
}
end: Datetime {
date: "2003-09-16",
time: Some("10:39"),
dayname: "Tue"
},
repeater: None,
delay: None,
duration: "1:00",
},
"CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00".len()
))
);
}

View file

@ -5,7 +5,7 @@ use memchr::{memchr, memchr_iter};
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
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;
}
@ -15,9 +15,15 @@ pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
let (name, para, off) = lines
.next()
.map(|i| {
memchr(b' ', &bytes[9..i])
.map(|x| (&text[9..9 + x], Some(text[9 + x..i].trim()), i + 1))
.unwrap_or((&text[9..i], None, i + 1))
memchr(b' ', &bytes["#+BEGIN: ".len()..i])
.map(|x| {
(
&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()))?;

View file

@ -7,11 +7,11 @@ pub fn parse(text: &str) -> Option<(&str, &str, usize)> {
let (label, off) = memchr(b']', text.as_bytes())
.filter(|&i| {
i != 4
&& text.as_bytes()[4..i]
&& text.as_bytes()["[fn:".len()..i]
.iter()
.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())
.map(|i| (&text[off..i], i))

View file

@ -17,112 +17,124 @@ pub enum Key<'a> {
Date,
Title,
Custom(&'a str),
// Babel Call
Call,
}
pub fn parse(text: &str) -> Option<(Key<'_>, &str, usize)> {
debug_assert!(text.starts_with("#+"));
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Keyword<'a> {
pub key: Key<'a>,
pub value: &'a str,
}
let bytes = text.as_bytes();
let (key, off) = memchr2(b':', b'[', bytes)
.filter(|&i| {
bytes[2..i]
.iter()
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
})
.map(|i| (&text[2..i], i + 1))?;
let (option, off) = if bytes[off - 1] == b'[' {
memchr(b']', bytes)
.filter(|&i| {
bytes[off..i].iter().all(|&c| c != b'\n') && i < text.len() && bytes[i + 1] == b':'
})
.map(|i| (Some(&text[off..i]), i + 2 /* ]: */))?
} else {
(None, off)
};
let (value, off) = memchr(b'\n', bytes)
.map(|i| (&text[off..i], i + 1))
.unwrap_or_else(|| (&text[off..], text.len()));
Some((
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()..],
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)
}
}
},
_ => Key::Custom(key),
},
value.trim(),
off,
))
}
value,
}
}
#[cfg(test)]
mod tests {
#[test]
fn parse() {
use super::*;
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, Option<&str>, &str, usize)> {
debug_assert!(text.starts_with("#+"));
assert_eq!(
parse("#+KEY:"),
Some((Key::Custom("KEY"), "", "#+KEY:".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);
let bytes = text.as_bytes();
assert_eq!(
parse("#+RESULTS:"),
Some((Key::Results { option: None }, "", "#+RESULTS:".len()))
);
let (key, off) = memchr2(b':', b'[', bytes)
.filter(|&i| {
bytes[2..i]
.iter()
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
})
.map(|i| (&text[2..i], i + 1))?;
assert_eq!(
parse("#+ATTR_LATEX: :width 5cm"),
Some((
Key::Attr { backend: "LATEX" },
":width 5cm",
"#+ATTR_LATEX: :width 5cm".len()
))
);
let (option, off) = if bytes[off - 1] == b'[' {
memchr(b']', bytes)
.filter(|&i| {
bytes[off..i].iter().all(|&c| c != b'\n')
&& i < text.len()
&& bytes[i + 1] == b':'
})
.map(|i| (Some(&text[off..i]), i + "]:".len()))?
} else {
(None, off)
};
assert_eq!(
parse("#+CALL: double(n=4)"),
Some((Key::Call, "double(n=4)", "#+CALL: double(n=4)".len()))
);
let (value, off) = memchr(b'\n', bytes)
.map(|i| (&text[off..i], i + 1))
.unwrap_or_else(|| (&text[off..], text.len()));
assert_eq!(
parse("#+CAPTION[Short caption]: Longer caption."),
Some((
Key::Caption {
option: Some("Short caption")
},
"Longer caption.",
"#+CAPTION[Short caption]: Longer caption.".len()
))
);
Some((key, option, value.trim(), off))
}
}
#[test]
fn parse() {
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!(
Keyword::parse("#+RESULTS:"),
Some(("RESULTS", None, "", "#+RESULTS:".len()))
);
assert_eq!(
Keyword::parse("#+ATTR_LATEX: :width 5cm"),
Some((
"ATTR_LATEX",
None,
":width 5cm",
"#+ATTR_LATEX: :width 5cm".len()
))
);
assert_eq!(
Keyword::parse("#+CALL: double(n=4)"),
Some(("CALL", None, "double(n=4)", "#+CALL: double(n=4)".len()))
);
assert_eq!(
Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
Some((
"CAPTION",
Some("Short caption"),
"Longer caption.",
"#+CAPTION[Short caption]: Longer caption.".len()
))
);
}

View file

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

View file

@ -1,4 +1,4 @@
use crate::objects::timestamp::Timestamp;
use crate::objects::Timestamp;
use memchr::memchr;
/// palnning elements
@ -58,31 +58,27 @@ impl<'a> Planning<'a> {
}
}
#[cfg(test)]
mod tests {
#[test]
fn prase() {
use super::Planning;
use crate::objects::timestamp::{Datetime, Timestamp};
#[test]
fn prase() {
use crate::objects::Datetime;
assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
Some((
Planning {
scheduled: Some(Timestamp::Active {
start: Datetime {
date: (2019, 4, 8),
time: None,
dayname: "Mon"
},
repeater: None,
delay: None
}),
closed: None,
deadline: None,
},
"SCHEDULED: <2019-04-08 Mon>\n".len()
))
)
}
assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
Some((
Planning {
scheduled: Some(Timestamp::Active {
start: Datetime {
date: "2019-04-08",
time: None,
dayname: "Mon"
},
repeater: None,
delay: None
}),
closed: None,
deadline: None,
},
"SCHEDULED: <2019-04-08 Mon>\n".len()
))
)
}

View file

@ -1,11 +1,11 @@
#[inline]
pub fn parse(src: &str) -> usize {
let end = memchr::memchr(b'\n', src.as_bytes())
.map(|i| i + 1)
.unwrap_or_else(|| src.len());
let rules = &src[0..end].trim();
if rules.len() >= 5 && rules.chars().all(|c| c == '-') {
end
pub fn parse(text: &str) -> usize {
let (text, off) = memchr::memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len()));
if text.len() >= 5 && text.as_bytes().iter().all(|&c| c == b'-') {
off
} else {
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> {
let level = if hdl.level <= 6 { hdl.level } else { 6 };
write!(w, "<h{}>", level)?;
self.escape(w, hdl.title)?;
self.text(w, hdl.title)?;
write!(w, "</h{}>", level)?;
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> {
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(())
}
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> {
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(())
}
fn inline_call(
&mut self,
w: &mut W,
name: &str,
args: &str,
inside_header: Option<&str>,
end_header: Option<&str>,
) -> Result<(), E> {
fn inline_call(&mut self, w: &mut W, call: InlineCall<'_>) -> Result<(), E> {
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>")?;
self.escape(w, body)?;
self.text(w, src.body)?;
write!(w, "</code>")?;
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=""#)?;
self.escape(w, path)?;
self.text(w, link.path)?;
write!(w, r#"">"#)?;
self.escape(w, desc.unwrap_or(path))?;
self.text(w, link.desc.unwrap_or(link.path))?;
write!(w, "</a>")?;
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(())
}
fn radio_target(&mut self, w: &mut W, target: &str) -> Result<(), E> {
Ok(())
}
fn snippet(&mut self, w: &mut W, name: &str, value: &str) -> Result<(), E> {
if name.eq_ignore_ascii_case("HTML") {
Ok(write!(w, "{}", value)?)
fn snippet(&mut self, w: &mut W, snippet: Snippet<'_>) -> Result<(), E> {
if snippet.name.eq_ignore_ascii_case("HTML") {
Ok(write!(w, "{}", snippet.value)?)
} else {
Ok(())
}
@ -236,13 +229,13 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
}
fn verbatim(&mut self, w: &mut W, cont: &str) -> Result<(), E> {
write!(w, "<code>")?;
self.escape(w, cont)?;
self.text(w, cont)?;
write!(w, "</code>")?;
Ok(())
}
fn code(&mut self, w: &mut W, cont: &str) -> Result<(), E> {
write!(w, "<code>")?;
self.escape(w, cont)?;
self.text(w, cont)?;
write!(w, "</code>")?;
Ok(())
}

View file

@ -40,21 +40,16 @@ macro_rules! handle_event {
TableCell => $handler.table_cell($writer)?,
LatexEnv => $handler.latex_env($writer)?,
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)?,
Cookie(cookie) => $handler.cookie($writer, cookie)?,
FnRef { label, def } => $handler.fn_ref($writer, label, def)?,
InlineSrc { lang, option, body } => $handler.inline_src($writer, lang, option, body)?,
InlineCall {
name,
args,
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)?,
FnRef(fn_ref) => $handler.fn_ref($writer, fn_ref)?,
InlineSrc(src) => $handler.inline_src($writer, src)?,
InlineCall(call) => $handler.inline_call($writer, call)?,
Link(link) => $handler.link($writer, link)?,
Macros(macros) => $handler.macros($writer, macros)?,
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)?,
BoldBeg => $handler.bold_beg($writer)?,
BoldEnd => $handler.bold_end($writer)?,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,67 @@
use jetscii::Substring;
use memchr::memchr;
/// returns (link path, link description, offset)
#[inline]
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> {
debug_assert!(text.starts_with("[["));
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Link<'a> {
pub path: &'a str,
pub desc: Option<&'a str>,
}
let (path, off) = memchr(b']', text.as_bytes())
.map(|i| (&text[2..i], i))
.filter(|(path, _)| {
path.as_bytes()
.iter()
.all(|&c| c != b'<' && c != b'>' && c != b'\n')
})?;
impl<'a> Link<'a> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(Link<'_>, usize)> {
debug_assert!(text.starts_with("[["));
if *text.as_bytes().get(off + 1)? == b']' {
Some((path, None, off + 2))
} else if text.as_bytes()[off + 1] == b'[' {
let (desc, off) = Substring::new("]]")
.find(&text[off + 1..])
.map(|i| (&text[off + 2..off + i + 1], off + i + 3))
.filter(|(desc, _)| desc.as_bytes().iter().all(|&c| c != b'[' && c != b']'))?;
Some((path, Some(desc), off))
} else {
None
let (path, off) = memchr(b']', text.as_bytes())
.map(|i| (&text["[[".len()..i], i))
.filter(|(path, _)| {
path.as_bytes()
.iter()
.all(|&c| c != b'<' && c != b'>' && c != b'\n')
})?;
if *text.as_bytes().get(off + 1)? == b']' {
Some((Link { path, desc: None }, off + 2))
} else if text.as_bytes()[off + 1] == b'[' {
let (desc, off) = Substring::new("]]")
.find(&text[off + 1..])
.map(|i| (&text[off + 2..off + 1 + i], off + 1 + i + "]]".len()))
.filter(|(desc, _)| desc.as_bytes().iter().all(|&c| c != b'[' && c != b']'))?;
Some((
Link {
path,
desc: Some(desc),
},
off,
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn parse() {
use super::parse;
assert_eq!(parse("[[#id]]"), Some(("#id", None, "[[#id]]".len())));
assert_eq!(
parse("[[#id][desc]]"),
Some(("#id", Some("desc"), "[[#id][desc]]".len()))
);
assert_eq!(parse("[[#id][desc]"), None);
}
#[test]
fn parse() {
assert_eq!(
Link::parse("[[#id]]"),
Some((
Link {
path: "#id",
desc: None
},
"[[#id]]".len()
))
);
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,60 +1,80 @@
use jetscii::Substring;
use memchr::memchr2;
/// returns (macros name, macros arguments, offset)
#[inline]
pub fn parse(text: &str) -> Option<(&str, Option<&str>, usize)> {
debug_assert!(text.starts_with("{{{"));
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Macros<'a> {
pub name: &'a str,
pub arguments: Option<&'a str>,
}
let bytes = text.as_bytes();
if text.len() <= 3 || !bytes[3].is_ascii_alphabetic() {
return None;
}
impl<'a> Macros<'a> {
#[inline]
pub fn parse(text: &str) -> Option<(Macros<'_>, usize)> {
debug_assert!(text.starts_with("{{{"));
let (name, off) = memchr2(b'}', b'(', bytes)
.filter(|&i| {
bytes[3..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
})
.map(|i| (&text[3..i], i))?;
let (args, off) = if bytes[off] == b'}' {
if text.len() <= off + 2 || bytes[off + 1] != b'}' || bytes[off + 2] != b'}' {
let bytes = text.as_bytes();
if text.len() <= 3 || !bytes[3].is_ascii_alphabetic() {
return None;
}
(None, off + 3 /* }}} */)
} else {
Substring::new(")}}}")
.find(&text[off..])
.map(|i| (Some(&text[off + 1..off + i]), off + i + 4 /* )}}} */))?
};
Some((name, args, off))
}
let (name, off) = memchr2(b'}', b'(', bytes)
.filter(|&i| {
bytes[3..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
})
.map(|i| (&text[3..i], i))?;
#[cfg(test)]
mod tests {
#[test]
fn parse() {
use super::parse;
let (arguments, off) = if bytes[off] == b'}' {
if text.len() <= off + 2 || bytes[off + 1] != b'}' || bytes[off + 2] != b'}' {
return None;
}
(None, off + "}}}".len())
} else {
Substring::new(")}}}")
.find(&text[off..])
.map(|i| (Some(&text[off + 1..off + i]), off + i + ")}}}".len()))?
};
assert_eq!(
parse("{{{poem(red,blue)}}}"),
Some(("poem", Some("red,blue"), "{{{poem(red,blue)}}}".len()))
);
assert_eq!(
parse("{{{poem())}}}"),
Some(("poem", Some(")"), "{{{poem())}}}".len()))
);
assert_eq!(
parse("{{{author}}}"),
Some(("author", None, "{{{author}}}".len()))
);
assert_eq!(parse("{{{0uthor}}}"), None);
assert_eq!(parse("{{{author}}"), None);
assert_eq!(parse("{{{poem(}}}"), None);
assert_eq!(parse("{{{poem)}}}"), None);
Some((Macros { name, arguments }, off))
}
}
#[test]
fn parse() {
assert_eq!(
Macros::parse("{{{poem(red,blue)}}}"),
Some((
Macros {
name: "poem",
arguments: Some("red,blue")
},
"{{{poem(red,blue)}}}".len()
))
);
assert_eq!(
Macros::parse("{{{poem())}}}"),
Some((
Macros {
name: "poem",
arguments: Some(")")
},
"{{{poem())}}}".len()
))
);
assert_eq!(
Macros::parse("{{{author}}}"),
Some((
Macros {
name: "author",
arguments: None
},
"{{{author}}}".len()
))
);
assert_eq!(Macros::parse("{{{0uthor}}}"), None);
assert_eq!(Macros::parse("{{{author}}"), None);
assert_eq!(Macros::parse("{{{poem(}}}"), None);
assert_eq!(Macros::parse("{{{poem)}}}"), None);
}

View file

@ -14,4 +14,10 @@ pub(crate) mod target;
pub(crate) mod timestamp;
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::*;

View file

@ -15,7 +15,7 @@ pub fn parse(src: &str) -> Option<(&str, usize)> {
.iter()
.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))
}

View file

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

View file

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

View file

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