137 lines
4.1 KiB
Rust
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()
|
|
))
|
|
);
|
|
}
|