static_regex! {
KEY_VALUE = r"^([A-Za-z][A-Za-z0-9 -]*?):[ \t]*(.*?)[ \t]*$";
}
pub(super) type Object<'a> = Vec<(String, &'a str)>;
pub(super) fn objects(raw: &str) -> Vec<Object<'_>> {
let mut out = Vec::new();
let mut current: Object<'_> = Vec::new();
for line in raw.lines() {
let line = line.trim_end_matches('\r');
if line.trim().is_empty() {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
continue;
}
if line.starts_with('%') {
continue;
}
if let Some(caps) = KEY_VALUE.captures(line) {
if let (Some(k), Some(v)) = (caps.get(1), caps.get(2)) {
current.push((k.as_str().to_ascii_lowercase(), v.as_str()));
}
}
}
if !current.is_empty() {
out.push(current);
}
out
}
pub(super) fn class<'a>(object: &'a Object<'_>) -> Option<&'a str> {
object.first().map(|(k, _)| k.as_str())
}
pub(super) fn first<'a>(object: &Object<'a>, key: &str) -> Option<&'a str> {
values(object, key).next()
}
pub(super) fn values<'a, 'o>(
object: &'o Object<'a>,
key: &'o str,
) -> impl Iterator<Item = &'a str> + 'o {
object
.iter()
.filter(move |(k, v)| k == key && !v.is_empty())
.map(|(_, v)| *v)
}
pub(super) fn find_by_handle<'o, 'a>(
objects: &'o [Object<'a>],
class_key: &str,
handle: &str,
) -> Option<&'o Object<'a>> {
objects.iter().find(|o| {
o.first()
.is_some_and(|(k, v)| k == class_key && v.eq_ignore_ascii_case(handle))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_at_blank_lines_and_skips_comments() {
let raw = "% banner\r\ndomain: a.cz\r\nstatus: x\r\n\r\ncontact: H1\r\nname: N\r\n";
let objs = objects(raw);
assert_eq!(objs.len(), 2);
assert_eq!(class(&objs[0]), Some("domain"));
assert_eq!(first(&objs[1], "name"), Some("N"));
assert_eq!(
find_by_handle(&objs, "contact", "h1").map(|o| first(o, "name")),
Some(Some("N"))
);
}
}