X-Git-Url: http://git.scottworley.com/tablify/blobdiff_plain/71e34cc04258862ab4ef7177c2719ca3ec8bedcf..d669f60d11bad036e4ac6c3da39f067d7bd2153a:/src/lib.rs diff --git a/src/lib.rs b/src/lib.rs index 74a54a2..f5d12f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ use std::iter::Iterator; struct Config { column_threshold: usize, static_columns: Vec>, + hidden_columns: HashSet, + substitute_labels: HashMap, } impl Config { fn apply_command(&mut self, line_num: usize, cmd: &str) -> Result<(), std::io::Error> { @@ -18,10 +20,24 @@ impl Config { format!("line {line_num}: col_threshold must be numeric: {e}"), ) })?; + } else if let Some(col) = cmd.strip_prefix("hide ") { + self.hidden_columns.insert(col.to_owned()); } else if let Some(col) = cmd.strip_prefix("col ") { self.static_columns.push(Some(col.to_owned())); } else if cmd == "colsep" { self.static_columns.push(None); + } else if let Some(directive) = cmd.strip_prefix("label ") { + match directive.split_once(':') { + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("line {line_num}: Annotation missing ':'"), + )) + } + Some((col, label)) => self + .substitute_labels + .insert(col.to_owned(), label.to_owned()), + }; } else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -31,11 +47,16 @@ impl Config { Ok(()) } } - -const DEFAULT_CONFIG: Config = Config { - column_threshold: 2, - static_columns: vec![], -}; +impl Default for Config { + fn default() -> Self { + Self { + column_threshold: 2, + static_columns: vec![], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), + } + } +} const HEADER: &str = r#" @@ -213,7 +234,7 @@ impl<'cfg, Input: Iterator>> Iterator } fn read_input(input: impl std::io::Read) -> Result<(Vec, Config), std::io::Error> { - let mut config = DEFAULT_CONFIG; + let mut config = Config::default(); let reader = Reader::new(&mut config, std::io::BufReader::new(input).lines()); reader .collect::, _>>() @@ -250,7 +271,10 @@ fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { column_counts(rows) .into_iter() .filter_map(|(n, col)| { - (n >= config.column_threshold && !static_columns.contains(col.as_str())).then_some(col) + (n >= config.column_threshold + && !static_columns.contains(col.as_str()) + && !config.hidden_columns.contains(&col)) + .then_some(col) }) .collect() } @@ -305,8 +329,12 @@ fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { } } -fn render_all_leftovers(row: &Row) -> HTML { - let mut order: Vec<_> = row.entries.keys().collect(); +fn render_all_leftovers(config: &Config, row: &Row) -> HTML { + let mut order: Vec<_> = row + .entries + .keys() + .filter(|&col| !config.hidden_columns.contains(col)) + .collect(); order.sort_unstable(); HTML( order @@ -327,15 +355,17 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM .static_columns .iter() .map(|ocol| match ocol { + Some(col) if config.hidden_columns.contains(col) => HTML::from(""), Some(col) => render_cell(col, row), None => HTML::from(r#""#), }) .collect::(); let dynamic_cells = columns .iter() + .filter(|&col| !config.hidden_columns.contains(col)) .map(|col| render_cell(col, row)) .collect::(); - let leftovers = render_all_leftovers(row); + let leftovers = render_all_leftovers(config, row); HTML(format!( "{row_label}{static_cells}{dynamic_cells}{leftovers}\n" )) @@ -343,27 +373,40 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM } } -fn render_column_headers(config: &Config, columns: &[String]) -> HTML { +fn column_header_labels<'a>( + config: &'a Config, + columns: &'a [String], +) -> impl Iterator> { let static_columns = config.static_columns.iter().map(|oc| oc.as_ref()); let dynamic_columns = columns.iter().map(Some); + static_columns + .chain(dynamic_columns) + .filter(|ocol| ocol.map_or(true, |col| !config.hidden_columns.contains(col))) + .map(|ocol| { + ocol.map(|col| match config.substitute_labels.get(col) { + None => col, + Some(substitute) => substitute, + }) + }) +} + +fn render_column_headers(config: &Config, columns: &[String]) -> HTML { HTML( String::from(r#""#) - + &static_columns - .chain(dynamic_columns) - .fold(String::new(), |mut acc, ocol| { - match ocol { - Some(col) => { - let col_header = HTML::escape(col); - write!( - &mut acc, - r#"
{col_header}
"# - ) - } - None => write!(&mut acc, r#""#), + + &column_header_labels(config, columns).fold(String::new(), |mut acc, ocol| { + match ocol { + Some(col) => { + let col_header = HTML::escape(col); + write!( + &mut acc, + r#"
{col_header}
"# + ) } - .unwrap(); - acc - }) + None => write!(&mut acc, r#""#), + } + .unwrap(); + acc + }) + "\n", ) } @@ -540,6 +583,12 @@ mod tests { read_config(&b"!col foo"[..]).unwrap().static_columns, vec![Some("foo".to_owned())] ); + assert_eq!( + read_config(&b"!label foo:bar"[..]) + .unwrap() + .substitute_labels["foo"], + "bar" + ); let bad_command = read_config(&b"!no such command"[..]); assert!(bad_command.is_err()); @@ -548,6 +597,10 @@ mod tests { let bad_num = read_config(&b"!col_threshold foo"[..]); assert!(bad_num.is_err()); assert!(format!("{bad_num:?}").contains("line 1: col_threshold must be numeric")); + + let bad_sub = read_config(&b"!label foo"[..]); + assert!(bad_sub.is_err()); + assert!(format!("{bad_sub:?}").contains("line 1: Annotation missing ':'")); } #[test] @@ -572,6 +625,38 @@ mod tests { ); } + #[test] + fn test_column_header_labels() { + let mut cfg = Config::default(); + + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([Some(&"foo".to_owned())])); + + cfg.static_columns.push(Some("bar".to_owned())); + assert!(column_header_labels(&cfg, &["foo".to_owned()]) + .eq([Some(&"bar".to_owned()), Some(&"foo".to_owned())])); + + cfg.static_columns.push(None); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([ + Some(&"bar".to_owned()), + None, + Some(&"foo".to_owned()) + ])); + + cfg.substitute_labels + .insert("foo".to_owned(), "foo (bits)".to_owned()); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([ + Some(&"bar".to_owned()), + None, + Some(&"foo (bits)".to_owned()) + ])); + + cfg.hidden_columns.insert("foo".to_owned()); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([Some(&"bar".to_owned()), None])); + + cfg.hidden_columns.insert("bar".to_owned()); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([None])); + } + #[test] fn test_render_cell() { assert_eq!( @@ -692,39 +777,63 @@ mod tests { #[test] fn test_render_leftovers() { assert_eq!( - render_all_leftovers(&Row { - label: "nope".to_owned(), - entries: HashMap::from([("foo".to_owned(), vec![None])]), - }), + render_all_leftovers( + &Config::default(), + &Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![None])]), + } + ), HTML::from("foo") ); assert_eq!( - render_all_leftovers(&Row { - label: "nope".to_owned(), - entries: HashMap::from([ - ("foo".to_owned(), vec![None]), - ("bar".to_owned(), vec![None]) - ]), - }), + render_all_leftovers( + &Config::default(), + &Row { + label: "nope".to_owned(), + entries: HashMap::from([ + ("foo".to_owned(), vec![None]), + ("bar".to_owned(), vec![None]) + ]), + } + ), HTML::from("bar, foo") ); assert_eq!( - render_all_leftovers(&Row { - label: "nope".to_owned(), - entries: HashMap::from([ - ("foo".to_owned(), vec![None]), - ("bar".to_owned(), vec![None, None]) - ]), - }), + render_all_leftovers( + &Config::default(), + &Row { + label: "nope".to_owned(), + entries: HashMap::from([ + ("foo".to_owned(), vec![None]), + ("bar".to_owned(), vec![None, None]) + ]), + } + ), HTML::from("bar: 2, foo") ); + assert_eq!( + render_all_leftovers( + &Config { + column_threshold: 2, + static_columns: vec![], + hidden_columns: HashSet::from(["private".to_owned()]), + substitute_labels: HashMap::new(), + }, + &Row { + label: "nope".to_owned(), + entries: HashMap::from([("private".to_owned(), vec![None]),]), + } + ), + HTML::from("") + ); } #[test] fn test_render_row() { assert_eq!( render_row( - &DEFAULT_CONFIG, + &Config::default(), &["foo".to_owned()], &mut Rowlike::Row(Row { label: "nope".to_owned(), @@ -741,6 +850,8 @@ mod tests { &Config { column_threshold: 0, static_columns: vec![Some("foo".to_owned()), Some("bar".to_owned())], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), }, &["baz".to_owned()], &mut Rowlike::Row(Row { @@ -762,6 +873,8 @@ mod tests { &Config { column_threshold: 0, static_columns: vec![Some("foo".to_owned()), None, Some("bar".to_owned())], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), }, &[], &mut Rowlike::Row(Row { @@ -774,6 +887,44 @@ mod tests { ), HTML::from( r#"nopefr +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec![], + hidden_columns: HashSet::from(["foo".to_owned()]), + substitute_labels: HashMap::new(), + }, + &[], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![Some("f".to_owned())]),]), + }) + ), + HTML::from( + r#"nope +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec![Some("foo".to_owned())], + hidden_columns: HashSet::from(["foo".to_owned()]), + substitute_labels: HashMap::new(), + }, + &[], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![Some("f".to_owned())]),]), + }) + ), + HTML::from( + r#"nope "# ) );