orgize/src/elements/keyword.rs
2019-02-07 15:54:16 +08:00

137 lines
4.1 KiB
Rust

use memchr::{memchr, memchr2};
pub struct Keyword;
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub enum Key<'a> {
// Affiliated Keywords
// Only "CAPTION" and "RESULTS" keywords can have an optional value.
Caption { option: Option<&'a str> },
Header,
Name,
Plot,
Results { option: Option<&'a str> },
Attr { backend: &'a str },
// Keywords
Author,
Date,
Title,
Custom(&'a str),
// Babel Call
Call,
}
impl Keyword {
// return (key, value, offset)
pub fn parse(src: &str) -> Option<(Key<'_>, &str, usize)> {
debug_assert!(src.starts_with("#+"));
let bytes = src.as_bytes();
let key_end = memchr2(b':', b'[', bytes).filter(|&i| {
bytes[2..i]
.iter()
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
})?;
let option = if bytes[key_end] == b'[' {
let option =
memchr(b']', bytes).filter(|&i| bytes[key_end..i].iter().all(|&c| c != b'\n'))?;
expect!(src, option + 1, b':')?;
option + 1
} else {
key_end
};
// includes the eol character
let end = memchr::memchr(b'\n', src.as_bytes())
.map(|i| i + 1)
.unwrap_or_else(|| src.len());
Some((
match &src[2..key_end] {
key if key.eq_ignore_ascii_case("CAPTION") => Key::Caption {
option: if key_end == option {
None
} else {
Some(&src[key_end + 1..option - 1])
},
},
key if key.eq_ignore_ascii_case("HEADER") => Key::Header,
key if key.eq_ignore_ascii_case("NAME") => Key::Name,
key if key.eq_ignore_ascii_case("PLOT") => Key::Plot,
key if key.eq_ignore_ascii_case("RESULTS") => Key::Results {
option: if key_end == option {
None
} else {
Some(&src[key_end + 1..option - 1])
},
},
key if key.eq_ignore_ascii_case("AUTHOR") => Key::Author,
key if key.eq_ignore_ascii_case("DATE") => Key::Date,
key if key.eq_ignore_ascii_case("TITLE") => Key::Title,
key if key.eq_ignore_ascii_case("CALL") => Key::Call,
key if key.starts_with("ATTR_") => Key::Attr {
backend: &src["#+ATTR_".len()..key_end],
},
key => Key::Custom(key),
},
&src[option + 1..end].trim(),
end,
))
}
}
#[test]
fn parse() {
assert_eq!(
Keyword::parse("#+KEY:"),
Some((Key::Custom("KEY"), "", "#+KEY:".len()))
);
assert_eq!(
Keyword::parse("#+KEY: VALUE"),
Some((Key::Custom("KEY"), "VALUE", "#+KEY: VALUE".len()))
);
assert_eq!(
Keyword::parse("#+K_E_Y: VALUE"),
Some((Key::Custom("K_E_Y"), "VALUE", "#+K_E_Y: VALUE".len()))
);
assert_eq!(
Keyword::parse("#+KEY:VALUE\n"),
Some((Key::Custom("KEY"), "VALUE", "#+KEY:VALUE\n".len()))
);
assert!(Keyword::parse("#+KE Y: VALUE").is_none());
assert!(Keyword::parse("#+ KEY: VALUE").is_none());
assert_eq!(
Keyword::parse("#+RESULTS:"),
Some((Key::Results { option: None }, "", "#+RESULTS:".len()))
);
assert_eq!(
Keyword::parse("#+ATTR_LATEX: :width 5cm"),
Some((
Key::Attr { backend: "LATEX" },
":width 5cm",
"#+ATTR_LATEX: :width 5cm".len()
))
);
assert_eq!(
Keyword::parse("#+CALL: double(n=4)"),
Some((Key::Call, "double(n=4)", "#+CALL: double(n=4)".len()))
);
assert_eq!(
Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
Some((
Key::Caption {
option: Some("Short caption")
},
"Longer caption.",
"#+CAPTION[Short caption]: Longer caption.".len()
))
);
}