use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use ahash::AHashMap;
use indexmap::map::Entry;
use log::warn;
type AttrMap<K, V> = indexmap::IndexMap<K, V, ahash::RandomState>;
macro_rules! unwrap_or_continue {
($e:expr) => {{
if let Some(x) = $e {
x
} else {
continue;
}
}};
}
#[derive(PartialEq, Eq, Debug, Default)]
pub struct FuncFrameAttrsMap(AHashMap<String, FrameAttrs>);
impl FuncFrameAttrsMap {
pub fn from_file(path: &Path) -> io::Result<FuncFrameAttrsMap> {
let file = BufReader::new(File::open(path)?);
FuncFrameAttrsMap::from_reader(file)
}
pub fn from_reader<R: BufRead>(mut reader: R) -> io::Result<FuncFrameAttrsMap> {
let mut funcattr_map = FuncFrameAttrsMap::default();
let mut line = Vec::new();
loop {
line.clear();
if reader.read_until(0x0A, &mut line)? == 0 {
break;
}
let l = String::from_utf8_lossy(&line);
let mut line = l.trim().splitn(2, '\t');
let func = unwrap_or_continue!(line.next());
if func.is_empty() {
continue;
}
let funcattrs = funcattr_map.0.entry(func.to_string()).or_default();
let namevals = unwrap_or_continue!(line.next());
for nameval in namevals.split('\t') {
let mut nameval = nameval.splitn(2, '=');
let name = unwrap_or_continue!(nameval.next()).trim();
if name.is_empty() {
continue;
}
let mut value = unwrap_or_continue!(nameval.next()).trim();
if value.starts_with('"') && value.ends_with('"') {
value = &value[1..value.len() - 1];
}
match name {
"title" => {
funcattrs.title = Some(value.to_string());
}
"href" => {
funcattrs.add_attr(func, "xlink:href".to_string(), value.to_string());
}
"id" | "class" | "target" => {
funcattrs.add_attr(func, name.to_string(), value.to_string());
}
"g_extra" | "a_extra" => funcattrs.parse_extra_attrs(func, value),
_ => warn!("invalid attribute {} found for {}", name, func),
}
}
if funcattrs.attrs.contains_key("xlink:href") && !funcattrs.attrs.contains_key("target")
{
funcattrs
.attrs
.insert("target".to_string(), "_top".to_string());
}
}
Ok(funcattr_map)
}
pub(super) fn frameattrs_for_func(&self, func: &str) -> Option<&FrameAttrs> {
self.0.get(func)
}
}
#[derive(PartialEq, Eq, Debug, Default)]
pub(super) struct FrameAttrs {
pub(super) title: Option<String>,
pub(super) attrs: AttrMap<String, String>,
}
impl FrameAttrs {
fn add_attr(&mut self, func: &str, name: String, value: String) {
match self.attrs.entry(name) {
Entry::Occupied(mut e) => {
warn!(
"duplicate attribute `{}` in nameattr file for `{}`; replacing value \"{}\" with \"{}\"",
e.key(), func, e.get(), value
);
e.insert(value);
}
Entry::Vacant(e) => {
e.insert(value);
}
};
}
fn parse_extra_attrs(&mut self, func: &str, s: &str) {
AttrIter { s }.for_each(|(name, value)| {
self.add_attr(func, name, value);
});
}
}
struct AttrIter<'a> {
s: &'a str,
}
impl<'a> Iterator for AttrIter<'a> {
type Item = (String, String);
fn next(&mut self) -> Option<(String, String)> {
let mut name_rest = self.s.splitn(2, '=');
let name = name_rest.next()?.trim();
if name.is_empty() {
warn!("\"=\" found with no name in extra attributes");
return None;
}
let mut split_name = name.split_whitespace();
let name = split_name.next_back()?;
for extra in split_name {
warn!(
"extra attribute {} has no value (did you mean to quote the value?)",
extra
);
}
let rest = name_rest.next()?.trim_start();
if rest.is_empty() {
warn!("no value after \"=\" for extra attribute {}", name);
}
let (value, rest) = if let Some(stripped_rest) = rest.strip_prefix('"') {
if let Some(eq) = stripped_rest.find('"') {
(&rest[1..=eq], &rest[eq + 1..])
} else {
warn!("no end quote found for extra attribute {}", name);
return None;
}
} else if let Some(w) = rest.find(char::is_whitespace) {
(&rest[..w], &rest[w + 1..])
} else {
(rest, "")
};
self.s = rest;
Some((name.to_string(), value.to_string()))
}
}
#[cfg(test)]
mod test {
use super::*;
use ahash::AHashMap;
use maplit::{convert_args, hashmap};
use pretty_assertions::assert_eq;
#[test]
fn func_frame_attrs_map_from_reader() {
let foo = vec![
"foo",
"title=foo title",
r#"class="foo class""#,
r#"g_extra=gextra1=gextra1 gextra2="foo gextra2""#,
"href=foo href",
"target=foo target",
r#"a_extra="aextra1="foo aextra1" aextra2="foo aextra2"""#,
]
.join("\t");
let bar = vec![
"bar",
"class=bar class",
"href=bar href",
r#"a_extra=aextra1=foo invalid aextra2=bar"#,
]
.join("\t");
let s = vec![foo, bar].join("\n");
let r = s.as_bytes();
let mut expected_inner = AHashMap::default();
let foo_attrs: AttrMap<String, String> = convert_args!(hashmap!(
"class" => "foo class",
"xlink:href" => "foo href",
"target" => "foo target",
"gextra1" => "gextra1",
"gextra2" => "foo gextra2",
"aextra1" => "foo aextra1",
"aextra2" => "foo aextra2",
))
.into_iter()
.collect();
expected_inner.insert(
"foo".to_owned(),
FrameAttrs {
title: Some("foo title".to_owned()),
attrs: foo_attrs,
},
);
let bar_attrs: AttrMap<String, String> = convert_args!(hashmap!(
"class" => "bar class",
"xlink:href" => "bar href",
"aextra1" => "foo",
"aextra2" => "bar",
"target" => "_top",
))
.into_iter()
.collect();
expected_inner.insert(
"bar".to_owned(),
FrameAttrs {
title: None,
attrs: bar_attrs,
},
);
let result = FuncFrameAttrsMap::from_reader(r).unwrap();
let expected = FuncFrameAttrsMap(expected_inner);
assert_eq!(result, expected);
}
}