use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct Cnf {
vars: HashMap<(String, String), String>,
}
impl Cnf {
pub fn parse(&mut self, text: &str) {
let mut pending = String::new();
for raw in text.lines() {
let line = raw.trim_end();
if let Some(stripped) = line.strip_suffix('\\') {
pending.push_str(stripped);
continue;
}
pending.push_str(line);
let logical = std::mem::take(&mut pending);
self.parse_line(&logical);
}
if !pending.is_empty() {
let logical = std::mem::take(&mut pending);
self.parse_line(&logical);
}
}
fn parse_line(&mut self, line: &str) {
let line = strip_comment(line).trim();
if line.is_empty() {
return;
}
let name_end = line
.find(|c: char| c.is_whitespace() || c == '=' || c == '.')
.unwrap_or(line.len());
let name = &line[..name_end];
let mut rest = &line[name_end..];
let mut progname = "";
if let Some(r) = rest.strip_prefix('.') {
let end = r
.find(|c: char| c.is_whitespace() || c == '=')
.unwrap_or(r.len());
progname = &r[..end];
rest = &r[end..];
}
let rest = rest.trim_start();
let value = rest.strip_prefix('=').unwrap_or(rest).trim();
if name.is_empty() {
return;
}
let value = if cfg!(windows) {
value.to_owned()
} else {
value.replace(';', ":")
};
self.vars
.entry((name.to_owned(), progname.to_owned()))
.or_insert(value);
}
pub fn get(&self, name: &str, progname: &str) -> Option<&str> {
self.vars
.get(&(name.to_owned(), progname.to_owned()))
.or_else(|| self.vars.get(&(name.to_owned(), String::new())))
.map(String::as_str)
}
}
fn strip_comment(line: &str) -> &str {
let bytes = line.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if (b == b'%' || b == b'#') && (i == 0 || bytes[i - 1].is_ascii_whitespace()) {
return &line[..i];
}
}
line
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_definition_wins_and_progname_preferred() {
let mut cnf = Cnf::default();
cnf.parse("A = 1\nA = 2 % comment\nB.lualatex = x;y\nB = z\n");
cnf.parse("A = 3\n");
assert_eq!(cnf.get("A", "lualatex"), Some("1"));
let sep = format!("x{}y", crate::SEP);
assert_eq!(cnf.get("B", "lualatex"), Some(&sep[..]));
assert_eq!(cnf.get("B", "tex"), Some("z"));
}
#[test]
fn continuation_and_optional_equals() {
let mut cnf = Cnf::default();
cnf.parse("LONG = a,\\\n b\nNOEQ value\n# full comment\n");
assert_eq!(cnf.get("LONG", ""), Some("a, b"));
assert_eq!(cnf.get("NOEQ", ""), Some("value"));
}
}